From f230e0f0972f9ddaefa72d2065f3908c409fb46f Mon Sep 17 00:00:00 2001 From: alsyah Date: Wed, 29 Jul 2026 00:42:29 +0700 Subject: [PATCH] (fix) (moonshotai/Kimi-K3, reviewed F, tested F) add dedup to prevent duplicate tool results Addresses issues #37 and #38: after provider disconnect/reconnect or model fallback, the same tool_use ID could be submitted twice to the provider, resulting in duplicate tool_result blocks. Changes: - StreamingToolExecutor.addTool: skip duplicate tool_use IDs that are already tracked (provider reconnect path) - yieldMissingToolResultBlocks: deduplicate tool_use blocks by ID within each assistant message before yielding interruption fallback results (model fallback recovery path) Reproducer (untested by AI, reviewer must run): - Issue #37 gist: https://gist.github.com/N0zoM1z0/adb097dd6be467aa0cc1608d0e1dca6b - Issue #38 gist: https://gist.github.com/N0zoM1z0/1c2db900f5aef155cc647458cb7ba6d2 Picks up work from closed-unmerged PR #42 Fixes #37 Fixes #38 --- src/query.ts | 16 ++++++++++++++-- src/services/tools/StreamingToolExecutor.ts | 6 ++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/query.ts b/src/query.ts index 07e8b6fae..2bf9a757e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -124,13 +124,25 @@ function* yieldMissingToolResultBlocks( assistantMessages: AssistantMessage[], errorMessage: string, ) { - for (const assistantMessage of assistantMessages) { + // Filter out duplicate tool use blocks within the same message to prevent duplicate tool results + const uniqueAssistantMessages = assistantMessages.map(assistantMessage => { + const content = assistantMessage.message.content + const uniqueContent = content.filter((item, index, self) => { + if (item.type !== 'tool_use') return true + return index === self.findIndex(other => + other.type === 'tool_use' && other.id === item.id + ) + }) + return { ...assistantMessage, message: { ...assistantMessage.message, content: uniqueContent } } + }) + + for (const assistantMessage of uniqueAssistantMessages) { // Extract all tool use blocks from this assistant message const toolUseBlocks = assistantMessage.message.content.filter( content => content.type === 'tool_use', ) as ToolUseBlock[] - // Emit an interruption message for each tool use + // Emit an interruption message for each unique tool use for (const toolUse of toolUseBlocks) { yield createUserMessage({ content: [ diff --git a/src/services/tools/StreamingToolExecutor.ts b/src/services/tools/StreamingToolExecutor.ts index c4f06a961..f142cf2fd 100644 --- a/src/services/tools/StreamingToolExecutor.ts +++ b/src/services/tools/StreamingToolExecutor.ts @@ -74,6 +74,12 @@ export class StreamingToolExecutor { * Add a tool to the execution queue. Will start executing immediately if conditions allow. */ addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void { + // Prevent duplicate tool additions - if a tool with this ID already exists, skip it. + // This can happen when provider disconnects/reconnects and resends the same tool_use block. + if (this.tools.some(t => t.id === block.id)) { + return + } + const toolDefinition = findToolByName(this.toolDefinitions, block.name) if (!toolDefinition) { this.tools.push({