Repository navigation
feat(tui): surface worker RPC failures instead of hanging on a blank scr - #42885
anotheroni wants to merge 3 commits into
Conversation
…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
Overall: excellent fix for a miserable failure mode — the listen-side error reply alone eliminates the classic "blank screen forever", the client-side lifecycle (dead latch, expectDisconnect, onDisconnect) is thoughtfully designed, and the 247-line test suite plus crashing-worker fixture is exactly the coverage this deserves. Items 1–2 are behavior decisions to confirm; nothing blocking. Thanks! |
- rpc.ts: unrecognized-but-well-formed message types now fail only the matching pending call (or log-and-skip if unaddressed) instead of bricking the whole client, so a newer worker's extra event type can't take down an older CLI host mid-session. - tui.ts: interrupt (not abandon) the run() fiber when the worker disconnects before onReady, via Effect.runFork + Fiber.interrupt, so finalizers restore the terminal before process.exit(). - tui.ts: register the disconnect handler where the early/done race is built so a worker that dies during the dynamic imports still replays through onDisconnect's dead-state check instead of being dropped. - tui.ts: keep expectDisconnect() after the shutdown call (not before), so a crash mid-shutdown still fast-rejects instead of idling out at the 5s timeout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7d41412 to
6ff80c4
Compare
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
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
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.
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.
preserves a non-zero exit code instead of always exiting 0.
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:listen()replies with anrpc.resulterror instead of leaving the request to hang.Proxy) still produces a safe fallback error string, rather than
throwing again inside the error path itself.
errorset causesclient.call()to reject rather than resolve.
client.call()normally, as a baseline against the error-path tests above.
target.postMessage()rejectsthe call immediately instead of leaving it pending forever.
errorevent rejects any currently in-flightcall.
real Bun
Worker(not a mocked EventTarget) running a fixture thatthrows on import, to confirm the mocked tests reflect real behavior.
closeevent rejects in-flight calls with an"exited unexpectedly" error.
messageerrorevent (an undeserializable message)rejects in-flight calls.
dataisn't valid JSON rejects in-flightcalls instead of throwing uncaught inside
onmessage.typefield rejectsin-flight calls.
typerejects in-flightcalls.
immediately instead of queueing forever.
onDisconnectfires on a crash even when no call iscurrently pending to reject.
onDisconnectdoes not fire whenworker.terminate()follows a call to
expectDisconnect(), i.e. our own intentionalshutdown isn't reported as a crash.
expectDisconnect()was called first, since a clean-looking exitcode doesn't distinguish our own shutdown from an unrelated one.
it, even if
expectDisconnect()is called moments later — guardsagainst
expectDisconnect()being called too early and swallowing areal crash.
onDisconnectafter the workerhas already died is invoked immediately rather than missed.
packages/tui/test/app-lifecycle.test.tsx:dispose()call during cleanup) is still printed to stderr and setsexit code 1, instead of being lost once teardown has already started.
Screenshots / recordings
Reproduced against a real corrupted
opencode.db(a bad"dummy"value wherea session ID was expected). Before this fix, the terminal just sat blank forever
with nothing printed. After:
Matching server log line:
Checklist