fix: skip incomplete tool calls in transcript repair [AI-assisted]#3194
fix: skip incomplete tool calls in transcript repair [AI-assisted]#3194koriyoshi2041 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
When a streaming response is terminated mid-tool-call, the assistant message contains tool call blocks with `partialJson` but no valid `arguments`. The existing repair logic treats these as complete tool calls and inserts synthetic error `toolResult` messages, which causes Anthropic's API to reject the request with "unexpected tool_use_id" on subsequent turns — corrupting the session. Add `isIncompleteToolCall()` to detect these terminated tool calls by checking for `partialJson` present AND `arguments` missing/empty. Incomplete tool calls are skipped during extraction so no synthetic result is generated, while the block itself remains in the assistant message preserving conversational context. This is more surgical than alternative approaches: - Unlike skipping the entire assistant message on stopReason "error", this preserves text content from the same turn. - Unlike deleting the incomplete block, this keeps the partial tool call visible for debugging/context. Includes 9 new tests covering incomplete, complete-with-partialJson, mixed, and empty-arguments scenarios.
| function isIncompleteToolCall(block: Record<string, unknown>): boolean { | ||
| if (typeof block.partialJson !== "string" || !block.partialJson) return false; | ||
|
|
||
| const args = block.arguments; | ||
| // No arguments at all → incomplete | ||
| if (args === undefined || args === null) return true; | ||
| // Empty object {} → incomplete (arguments were not parsed) | ||
| if (typeof args === "object" && !Array.isArray(args) && Object.keys(args as object).length === 0) | ||
| return true; | ||
|
|
There was a problem hiding this comment.
isIncompleteToolCall() treats arguments: {} as incomplete whenever partialJson is non-empty. This can misclassify valid tool calls whose schema allows an empty object (several existing tests use arguments: {} as a “complete” call), causing their real toolResult to be dropped as an orphan and potentially corrupting otherwise-valid transcripts.
If the intent is “provider wrote {} as a placeholder before parsing args”, consider only treating {} as incomplete when it’s missing required fields for that tool (if known), or gate the {} case behind an additional signal (e.g. stopReason/error context) instead of partialJson alone.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 24:33
Comment:
`isIncompleteToolCall()` treats `arguments: {}` as incomplete whenever `partialJson` is non-empty. This can misclassify valid tool calls whose schema allows an empty object (several existing tests use `arguments: {}` as a “complete” call), causing their real `toolResult` to be dropped as an orphan and potentially corrupting otherwise-valid transcripts.
If the intent is “provider wrote `{}` as a placeholder before parsing args”, consider only treating `{}` as incomplete when it’s *missing* required fields for that tool (if known), or gate the `{}` case behind an additional signal (e.g. stopReason/error context) instead of `partialJson` alone.
How can I resolve this? If you propose a fix, please make it concise.|
Closing as duplicate of #5822. If this is incorrect, comment and we can reopen. |
|
Hi @sebslight — thanks for the review. I see this was closed as a duplicate of #5822, which itself was closed in favor of #4598 (merged Feb 6). However, our approach is materially different from #4598 and handles an edge case it misses:
Real-world scenario: A streaming response completes Additionally, this PR has 9 tests (vs 4 in #4598) covering mixed complete/incomplete scenarios and empty Would you consider reopening, or should I open a new PR targeting the gap? |
Summary
When a streaming response is terminated mid-tool-call (e.g.
stopReason: "error",errorMessage: "terminated"), the assistant message contains tool call blocks withpartialJsonbut no validarguments. The existingrepairToolUseResultPairing()treats these as complete tool calls and inserts synthetic errortoolResultmessages. On the next API call, Anthropic rejects the request with "unexpected tool_use_id" — corrupting the session permanently.This PR adds an
isIncompleteToolCall()check that detects terminated tool calls by verifying:partialJsonis present (streaming was in progress), ANDargumentsis missing,null, or an empty{}objectIncomplete tool calls are skipped during extraction so no synthetic result is generated, while the block itself remains in the assistant message — preserving conversational context for the model.
How this differs from existing approaches
stopReason: "error"(#1859)Changes
session-transcript-repair.ts: AddedisIncompleteToolCall()function; modifiedextractToolCallsFromAssistant()to skip incomplete tool calls; exportedisIncompleteToolCallfor testingsession-transcript-repair.test.ts: Added 9 new tests (5 forsanitizeToolUseResultPairing, 4 forisIncompleteToolCall)Test plan
toolResultemittedpartialJsonstill work normallynpm run lint— 0 warnings, 0 errors)AI Disclosure
🤖 This PR was AI-assisted (Claude Opus 4.5 via Claude Code). The contributor understands what the code does — the fix was designed after analyzing 4 existing open PRs (#1859, #2253, #2557, #2806) and real corrupted session data. Fully tested — 9 new tests + verified against production session files.
Greptile Overview
Greptile Summary
This PR updates the transcript repair logic so that tool-call blocks that were cut off mid-stream (identified by
partialJsonpresent butargumentsmissing/empty) are ignored during tool-call extraction. That prevents generating synthetictoolResultentries for tool calls that never actually executed, which avoids downstream provider errors like “unexpected tool_use_id”. The change is localized toextractToolCallsFromAssistant()insrc/agents/session-transcript-repair.tsand is covered by new Vitest cases insrc/agents/session-transcript-repair.test.tsfor incomplete vs complete tool calls (including mixed messages).Confidence Score: 3/5
arguments: {}as incomplete based solely onpartialJsonrisks false positives for tools that legitimately accept empty objects (and the existing tests commonly model tool calls that way). That could cause real tool results to be dropped as orphans in repaired transcripts.(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!