fix(daemon): move supervisor authority records out of $TMPDIR - #1449
Open
snimu wants to merge 6 commits into
Open
fix(daemon): move supervisor authority records out of $TMPDIR#1449snimu wants to merge 6 commits into
snimu wants to merge 6 commits into
Conversation
…mission RenewableRegistryRecord owns the shutdown-admission lease-renew loop: the unref()'d interval, a single-flight refresh shared by timer-fired and direct assertOrRenew calls (whose rejection reaches direct awaiters), a stopped/lost re-check inside the guarded section so a renew that loses a race with stop() can never write, and disposal. This replaces DaemonShutdownAdmission's hand-rolled loop, which had a latent race: direct assertOrRenew calls bypassed the single-flight slot, so release() could return while a renew was still queued on the registry guard and the record could be rewritten after removal. Public semantics (assertOrRenew, release awaiting in-flight refresh, lease timings) are unchanged.
macOS com.apple.bsd.dirhelper deletes files older than three days under $TMPDIR daily at 03:35. The supervisor ownership registry (owner.json/ scope.json, startup fences, the shutdown admission) lived there only because it rode along with the socket directory, whose location is forced by the 104-byte sun_path limit — a constraint JSON records do not share. Any supervisor alive past three days lost its record and wedged permanently with supervisor_generation_stale on every command. The registry now defaults to ~/.prime/supervisor-owners: durable, global per user (ownerConflicts must see every daemon on the box, so it must not shard per agent dir), with the PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_ REGISTRY_DIR override unchanged. All registry tenants move together. No record migration: records are per-process-lifetime. A running old-build daemon keeps its tmpdir records and works until restarted; its leftover records self-clean via dirhelper within three days. During the overlap a new-build daemon sees an empty registry and fails at socket bind instead, which surfaces like any bind conflict and unwinds acquire cleanly.
Both daemon-supervisor.ts (this.ownership undefined: never acquired or already released) and DaemonSupervisorOwnershipLostError (record on disk missing or replaced) emitted the identical 'no longer owns its registry entry' string, making the failure mode impossible to tell apart from the message alone. Each message is now distinct and appends the socket path, the registry dir where available, and the remedy (restart the daemon; sessions are preserved). Both keep code: supervisor_generation_stale.
Pin the RenewableRegistryRecord hardening at its consumer: a direct assertOrRenew queued behind a held registry guard while release() runs must reject with the admission-lost error and must not rewrite shutdown-admission.json after removal.
snimu
force-pushed
the
feat/daemon-ownership-renewal
branch
from
August 16, 2026 16:19
fe42f56 to
4c28e22
Compare
A new-build CLI could not see a still-running pre-move daemon's owner record: persistDaemonStartupFenceFromOwner scanned only the new (empty) registry and threw AFTER prepare_update_restart had already drained and fenced the old supervisor, whose updateRestartPhase never leaves 'prepared' without a shutdown — wedging the standard upgrade path. The worker-auth validation (assertDaemonSupervisorOwnerCurrent) had the same blind spot for a new-build worker under an old-build supervisor. Owner-record READS now fall back to the legacy tmpdir location (read-only, no abandoned-dir reclaim — old-build daemons own that location's lifecycle; unlocked relative to old-build writers, acceptable because records are rename-atomic). Writes, including fences, go only to the new registry. The fallback is resolved structurally where registryDir is resolved: it exists iff no env override and no explicit registryDir, so tests with explicit registries can never leak reads to the machine's real tmpdir. Remove after one release. acquire's conflict scan keeps no fallback: real socket contention is still caught at bind by the socket lease.
…nce matches Add the missing test for assertDaemonSupervisorOwnerCurrent's legacy read (a pre-move owner claim validates through an injected legacy dir and never falls back for an explicit registry), replace the silent processStartId early-out in the fence-via-legacy test with a visible assertion, and filter legacy fence matches by the caller-held token/pid so stale legacy leftovers cannot produce a spurious multiple-owners failure.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 15c7470. Configure here.
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.

