Repository navigation
Description
Description
When the model reaches the max output tokens (finish_reason: length) in the middle of a tool call, opencode
closes the turn as succeeded and persists the partial tool call with state.input as a truncated string.
The tool part then stays status: "streaming" forever and the session becomes unreadable:
GET /api/session/{id}/message returns HTTP 500. In the client the session stays "writing" — ESC does
nothing and input is not accepted — and the user cannot use that session at all.
Plugins
Local plugins, all three from config: ./plugins/opencode-anti-loop, ./plugins/sglang-guard,
./plugins/plur-memory. (Anti-loop had fired earlier in the same session for an unrelated reason; this defect
reproduces independently of it.)
OpenCode version
2.0.21 (opencode --version). The affected session was created under 2.0.20 — its session_v2.version is
still "2.0.20" and it is the only session in the DB not on 2.0.21.
Steps to reproduce
- Use a local OpenAI-compatible provider with a small output cap and heavy reasoning, e.g.
limit.output: 19000,limit.context: 95000, variant withreasoning_effort: xhigh. - Send a turn in which the model spends most of the output budget on reasoning and then starts a large
writetool call (a long file). - The provider ends the stream with
finish_reason: lengthwhile the tool-call JSON is still incomplete.
Measured on the affected turn: reasoning = 57,807 chars,tokens.output = 19000(the cap),input = 48052. - Observe the session stop there: turn idle with
outcome: succeeded, the file was never written, and the
tool call never executed.
Expected behaviour
- A tool call that did not execute must not be persisted mid-flight: either discard it or persist it as an
error withtime.completed. - The turn must not be reported as
succeededwhen the model's final action never ran; the truncation should
surface as an error. - No persisted message may be undecodable — a partial tool input must never make the whole session unreadable.
- Clients should reconcile: when the server reports the session idle, a stale in-flight tool must not leave the
client stuck ("writing",ESCinert).
Actual behaviour
- Tool part:
state.status: "streaming",executed: false, notime.completed, and
state.input="{\"path\": \"/…/workspace/<project>/docs/temp/briefs-s5.md\""— a string, truncated. - Message:
finish: "length",tokens: {input: 48052, output: 19000}; session rowidle_outcome: "succeeded"
withtime_idleset. GET /api/session/<id>/message→ HTTP 500; server log:
level=ERROR message="failed to decode session message" ref=err_cfe2b809 sessionID=… messageID=…- Client: session stuck as "writing",
ESCno-op, cannot interact. Meanwhile
POST /api/session/<id>/interrupt→{"interrupted": false}(200) and the session is not listed by
GET /api/session/active— the server already considers the session finished.
Workaround used (and what it shows)
Rewriting the tool part in ~/.local/share/opencode/opencode.db (session_message.data, content[2]) to
{"status":"error","input":{…},"error":{…},"time":{…,"completed":…}} makes the server serve the session again
(200). But the client stays stuck on its in-memory pending state until it is restarted — so two independent
layers are involved: the persisted undecodable message, and the client never reconciling.
Screenshot and/or share link
Screenshots available (session rendering, tool shown as failed after the workaround). No share link.
Operating System
Ubuntu 24.04 — Linux 7.0.0-34-generic, x86_64
Terminal
TERM=xterm-256color, COLORTERM=truecolor, TERM_PROGRAM unset, shell /bin/bash
Activity
github-actions commentedon Oct 1, 2026
This issue might be a duplicate of existing issues. Please check:
finish: "length"incorrectly ending as a successful idle session.For keybind-related issues, please also check our pinned keybinds documentation: #4997
rekram1-node commentedon Oct 8, 2026
Automated update
Fixed by #53685, now merged into
v2.When a provider ends a response with unfinished local tool input (including
finish_reason: length), the runner now settles that call as an unexecuted tool error instead of leaving it permanently streaming. The existing failure projection records a completion timestamp and replaces the partial string input with an object, so the unfinished call no longer strands the session or leaves that terminal tool state undecodable. The error tool result also allows the existing model loop to request a corrected call.The merged change includes regression coverage for unfinished tool input after an output limit. Additional follow-up assertions were checked locally for message-list readability and durable-event replay; those extra assertions are not part of the merged PR.
This resolves the reported failure path for new occurrences in builds containing #53685. It does not retroactively repair already-corrupted sessions, add independent client-state reconciliation, or make every output-limit response a session-level failure. Those are separate concerns.
Closing as fixed.