fix(chat): catch ProcessTransport errors on interrupt#416
Conversation
Centaur ReviewFound 3 issue(s) (1 warning).
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 2 issue(s) (1 warning).
server/chat.ts
Correct defensive fix — catches a real crash vector. Missing a test for the new try/catch path; the two-layer error handling (warn inside, error outside) could confuse log triage.
- 🟡 missing_tests (L1271): No test covers the case where
queryInstance.interrupt()rejects (e.g. dead ProcessTransport). The existing tests insend-to-chat.test.tsall mockinterruptto resolve. A test that mocksinterruptto reject and asserts thatinterruptChatstill returnstrue(and logs a warning) would lock in this fix.[fixable]
server/ws-handler-v2.ts
Correct defensive fix — catches a real crash vector. Missing a test for the new try/catch path; the two-layer error handling (warn inside, error outside) could confuse log triage.
- 🔵 style (L734): The
.catch()was added to handle the fire-and-forgetinterruptChat()call, butinterruptChatnow catches its owninterrupt()error internally and doesn't rethrow. The only remaining throw paths are earlier guards (registry.getreturning null — returnsfalsesilently, not a throw) or thestoreAndEchoIfNew/assemblePromptpath. The outer.catch()is still reasonable as defense-in-depth, but the log level (log.error) could be misleading since the most likely failure (dead transport) is already handled aslog.warninsideinterruptChat. Consider aligning the severity or noting that this is a fallback for unexpected errors.[fixable]
| await Promise.allSettled(stops); | ||
| } | ||
| await session.queryInstance.interrupt(); | ||
| try { |
There was a problem hiding this comment.
🟡 missing_tests: No test covers the case where queryInstance.interrupt() rejects (e.g. dead ProcessTransport). The existing tests in send-to-chat.test.ts all mock interrupt to resolve. A test that mocks interrupt to reject and asserts that interruptChat still returns true (and logs a warning) would lock in this fix. [fixable]
| msg.contextBlocks, | ||
| msg.clientMsgId, | ||
| msg.model, | ||
| ).catch((err: unknown) => |
There was a problem hiding this comment.
🔵 style: The .catch() was added to handle the fire-and-forget interruptChat() call, but interruptChat now catches its own interrupt() error internally and doesn't rethrow. The only remaining throw paths are earlier guards (registry.get returning null — returns false silently, not a throw) or the storeAndEchoIfNew/assemblePrompt path. The outer .catch() is still reasonable as defense-in-depth, but the log level (log.error) could be misleading since the most likely failure (dead transport) is already handled as log.warn inside interruptChat. Consider aligning the severity or noting that this is a fallback for unexpected errors. [fixable]
When the SDK child process has already exited, calling interrupt() throws "ProcessTransport is not ready for writing" as an unhandled rejection, crashing the server. Wrap the call in try/catch — if the transport is dead the interrupt is moot anyway. Also add .catch() to the fire-and-forget interruptChat() call in ws-handler-v2 to prevent any future unhandled rejections from that path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
adfb3b8 to
11c5b82
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s) (1 warning).
server/chat.ts
Correct and focused fix — wraps a crash-causing interrupt() call in try/catch and adds a .catch() guard on the fire-and-forget caller. Both changes are complementary (inner catch allows normal completion; outer catch guards other failures). Main gap is missing test coverage for the new error paths.
- 🟡 missing_tests (L1276): No test covers the new try/catch around
session.queryInstance.interrupt().interruptChathas zero test coverage inchat.test.ts. A test wherequeryInstance.interrupt()rejects would verify the error is swallowed and the function still pushes toinputQueue(the happy path after catch).[fixable] - 🔵 style (L1279): The multi-line comment inside the catch block ('ProcessTransport may already be dead…') is useful on the first read but could be trimmed to one line per project convention ('No comments unless the WHY is non-obvious'). The log message already captures the intent.
[fixable]
server/ws-handler-v2.ts
Correct and focused fix — wraps a crash-causing interrupt() call in try/catch and adds a .catch() guard on the fire-and-forget caller. Both changes are complementary (inner catch allows normal completion; outer catch guards other failures). Main gap is missing test coverage for the new error paths.
- 🔵 missing_tests (L736): The
.catch()on the fire-and-forgetinterruptChatcall is untested.interruptChatis mocked in the ws-handler-v2 test suite — a test where the mock rejects would confirm the rejection is caught and logged rather than becoming an unhandled promise rejection.[fixable]
| await Promise.allSettled(stops); | ||
| } | ||
| await session.queryInstance.interrupt(); | ||
| try { |
There was a problem hiding this comment.
🟡 missing_tests: No test covers the new try/catch around session.queryInstance.interrupt(). interruptChat has zero test coverage in chat.test.ts. A test where queryInstance.interrupt() rejects would verify the error is swallowed and the function still pushes to inputQueue (the happy path after catch). [fixable]
| try { | ||
| await session.queryInstance.interrupt(); | ||
| } catch (err: unknown) { | ||
| // ProcessTransport may already be dead (process exited) — the interrupt |
There was a problem hiding this comment.
🔵 style: The multi-line comment inside the catch block ('ProcessTransport may already be dead…') is useful on the first read but could be trimmed to one line per project convention ('No comments unless the WHY is non-obvious'). The log message already captures the intent. [fixable]
| msg.contextBlocks, | ||
| msg.clientMsgId, | ||
| msg.model, | ||
| ).catch((err: unknown) => |
There was a problem hiding this comment.
🔵 missing_tests: The .catch() on the fire-and-forget interruptChat call is untested. interruptChat is mocked in the ws-handler-v2 test suite — a test where the mock rejects would confirm the rejection is caught and logged rather than becoming an unhandled promise rejection. [fixable]
Summary
session.queryInstance.interrupt()in try/catch inchat.ts— when the SDK child process has already exited, the call throwsProcessTransport is not ready for writingas an unhandled promise rejection, crashing the Node server.catch()to the fire-and-forgetinterruptChat()call inws-handler-v2.tsto prevent unhandled rejections from that pathTest plan
🤖 Generated with Claude Code