Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit e561754

Browse files
committed
fix: use ID-based lookup for parallel tool finalization
Replaces map-based index tracking with direct ID lookup when finalizing streaming tool calls. This prevents duplicate rendering of parallel tools by ensuring idempotent processing. Changes: - Use findIndex with partial===true check instead of streamingToolCallIndices - Skip already-finalized tools to eliminate race conditions - Works for both sequential and parallel tool calling modes The previous approach relied on streamingToolCallIndices map which could become stale when multiple code paths (finalization loop, sweep fix, pending updates) process the same tool, causing duplicate rendering.
1 parent dc5e765 commit e561754

1 file changed

Lines changed: 19 additions & 8 deletions

File tree

src/core/task/Task.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3240,17 +3240,28 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32403240
// Finalize the streaming tool call
32413241
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
32423242

3243-
// Get the index for this tool call
3244-
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
3243+
// Find by ID, not tracked index - can't get stale
3244+
// Only match partial blocks to ensure idempotent processing
3245+
// If a tool was already finalized (partial === false), skip it
3246+
const toolIndex = this.assistantMessageContent.findIndex(
3247+
(block) =>
3248+
(block.type === "tool_use" || block.type === "mcp_tool_use") &&
3249+
(block as any).id === event.id &&
3250+
block.partial === true,
3251+
)
3252+
3253+
if (toolIndex === -1) {
3254+
// Already finalized - clean up tracking and skip
3255+
this.streamingToolCallIndices.delete(event.id)
3256+
continue
3257+
}
32453258

32463259
if (finalToolUse) {
32473260
// Store the tool call ID
32483261
;(finalToolUse as any).id = event.id
32493262

3250-
// Get the index and replace partial with final
3251-
if (toolUseIndex !== undefined) {
3252-
this.assistantMessageContent[toolUseIndex] = finalToolUse
3253-
}
3263+
// Replace partial with final
3264+
this.assistantMessageContent[toolIndex] = finalToolUse
32543265

32553266
// Clean up tracking
32563267
this.streamingToolCallIndices.delete(event.id)
@@ -3260,11 +3271,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32603271

32613272
// Present the finalized tool call
32623273
presentAssistantMessage(this)
3263-
} else if (toolUseIndex !== undefined) {
3274+
} else {
32643275
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
32653276
// We still need to mark the tool as non-partial so it gets executed
32663277
// The tool's validation will catch any missing required parameters
3267-
const existingToolUse = this.assistantMessageContent[toolUseIndex]
3278+
const existingToolUse = this.assistantMessageContent[toolIndex]
32683279
if (existingToolUse && existingToolUse.type === "tool_use") {
32693280
existingToolUse.partial = false
32703281
// Ensure it has the ID for native protocol

0 commit comments

Comments
 (0)