fix: stop malformed input and lost sessions from crashing the server - #509
Open
nichochar wants to merge 1 commit into
Open
fix: stop malformed input and lost sessions from crashing the server#509nichochar wants to merge 1 commit into
nichochar wants to merge 1 commit into
Conversation
Three of these share a shape worth naming: a throw inside an event-emitter callback is an uncaught exception, not a failed operation. The surrounding try/catch or promise looks like it covers the code, and doesn't. - The websocket envelope was JSON.parsed and zod-parsed inline in a 'message' listener, so one malformed frame ended the process. Per-event payloads were already parsed safely; only the envelope wasn't. - Websocket handlers are async and throw freely — findSession throws for an unknown id, which a reconnecting client with a stale session triggers on its own. They were called unawaited, so that was an unhandled rejection, and Node exits on those by default. - tsserver's stdout parser throws on framing it doesn't recognise, inside a 'data' listener. Now logged, with the buffer dropped; tsserver output is a best-effort feature, not worth ending the process over. Also: - Running a cell twice before the first exits used to corrupt the process registry, because the first process's exit handler deleted the key now owned by the second. The handler now only clears the key if it still holds the same process. What double-exec *should* do is left open in task 0005 — the registry no longer corrupts itself, but the first process is orphaned rather than killed, and that's a product question. - tsserver request promises never settled if the server died or never replied, hanging the caller and leaking the resolver. They now time out at 10s and reject on process exit. 10s because these are interactive requests — an answer that late is no more useful than none; what matters is that they settle. 24 new tests. ws-client.mts had no coverage at all despite being the dispatch path for every notebook operation, so this covers topic matching and wildcard extraction, subscribe/broadcast/unsubscribe, and each crash case. Suite is 62 tests, up from 11 at the start of the stack.
This was referenced Jul 29, 2026
Contributor
Author
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
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.

Stacked on #508. Closes task 0003, partially closes task 0005.
Last PR in the stack. Several inputs could take the whole server down, which matters more than each one's individual severity — the server holds notebook state that hasn't been flushed.
The shape worth naming
Three of these are the same mistake: a throw inside an event-emitter callback is an uncaught exception, not a failed operation. The surrounding
try/catchor promise looks like it covers the code, and doesn't.handleIncomingMessage, in a socket'message'listenerparse(), in a'data'listenerexeccallbackThe websocket case is the sharpest: per-event payloads were already
safeParsed, but the envelope wasJSON.parsed andzod.parsed inline. So the careful validation was one line below the unguarded one.Also fixed
Unhandled handler rejections. Websocket handlers are async and throw freely —
findSessionthrows for an unknown id, which a reconnecting client with a stale session triggers by itself. They were invoked unawaited, so that was an unhandled rejection, and Node exits on those by default.Process registry clobbering. Running a cell twice before the first exits replaced the map entry, and then the first process's exit handler deleted the key now owned by the second — leaving a process running that
cell:stopinsisted didn't exist. The handler now only clears the key if it still holds the same process reference.tsserver promises that never settled. A resolver was stored by
seqwith no timeout and no reject path, so if tsserver died or never answered, hover/completions/go-to-definition hung forever and the resolver map leaked. Now a 10s timeout plus rejection on process exit. 10s because these are interactive — an answer that late is no more useful than none; what matters is that they settle at all.Tests
24 new.
ws-client.mtshad no coverage at all despite being the dispatch path for every notebook operation, so this adds topic matching and wildcard extraction, subscribe/broadcast/unsubscribe, and each crash case. Suite is 62, up from 11 at the start of the stack.The handler-error tests silence
console.error— they deliberately exercise the logging path, and a passing run shouldn't print stack traces that look like failures.Left open on purpose
What double-exec should do. The registry no longer corrupts itself, but the first process is orphaned rather than killed or refused. That's a product question — replace, refuse, queue? — and it's coupled to the execution-model decision in task 0006, since a stateful kernel would change the answer entirely.
Whether stop actually kills TypeScript cells.
SIGTERMgoes to thetsxwrapper, not the process group. Iftsxforks a child node process, stop is only cosmetic for TypeScript cells. Needs a running notebook and a long-running cell to check; noted in task 0005 rather than quietly closed.A rejected tsserver request still leaves the client hanging, because responses are broadcasts with no request correlation. Task 0009.
No top-level
unhandledRejectionhandler: with the handler-level catch there's no known path to one, and a blanket handler would hide the next one rather than surface it.🤖 Generated with Claude Code