Bug
When a stream attempt is aborted or retried mid-tool, opencode's
processor.cleanup() force-marks the orphaned tool_use as
state.status: "error" with metadata.interrupted: true. The next
iteration of the run loop's tool-call check counts this orphan as open
work and fires another LLM request. convertToModelMessages then splits
the assistant message around the orphan and the resulting history ends
with an assistant turn — which Anthropic rejects with HTTP 400:
This model does not support assistant message prefill.
The conversation must end with a user message.
The session is then stuck: every retry replays the same corrupted
history from storage and re-triggers the 400.
Trigger sequence
- Model emits a
tool_use block.
- Stream is aborted/retried before the tool completes (network blip,
user cancel, EmptyOther truncation, etc.).
processor.cleanup() writes
{ status: "error", metadata: { interrupted: true } } on the tool
part to mark it as not-running.
- The run loop's
hasToolCalls check at the bottom of prompt.ts
sees the error tool, treats it as open work, and continues the loop.
- Next LLM request is built from the assistant message that contains
the orphan; the AI SDK splits the message and the request history
ends with an assistant turn.
- Anthropic returns 400 (or, for non-prefill-supporting models in
general, an equivalent error).
Real-world evidence
In my own opencode session database I found:
- 71 orphaned-interrupted tool parts matching the shape
{ state.status: "error", metadata: { interrupted: true }, error: "Tool execution aborted" }.
- 15 explicit prefill 400 errors across 6 sessions,
predominantly on claude-opus-4-6 (14 occurrences) and
claude-opus-4-7 (1).
In one session the temporal correlation is direct:
- orphan tool part created at
time.start: 1777464448632
- prefill 400 error logged at
time_created: 1777464448637
- 5 ms apart — the next LLM request was the failure
Suggested fix
Refactor the inline tool-call check in the run loop into a helper that
excludes orphaned-interrupted tool parts in addition to provider-executed
ones. They don't represent work the model is waiting on — the model
never saw the result and the next user turn supersedes them.
Environment
Bug
When a stream attempt is aborted or retried mid-tool, opencode's
processor.cleanup()force-marks the orphanedtool_useasstate.status: "error"withmetadata.interrupted: true. The nextiteration of the run loop's tool-call check counts this orphan as open
work and fires another LLM request.
convertToModelMessagesthen splitsthe assistant message around the orphan and the resulting history ends
with an assistant turn — which Anthropic rejects with HTTP 400:
The session is then stuck: every retry replays the same corrupted
history from storage and re-triggers the 400.
Trigger sequence
tool_useblock.user cancel, EmptyOther truncation, etc.).
processor.cleanup()writes{ status: "error", metadata: { interrupted: true } }on the toolpart to mark it as not-running.
hasToolCallscheck at the bottom ofprompt.tssees the error tool, treats it as open work, and continues the loop.
the orphan; the AI SDK splits the message and the request history
ends with an assistant turn.
general, an equivalent error).
Real-world evidence
In my own opencode session database I found:
{ state.status: "error", metadata: { interrupted: true }, error: "Tool execution aborted" }.predominantly on
claude-opus-4-6(14 occurrences) andclaude-opus-4-7(1).In one session the temporal correlation is direct:
time.start: 1777464448632time_created: 1777464448637Suggested fix
Refactor the inline tool-call check in the run loop into a helper that
excludes orphaned-interrupted tool parts in addition to provider-executed
ones. They don't represent work the model is waiting on — the model
never saw the result and the next user turn supersedes them.
Environment
stream mid-tool, cleanup creates the orphan that this bug then turns
into a 400.