Skip to content

[BUG] opencode run --format json stdout only outputs step_start, missing text/reasoning/step_finish events #3035

Description

@xulongzhe

Description

When running opencode run --format json (or crush run --format json), the stdout stream only outputs the step_start event and silently drops all subsequent events (text, reasoning, step_finish). The AI model actually generates a complete response (confirmed in the SQLite database), but the JSON-formatted stdout stream is truncated after the first event.

This makes --format json mode completely unusable for programmatic consumers that rely on parsing stdout to capture AI responses.

Steps to Reproduce

  1. Run any simple prompt with --format json:

    opencode run "say hello" --format json --dangerously-skip-permissions --dir /tmp --model deepseek/deepseek-v4-pro
  2. Observe the stdout output — it only contains:

    {"type":"step_start","timestamp":1780013193618,"sessionID":"ses_18ef22a63ffeYvPBilm6eD8TSy","part":{"id":"prt_e710dd98d001MXtxfuunJqXc8F","messageID":"msg_e710dd6e60012YH30gCmnLQtVv","sessionID":"ses_18ef22a63ffeYvPBilm6eD8TSy","type":"step-start"}}

    No text, reasoning, or step_finish events follow. The process exits with code 0 and empty stderr.

  3. Verify the AI response is actually complete in the SQLite database:

    sqlite3 ~/.local/share/opencode/opencode.db \
      "SELECT json_extract(p.data, $.type), substr(json_extract(p.data, $.text), 1, 80)
       FROM part p WHERE p.message_id IN (
         SELECT id FROM message WHERE session_id = ses_18ef22a63ffeYvPBilm6eD8TSy
         AND json_extract(data, $.role) = assistant
       ) ORDER BY p.time_created"

    Output:

    step-start|
    reasoning|The user is asking me to say hello. This is a very simple conversational request
    text|Hello
    step-finish|
    
  4. The AI did generate "Hello" and it was saved to the database, but it was never written to stdout.

Reproducibility

This bug is highly reproducible (~80-100% failure rate). I ran 5 consecutive tests:

for i in 1 2 3 4 5; do
  opencode run "say number $i" --format json --dangerously-skip-permissions --dir /tmp --model deepseek/deepseek-v4-pro
  echo "---"
done

Result: All 5 runs only output step_start. In a separate batch of 5 runs, 1 run succeeded (output both step_start + text), suggesting this is a race condition in the stdout writer — the process exits before the text/reasoning/step_finish events are flushed to stdout.

Expected Behavior

--format json should output all events to stdout in order:

{"type":"step_start",...}
{"type":"reasoning",...}
{"type":"text","part":{"type":"text","text":"Hello",...}}
{"type":"step_finish",...}

Actual Behavior

Only step_start is written to stdout. The remaining events are lost, even though they are persisted in the internal SQLite database.

Root Cause Analysis

Based on the internal debug logs (via --print-logs --log-level DEBUG), the event bus correctly processes all events:

INFO  service=bus type=message.part.delta publishing
INFO  service=bus type=message.part.updated publishing
INFO  service=bus type=message.updated publishing

The session.prompt loop exits immediately after step 1:

INFO  service=session.prompt session.id=ses_xxx step=1 loop
INFO  service=session.prompt session.id=ses_xxx exiting loop

This strongly suggests the --format json stdout writer subscribes to the event bus for step_start but the process exits (or the writer unsubscribes) before text/reasoning/step_finish events are emitted to stdout. The database writer persists events asynchronously and completes successfully, but the stdout stream is closed too early.

Workaround

Consumers can read the response from the OpenCode/Crush SQLite database at ~/.local/share/opencode/opencode.db (or equivalent) by querying the part table with the session ID. However, this is fragile and not documented.

Version

  • OpenCode CLI: 1.15.12 (also reproduced on 1.15.10)
  • Also applies to crush run --format json based on shared codebase

Environment

  • OS: Linux 6.17.0-23-generic x86_64 (Ubuntu)
  • Shell: bash
  • Model provider: DeepSeek (deepseek-v4-pro)
  • Install method: npm (opencode-ai package)

Additional Context

This bug was discovered while integrating OpenCode as a backend in ClawBench, a multi-AI-backend chat platform. We parse opencode run --format json stdout to stream AI responses to users, and this bug causes all responses after the first message to appear as "AI returned no content".

Related issue: #2412 (mentions --format json headless mode event structure, confirming programmatic consumers rely on this feature).

Metadata

Metadata

Assignees

No one assigned

    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