fix(opencode): read the error message opencode actually writes - #225
Merged
Conversation
Every opencode error is a NamedError, `{ name, data: { message, ... } }`. No error in
its SDK carries a top-level `error.message`. This backend read `ev.error.message` and
nothing else, so it missed EVERY error and fell through to the raw-event fallback.
opencode's own reader, from the shipped 1.18.30 binary, states the contract:
function b(j){ if(j.data?.message) return j.data.message
if(j.message) return j.message
if(j.name) return j.name
return "unknown error" }
The fallback stringifies the event and truncates at 400 characters, which is where
`data` was being cut off, so the one field worth reading was the one the operator
never saw. A run whose provider said "Rate limit reached for requests" recorded
`opencode error event without a message: {"type":"error",...,"error":{"name":"APIError"`
instead. This host's bridge logs hold 16 of those.
The comment above this code was written after four director rounds died with an
unreadable cause and ~48 minutes of retry burned per round. The lesson was right and
the fix was incomplete: it stopped printing a bare constant and started printing an
unreadable dump.
`data.message` now comes first, matching opencode's precedence exactly, with
`error.name` ahead of the raw dump so even a data-less error names itself.
Note for whoever reconciles the branches: `feat/opencode-serve-reliable` already
carries `extractSessionError` with this precedence, written 2026-07-17 against the
persistent `opencode serve` transport. That branch is unmerged and a different
architecture; this line, the one the live bridges run, never had it.
Verified: 4 new tests pass here and 2 of them fail on origin/main, reproducing the
exact dump above. Typecheck clean. `tests/failure-attribution.test.ts` has one
container failure that reproduces identically on clean origin/main and is unrelated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Every opencode error is a NamedError —
{ name, data: { message, ... } }. No error in its SDK carries a top-levelerror.message. This backend readev.error.messageand nothing else, so it missed every error and fell through to the raw-event fallback.opencode's own reader, extracted from the shipped 1.18.30 binary, states the contract:
data.messagefirst. We never looked there.The fallback stringifies the event and truncates at 400 characters — which is exactly where
datagets cut off. So the one field worth reading is the one the operator never sees:The provider had said "Rate limit reached for requests". This host's bridge logs hold 16 of these.
Why this matters more than it looks
The comment above this code was written after four director rounds died with an unreadable cause and ~48 minutes of retry burned per round. That lesson was right; the fix was incomplete. It stopped printing a bare constant and started printing an unreadable dump.
Separately, this host's
opencode.logholds 783AI_APICallError, 413 "overloaded", 278 "rate limit" — all from our own concurrency against one model. Every one of those explanations was being discarded at this line.The change
data.messagefirst, matching opencode's precedence exactly, witherror.nameahead of the raw dump so even a data-less error names itself.Verification
error.data.messageorigin/mainwith the dump aboveerror.messagewhen presenterror.namerather than dumpingorigin/main4/4 here, 2/4 on
origin/main. Typecheck clean.Honest caveat on the full suite: this host is at load 461 and the full run was killed twice for memory, so I did not baseline all 1,134 tests. I ran the four opencode-adjacent files instead: 42/43 pass, and the one failure (
failure-attribution.test.ts > serves a chat request that carries no cwd at all, a container test) reproduces identically on cleanorigin/main, so it is pre-existing and unrelated.For whoever reconciles the branches
feat/opencode-serve-reliablealready carriesextractSessionErrorwith this exact precedence, written 2026-07-17 against the persistentopencode servetransport. That branch is unmerged and a different architecture. This line — the one the live bridges actually run — never had it.🤖 Generated with Claude Code