Skip to content

[stack 4/8] fix(daemon): fence worker and supervisor lifecycle state - #1161

Open
sethkarten wants to merge 11 commits into
stack/external-03-persistencefrom
stack/external-04-daemon-foundations
Open

[stack 4/8] fix(daemon): fence worker and supervisor lifecycle state#1161
sethkarten wants to merge 11 commits into
stack/external-03-persistencefrom
stack/external-04-daemon-foundations

Conversation

@sethkarten

@sethkarten sethkarten commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Stack 4/8 — fix(daemon): fence worker and supervisor lifecycle state

Active review snapshot — do not merge yet. The complete stack is open for architecture/design review, while final cumulative audit, CI, Cursor Bug Bot, and Macroscope findings are being remediated. Branches will be force-updated after validation.

Base: stack/external-03-persistence
Review order: merge only after the preceding stack layer is accepted. This PR is not intended to merge independently out of order.

Stack navigation

  1. #1158 — ci: harden verification and release compatibility
  2. #1159 — fix(security): harden session and autonomous execution boundaries
  3. #1160 — fix(coding-agent): make persisted state crash-safe
  4. #1161 — fix(daemon): fence worker and supervisor lifecycle state
  5. #1162 — fix(coding-agent): repair queued and archived session lifecycle
  6. #1163 — fix(coding-agent): complete Windows kernel and daemon startup
  7. #1164 — fix(providers): harden MCP OAuth and Codex transports
  8. #1165 — fix(runtime): bound transcript and autonomous recovery

Summary

  • Prevent PID reuse or unverifiable process identity from signaling unrelated workers.
  • Generation-fence and renew supervisor launch locks across reclaim, release, and spawn races.
  • Refresh registry ownership safely and preserve healthy cached heartbeat snapshots.

Validation

  • npm run check; final focused daemon/ownership/heartbeat/session-lease suite 34/34.

Provenance

  • Authored independently from upstream/main using issue reports and PR descriptions/comments only.
  • No external contributor branch, diff, commit, implementation code, or test code was fetched, inspected, copied, or reused.
  • The implementation and regression tests in this stack are maintainer-owned.

Linked-item disposition

Fixed on merge

Independently superseded pull requests

Reviewer notes

  • Please review this layer against its immediate stack base, not against main, to avoid cumulative duplicate diffs.
  • No merge is requested; the complete stack is being left for human review.

@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 1 potential issue.

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 f9f6348. Configure here.

} catch (error) {
this.log(`could not verify supervisor launch lock: ${String(error)}`);
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Launch lock false release race

High Severity

assertCurrentLockGeneration treats lockSync guard contention as generation loss and returns false, while the refresh path retries the same error. Callers then abort and finally still runs removeSupervisorLaunchLockGeneration, so a live holder can delete its own valid lock while a replacement may already be spawning. Concurrent workers noticing a dead supervisor can then launch a second supervisor or leave recovery abandoned.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f9f6348. Configure here.

@sethkarten
sethkarten force-pushed the stack/external-04-daemon-foundations branch from f9f6348 to e669d07 Compare August 10, 2026 19:23
const now = Date.now();
if (forceIdentityCheck || lastStatus === undefined || now >= nextIdentityCheckAt) {
nextIdentityCheckAt = now + identityPollIntervalMs;
const observedProcessStartId = getProcessStartId(descriptor.pid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High daemon/daemon-supervisor.ts:549

createWorkerProcessIdentityProbe can return "match" for a PID that has been reused by an unrelated process. On macOS/BSD, getProcessStartId falls back to ps -o lstart, which has whole-second precision. If a worker exits and another process reuses the same PID within that same second, the observed start-time strings are equal, so status() returns "match" and isAlive() returns true. Callers then treat the unrelated process as the original worker and may send SIGTERM/SIGKILL to it. Consider treating a coarse ps:-based identity as "unknown" rather than "match", or using a higher-resolution identity source so PID reuse within the same second is detected.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/daemon-supervisor.ts around line 549:

`createWorkerProcessIdentityProbe` can return `"match"` for a PID that has been reused by an unrelated process. On macOS/BSD, `getProcessStartId` falls back to `ps -o lstart`, which has whole-second precision. If a worker exits and another process reuses the same PID within that same second, the observed start-time strings are equal, so `status()` returns `"match"` and `isAlive()` returns `true`. Callers then treat the unrelated process as the original worker and may send `SIGTERM`/`SIGKILL` to it. Consider treating a coarse `ps:`-based identity as `"unknown"` rather than `"match"`, or using a higher-resolution identity source so PID reuse within the same second is detected.

if ((reclaimError as NodeJS.ErrnoException).code !== "ENOENT") {
throw reclaimError;
return current;
} catch (error) {

@varindersingh83 varindersingh83 Aug 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This catch path gives a registry-guard error the same false result as proven generation loss. A transient contention or I/O error can therefore abort launch, after which finally may remove the still-valid generation.

Could this return distinct current, lost, and unknown/error outcomes, with bounded retry for the last case, and only release after a guarded read proves ownership loss?

cc @sethkarten @snimu

lastStatus =
observedProcessStartId === undefined
? "unknown"
: observedProcessStartId === descriptor.processStartId

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The portable ps -o lstart identity has whole-second precision, but equality here is treated as sufficient authorization for later SIGTERM/SIGKILL paths. If a PID is reused within that second, an unrelated process can compare as match.

Could ps: identities be treated as coarse and fail closed for destructive signalling, with a regression covering same-second PID reuse?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants