Bug Description
In non-interactive mode (opencode run "..." --format json), the event processing loop is started with fire-and-forget, causing the process to exit before the model finishes generating — only the step_start event is emitted, all text, tool_use, and step_finish events are lost.
Steps to Reproduce
# 1. Run a simple query with --format json
$ opencode run "what is 2+2? reply with just the number" \
--format json \
--dangerously-skip-permissions
# 2. Observe: only `step_start` is emitted, then the process exits
{"type":"step_start",...}
# Expected: `step_start`, `text` (with answer), `step_finish`
{"type":"step_start",...}
{"type":"text","part":{"type":"text","text":"4",...}}
{"type":"step_finish","part":{"reason":"stop","tokens":{...},...}}
Root Cause
In packages/opencode/src/cli/cmd/run.ts, the non-interactive path in execute():
const events = await client.event.subscribe()
loop(client, events).catch((e) => { console.error(e); process.exit(1) }) // fire-and-forget
const result = await client.session.prompt({...}) // returns HTTP ack
// execute() returns → finally block disposes in-process server → SSE killed
When prompt() returns the HTTP acknowledgment, execute() returns. The Effect framework's cleanup (effect-cmd.ts) then disposes the in-process server, killing the SSE stream before the model finishes generating.
Only step_start is emitted (it arrives before prompt() returns). All other events are lost.
Fix
Store the loop promise and await it after the prompt/command call completes:
const loopDone = loop(client, events)
// ... prompt or command call ...
try {
const loopError = await loopDone
if (loopError) process.exitCode = 1
} catch (e) {
console.error(e)
process.exitCode = 1
}
This keeps the process alive until the session reaches idle and all events are processed.
Testing
- Before fix: only
step_start (1 line of JSON)
- After fix:
step_start, text, step_finish (3 lines)
- Confirmed with DeepSeek API and Tavily web search tool outputs
Impact
This breaks any automated tool or CI pipeline relying on opencode run --format json for structured output capture (web search results, reasoning content, tool use events).
Bug Description
In non-interactive mode (
opencode run "..." --format json), the event processing loop is started with fire-and-forget, causing the process to exit before the model finishes generating — only thestep_startevent is emitted, alltext,tool_use, andstep_finishevents are lost.Steps to Reproduce
Root Cause
In
packages/opencode/src/cli/cmd/run.ts, the non-interactive path inexecute():When
prompt()returns the HTTP acknowledgment,execute()returns. The Effect framework's cleanup (effect-cmd.ts) then disposes the in-process server, killing the SSE stream before the model finishes generating.Only
step_startis emitted (it arrives beforeprompt()returns). All other events are lost.Fix
Store the loop promise and await it after the prompt/command call completes:
This keeps the process alive until the session reaches
idleand all events are processed.Testing
step_start(1 line of JSON)step_start,text,step_finish(3 lines)Impact
This breaks any automated tool or CI pipeline relying on
opencode run --format jsonfor structured output capture (web search results, reasoning content, tool use events).