You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Interrupted tool calls permanently corrupt session history (orphaned tool_use without tool_result) #21326
Bug: Interrupted tool calls permanently corrupt session history
Problem
When a tool call is interrupted (network timeout, proxy error, abort), OpenCode records the tool_use block in the assistant message but does not insert a corresponding tool_result block (even a synthetic error one). This makes the session permanently broken -- every subsequent message attempt is rejected by the Claude API with:
messages.N: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_bdrk_XXXX
The session cannot recover without manual intervention (revert/undo).
Compounding issue: empty error in TUI
When this validation error occurs, session.error fires with an empty error field, and the TUI shows zero feedback. The user sees a silent hang with no indication of what went wrong.
Root Cause
The Go core does not ensure message history integrity when tool execution is interrupted. The fix should be: always insert a tool_result with error status when a tool_use does not complete, regardless of the interruption cause.
Evidence
Session errors occurring 1-4ms after user message (pre-request validation, not network timeout):
11:04:35.902 user_message
11:04:35.906 session_error error:"" (4ms -- request never left the client)
Full Bedrock error when error IS surfaced:
InvokeModelWithResponseStream: operation error Bedrock Runtime,
StatusCode: 400, ValidationException:
messages.53: tool_use ids were found without tool_result blocks
immediately after: toolu_bdrk_012CSgTxpSpwK877z7LK5M2Y
This affects both old sessions AND newly created sessions (corruption happens actively).
Expected Behavior
When a tool call is interrupted, insert a synthetic tool_result with error status to maintain message format compliance
session.error should include the actual error message, not an empty string
TUI should show visible feedback on any session error
Workaround
Plugin that hooks session.error, scans for orphaned tool_use blocks, and attempts session.revert().
Environment
OpenCode: latest
OS: Windows 11
Provider: API proxy -> AWS Bedrock -> Claude Sonnet 4 / Opus 4
This affects me as well — v1.4.3, WSL2/Linux, Claude Opus 4.6 via Anthropic API. Sessions become permanently unrecoverable once hit.
Regarding the bot's duplicate flag: this is the same symptom as #1662 and #10616, but the root cause has multiple entry points. The partial fixes applied so far address individual vectors but miss the systemic issue. Adding source-level analysis below.
Healing logic exists but is insufficient
There IS healing logic in message-v2.ts (lines 779-790) that catches pending/running tool calls and converts them to output-error parts:
if(part.state.status==="pending"||part.state.status==="running")assistantMessage.parts.push({type: ("tool-"+part.tool),state: "output-error",toolCallId: part.callID,input: part.state.input,errorText: "[Tool execution was interrupted]",})
And processor.ts (lines 489-512) marks interrupted tool calls as status: "error" with metadata: { interrupted: true } during cleanup. All four ToolState values (pending, running, completed, error) are handled in toModelMessages.
Despite this, the bug persists because three code paths bypass the healing entirely.
Vector 1 — normalizeMessages in transform.ts (lines 56-74)
For Anthropic/Bedrock providers, messages with empty content after filtering get dropped:
This runs AFTER convertToModelMessages has already separated tool_use and tool_result into distinct ModelMessage entries. If a message in the ModelMessage[] chain gets eliminated here, it breaks the tool_use → tool_result pairing that the Anthropic API requires.
Vector 2 — Error-skip in message-v2.ts (lines 697-705)
When a previous assistant message failed with an APIError (including this exact tool_use without tool_result error), the message is skipped entirely on the next attempt. But its tool parts may have already influenced the ModelMessage[] chain via convertToModelMessages. The skip removes the message but not its downstream effects.
Vector 3 — Lost step boundaries during retries (#16749)
When the finish-step handler throws during a retryable error, step-finish for step N and step-start for step N+1 are never persisted. Both steps' content merges into a single DB message. On replay, convertToModelMessages produces interleaved tool_use/text/reasoning content in a single assistant block — which Anthropic rejects. #16749 documents this in detail with SQL evidence from a real session.
The retry policy doesn't help
In retry.ts, retryable() returns undefined for non-retryable errors. Anthropic's 400 Bad Request (isRetryable: false) means the corrupted request fails permanently on first attempt — no retry, no recovery.
This is a long-standing systemic issue
This has been open since #1662 (February 2026). Related issues include #8377, #10616, #16749, #17065, #14367, #21489, #2720, #5750, #2214, #8312, #8010. Partial fixes have been applied to individual vectors, but the core problem persists because each fix targets one entry point while the others remain open.
Suggested fix: post-transform validation at the ModelMessage[] level
The healing logic in toModelMessages and the cleanup in processor.ts work at the part level — before the data passes through convertToModelMessages and normalizeMessages. All three vectors corrupt the data after that point.
The fix that catches all three vectors is a validation pass on the final ModelMessage[] array, after both convertToModelMessages and normalizeMessages have run. Pseudocode:
functionensureToolResultIntegrity(messages: ModelMessage[]): ModelMessage[]{// 1. Collect all tool_use IDs from assistant messages// 2. Collect all tool_result IDs from tool messages// 3. For any tool_use ID without a matching tool_result:// - Inject a synthetic tool_result with error content, OR// - Remove the orphaned tool_use block// 4. Ensure no assistant message has text/reasoning after tool_use// (split into separate messages if needed)returnsanitized}
This is the only point in the pipeline that sits downstream of all three corruption vectors. Fixing individual vectors is still worthwhile, but without this safety net, new edge cases will keep producing the same symptom.
Activity
github-actions commentedon Apr 7, 2026
This issue might be a duplicate of existing issues. Please check:
tool_useids were found withouttool_resultblocks immediately #10616: Same errortool_use ids were found without tool_result blocks immediately aftertool_useids were found withouttool_resultblocks immediately after #1662: Sametool_use ids were found without tool_result blocks immediately aftererrorIf your issue is distinct or adds new context not covered by the above, please clarify the differences so it can be tracked separately.
bvironn commentedon Apr 11, 2026
This affects me as well — v1.4.3, WSL2/Linux, Claude Opus 4.6 via Anthropic API. Sessions become permanently unrecoverable once hit.
Regarding the bot's duplicate flag: this is the same symptom as #1662 and #10616, but the root cause has multiple entry points. The partial fixes applied so far address individual vectors but miss the systemic issue. Adding source-level analysis below.
Healing logic exists but is insufficient
There IS healing logic in
message-v2.ts(lines 779-790) that catches pending/running tool calls and converts them tooutput-errorparts:And
processor.ts(lines 489-512) marks interrupted tool calls asstatus: "error"withmetadata: { interrupted: true }during cleanup. All fourToolStatevalues (pending, running, completed, error) are handled intoModelMessages.Despite this, the bug persists because three code paths bypass the healing entirely.
Vector 1 —
normalizeMessagesintransform.ts(lines 56-74)For Anthropic/Bedrock providers, messages with empty content after filtering get dropped:
This runs AFTER
convertToModelMessageshas already separatedtool_useandtool_resultinto distinctModelMessageentries. If a message in theModelMessage[]chain gets eliminated here, it breaks thetool_use→tool_resultpairing that the Anthropic API requires.Vector 2 — Error-skip in
message-v2.ts(lines 697-705)When a previous assistant message failed with an
APIError(including this exacttool_use without tool_resulterror), the message is skipped entirely on the next attempt. But its tool parts may have already influenced theModelMessage[]chain viaconvertToModelMessages. The skip removes the message but not its downstream effects.Vector 3 — Lost step boundaries during retries (#16749)
When the
finish-stephandler throws during a retryable error,step-finishfor step N andstep-startfor step N+1 are never persisted. Both steps' content merges into a single DB message. On replay,convertToModelMessagesproduces interleavedtool_use/text/reasoningcontent in a single assistant block — which Anthropic rejects. #16749 documents this in detail with SQL evidence from a real session.The retry policy doesn't help
In
retry.ts,retryable()returnsundefinedfor non-retryable errors. Anthropic's 400 Bad Request (isRetryable: false) means the corrupted request fails permanently on first attempt — no retry, no recovery.This is a long-standing systemic issue
This has been open since #1662 (February 2026). Related issues include #8377, #10616, #16749, #17065, #14367, #21489, #2720, #5750, #2214, #8312, #8010. Partial fixes have been applied to individual vectors, but the core problem persists because each fix targets one entry point while the others remain open.
Suggested fix: post-transform validation at the
ModelMessage[]levelThe healing logic in
toModelMessagesand the cleanup inprocessor.tswork at the part level — before the data passes throughconvertToModelMessagesandnormalizeMessages. All three vectors corrupt the data after that point.The fix that catches all three vectors is a validation pass on the final
ModelMessage[]array, after bothconvertToModelMessagesandnormalizeMessageshave run. Pseudocode:This is the only point in the pipeline that sits downstream of all three corruption vectors. Fixing individual vectors is still worthwhile, but without this safety net, new edge cases will keep producing the same symptom.
rekram1-node commentedon Apr 16, 2026
fix: 348a849
phazei commentedon May 5, 2026
Totally not fixed