fix(server): correct process-handler and shutdown lifecycle#35
Merged
Conversation
Second-wave audit (#33), verified by hand. The plugin factory runs once per workspace/worktree in the same process; these defects compound per invocation. - D1: uncaughtException/unhandledRejection used process.on, appending a new pair every invocation (N duplicate emits/audits per error, Max ListenersExceededWarning, feedback loop). Fixed via a module-scoped install-once guard (extracted to server/lifecycle.ts). NOT process.once — once() removes the listener after the first fire, leaving a second rejection unhandled. Handlers route through the shared singletons. - D2: shutdown() never closed SSE clients → per-client keepalive setIntervals kept firing after server.stop(). shutdown now calls eventBus.closeAll() before server.stop(); closeAll() clears each client's pingInterval explicitly (stored on SSEClient) instead of relying on controller.close() propagating to the stream cancel() callback (a Bun impl detail, not a WhatWG guarantee). - D3: shutdown() had no re-entrancy guard → SIGINT+SIGTERM/exit ran cleanup twice. Added a closure-scoped guard; original step order is preserved exactly (only closeAll prepended). - D4: removed process.once("exit", () => void shutdown()) — "exit" fires during event-loop drain so all async cleanup was silently dropped; SIGINT/SIGTERM (intact) are the correct graceful hooks. Extracted the install-once + shutdown-guard logic to server/lifecycle.ts (testable seam; documented in server/AGENTS.md). +9 tests. bun test 633/0, tsc clean. Independent fresh-context review: APPROVE, no blocking issues (shutdown step-ordering exhaustively diffed — preserved); the 3 nits it raised are fixed here. Pre-existing multi-instance note (out of scope): SIGINT/SIGTERM use process.once so only the first invocation's shutdown runs on a signal. Refs #33
This was referenced May 17, 2026
11 tasks
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.
Summary
Resolves the process/shutdown lifecycle items of #33 (Tier-1/2). The plugin factory runs once per workspace/worktree in one process, so these defects compound per invocation. Verified by hand.
process.onfor uncaught/unhandled → listeners accumulate per invocation (dup emits, MaxListenersWarning, feedback loop)server/lifecycle.ts). Deliberately notprocess.once—onceremoves the listener after one fire, leaving a 2nd rejection unhandled. Handlers route through shared singletons.shutdown()never closed SSE clients → keepalivesetIntervals leak afterserver.stop()shutdown()callseventBus.closeAll()beforeserver.stop();closeAll()now clears each client'spingIntervalexplicitly (stored onSSEClient) rather than relying oncontroller.close()→ streamcancel()(a Bun impl detail)closeAllprepended)process.once("exit", () => void shutdown())dropped all async cleanup (fires during event-loop drain)Design notes
server/lifecycle.ts(testable seam;server/may composecore/— no dependency-rule violation; documented inserver/AGENTS.md).pingIntervalclear incloseAll) was added in response to the fresh review's nit — it makes the interval-leak fix self-contained instead of depending on undocumented BunReadableStreambehavior.Testing
exitlistener). Non-flaky, no leaked process listeners.bun test633/0 ·bunx tsc --noEmitclean (bus.ts keepalive change did not regress existing bus tests)Pre-existing, out of scope (noted, not fixed): SIGINT/SIGTERM use
process.once, so in the multi-instance model only the first invocation's shutdown runs on a signal — a separate multi-instance design question.Refs #33 (Tier-1/2, partial — does not close).