What this fixes
Any daemon supervisor alive for more than three days wedged permanently: macOS's
com.apple.bsd.dirhelperdeletes files older than 3 days under$TMPDIRdaily, and the supervisor's ownership registry (owner.json/scope.json, plus startup fences and the shutdown admission) lived exactly there. Once reaped, every command against that daemon failed with "no longer owns its registry entry" and the only recovery was killing it. This is a clock, not a race — it hit every long-lived daemon on macOS.The fix: move the records, don't outrun the cleaner
The registry only lived in
$TMPDIRby proximity: the daemon socket needs a short path (the ~104-bytesun_pathlimit), and the registry rode along in the socket dir. But these are JSON files — no path-length constraint applies, and they are durable authority records sitting in the one directory the OS is explicitly allowed to prune.The registry now lives at
~/.prime/supervisor-owners(still one global per-user location, which cross-agent-dir conflict detection requires; the internal env override still wins). All registry tenants move together: owner records, startup fences, the shutdown admission. Nothing deletes files behind a live daemon anymore, so the failure mode is simply gone — no renewal machinery, no self-heal, no timer.Version-overlap and migration
No record migration — ownership records are per-process-lifetime — but owner-record READS fall back to the legacy
$TMPDIRlocation for one release (writes never do). This keeps the standard upgrade path working while a pre-move daemon is still running: the self-update coordinator's fence persist and worker auth both need to read the old daemon's record, and without the fallback a routine upgrade would drain and fence the old daemon, then abort — wedging it until manual restart. The fallback is read-only, exists only when no explicit registry dir or env override is set, and is marked for removal next release. Real socket contention is still resolved by the version-independent socket-path lease at bind. Stale records in the old location are inert and the OS cleaner removes them within 3 days.Also in this PR
package-manager-clianddaemon-pscallassertOrRenewdirectly, bypassing the timer's single-flight promise — sorelease()could complete while a direct renew sat queued on the registry guard and then resurrectshutdown-admission.jsonfor up to ~5s. The extracted lease-renew loop funnels every caller through one single-flight slot that release genuinely awaits, and re-checks stopped-state first inside the guarded section. Covered by a deterministic race test (guard held, release overtakes, admission provably not resurrected).Known follow-up (not this PR)
Acquire only reclaims conflicting dead owners, and nothing prunes the durable dir, so crashed daemons on never-reused socket paths leave inert
.ownerdirs behind — bounded growth, a naturaldoctor --fixcandidate.Testing
Registry derivation + env-override precedence; missing-or-mismatched record stays a coded loss with no overwrite; the admission release-race (deterministic, no sleeps); both disambiguated messages. Supervisor lifecycle/regression suites green (4600 singleton, 4603 worker recovery, package self-update, monitor, 4606 update coordinator);
npm run checkclean.Net src +71 (+118/−47): the relocation is a one-line derivation change; the rest is the admission-race hardening and the error messages.
Note
Move daemon supervisor registry from
$TMPDIRto~/.prime/supervisor-ownersdefaultDaemonSupervisorRegistryDirto use~/.prime/supervisor-ownersinstead of a path derived from the socket directory, making ownership records durable across temp directory cleans.assertDaemonSupervisorOwnerCurrentandpersistDaemonStartupFenceFromOwnerso existing records written under the old location are still readable during transition.RenewableRegistryRecordto manage interval-based lease renewal inDaemonShutdownAdmissionwith single-flight deduplication and stop fencing.DaemonSupervisorOwnershipLostErrorwith socket and registry path context to aid recovery.Macroscope summarized 15c7470.
Note
Medium Risk
Changes durable daemon authority state location and cross-version ownership/fence behavior; incorrect legacy fallback could mis-validate owners, though changes are guarded and heavily tested.
Overview
Fixes long-lived macOS daemons wedging when supervisor authority records (ownership, startup fences, shutdown admission) were stored under
$TMPDIRand reaped after ~3 days. The default registry path is now~/.prime/supervisor-owners(env override unchanged); sockets stay short-path under temp.Version overlap: read-only legacy registry fallback under the old socket-relative dir for owner checks and startup-fence persistence while pre-move daemons are still running—no migration of records; explicit registry args do not fall back.
Shutdown admission: lease renewal is centralized in
RenewableRegistryRecord(single-flight renew,stop()awaits in-flight work, no rewrite after release) sorelease()cannot resurrectshutdown-admission.jsonwhen a directassertOrRenewwas queued on the registry lock.Errors: ownership-loss messages include socket/registry paths and recovery hint; never acquired vs record missing/replaced on disk are distinguished in the supervisor.
Reviewed by Cursor Bugbot for commit 15c7470. Bugbot is set up for automated code reviews on this repo. Configure here.