Repository navigation
Description
Summary
opencode run --format json can complete a run without emitting the final step_finish JSON event to stdout, even though the session data contains the corresponding step-finish part with tokens and cost.
This makes downstream tooling miss usage accounting when it relies on the documented raw JSON event stream.
Reproduction
Run opencode non-interactively with JSON output, for example:
opencode run --format json --thinking --dir /tmp/agentbench-item-6zkldejw --model gpt-5.4-mini --dangerously-skip-permissionswith stdin:
Calculate `46457 * 352456`.
Observed stdout JSONL contained only:
{"type":"step_start","timestamp":1778479990756,"sessionID":"ses_1ea551140ffedVQFo35xGxPtJ8","part":{"id":"prt_e15ab07df001XgfGRrfSczzEEt","messageID":"msg_e15aaef93001ze1a2JzDi9w19F","sessionID":"ses_1ea551140ffedVQFo35xGxPtJ8","type":"step-start"}}
{"type":"text","timestamp":1778479990912,"sessionID":"ses_1ea551140ffedVQFo35xGxPtJ8","part":{"id":"prt_e15ab07e5001kU51w946TPSWit","messageID":"msg_e15aaef93001ze1a2JzDi9w19F","sessionID":"ses_1ea551140ffedVQFo35xGxPtJ8","type":"text","text":"`46457 * 352456 = 16,374,048,392`","time":{"start":1778479990757,"end":1778479990907}}}No step_finish event was emitted to stdout.
However, the SQLite session DB contained both the assistant message usage and the final step-finish part:
{
"reason": "stop",
"type": "step-finish",
"tokens": {
"total": 19362,
"input": 209,
"output": 209,
"reasoning": 0,
"cache": { "write": 0, "read": 18944 }
},
"cost": 0
}Source-level analysis
In packages/opencode/src/cli/cmd/run.ts, the JSON stdout loop emits step_finish only when it observes a message.part.updated event whose part has type === "step-finish":
if (part.type === "step-finish") {
if (emit("step_finish", { part })) continue
}The same loop terminates as soon as it observes session.status=idle:
if (
event.type === "session.status" &&
event.properties.sessionID === sessionID &&
event.properties.status.type === "idle"
) {
break
}The step-finish part itself is definitely produced in packages/opencode/src/session/processor.ts during finish-step and includes tokens/cost before the assistant message is updated.
So the likely failure mode is that the run loop observes session.status=idle and exits before it has emitted/drained the final message.part.updated(step-finish) event to stdout.
Expected behavior
For opencode run --format json, a completed successful turn should either:
- emit the final
step_finishevent before exiting, or - emit an authoritative final summary event containing final message usage (
tokens,cost, finish reason), or - document that
--format jsonis a best-effort live stream and may omit final accounting events even though the session DB has them.
For downstream benchmark/tooling use cases, option 1 or 2 would be much easier to consume correctly.
Environment
- opencode version:
1.14.48 - command mode: non-interactive
opencode run --format json
Activity
github-actions commentedon May 11, 2026
This issue might be a duplicate of existing issues. Please check:
session.idleteardown race inopencode run— that issue also describes the run loop disposing before async events (continuation/parts) are fully flushed. The root cause described in run --format json can exit before emitting final step_finish event #26855 (loop breaks onsession.status=idlebefore draining the finalmessage.part.updated(step-finish)event) appears to be the same underlying sequencing issue.fix: treat non-empty finalText as success regardless of finish reason
hardes11 commentedon May 24, 2026
I hit this same bug and tracked down the root cause. Submitted a fix in #29132 — the loop was called fire-and-forget (not awaited), so the process exited before the model finished generating. The fix stores the loop promise and awaits it after the prompt/command call completes.
opencode runoutput truncated in non-interactive mode - missing assistant response and step_finish #31365 as a duplicate of this issuefix(#1990): gate gemini + qwen exit-0 runs on terminal result event
fix(adapters): OpenCode production parity — usage keys, model surfaci…
fix(orchestrator): scope opencode parser dedup state to one exec stre…