Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions