Skip to content

fix(daemon-shutdown): sequence exit on response flush, not a 50ms sleep (1qn)#102

Merged
brandon-fryslie merged 1 commit into
mainfrom
brandon-daemon-shutdown-1qn
Jun 10, 2026
Merged

fix(daemon-shutdown): sequence exit on response flush, not a 50ms sleep (1qn)#102
brandon-fryslie merged 1 commit into
mainfrom
brandon-daemon-shutdown-1qn

Conversation

@brandon-fryslie

Copy link
Copy Markdown
Contributor

Summary

  • Closes the last child of epic 58c (sheriff law-remediation): brandon-daemon-shutdown-1qn, finding feat(render): capture terminal width at the wire boundary (kz8.4) #9 [LAW:no-ambient-temporal-coupling].
  • Deletes both setTimeout(() => shutdown(0), 50) guesses in src/daemon/server.ts. Exit is now sequenced on the response flush completion: handleRequest returns { resp, exitAfterFlush: number | null } (the lifecycle wish as data), and respond() — the socket boundary — performs it via sock.end(frame, cb). Node invokes that callback on 'finish' or 'error', so the signal is total: a peer that vanishes mid-flush still settles and the exit is never stranded.
  • The version-mismatch asymmetry (newer client ⇒ stale binary ⇒ exit; older client ⇒ stay up, the 452-spiral breaker) is now the literal value req.v > PROTOCOL_VERSION ? 0 : null.
  • The SIGKILL backstop inside shutdown() is untouched, per the ticket's locked decision.

Why it matters

The failure mode landed exactly when users debug an upgrade: a slow flush let the daemon exit first, so the client saw a dead socket (transient/io_error, triggering a pointless kick) instead of the flushed VERSION_MISMATCH diagnostic (permanent/version_mismatch).

Verification

  • New integration test against the real daemon: a v+1 client receives the flushed VERSION_MISMATCH frame (receiving it at all proves flush-before-exit — premature exit kills the socket frameless), then the process exits within budget.
  • 137 tests across all daemon/client suites pass; check:protocol 12/12; lint, typecheck, build clean.
  • Residue: no setTimeout(... shutdown ...) remains (the 500ms SIGKILL backstop uses process.kill and is the documented, owned safety).

…ep (1qn)

Sheriff finding #9 [LAW:no-ambient-temporal-coupling]: handleRequest
scheduled setTimeout(() => shutdown(0), 50) after a newer-client version
mismatch and after the shutdown verb, encoding "the response has flushed
by then" as a magic delay nobody owned. A slow flush meant the client saw
a dead socket (transient/io_error) instead of the VERSION_MISMATCH
diagnostic (permanent/version_mismatch) — exactly during upgrades.

The flush completion now owns the ordering: handleRequest returns
{ resp, exitAfterFlush: number | null } — the exit wish as data
[LAW:effects-at-boundaries] — and respond() at the socket boundary
performs it via sock.end(frame, cb), whose callback fires on finish OR
error (total signal; a vanished peer cannot strand the exit). The
version-mismatch asymmetry is now literally the value
`req.v > PROTOCOL_VERSION ? 0 : null` [LAW:dataflow-not-control-flow].
The SIGKILL backstop inside shutdown() is untouched.

New integration test pins the contract: a v+1 client receives the flushed
VERSION_MISMATCH frame, then the daemon exits within budget.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Z.ai Coding Agent Review

No must-change items found. The diff is a clean elimination of [LAW:no-ambient-temporal-coupling] — the setTimeout(() => shutdown(0), 50) is replaced by returning the exit code as data (HandledRequest.exitAfterFlush) from handleRequest, sequenced on sock.end's write-completion callback. The old empty catch {} around sock.write is replaced with a logged catch that still fires the settle thunk, fixing a pre-existing [LAW:no-silent-failure] gap. The new version-mismatch test validates both the flushed response and the process exit within budget. The stay() helper for the common non-exit path and the HandledRequest interface are well-factored. No violations introduced.

✅ Approved

@brandon-fryslie brandon-fryslie merged commit dc7a8ae into main Jun 10, 2026
7 checks passed
@brandon-fryslie brandon-fryslie deleted the brandon-daemon-shutdown-1qn branch June 10, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants