fix(dev): stop dev.ts signal handlers preempting lifecycle shutdown (#459)#491
Merged
Conversation
New scripts/dev-shutdown.ts: signal handling for the dev loop that only owns process exit while it is the sole SIGINT/SIGTERM listener (build phase). Once lifecycle.registerShutdownHandlers() registers on the same signals, the dev handler kills tailwind and falls through so the full graceful chain (including search.shutdown() -> antfly stop) runs. A second signal forces exit 130 as the escape hatch for a hung shutdown. Process object is dependency-injected for unit testing (emitting real signals would kill the test runner). dev.ts wiring lands separately. Part of #459 (defect 1). Spec: .claude/specs/dev-shutdown-signal-ordering.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/dev.ts's old signal handlers called process.exit(0) synchronously. They register before server.ts imports, and signal listeners run in registration order — so lifecycle.ts's graceful shutdown (which stops the antfly child via search.shutdown()) never ran under bun run dev, leaving :3738 orphaned. registerShutdown() now delegates to registerDevShutdown(): tailwind is always killed, but exit is only owned while dev.ts is the sole listener (build phase); a second signal forces exit 130. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a Shutdown ordering section to .claude/knowledge/dev-loop.md covering the build-phase/post-boot exit ownership split, the second-signal escape hatch, the accepted pre-registration gap (covered by PR #457), and the pre-existing tailwind bunx-wrapper grandchild leak. Check in the approved implementation plan alongside the spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assert the lifecycle-style listener registered after the dev handler actually runs when a signal arrives — the core defect was the dev handler exiting before it could. Also assert the force-exit warning names the signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Force-exit code follows the second signal (130 SIGINT / 143 SIGTERM) - Name the two-registrant invariant at the listenerCount check - Fix the misleading exit-hook test: the module does not dedupe killTailwind; assert the double call and note dedup is the caller's .killed guard in dev.ts - Import order: node builtin before bun:test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Closes #459 (defect 1 — the last open half; defect 2 + the singleton lock shipped in #465).
scripts/dev.tsregisters SIGINT/SIGTERM handlers beforeawait import('../server'), and signal listeners run in registration order — so its synchronousprocess.exit(0)preemptedlifecycle.ts's graceful chain (plugins → dispatch → watcher →search.shutdown()→ SSE → HTTP → ledger → server lock). The antfly child survived parent death and squatted its port; overlapping half-dead generations produced phantom UI behavior.Changes
scripts/dev-shutdown.ts(registerDevShutdown, DI for testability): always kills the tailwind child, but only ownsprocess.exit(0)while it is the sole listener on the signal (build phase, vialistenerCount). Oncelifecycle.registerShutdownHandlers()registers on the same signals, the dev handler falls through and the lifecycle listener runs the full graceful chain and owns the exit.bun run devis never unkillable at the terminal.tests/scripts/dev-shutdown.test.ts(8 cases, fake proc — real signals would kill the runner), including a regression test asserting the later-registered lifecycle listener actually runs..claude/knowledge/dev-loop.md; spec + plan under.claude/specs/.Out of scope (owned by #457, not duplicated here): the adapter-side sync exit hook, the
antfly/watcher ignore, and the boot-window gap before handler registration. Also surfaced but untouched: a pre-existing leak where killing the tailwindbunxwrapper orphans the underlying nodetailwindcssgrandchild (documented in dev-loop.md).Verification
bun run dev→ ready →kill -TERM→ full lifecycle chain logged ("Stopping Antfly server..." → "Antfly stopped"), antfly child dead, ports 3737/3738 free🤖 Generated with Claude Code