Skip to content

fix(coding-agent): harden daemon startup and recovery ownership - #1929

Merged
snimu merged 12 commits into
mainfrom
fix/daemon-recovery-hardening
Sep 1, 2026
Merged

fix(coding-agent): harden daemon startup and recovery ownership#1929
snimu merged 12 commits into
mainfrom
fix/daemon-recovery-hardening

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Linear: ENG-5827

Summary

  • Treat a connected daemon without hello as unresponsive, give it the full startup window, and revalidate stale observations before shutdown.
  • Fence the supervisor after socket lock loss and fail closed at every proper-lockfile call site in packages/coding-agent.
  • Preserve verified-live and identity-unavailable workers after recovery probe timeouts. Keep them in recovering and retry asynchronously.
  • Start the catalog and persist interrupted operations before orphan cleanup. Use a lightweight catalog entrypoint with a 30 second cold-start timeout.

No daemon wire shape changes are included. Windows named-pipe behavior is unchanged.

Prior-art audit

This implements only behavior still missing from the closed, unmerged PRs:

I also checked #1523, #1756, #1895, #1897, #1900, #1909, #1926, #1123, and #1236. Only #1756 is merged. Sebastian's stack does not implement the startup, catalog, or lock-loss fixes here. #1897 adds PID-reuse protection and roster recovery state in the same supervisor functions, so it will need a deliberate conflict resolution, but it does not preserve live workers after probe timeouts or gate destructive cleanup on catalog durability. #1895, #1900, and #1909 do not duplicate these behaviors.

Invariants

  • A proper-lockfile compromise callback records state and never throws through the refresh callback.
  • A supervisor that loses its socket lease stops serving and relinquishes ownership.
  • Cleanup under a compromised lease never unlinks a successor socket.
  • A connected daemon without hello is never classified as stale.
  • A stale observation is revalidated before any shutdown request.
  • A live worker with matching identity is not killed or relaunched after hello, subscribe, or list timeouts.
  • Catalog startup and interruption persistence complete before orphan cleanup.

Validation

  • npm run check: passed.
  • Affected coding-agent tests: 330 passed across 13 files.
  • test/daemon-supervisor-process.test.ts: 10 passed, 8 skipped.
  • Two independent code reviews found no blocking issues.

The process suite was run with inherited PRIME_AGENT_INTERNAL_DAEMON_* variables removed because this development shell is itself a daemon worker.


Note

High Risk
Changes core daemon lifecycle, distributed locking, and worker recovery paths where mistaken kills or socket unlink could interrupt active sessions; behavior is intentionally more conservative but affects every CLI daemon attach.

Overview
Hardens daemon startup, lock ownership, and live-worker recovery so the CLI and supervisor fail closed instead of replacing or killing processes they cannot safely own.

CLI startup now treats a connected socket with no daemon_hello as unresponsive (not stale), waits through the startup window, and errors with force-shutdown guidance rather than shutting down a possibly-busy daemon. Stale replacement re-checks the connected daemon’s hello before issuing shutdown and can reuse a peer that finishes starting mid-check.

proper-lockfile compromise is wired through daemon socket leases, supervisor/coordinator registry guards, session leases, auth, settings, and cron jobs: compromised locks abort mutations, skip unsafe socket unlink, and (for the supervisor) fence the server and relinquish ownership.

Worker recovery introduces typed probe/auth errors, keeps verified-live or identity-unknown workers in recovering with deferred probes (capped at ~10 rounds), and only SIGKILLs a live process for the identity-verified pre-roster adoption path. Destructive cleanup no longer kills resident workers from recoverUncertainWorkerOperations; catalog start and interruption persistence run before orphan reaping.

Catalog spawns a dedicated entrypoint (Node/Bun), 30s cold-start timeout, and fails fast if the child exits during startup.

Reviewed by Cursor Bugbot for commit 4659595. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Harden daemon startup and socket lease compromise recovery

  • Adds compromise tracking to DaemonSocketPathLease and all subsystem locks (auth, settings, cron, session-lease, update-coordinator) so operations fail fast instead of proceeding or releasing a lock a successor may already own
  • CLI's probeDaemonVersion now returns unresponsive for connected-but-silent daemons instead of stale; ensureDaemonRunning waits through the startup window before surfacing an error, and shutdownStaleDaemonIfNotBusy returns precise dispositions (current/stopped/busy)
  • Worker recovery uses new DaemonWorkerProbeTimeoutError and DaemonWorkerAuthenticationError types, bounds deferred probe rounds to MAX_DEFERRED_RECOVERY_ROUNDS (10), and avoids destructive cleanup or orphan reaping when process identity is uncertain
  • On socket lease compromise, DaemonSupervisor fences all client connections via fenceSupervisorSocket and begins async cleanup; mutating commands re-validate serving state after an idle-eviction fence
  • Daemon catalog startup timeout raised from 5s to 30s (DAEMON_CATALOG_START_TIMEOUT_MS), and the spawner resolves .ts entrypoints via tsx or compiled .js depending on run mode
  • Risk: recoverUncertainWorkerOperations signature changed (boolean pre-cleanup param removed); in-tree callers and tests in daemon-agent-roster.test.ts and daemon-supervisor-monitor.test.ts are updated. DaemonVersionProbe type now requires hello for stale and adds unresponsive — any out-of-tree callers of probeDaemonVersion need to handle the new union member.

