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-permissions
with 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_finish event before exiting, or
- emit an authoritative final summary event containing final message usage (
tokens, cost, finish reason), or
- document that
--format json is 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
Summary
opencode run --format jsoncan complete a run without emitting the finalstep_finishJSON event to stdout, even though the session data contains the correspondingstep-finishpart withtokensandcost.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:
with stdin:
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_finishevent was emitted to stdout.However, the SQLite session DB contained both the assistant message usage and the final
step-finishpart:{ "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 emitsstep_finishonly when it observes amessage.part.updatedevent whose part hastype === "step-finish":The same loop terminates as soon as it observes
session.status=idle:The
step-finishpart itself is definitely produced inpackages/opencode/src/session/processor.tsduringfinish-stepand includestokens/costbefore the assistant message is updated.So the likely failure mode is that the run loop observes
session.status=idleand exits before it has emitted/drained the finalmessage.part.updated(step-finish)event to stdout.Expected behavior
For
opencode run --format json, a completed successful turn should either:step_finishevent before exiting, ortokens,cost, finish reason), or--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
1.14.48opencode run --format json