Skip to content

feat(tui): surface worker RPC failures instead of hanging on a blank scr - #42885

Open
anotheroni wants to merge 2 commits into
anomalyco:devfrom
anotheroni:tui-rpc-error
Open

feat(tui): surface worker RPC failures instead of hanging on a blank scr#42885
anotheroni wants to merge 2 commits into
anomalyco:devfrom
anotheroni:tui-rpc-error

Conversation

@anotheroni

Copy link
Copy Markdown

Covered by new tests in test/util/rpc.test.ts.

Reviewed-by: GPT-5.6 Sol

Issue for this PR

Closes #34981
Related: #41284, #35494

Type of change

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

What does this PR do?

Previously a throwing RPC handler, a crashed/killed worker, or an
unhandled rejection after the TUI entered alt-screen/raw mode all left
the process hanging on a blank terminal with nothing printed and no
useful exit code.

  • rpc.ts: listen() replies with an error instead of dropping it;
    client() rejects all in-flight and future calls when the worker
    errors, exits unexpectedly, or sends an undeserializable message;
    a postMessage() throw only rejects that one call. Adds
    onDisconnect() for callers with nothing in flight, and
    expectDisconnect() to distinguish our own worker.terminate() from a
    real crash.
  • tui.ts: wires worker disconnects into the TUI's fatal-error path and
    preserves a non-zero exit code instead of always exiting 0.
  • app.tsx: catches unhandledRejection/uncaughtException once alt-screen
    mode is active, restores the terminal, and reports the error.

How did you verify your code works?

Ran opencode with a bad database to verify that I get an error message instead of a blank screen

18 tests in rpc.test.ts covering throwing handlers, worker error/close/
messageerror events, postMessage failures, and (not just a synthetic
EventTarget) a real Bun Worker that crashes on import. Plus 1 new test
in the tui package's app-lifecycle.test.tsx. See the Tests section below
for what each one covers.

Cross-checked against a previous attempt at this same bug (#34974, closed,
never merged) to see if it covered something that this patch didn't.

Tests

Process-level fatal handling (unhandledRejection/uncaughtException)
is only partially covered: the app-lifecycle.test.tsx test invokes
the triggerFatal() callback directly rather than emitting a real
unhandledRejection or uncaughtException event.

packages/opencode/test/util/rpc.test.ts:

  • Tests that a throwing RPC handler in listen() replies with an
    rpc.result error instead of leaving the request to hang.
  • Tests that a thrown value whose message can't be read (e.g. a hostile
    Proxy) still produces a safe fallback error string, rather than
    throwing again inside the error path itself.
  • Tests that a result envelope with error set causes client.call()
    to reject rather than resolve.
  • Tests that a successful result envelope still resolves client.call()
    normally, as a baseline against the error-path tests above.
  • Tests that a synchronous throw from target.postMessage() rejects
    the call immediately instead of leaving it pending forever.
  • Tests that a worker error event rejects any currently in-flight
    call.
  • Tests the same crash-rejects-the-in-flight-call behavior against a
    real Bun Worker (not a mocked EventTarget) running a fixture that
    throws on import, to confirm the mocked tests reflect real behavior.
  • Tests that a worker close event rejects in-flight calls with an
    "exited unexpectedly" error.
  • Tests that a worker messageerror event (an undeserializable message)
    rejects in-flight calls.
  • Tests that a message whose data isn't valid JSON rejects in-flight
    calls instead of throwing uncaught inside onmessage.
  • Tests that a parsed message missing a well-formed type field rejects
    in-flight calls.
  • Tests that a message with an unrecognized type rejects in-flight
    calls.
  • Tests that a call made after the worker has already died rejects
    immediately instead of queueing forever.
  • Tests that onDisconnect fires on a crash even when no call is
    currently pending to reject.
  • Tests that onDisconnect does not fire when worker.terminate()
    follows a call to expectDisconnect(), i.e. our own intentional
    shutdown isn't reported as a crash.
  • Tests that an exit with code 0 is still treated as fatal unless
    expectDisconnect() was called first, since a clean-looking exit
    code doesn't distinguish our own shutdown from an unrelated one.
  • Tests that a crash occurring while a call is in flight still rejects
    it, even if expectDisconnect() is called moments later — guards
    against expectDisconnect() being called too early and swallowing a
    real crash.
  • Tests that a handler registered on onDisconnect after the worker
    has already died is invoked immediately rather than missed.

packages/tui/test/app-lifecycle.test.tsx:

  • Tests that a fatal error raised from within a finalizer (a plugin's
    dispose() call during cleanup) is still printed to stderr and sets
    exit code 1, instead of being lost once teardown has already started.

Screenshots / recordings

Reproduced against a real corrupted opencode.db (a bad "dummy" value where
a session ID was expected). Before this fix, the terminal just sat blank forever
with nothing printed. After:

                  ┃                                                          ┃
                  ┃  Unexpected server error. Check server logs for          ┃
                  ┃  details.                                                ┃
                  ┃                                                          ┃
                     █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
                     █  █ █  █ █▀▀▀ █  █ █    █  █ █  █ █▀▀▀
                     ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀


   ┃
   ┃  Ask anything... "What is the tech stack of this project?"
   ┃
   ┃  Build · Big Pickle OpenCode Zen
   ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
   tab agents  ctrl+p commands

            ● Tip Run /connect to add an AI provider and start coding

  /tmp/repo-rw/packages/opencode                                         local

Matching server log line:

level=ERROR message=failed ref=err_1b5e30e4 error="Error: Expected a string starting with \"ses\", got \"dummy\""
cause="... at workspace-routing.ts:222:23 ..."

Checklist

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

Oskar Nilsson and others added 2 commits August 16, 2026 13:40
…screen

Covered by new tests in test/util/rpc.test.ts.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-by: GPT-5.6 Sol
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.

bug(rpc): pending RPC calls hang forever when target Worker disconnects

1 participant