You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A health recovery that starts while the daemon stop waits for cleanup is not waited for #1000
When a daemon stops, DaemonServer.stop waits for the cleanup reaper's runs in progress (#985, PR #999) and then disposes the health monitor. A health recovery that started while the stop was waiting for the reaper is not waited for, so it can still act on a device after state.db or the drivers are closed.
What you expected
A graceful stop waits for work already started (architecture rule 12), including a health recovery that begins during the stop.
How to reproduce
Not reproduced. The code lens of the #999 review saw it while reading src/daemon/server.ts (stop order around line 642) and the health monitor's dispose.
Environment
main at 2ccae23. Found by reading code, not by a user report.
Defect, and wider than reported: a stop waits for no health-monitor work, so a recovery reboot started before the stop also outlives it.
src/daemon/main.test.ts, "leaves no recovery reboot $name running once the drivers are disposed" (describe … (#1000)), driven through startDaemon with a driver that holds the recover boot:
pnpm exec vitest run --project unit src/daemon/main.test.ts -t "#1000"
started before the stop:
AssertionError: expected [ [ 'recover started', 'recover finished', 'dispose' ] ]
to deep equally contain [ 'recover started', 'dispose', 'recover finished' ]
due while the stop waits for a cleanup run:
AssertionError: expected [ [ 'recover started', 'recover finished', 'dispose' ], [ 'dispose' ] ]
to deep equally contain [ 'recover started', 'dispose', 'recover finished' ]
Case 2 accepts both fixes: the stop waits, or the recovery never starts.
Root cause
LeaseHealthMonitor.dispose() (src/leasing/lease-health-monitor.ts:77) only cancels timers. The tick (:101) and each recovery (:257) are fire-and-forget promises nobody keeps, so there is nothing to await, and DaemonServer.#stop (src/daemon/server.ts:643) awaits nothing from the monitor before options.dispose (:649) disposes drivers and closes state.db. The armed tick during reaper.settle() (:642) is just one way in; #dispatchRecovery (:251) also ignores #disposed, so a running tick can start a recovery after dispose.
Simplest fix
LeaseHealthMonitor.settle() awaiting the running tick and recoveries; #dispatchRecovery returns once disposed. In #stop, dispose the monitor beside reaper.dispose(), then await its settle() after reaper.settle(); update step 4 of docs/internal/flows/startup.md. Tried locally (not pushed): the test, server.test.ts and lease-health-monitor.test.ts pass.
Alternatives rejected
Only disposing before the reaper wait: case 1 still fails.
Draining via leasing.settle(): runs after the monitor is disposed; the server owns its stop, like the reaper.
Aborting the reboot: makeReady takes no signal; a half reboot is worse than waiting.
Risk
A stop may now wait up to one recovery reboot (~30 s); check the SIGTERM and update paths.
After dispose() resolves a backoff early, #recover must still return without rebooting.
A tick's #giveUp → releaseDeviceLost may run during the stop, before leasing.settle() drains reclaims.
Activity
V3RON commentedon Oct 25, 2026
Reproduction
Defect, and wider than reported: a stop waits for no health-monitor work, so a recovery reboot started before the stop also outlives it.
src/daemon/main.test.ts, "leaves no recovery reboot $name running once the drivers are disposed" (describe… (#1000)), driven throughstartDaemonwith a driver that holds the recover boot:Case 2 accepts both fixes: the stop waits, or the recovery never starts.
Root cause
LeaseHealthMonitor.dispose()(src/leasing/lease-health-monitor.ts:77) only cancels timers. The tick (:101) and each recovery (:257) are fire-and-forget promises nobody keeps, so there is nothing to await, andDaemonServer.#stop(src/daemon/server.ts:643) awaits nothing from the monitor beforeoptions.dispose(:649) disposes drivers and closesstate.db. The armed tick duringreaper.settle()(:642) is just one way in;#dispatchRecovery(:251) also ignores#disposed, so a running tick can start a recovery after dispose.Simplest fix
LeaseHealthMonitor.settle()awaiting the running tick and recoveries;#dispatchRecoveryreturns once disposed. In#stop, dispose the monitor besidereaper.dispose(), then await itssettle()afterreaper.settle(); update step 4 ofdocs/internal/flows/startup.md. Tried locally (not pushed): the test,server.test.tsandlease-health-monitor.test.tspass.Alternatives rejected
leasing.settle(): runs after the monitor is disposed; the server owns its stop, like the reaper.makeReadytakes no signal; a half reboot is worse than waiting.Risk
dispose()resolves a backoff early,#recovermust still return without rebooting.#giveUp→releaseDeviceLostmay run during the stop, beforeleasing.settle()drains reclaims.Written by an agent.
fix(daemon): a stop waits for health ticks and recoveries in flight (#…
test(leasing): settle waits for a health tick running at dispose and …
fix(daemon): a stop waits for health ticks and recoveries in flight (#…
V3RON commentedon Oct 25, 2026
Delivery run
Merged: #1004 (#1000)
Written by an agent.