Skip to content

run --format json can exit before emitting final step_finish event #26855

@g199209

Description

@g199209

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:

  1. emit the final step_finish event before exiting, or
  2. emit an authoritative final summary event containing final message usage (tokens, cost, finish reason), or
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions