Repository navigation
Description
Description
We run opencode run non-interactively in automation with --format json. When the upstream model provider is flaky and a step fails mid-stream, opencode reports a provider.transport error (for example ECONNRESET: The socket connection was closed unexpectedly, with HTTP status 200 because the response headers had already arrived), the runner retries the step, and the session completes normally with finish reason stop. The process still exits with code 1. stderr is empty; the only trace is the earlier error event in the JSON stream.
Cause in the v2 runner, packages/cli/src/run/noninteractive.ts:
- The
session.step.failedhandler setsemittedError = trueandprocess.exitCode = 1, thencontinues:
https://github.com/anomalyco/opencode/blob/v2.0.10/packages/cli/src/run/noninteractive.ts#L470-L481
(permalink:)opencode/packages/cli/src/run/noninteractive.ts
Lines 470 to 481 in b8cedc1
if (event.type === "session.step.failed") { if (input.compatibility === "v1" && event.data.error.message === "The provider response ended unexpectedly.") { pendingStep = undefined v1InvalidOutput = true continue } if (interrupted || permissionRejected || formCancelled) continue flushStep() emittedError = true process.exitCode = 1 if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message) continue session.execution.succeededjust returns, so a later success never clears the code:
https://github.com/anomalyco/opencode/blob/v2.0.10/packages/cli/src/run/noninteractive.ts#L504
The file is identical from v2.0.0 through v2.0.10. In v1.18.31 the exit code is set only on startup errors, and a retried step does not affect it.
Observed: any non-interactive run in which one step failed transiently and was retried exits 1, even when the run finishes with reason stop and a complete answer.
Expected: exit 0 when the session completes successfully. The exit code should report the outcome of the run, not whether a recoverable fault happened along the way. Comparable tools (curl --retry, wget, pip, rsync) exit 0 when a retry succeeds and keep the fault in their logs.
Why it matters: anything scripting opencode run trusts the exit code. With this behavior a caller must either treat 1 as "maybe fine, parse the event stream" or discard good output.
Suggested fix: do not set process.exitCode = 1 on session.step.failed when the step is going to be retried, or clear it on session.execution.succeeded. If "completed, but errors were emitted" is worth surfacing, use a distinct documented exit code or an opt-in strict flag rather than 1.
Plugins
None.
OpenCode version
v2.0.10 (git tag; the v2 line is on the @opencode-ai/cli beta channel, 0.0.0-beta-*). Also v2.0.8. Not reproducible on v1.18.31 (opencode-ai@latest).
Steps to reproduce
- Run
opencode run --format json "<any multi-step prompt>"against a provider that drops one streaming response mid-body (a proxy that closes the socket after the headers and a few SSE events reproduces it; opencode run exits 1 after auto-compaction recovers from a provider 413 #39573 mentions a test-LLM harness that can inject a failure followed by successful replies). - Observe in the JSON stream: an
errorevent witherror.type == "provider.transport", followed by further steps and a finalstep_finishwith reasonstop. echo $?prints1.
Screenshot and/or share link
Not applicable (non-interactive; JSON event stream shows the error event followed by a normal finish).
Operating System
Linux (x86_64).
Terminal
Not applicable (headless).
Related: #39573 reports the same symptom class on the v1 runner (exit 1 after a recovered provider 413 during auto-compaction) through a different code path.
Activity
rajmehta53 commentedon Oct 16, 2026
Still reproduces on v2.0.30, macOS arm64 (latest channel). Our variant is
provider.invalid-output(OpenAI Chat stream ended without finish_reason, HTTP 200)rather than a transport error, but the outcome is identical.
One automation run of:
Timeline (local time):
02:54:17provider stream dies mid-run02:54:56runner auto-retries, agent resumes and completes all work02:57:00commit lands, tests pass, session finalizesoutcome: "succeeded"02:57:02CLI exits 1, stderr empty after the retryChecked the
v2.0.30tag ofpackages/cli/src/run/noninteractive.ts:session.step.failedstill setsemittedError = true; process.exitCode = 1,session.execution.succeededis still justreturn, and nothing in the file resetsthe exit code after a success.
Downstream: our runner gates on exit code, so the successful attempt was recorded as a
failure, and the next attempt found the work already committed, so retries could never
produce a new commit. DB rows and server log available if useful.
Watching #50068 and #52885.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I was able to reproduce this on v2.0.10, and it still happens on the latest
dev(62ae125).Setup: a small OpenAI-compatible mock provider. On the agent's first step request it sends
200, the SSE headers and a couple of text deltas, then drops the socket. Every later request gets a normal answer ending withfinish_reason: "stop". Runningopencode run --format json -m mock/mock "Say hello"against it:errorevent withprovider.transport(ECONNRESET: The socket connection was closed unexpectedly, status 200);stopand outcomesucceeded;1.The cause matches what you describe. Core publishes
session.step.failedfor a step it then retries, and the run command's handler for that event setsprocess.exitCode = 1without checking for a retry. A later success never resets it. The script below reproduces it, either against the installedopencodeor against a source checkout withOC_DEV=<checkout>.Related: #39573 (the same symptom on the v1 runner, through a different code path).
Reproduction script (repro.sh)
Run it:
bash repro.sh