fix(opencode): retry a step the provider cut off instead of replaying it
Stops a turn from dying with The final block in an assistant message cannot be `thinking` after the provider cuts a step short.
Symptom
Two production turns (2026-09-29 via Bedrock, 2026-10-14 via OpenRouter ā Anthropic/AWS/Azure) ended in APIError 400 ⦠messages.N: The final block in an assistant message cannot be thinking. from every provider tried. The chat showed "Something went wrong with this turn." and the final reply never arrived, even though the agent's file edits had already landed.
Cause
The step right before the error finished tool-calls with 9ā12 output tokens and a single reasoning part reading This part of the response was interrupted before it finished. ā Anthropic's own marker for a cut-off response. The loop only exits on a finish outside tool-calls / unknown, so it ran another step with that message replayed last. A trailing assistant message is a prefill, and a prefill ending in a thinking block is rejected. OpenRouter's provider fallback and our four retries could not help; the 400 is deterministic.
Fix
At the loop head, a tool-calls finish with no tool part and no text is treated as a cut-off response: remove the message and run the step again. A second cut-off in the same turn fails the assistant message with MessageOutputLengthError and publishes session.error, so the client shows the turn as failed instead of looping.
Removing the step rather than appending a continuation user turn (Anthropic's documented path, and what upstream v2's runner does with CONTINUE_AFTER_INCOMPLETE_STREAM): the coordinator rebuilds a retry from the latest user message and drops synthetic parts, so a synthetic user turn would need matching coordinator and UI changes. Dropping an empty step keeps the fix in one place; the discarded thinking block carries only the placeholder.
Tests
prompt.test.ts: a cut-off step is dropped and the rerun's text reply becomes the result; two cut-offs in a row fail the turn after exactly two requests. Both fail on dev, where the loop makes a third request.
Release
Needs v1.18.32-10 per RELEASING.md; andytown then bumps OPENCODE_VERSION in apps/daytona-snapshot/Dockerfile and the coordinator's @opencode-ai/sdk pin.
š¤ Generated with Claude Code
Summary by cubic
Fixes turns dying when a provider cuts a step short. The loop replayed the cut-off message as a prefill, which Anthropic rejects with "The final block in an assistant message cannot be thinking". Now a cut-off step is dropped and rerun once; a second cut-off fails the turn with MessageOutputLengthError and publishes session.error.
A cut-off is an assistant message that finishes with tool-calls but streams no tool or text parts.