Macroscope summarized 4659595.

Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 59e09c1. Configure here.

Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
sethkarten and others added 10 commits September 1, 2026 12:49
…icts

Adoption and recovery classify liveness through processIdentity() instead of
re-pairing isProcessAlive+getProcessStartId at each site; raw start-id reads
remain only where the value itself is persisted. The sole surviving kill of a
live worker is the identity-verified pre-roster replacement, now pinned:
adoption of a live pre-roster worker still kills-and-relaunches, while an
unverifiable identity still parks failed with no replacement.
A hung-but-alive worker (or a live pid with no verifiable start id) probed
forever: ~11s probe pass, 5s defer, repeat. After MAX_DEFERRED_RECOVERY_ROUNDS
(10 rounds, ~2.5 minutes) the worker parks failed — user-visible through the
existing roster failed status — with its process left alive for a manual
retry_worker, which resets the round count like any successful recovery.
isDaemonWorkerProbeTimeout classified by message prefixes defined in two
other files. The timeout sites in daemon-worker-client.ts (hello, response)
and the supervisor's connect deadline now throw DaemonWorkerProbeTimeoutError,
and the classifier is a plain instanceof — one truth, same shape as
DaemonWorkerAuthenticationError.
The recovery SIGKILL removal applies to every failure class, not just probe
timeouts. Verified: a live verified-identity failed worker IS reclaimed by the
next fresh create (graceful stop), so the true leak is only the unverifiable
survivor — the deliberate fail-safe. One comment at the decision point and a
changelog line make the tradeoff explicit.
@snimu
snimu force-pushed the fix/daemon-recovery-hardening branch from fe1062a to c86123d Compare September 1, 2026 11:13
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts Outdated
The carve ran recoverUncertainWorkerOperations (interruption marking, orphan
reaping) before the identity-gated SIGKILL, so destructive cleanup hit a
still-active worker; pre-rebase the kill preceded cleanup inside the old
helper. Reordered: recheck + SIGKILL + bounded teardown wait first, cleanup
only after the predecessor is confirmed gone/replaced, and an unverifiable
survivor parks failed with NO destructive cleanup at all (matching recovery's
rule that a possibly-live worker is never cleaned destructively). The carve
pin asserts the order and the no-cleanup branch.
proper-lockfile verified: compromise detection is timer-driven (mtime updates),
and release() removes the lockfile unconditionally when the steal has not been
detected yet — so a caller resuming from a stall past the stale threshold could
delete the successor'"'"'s lock. The guard directory'"'"'s inode is the ownership
identity (a steal is rmdir+mkdir): it is captured at acquisition and checked
synchronously where the timer cannot run — before returning an action'"'"'s result
and before release. A stolen-but-undetected guard is left to its own updater,
which notices the foreign mtime and cleans itself. A truly synchronous stall
still cannot be preempted mid-action; that limitation is stated at the guard.
@snimu
snimu merged commit 74c8d39 into main Sep 1, 2026
33 of 35 checks passed
@snimu
snimu deleted the fix/daemon-recovery-hardening branch September 1, 2026 11:54
olety added a commit to oneiron-dev/prime-agent that referenced this pull request Sep 1, 2026
Takes upstream's event-driven supervisor roster ledger + push (PrimeIntellect-ai#1897, PrimeIntellect-ai#1900),
direct TUI<->worker transport (ENG-5817), daemon startup/recovery hardening
(PrimeIntellect-ai#1929, PrimeIntellect-ai#1909), single-dump kernel snapshots (PrimeIntellect-ai#1945), empty-draft eviction
(PrimeIntellect-ai#1946), rlm_child_update suppression (PrimeIntellect-ai#1944), bash-skill preview (PrimeIntellect-ai#1911).

Fork laws re-expressed on the roster architecture:
- stable-target follow-up honesty kept (capability proof via worker hello,
  target_unavailable never not_found when ownership unproven)
- schema revision 26 (union of fork rev-24 stable-target + upstream
  rev-24/25 roster+transport); digest minted by the repo's own algorithm
- summary freshness reuse + single-flight + staleness + root-omission
  rejection restored on upstream's refresh pull
- adoption/recovery never fails a live worker on a slow or root-omitting
  catalog: get_state root seed + stale mark + bounded background rehydration
- repl.py keeps fork prune-on-aggregate-overflow
- delete handlers keep fork persistence reporting; eviction fence test keeps
  the stronger two-worker contention variant

Known test debt (deferred to post-Wave cleanup per owner): roster-era fixture
migrations in daemon-supervisor-monitor (2), plus un-triaged failures in
package-command-paths, agent-session-recursion, daemon-runtime-stress,
4600-supervisor-singleton, 4603-worker-recovery, 4606-update-restart-
coordinator, agents-view-roster. Production laws preserved; failures are
fixture-era artifacts or mechanism assertions to rewrite.
ketema added a commit to ketema/prime-agent that referenced this pull request Sep 1, 2026
- Direct session transport between TUI and worker (ENG-5817, PrimeIntellect-ai#1926)
- Event-driven supervisor agent roster with push subscriptions (PrimeIntellect-ai#1897, PrimeIntellect-ai#1900, PrimeIntellect-ai#1895)
- Hardened daemon startup, recovery ownership, and worker launch diagnostics (PrimeIntellect-ai#1929, PrimeIntellect-ai#1918)
- Python REPL runtime single-dump snapshots and bash preview tool (PrimeIntellect-ai#1945, PrimeIntellect-ai#1911)
- Non-blocking RLM subagent deletion and snapshot update suppression (PrimeIntellect-ai#1954, PrimeIntellect-ai#1944)
- Saved catalog loading on Agents View open (PrimeIntellect-ai#1960)
- Advanced Anthropic prompt caching marker across tool results (PrimeIntellect-ai#1927)
- TUI process replacement on update and empty draft eviction (PrimeIntellect-ai#1631, PrimeIntellect-ai#1946, PrimeIntellect-ai#1920)
paralin pushed a commit to paralin/prime-agent that referenced this pull request Sep 2, 2026
…eIntellect-ai#1929)

* fix(coding-agent): harden daemon recovery and socket ownership

* fix(coding-agent): address daemon recovery review findings

* fix(coding-agent): preserve prompt admission scheduling

* test(coding-agent): consolidate daemon recovery coverage

* fix(coding-agent): preserve safe shutdown outcomes

* fix(coding-agent): close recovery admission gaps

* refactor(coding-agent): one process-identity oracle for recovery verdicts

Adoption and recovery classify liveness through processIdentity() instead of
re-pairing isProcessAlive+getProcessStartId at each site; raw start-id reads
remain only where the value itself is persisted. The sole surviving kill of a
live worker is the identity-verified pre-roster replacement, now pinned:
adoption of a live pre-roster worker still kills-and-relaunches, while an
unverifiable identity still parks failed with no replacement.

* fix(coding-agent): bound live-worker probing at ten defer rounds

A hung-but-alive worker (or a live pid with no verifiable start id) probed
forever: ~11s probe pass, 5s defer, repeat. After MAX_DEFERRED_RECOVERY_ROUNDS
(10 rounds, ~2.5 minutes) the worker parks failed — user-visible through the
existing roster failed status — with its process left alive for a manual
retry_worker, which resets the round count like any successful recovery.

* refactor(coding-agent): type the worker probe timeout

isDaemonWorkerProbeTimeout classified by message prefixes defined in two
other files. The timeout sites in daemon-worker-client.ts (hello, response)
and the supervisor's connect deadline now throw DaemonWorkerProbeTimeoutError,
and the classifier is a plain instanceof — one truth, same shape as
DaemonWorkerAuthenticationError.

* docs(coding-agent): state the leak-over-kill recovery tradeoff

The recovery SIGKILL removal applies to every failure class, not just probe
timeouts. Verified: a live verified-identity failed worker IS reclaimed by the
next fresh create (graceful stop), so the true leak is only the unverifiable
survivor — the deliberate fail-safe. One comment at the decision point and a
changelog line make the tradeoff explicit.

* fix(coding-agent): kill before cleanup in the pre-roster replacement

The carve ran recoverUncertainWorkerOperations (interruption marking, orphan
reaping) before the identity-gated SIGKILL, so destructive cleanup hit a
still-active worker; pre-rebase the kill preceded cleanup inside the old
helper. Reordered: recheck + SIGKILL + bounded teardown wait first, cleanup
only after the predecessor is confirmed gone/replaced, and an unverifiable
survivor parks failed with NO destructive cleanup at all (matching recovery's
rule that a possibly-live worker is never cleaned destructively). The carve
pin asserts the order and the no-cleanup branch.

* fix(coding-agent): never release a registry guard a successor reclaimed

proper-lockfile verified: compromise detection is timer-driven (mtime updates),
and release() removes the lockfile unconditionally when the steal has not been
detected yet — so a caller resuming from a stall past the stale threshold could
delete the successor'"'"'s lock. The guard directory'"'"'s inode is the ownership
identity (a steal is rmdir+mkdir): it is captured at acquisition and checked
synchronously where the timer cannot run — before returning an action'"'"'s result
and before release. A stolen-but-undetected guard is left to its own updater,
which notices the foreign mtime and cleans itself. A truly synchronous stall
still cannot be preempted mid-action; that limitation is stated at the guard.

---------

Co-authored-by: Sebastian <sebastian@primeintellect.ai>
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