Skip to content

fix: await event loop in non-interactive opencode run#29132

Open
hardes11 wants to merge 1 commit into
anomalyco:devfrom
hardes11:fix/run-format-json-missing-events
Open

fix: await event loop in non-interactive opencode run#29132
hardes11 wants to merge 1 commit into
anomalyco:devfrom
hardes11:fix/run-format-json-missing-events

Conversation

@hardes11

@hardes11 hardes11 commented May 24, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #26855 (originally reported as #29131, which was a duplicate)

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Fixes opencode run --format json exiting before the event stream completes. In non-interactive mode, loop() was called fire-and-forget (.catch(), not awaited). When prompt() returns the HTTP ack, execute() returns, the Effect framework disposes the in-process server, and the SSE stream dies before the model finishes generating.

Only step_start was emitted. text and step_finish were lost.

The change stores the loop promise and awaits it after the prompt/command call, keeping the process alive until the session reaches idle.

How did you verify your code works?

  • Build: bun run build --single --skip-embed-web-ui → smoke test passed
  • Before fix: opencode run "what is 2+2?" --format json emits only step_start
  • After fix: emits step_start, text (with "4"), step_finish
  • Also verified with opencode run "search arxiv for..." --format json — tool_use events now appear
  • Full typecheck passed (bun turbo typecheck, 15/15 packages)

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

In non-interactive mode, the event processing loop was started with
fire-and-forget (Promise.catch but not awaited). When the prompt()
call returned its HTTP acknowledgment, execute() returned immediately,
and the Effect framework's cleanup disposed the in-process server,
killing the SSE stream before the model finished generating.

Only the step_start event was emitted (it arrives before prompt()
returns). All text, tool_use, and step_finish events were lost because
the stream was torn down mid-generation.

Fix: store the loop promise and await it after the prompt/command call
completes. This keeps the process alive until the session reaches idle
and all events have been processed.

Tested: `opencode run 'what is 2+2?' --format json` now correctly
emits step_start + text + step_finish events (previously only step_start).
@lmeyerov

lmeyerov commented Jun 8, 2026

Copy link
Copy Markdown

This matches a bug we keep hitting. We run opencode headless (opencode run --format json) to benchmark Bedrock models, and the assistant's final text gets dropped: execute() returns as soon as prompt() resolves, before the loop drains the final text/step-finish/idle events, so the in-process server tears down mid-flush. This PR's await fixes it. Minimal repro: ask for 58 + 71 and you get empty stdout on dev, 129 with this PR. Confirmed across several Bedrock models.

It's actually a re-regression: #26955 added this await, then #27371 dropped it while reworking exit handling (moved the exit code onto result.error and detached the loop again).

On testing: the run-process.test.ts happy-path assertion passes even on the un-awaited code, because the in-process TestLLMServer delivers events fast enough that the detached loop wins the race. So a non-empty-stdout assertion isn't a sufficient guard; catching this needs latency injected before the final events so a detached loop deterministically loses.

One small suggestion: early-returning on result.error before the await avoids a hang if a request never starts a run (e.g. an unknown command, which never emits idle).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants