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 cleanup.run parked on startup readiness can start a run after a mid-startup stop #1001
A cleanup.run request that arrives while the daemon is still starting parks behind the startup-readiness gate. If a stop is called in the middle of startup, that parked request can still start a cleanup run once it is released. CleanupReaper.settle() (#985, PR #999) has already taken its snapshot by then, so the run is not waited for and can outlive state.db and the drivers.
What you expected
A stop during startup fails or drops parked requests instead of letting them start new work after settle().
How to reproduce
Not reproduced. The code lens of the #999 review saw it while reading the stop path and the startup-readiness gate in src/daemon/server.ts.
Environment
main at 2ccae23. Found by reading code, not by a user report. Rare: needs a manual cleanup.run during startup and a stop mid-converge.
Defect, reproduced as the issue describes. A cleanup.run parked on readiness runs after stop() has finished, and shuts down a device.
src/daemon/server.test.ts, "DaemonServer startup readiness" › "refuses a cleanup.run parked on startup with DAEMON_STOPPING once a stop lands mid-converge, and shuts down no device" (branch bug/1001-repro). The test seeds an idle ready device, parks cleanup.run {rule: "idle-shutdown"} through DaemonServer.dispatch (the HTTP seam), awaits stop(), then lets convergence resolve.
pnpm exec vitest run --project unit src/daemon/server.test.ts -t "refuses a cleanup.run parked on startup"
AssertionError: promise resolved "[ { action: 'shutdown', …(3) } ]" instead of rejecting
❯ src/daemon/server.test.ts:1537 await expect(parked).rejects.toMatchObject({ code: "DAEMON_STOPPING" })
Root cause
src/daemon/server.ts:609 (#awaitReady) is the cause. It resolves when convergence succeeds and never checks #stopping, so runDispatch (src/daemon/dispatch.ts:327) goes on to the handler. The only stopping gates are at the entry points (server.ts:425 in dispatch(), :819 in #dispatchLine), and a parked request has already passed them. The symptom shows in CleanupReaper.run (src/core/reaper.ts:76): it starts a run after reaper.settle() (server.ts:642) has already taken its snapshot. This affects every parked operation, not just cleanup: lease.request, device.exec and nuke.run would also run on a stopped daemon. On the socket path the client never gets an answer, because stop() closes the connection, but the work still runs.
Simplest fix
At the end of #awaitReady, add if (this.#stopping) throw new DispatchError("DAEMON_STOPPING", "Daemon is stopping"). I tried this locally: the test passes, and so do all 910 tests in src/daemon/ and src/http/.
Alternatives rejected
Make CleanupReaper.run refuse after dispose(): this fixes cleanup only, and other parked operations still run.
Make stop() await #readyPromise before it settles: a slow or hung convergence would then block daemon stop.
Risk
src/daemon/AGENTS.md Readiness should add "a stop during converge → parked requests get DAEMON_STOPPING".
The convergence-failed path should still answer DAEMON_STARTUP_FAILED. The catch runs before #stopping is checked, so it should, but a reviewer should confirm it.
Activity
V3RON commentedon Oct 25, 2026
Reproduction
Defect, reproduced as the issue describes. A
cleanup.runparked on readiness runs afterstop()has finished, and shuts down a device.src/daemon/server.test.ts, "DaemonServer startup readiness" › "refuses a cleanup.run parked on startup with DAEMON_STOPPING once a stop lands mid-converge, and shuts down no device" (branchbug/1001-repro). The test seeds an idlereadydevice, parkscleanup.run {rule: "idle-shutdown"}throughDaemonServer.dispatch(the HTTP seam), awaitsstop(), then lets convergence resolve.Root cause
src/daemon/server.ts:609(#awaitReady) is the cause. It resolves when convergence succeeds and never checks#stopping, sorunDispatch(src/daemon/dispatch.ts:327) goes on to the handler. The only stopping gates are at the entry points (server.ts:425indispatch(),:819in#dispatchLine), and a parked request has already passed them. The symptom shows inCleanupReaper.run(src/core/reaper.ts:76): it starts a run afterreaper.settle()(server.ts:642) has already taken its snapshot. This affects every parked operation, not just cleanup:lease.request,device.execandnuke.runwould also run on a stopped daemon. On the socket path the client never gets an answer, becausestop()closes the connection, but the work still runs.Simplest fix
At the end of
#awaitReady, addif (this.#stopping) throw new DispatchError("DAEMON_STOPPING", "Daemon is stopping"). I tried this locally: the test passes, and so do all 910 tests insrc/daemon/andsrc/http/.Alternatives rejected
CleanupReaper.runrefuse afterdispose(): this fixes cleanup only, and other parked operations still run.stop()await#readyPromisebefore it settles: a slow or hung convergence would then blockdaemon stop.Risk
src/daemon/AGENTS.mdReadiness should add "a stop during converge → parked requests getDAEMON_STOPPING".DAEMON_STARTUP_FAILED. The catch runs before#stoppingis checked, so it should, but a reviewer should confirm it.Written by an agent.
V3RON commentedon Oct 25, 2026
Delivery run
Merged: #1003 (#1001)
Written by an agent.