Skip to content

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

Description

@rysweet

Summary

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 NoWorktree immediate-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.

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_key in the same journal:

  • PID 2802920 (careful): 7× NOT reaping … (investigation verdict=pending, claim + evidence preserved) from 16:05:12 through 20:04:40, idle escalating 3196 → 8452 → 12894 → 17564.
  • PID 1664591 (naive), 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 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:

$ ps -eo pid,comm | awk '$2=="simard"'
1332853 simard
1664591 simard

(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)

  1. 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.
  2. Fence the release chokepoint. Carry the lease fenceEpoch into ClaimLedger::release_engineer_claim so 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.
  3. Make the NoWorktree branch 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 treating NoWorktree as 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 same no-worktree churn.)

Deduplication (explicitly NOT a duplicate)

This is the concurrency amplifier that none of the following cover; each is cross-referenced:

Acceptance

  • reap_stale_claims acquires a fenced same-host leader lease; a second concurrent daemon's sweep is a proven no-op (test with two ledger handles).
  • NoWorktree reclaim is debounced so a worktree recreate window cannot trigger a reclaim.
  • No structured print!/eprintln! added; structured tracing + OTel only.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions