Skip to content

fix(core): render a tag when a task error has no message - #41376

Open
iceteaSA wants to merge 4 commits into
anomalyco:devfrom
iceteaSA:error-text-fallback
Open

fix(core): render a tag when a task error has no message#41376
iceteaSA wants to merge 4 commits into
anomalyco:devfrom
iceteaSA:error-text-fallback

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 9, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #41375

Type of change

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

What does this PR do?

A subagent task that fails with a tagged error reaches the parent as a frame with an empty error string. Two defects combine, one per commit.

errorText returns "" for most tagged errors. Schema.TaggedErrorClass populates .message only from a field literally named message. Of the 114 TaggedErrorClass definitions in packages/opencode/src and packages/core/src, 37 have one — the other 77 have a blank .message. errorText in packages/core/src/background-job.ts returned error.message verbatim, so settle stored an empty string as the job's error.

It now prefers a non-blank message, then _tag, then name, then a non-blank String(error), and never returns an empty string. Tagged errors carry their tag on both _tag and name; _tag is preferred because a plain new Error("") has name === "Error", which is less useful but still better than nothing. Errors that already carry a real message take the first branch and are unaffected.

?? does not fire on an empty string. Both consumers in packages/opencode/src/tool/task.ts used result.error ?? "Task failed". ?? substitutes only for null/undefined, so a blank error passed straight through and the fallback was unreachable in exactly the case it was written for. Both now use ||.

Either fix alone is incomplete: fixing only errorText leaves ?? dead code for any other blank-string source, and fixing only ?? throws away the tag that would have named the failure.

This is diagnosability, not behaviour. It does not change which errors occur or when a task fails — it makes the resulting frame say what went wrong.

How did you verify your code works?

Red-first, with a mutation check on each fix: revert the source change, keep the test, confirm it goes red again.

core     bun typecheck && bun test test/background-job.test.ts     5 pass, 0 fail
opencode bun typecheck && bun test test/tool/task.test.ts          21 pass, 0 fail
opencode bun test                                                  3259 pass, 0 fail

Mutation, both source files reverted with tests kept:

core     4 pass, 1 fail   Expected "MessageLessError", Received ""
opencode 20 pass, 1 fail  Expected "Task failed", Received ""

Each test has a regression arm asserting an error with a real message still renders that message unchanged.

errorText is module-private, so it is tested through its observable effect — settle a job with a message-less tagged error, then read info.error — rather than exporting it for the test.

Screenshots / recordings

Not a UI change.

Checklist

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

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@iceteaSA

iceteaSA commented Aug 9, 2026

Copy link
Copy Markdown
Author

Two commits added since the original description, both worth calling out because the second one is a defect this PR would otherwise have shipped.

45dc93b083 — regression test for the notify/background consumer. The original tests only covered the foreground Effect.fail exit; the inject("error", …) exit got the same operator change untested. That is the path the reported incident actually came through, so it should not have been the untested one.

60b30f1353 — a real bug in the first commit. Effect's TimeoutError is instanceof Error but its message is undefined, not a string, so error.message.trim() threw a TypeError inside BackgroundJob.settle. The job then never settled and background.wait() hung indefinitely. Fixed with typeof guards on .message and .name, matching the guard already on _tag.

Worth being explicit about why CI did not catch that: it could not. There is no test on dev that routes a TimeoutError through settle, so the suite is green and correct. It surfaced on a downstream branch that has a task-timeout feature and therefore exercises that path. The regression test added here builds the error from the real producer rather than a hand-made { message: undefined } object — I checked that new Cause.TimeoutError() matches what an actual Effect.timeout emits (same constructor, _tag, and typeof message), so the fixture pins the production shape.

On completeness of the guard, since "add a typeof check" invites "what else is unguarded": the two mechanisms that produce a blank or non-string message here are disjoint and both covered. Effect built-ins with message === undefined (TimeoutError, NoSuchElementError, ExceededCapacityError, IllegalArgumentError, UnknownError) all carry a string _tag, so the second rung catches them. Tagged errors defined in this repo inherit message: "" from Error.prototype, so the string check passes, trim() is falsy, and they also fall through to _tag. Verified against effect@4.0.0-beta.83.

There is a residual: a non-Error value with a throwing toString or Symbol.toPrimitive can still make the final String(error) throw. I left it alone — it is not producible by Effect or by anything in this repo, and wrapping the function in try/catch to cover it would hide the next real shape the way the missing guard hid this one.

Updated totals: 4 commits, +186/-6. core 7/0, opencode focused 22/0, opencode full 3260 pass / 0 fail, typecheck clean. Each fix carries its own mutation check.

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.

Task error frames render empty: 77 of 114 tagged errors have a blank .message, and ?? "Task failed" never fires on ""

1 participant