Skip to content

fix(opencode): read the error message opencode actually writes - #225

Merged
drewstone merged 1 commit into
mainfrom
fix/opencode-error-data-message
Sep 11, 2026
Merged

fix(opencode): read the error message opencode actually writes#225
drewstone merged 1 commit into
mainfrom
fix/opencode-error-data-message

Conversation

@drewstone

Copy link
Copy Markdown
Owner

The bug

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, extracted 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" }

data.message first. We never looked there.

The fallback stringifies the event and truncates at 400 characters — which is exactly where data gets cut off. So the one field worth reading is the one the operator never sees:

opencode error event without a message: {"type":"error","timestamp":1789101762046,
  "sessionID":"ses_...","error":{"name":"APIError"

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.log holds 783 AI_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.message first, matching opencode's precedence exactly, with error.name ahead of the raw dump so even a data-less error names itself.

Verification

test result
reads the provider message out of error.data.message pass; fails on origin/main with the dump above
still reads a top-level error.message when present pass
falls back to error.name rather than dumping pass; fails on origin/main
keeps the raw-event fallback when nothing is readable pass

4/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 clean origin/main, so it is pre-existing and unrelated.

For whoever reconciles the branches

feat/opencode-serve-reliable already carries extractSessionError with this exact 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 actually run — never had it.

🤖 Generated with Claude Code

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>
@drewstone
drewstone merged commit 265e71b into main Sep 11, 2026
1 check passed
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.

1 participant