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
In packages/opencode/src/provider/transform.ts, normalizeMessages() splits assistant turns where text trails tool-call blocks for Anthropic / Vertex Anthropic models. The current split filter groups only tool-call parts together and treats tool-result as non-tool content, so an assistant turn shaped like:
[tool-call, tool-result, text]
becomes:
msg A: [tool-result, text]
msg B: [tool-call]
After provider/SDK merge this still presents tool_use blocks without a paired tool_result immediately after, so Anthropic returns:
tool_use ids were found without tool_result blocks immediately after
OpenCode version
1.14.33 (also reproduces against current dev).
Steps to reproduce
Use a Claude / Vertex Anthropic model.
Have an assistant turn whose content ends up shaped like [tool-call, tool-result, text] or [tool-call, text, tool-result] (this can happen during reconstruction or when tool-result is co-located with the originating tool-call).
Send another user turn so the assistant turn is replayed in normalizeMessages().
The provider rejects the request with tool_use ids were found without tool_result blocks immediately after.
The fix is small: keep tool-call and tool-result paired during the split (treat both as "tool" parts).
It does two things: heals pending/running tool calls into error results before each request (covers the interrupted-tool case), and reverts the failed turn if the 400 still comes back (debounced, reversible with /unrevert).
To be straight about the limit here: the split you're describing lands after convertToModelMessages/normalizeMessages, which is past where the transform hook runs, so for that exact path it relies on the revert rather than preventing it. It's a stopgap until the real fix ships. Approaches are from #8497 and #1662.
Confirming this root cause is alive in 1.18.4 (this issue was filed against 1.14.33). Adding a second, independent producer of the orphaned tool_use — the compaction / summary request — plus the "permanently stuck" amplification, since they compound.
compaction.ts slices the summary input on message boundaries by token budget alone — no tool_use/tool_result pairing check
In packages/opencode/src/session/compaction.ts (v1.18.4), splitTurn() picks a cut point inside a turn purely by budget:
// L105-L121functionsplitTurn(input: {messages;turn;model;budget}){
...
for(letstart=input.turn.start+1;start<input.turn.end;start++){constsize=yield*input.estimate({messages: input.messages.slice(start,input.turn.end),model: input.model})if(size>input.budget)continuereturn{ start,id: input.messages[start].info.id}// cut point — no pairing check}}
When the history contains a run of consecutive assistant tool_use turns with no user turn between them (e.g. an agent dispatching several task/subagent tool calls back-to-back), the cut point can land inside that run. The head then ends on an assistant tool_use, and the request becomes [..., assistant(tool_use), user(summary prompt)] — the tool_use is immediately followed by summary text instead of a tool_result:
messages.2: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_XXXX.
Two things worth flagging:
The offending tool part is a completedtask tool with a real output — so the "pending/running → synthesize output-error" guard in message-v2.ts does not catch it. The pairing was broken by the compaction slice, not by an unfinished tool.
For the compaction path the boundary is fixed by splitTurn()before the hook runs (L350 operates on the already-sliced head), and the orphan only materializes in toModelMessagesEffect + the appended summary prompt — so a transform-hook plugin can't repair it, matching @viiqswim's point above about normalizeMessages happening after the hooks.
Why it's non-recoverable
The compaction 400 is isRetryable: false. If anything auto-retries compaction (opencode's own post-compaction auto-continue, or an agent framework's idle continuation), the session deadlocks: retry → same 400 → new failed compaction message → retry. In one real session I saw 8 consecutive failed compactions ~7s apart, the run frozen, the session unrecoverable — same shape as #27594. (I filed the continuation-loop fail-safe separately on the agent layer: code-yeongyu/oh-my-openagent#6303, but the trigger is here.)
So the fix proposed in this issue (keep tool-call/tool-result paired in normalizeMessages) is necessary but not sufficient: splitTurn() / select() in compaction.ts need the same pairing-aware boundary, otherwise the summary request keeps emitting orphaned tool_use independent of normalizeMessages().
Env: opencode 1.18.4, macOS 14.2.1 (arm64), Anthropic Claude models.
Confirmed on OpenCode 1.17.10, macOS, using Anthropic Claude.
Auto-compaction triggered at 103% context usage and failed with tool_use IDs missing immediately following tool_result blocks. Retrying produced the same error and left the session unusable.
I checked the local session database: both referenced tool calls were marked completed and had stored outputs. This suggests compaction/request construction separated valid tool calls from their results, rather than the tools being interrupted.
The logs show the failure occurred in agent=compaction.
Activity
viiqswim commentedon Jun 29, 2026
Really good writeup. The normalizeMessages split is exactly the kind of thing a plugin can't catch, since it happens after the hooks have already run.
While the transform.ts fix is pending, I put together a plugin that at least stops sessions from getting permanently wedged:
https://github.com/viiqswim/opencode-compaction-guard
It does two things: heals pending/running tool calls into error results before each request (covers the interrupted-tool case), and reverts the failed turn if the 400 still comes back (debounced, reversible with /unrevert).
To be straight about the limit here: the split you're describing lands after convertToModelMessages/normalizeMessages, which is past where the transform hook runs, so for that exact path it relies on the revert rather than preventing it. It's a stopgap until the real fix ships. Approaches are from #8497 and #1662.
sonny-travix-dev commentedon Jul 23, 2026
Confirming this root cause is alive in 1.18.4 (this issue was filed against 1.14.33). Adding a second, independent producer of the orphaned
tool_use— the compaction / summary request — plus the "permanently stuck" amplification, since they compound.compaction.tsslices the summary input on message boundaries by token budget alone — no tool_use/tool_result pairing checkIn
packages/opencode/src/session/compaction.ts(v1.18.4),splitTurn()picks a cut point inside a turn purely by budget:and
select()takes the head as a raw prefix:The head is then cloned, the transform hook fires, it's converted, and a summary user prompt is appended:
When the history contains a run of consecutive assistant
tool_useturns with no user turn between them (e.g. an agent dispatching severaltask/subagent tool calls back-to-back), the cut point can land inside that run. The head then ends on an assistanttool_use, and the request becomes[..., assistant(tool_use), user(summary prompt)]— thetool_useis immediately followed by summary text instead of atool_result:Two things worth flagging:
tasktool with a real output — so the "pending/running → synthesize output-error" guard inmessage-v2.tsdoes not catch it. The pairing was broken by the compaction slice, not by an unfinished tool.splitTurn()before the hook runs (L350 operates on the already-sliced head), and the orphan only materializes intoModelMessagesEffect+ the appended summary prompt — so atransform-hook plugin can't repair it, matching @viiqswim's point above aboutnormalizeMessageshappening after the hooks.Why it's non-recoverable
The compaction 400 is
isRetryable: false. If anything auto-retries compaction (opencode's own post-compaction auto-continue, or an agent framework's idle continuation), the session deadlocks: retry → same 400 → new failed compaction message → retry. In one real session I saw 8 consecutive failed compactions ~7s apart, the run frozen, the session unrecoverable — same shape as #27594. (I filed the continuation-loop fail-safe separately on the agent layer: code-yeongyu/oh-my-openagent#6303, but the trigger is here.)So the fix proposed in this issue (keep
tool-call/tool-resultpaired innormalizeMessages) is necessary but not sufficient:splitTurn()/select()incompaction.tsneed the same pairing-aware boundary, otherwise the summary request keeps emitting orphanedtool_useindependent ofnormalizeMessages().Env: opencode 1.18.4, macOS 14.2.1 (arm64), Anthropic Claude models.
hacker-h commentedon Aug 2, 2026
Confirmed on OpenCode 1.17.10, macOS, using Anthropic Claude.
Auto-compaction triggered at 103% context usage and failed with
tool_useIDs missing immediately followingtool_resultblocks. Retrying produced the same error and left the session unusable.I checked the local session database: both referenced tool calls were marked
completedand had stored outputs. This suggests compaction/request construction separated valid tool calls from their results, rather than the tools being interrupted.The logs show the failure occurred in
agent=compaction.github-actions commentedon Oct 1, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.