Skip to content

fix: non-interactive exits before stream completes — only emitted #29131

@hardes11

Description

@hardes11

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).

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