fix(opencode): stop a repeated tool call id from failing the session for good
Issue for this PR
A customer's chat on Replo (the NURA product page) returned only errors for 13 turns in a row on 2026-09-29. Every turn failed with Anthropic's 400 messages.291.content.1: tool_use ids must be unique. The same error has bricked 6 sessions in 6 projects since 2026-09-22.
Type of change
- Bug fix
What does this PR do?
Every affected session holds one assistant step with two tool parts sharing a call id: the real call (read, completed) and a phantom named unknown, with input {} and state "Tool execution aborted" (interrupted: true). The phantom was created about 0.6s after the real call finished.
How it happens: when a call completes, settleToolCall drops it from ctx.toolcalls, and the AI SDK adapter drops its name from toolNames. If the provider stream then sends another tool-input-delta or tool-input-end for that id, ensureToolCall finds nothing and creates a fresh pending part. Its name is unknown because those two events are the only path that falls back to "unknown". The step-finish cleanup then marks it aborted. From then on toModelMessages replays both parts as tool_use blocks with the same id, Anthropic rejects the request as non-retryable, and the session can't recover.
Two changes:
processor.ts:settleToolCallrecords each call it settles, andensureToolCallignores any later input or call event for that id. This also stops a repeatedtool-callevent from starting a finished tool again.message-v2.ts: history replay sends each call id once per assistant message and keeps the first part, which is the real call because the phantom is always created after it. The scope is one message rather than the whole history because some providers reuse ids likecall_0across responses, and deduping across messages would drop their real calls. This change is also what repairs sessions that already contain a duplicate: their next turn goes through without editing any stored data.
All 6 production hits were tagged [Google], i.e. OpenRouter serving Anthropic through Vertex. I haven't confirmed which exact late event that route sends. The fix doesn't depend on it.
How did you verify your code works?
- New processor test: a stream that sends
tool-input-deltaandtool-input-endafter the call's result. Before the fix it recorded[call-1, unknown, error]next to the real part; now it records only[call-1, lookup, completed]. - New
toModelMessagestest with the exact persisted shape from production (completedreadplus an abortedunknownpart with the same call id). Before the fix it emitted twotool-callblocks forcall-1; now it emits one. packages/opencode:bun typecheckis clean andbun testpasses 3660. The one failure,test/cli/cmd/tui/attention.test.ts, is flaky under full-suite load: it passes 3/3 run alone and doesn't import either changed file.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
š¤ Generated with Claude Code
Summary by cubic
Fixes sessions being permanently bricked by duplicated tool call ids, which Anthropic rejects on every subsequent turn. The processor now ignores late input and repeated call events for already-finished tool calls, and history replay sends each call id only once, which also repairs sessions that already contain a duplicate. Deduplication is scoped to a single assistant message so providers that reuse call ids across responses keep their real calls.
- When a fallback model takes over a dead route, discarding the route's pending tool parts now forgets those call ids instead of settling them, so the fallback model can reuse the same id for its own real call.