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
claim-reaper is not single-writer: concurrent same-host simard daemons race the claim ledger → naive no-worktree reclaim overrides another daemon's in-flight investigate-before-reap (false-reclaims a live engineer mid-recreate) #4477
The claim-reaper sweep (overseer::claim_reaper::reap_stale_claims) is not single-writer / not leader-gated. It mutates the shared engineer_claims ledger (release + worktree cleanup) with no lease acquired before the sweep. When more than one simard daemon runs on the same host/state-root — which is happening right now on ia2 (two live daemons: PID 1332853 and PID 1664591) — both sweep the same ledger concurrently and there is no mutual exclusion. A naive daemon's NoWorktreeimmediate-reclaim (which by design skips investigate-before-reap) then overrides another daemon's careful in-flight Pending investigation, false-reclaiming a live engineer during a transient worktree-recreate window.
Verdict of the investigation that surfaced this: still-alive false positive — the engineer was NOT dead; it was mid-relaunch.
The reclaim fired on a transient worktree absence. The engineer's new worktree was born 2026-07-22 20:45:05 (stat …/advance-…-f29bb15c-1784753105-3b2899, Birth ts) — ~9 min AFTER the 20:35:42 no-worktree reclaim. So NoWorktree at 20:35 was a worktree being re-created, not a dead engineer.
Live duplicate-daemon confirmation at investigation time:
(the careful investigator PID 2802920 has since exited).
Root cause
reap_stale_claims (src/overseer/claim_reaper.rs:282) iterates ledger.list_engineer_claims() and, on ClaimLiveness::Dead { NoWorktree } (lines 308-311, 332-333), reclaims unconditionally (verdict = None, logged verdict=no-investigation) — no investigation, no evidence preservation. That branch is correct for a genuinely leaked slot, but the sweep holds no leader lease, so a second concurrent daemon can execute it against a claim that another daemon is deliberately keeping (Pending) while its engineer re-creates its worktree. There is a classic TOCTOU/last-writer-wins hazard across daemons: one is patiently investigating, the other reclaims.
The reaper acquires no lock (grep for lock|lease|flock|Mutex in claim_reaper.rs finds none in the sweep path).
Fix design (single-writer / leased reaper)
Gate the whole sweep behind a fenced, same-host leader lease. Acquire a leader token (reuse/repair self_relaunch_semaphore::LeaderSemaphore, whose own read→decide→write race is tracked in LeaderSemaphore: concurrent try_acquire race + fencing-test overclaims same-host mutual exclusion #2947 and must be fixed first — O_EXCL/flock/CAS + fsync, unique tmp path) at the START of reap_stale_claims; return a no-op ReapSummary if the lease is not held. Exactly one daemon reaps per host/state-root.
Summary
The claim-reaper sweep (
overseer::claim_reaper::reap_stale_claims) is not single-writer / not leader-gated. It mutates the sharedengineer_claimsledger (release + worktree cleanup) with no lease acquired before the sweep. When more than onesimarddaemon runs on the same host/state-root — which is happening right now onia2(two live daemons: PID1332853and PID1664591) — both sweep the same ledger concurrently and there is no mutual exclusion. A naive daemon'sNoWorktreeimmediate-reclaim (which by design skips investigate-before-reap) then overrides another daemon's careful in-flightPendinginvestigation, false-reclaiming a live engineer during a transient worktree-recreate window.Verdict of the investigation that surfaced this: still-alive false positive — the engineer was NOT dead; it was mid-relaunch.
Evidence (durable archive)
Goal
advance-rysweet-agent-kgpacks-rs-to-full-parity-f29bb15c, archive/home/azureuser/.simard/reaped-engineers/rysweet_Simard_advance-rysweet-agent-kgpacks-rs-to-full-parity-f29bb15c-1784755469/(manifest.json,evidence.txt,journal.txt).Two distinct daemon PIDs act on the SAME
claim_keyin the same journal:NOT reaping … (investigation verdict=pending, claim + evidence preserved)from16:05:12through20:04:40, idle escalating3196 → 8452 → 12894 → 17564.20:35:42:WARN … claim-reaper: reclaimed … (reason=no-worktree, age=n/a, verdict=no-investigation).The reclaim fired on a transient worktree absence. The engineer's new worktree was born
2026-07-22 20:45:05(stat …/advance-…-f29bb15c-1784753105-3b2899, Birth ts) — ~9 min AFTER the20:35:42no-worktree reclaim. SoNoWorktreeat20:35was a worktree being re-created, not a dead engineer.Live duplicate-daemon confirmation at investigation time:
(the careful investigator PID
2802920has since exited).Root cause
reap_stale_claims(src/overseer/claim_reaper.rs:282) iteratesledger.list_engineer_claims()and, onClaimLiveness::Dead { NoWorktree }(lines308-311,332-333), reclaims unconditionally (verdict = None, loggedverdict=no-investigation) — no investigation, no evidence preservation. That branch is correct for a genuinely leaked slot, but the sweep holds no leader lease, so a second concurrent daemon can execute it against a claim that another daemon is deliberately keeping (Pending) while its engineer re-creates its worktree. There is a classic TOCTOU/last-writer-wins hazard across daemons: one is patiently investigating, the other reclaims.The reaper acquires no lock (grep for
lock|lease|flock|Mutexinclaim_reaper.rsfinds none in the sweep path).Fix design (single-writer / leased reaper)
self_relaunch_semaphore::LeaderSemaphore, whose own read→decide→write race is tracked in LeaderSemaphore: concurrent try_acquire race + fencing-test overclaims same-host mutual exclusion #2947 and must be fixed first —O_EXCL/flock/CAS +fsync, unique tmp path) at the START ofreap_stale_claims; return a no-opReapSummaryif the lease is not held. Exactly one daemon reaps per host/state-root.fenceEpochintoClaimLedger::release_engineer_claimso a stale-lease holder's DELETE is rejected (defence-in-depth if two daemons briefly overlap during handoff). Aligns with the fencing hardening in Live-migration KEYSTONE hardening: lease-record integrity (write-authorization + tamper-evidence + fenceEpoch durability/monotonic-restore) #2945.NoWorktreebranch race-safe even single-writer: require the worktree to be absent across a short confirm-twice / debounce (or a minimum grace since last-known worktree mtime) before treatingNoWorktreeas a leaked slot, so a mid-recreate gap is not read as death. (Complements Engineer claim not released on session completion → leaked claim + 'goal disappeared before effect dispatch' (DownstreamFailed) no-worktree churn #4464, which tracks the single-daemon completion→claim-release side of the sameno-worktreechurn.)Deduplication (explicitly NOT a duplicate)
This is the concurrency amplifier that none of the following cover; each is cross-referenced:
Pending-only). Orthogonal state-machine bug; this issue is about two daemons racing, not the verdict loop.no-worktreechurn. Same symptom (reason=no-worktree), different cause (leaked-on-complete vs concurrent-daemon override during recreate).evidence.txthere is exactly those repo fixtures — noted, tracked there).simarddaemons running onia2at all? (supervisor/relaunch gap worth its own investigation).Acceptance
reap_stale_claimsacquires a fenced same-host leader lease; a second concurrent daemon's sweep is a proven no-op (test with two ledger handles).NoWorktreereclaim is debounced so a worktree recreate window cannot trigger a reclaim.print!/eprintln!added; structuredtracing+ OTel only.