fix(claim-reaper): distinguish COMPLETED engineer from wedged to end re-archival churn (#4467, #4500, #4464) - #4712
Conversation
… to end re-archival churn (#4467, #4500, #4464) The stale-engineer liveness probe classified a worktree purely by newest-file mtime, so a cleanly-COMPLETED engineer (whose worktree naturally stops writing files) was indistinguishable from a WEDGED one. It was labelled `HeartbeatStale` and driven into the investigate-before- reap path every tick — re-archiving evidence forever. Goal `advance-rysweet-agent-kgpacks-rs-to-full-parity-f29bb15c` was archived 54× over ~3.7 days (up from the 10× recorded in #4467) with monotonically climbing idle_age and never converging. The archived evidence shows the engineer's own session record was `{"status":"completed"}` — a positively completed run, not a wedge. This is the completed-vs-wedged diagnostic conflation of #4500 (closed but recurring) compounded by the leaked- claim-on-completion path of #4464. Fix: the `WorktreeClaimLivenessProbe` now reads the engineer's terminal session record (`.claude/runtime/sessions.jsonl`). When the LAST status is `completed`, the claim is classified `DeadReason::Completed` (a positive terminal signal). The reaper reclaims a `Completed` worktree DIRECTLY — releasing the leaked claim and removing the residual worktree exactly once, with no agentic investigation — which terminates the unbounded re-archival churn. The same idle THRESHOLD guards it, so a completion whose claim- release / cleanup is still in flight is never raced, and a worktree a NEW session has re-entered (last status back to `active`) stays on the ordinary path. Fail-closed: any read/parse error or absent record ⇒ NOT completed (safe investigate-before-reap path preserved). Tests: completed worktree reclaimed without investigation; fresh completed worktree protected by the threshold; probe returns Completed only when the terminal status is `completed`, not when re-entered/active or when the sessions file is absent; label stability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Coverage Summary
Coverage data from CI run. Test files matching |
rysweet
left a comment
There was a problem hiding this comment.
Step 17b — Comprehensive Code Review (PR #4712)
Scope reviewed: src/overseer/claim_reaper.rs (+254 / −9). Validation run locally on head 8fa7f62:
cargo clippy --lib— clean, no new warningscargo test --lib overseer::claim_reaper— 41 passed, 0 failed
Verdict: The implementation is correct, well-tested, and philosophy-aligned (ruthless simplicity, fail-closed, zero swallowed errors). No code defects found. Findings below are one traceability question and two minor/optional notes.
✅ Strengths
- Correct root-cause fix. Distinguishing a positively-
completedengineer (DeadReason::Completed) from aHeartbeatStaleone directly severs the completed-vs-wedged conflation that drove the 54× re-archival churn (#4467/#4500/#4464). A completed worktree naturally stops writing files, so mtime alone mislabels it stale — reading the engineer's own terminal session record is the right signal. - Fail-closed parsing.
engineer_session_completedreturnsfalseon missing file, read error, parse error, or nostatusrecord. Acompletedverdict must be positively proven — being wrong only in thefalsedirection keeps the safe investigate-before-reap path. Verified byprobe_not_completed_when_no_sessions_file. - Re-entry handling. Uses the last status record, so a worktree a new session re-entered (
statusback toactive) correctly staysHeartbeatStale. Verified byprobe_not_completed_when_last_status_is_active. - Threshold guard reused. The
Completedarm applies the sameage > stale_secsboundary asHeartbeatStale, so a just-finished worktree whose claim-release/cleanup is still in flight is protected (t3d_fresh_completed_worktree_is_not_reclaimed). Boundaryage == thresholdprotected. - Shared release chokepoint. Completed reclaim maps
verdict = Noneand flows throughrelease_engineer_claim+ best-effort cleanup, identical toNoWorktree. No hand-rolled SQL; release/cleanup errors contained so one bad row never aborts the sweep. - Test coverage is strong: positive, boundary, fail-closed, no-file, re-entered, direct-reclaim-without-investigation, and label stability all covered.
🟠 F1 (traceability — needs author confirmation, not a code defect)
The Step 17b requirements handed to this review (R1–R7) specify a different remediation: persist a WipRef { kind: "merged_pr" } and consume it in GhCliEvidenceSource::any_pr_merged, editing src/ooda_loop/cycle.rs, src/goal_curation/completion_gate.rs, and src/goal_curation/types.rs. This PR touches none of those files — it solves the same symptom (unbounded re-archival / BLOCKED churn) one layer down, at the claim-reaper.
These are complementary, not conflicting: the reaper fix stops re-investigating completed worktrees; the merged-PR-evidence fix (R1–R7) stops the completion gate from failing to recognize merged PRs. Please confirm whether (a) R1–R7 is a separate parallel workstream, or (b) this PR is intended to replace that approach. If (b), the requirement doc should be updated so the merged-PR-evidence work isn't later duplicated.
🟡 F2 (design note — intentional, worth documenting the assumption)
The Completed path reclaims without archiving worktree evidence (unlike the investigate-before-reap path for HeartbeatStale). The comment justifies this as "durable output is already committed/pushed." This is correct and is precisely the fix for the wasteful repeated archival in #4467. The residual risk: a session that wrote status=completed but whose push failed would have its worktree removed without evidence capture. Given completed is a positive terminal signal and the whole bug was excessive archival, the tradeoff is sound — recommend only that the "output already pushed" assumption stay documented (it currently is, in the L360 comment). No change required.
🔵 F3 (nit — optional)
A completed reclaim emits two log lines: the info! at ~L371 ("engineer session COMPLETED … reclaiming directly") and the standard fail-visible warn! reclaim line at ~L421 (verdict=no-investigation). Slightly redundant. Harmless — the info line adds churn-context — but could be dropped if log volume matters.
Checklist:
- Code quality and standards — clean, idiomatic, well-commented
- Test coverage adequate — 41 tests incl. positive/boundary/fail-closed/re-entry
- No TODOs, stubs, or swallowed exceptions — errors counted + logged, none swallowed
- No unimplemented functions
- Logic correctness — verified reclaim path, threshold guard, last-status semantics
- Edge case handling — no file, parse error, re-entered session, boundary age, in-flight cleanup
Recommendation: Approve on code merits; resolve F1 traceability before merge so the R1–R7 merged-PR-evidence track isn't inadvertently dropped.
rysweet
left a comment
There was a problem hiding this comment.
Step 17c — Security Review (PR #4712)
Scope: src/overseer/claim_reaper.rs (+254 / −9) on head 8fa7f622. Reviewed the new DeadReason::Completed path and the engineer_session_completed() filesystem/JSON seam introduced by this change. This is a self-contained internal reaper module — no network, auth, or user-request surface — so the review focuses on input handling, filesystem safety, injection, DoS, and information disclosure.
Security requirements — verified
- No new injection surface. No shell/exec, no SQL, no command construction, no format-string-into-command. The only new inputs are read from a local
sessions.jsonland parsed withserde_json(data-only). ✅ - No path traversal from untrusted input. The
worktreepassed toengineer_session_completed()is never derived by interpolating the untrustedclaim_keyinto a path.assess()enumerates real directory entries under the controlled<state_root>/engineer-worktrees/root (read_dir+goal_id_from_worktree_dirmatch), andengineer_session_completed()only appends the fixed literal suffix.claude/runtime/sessions.jsonl. A hostileclaim_keycannot escape the root or select an arbitrary file. ✅ - Sensitive-data handling. The new file read extracts only the
statusstring field; message/tool/credential content in the session record is ignored and never logged. Newtracinglines emit onlyclaim_key(a goal identifier, already logged elsewhere) — no secrets, tokens, paths-with-secrets, or file contents. ✅ - AuthZ/AuthN. N/A — internal reaper with no principal/permission boundary; the change adds no privilege decision.
- Memory-safety / panics. All fallible operations use
let-else/matchreturning early; nounwrap/expect/indexing on the untrusted read+parse path. Malformed lines are skipped, not fatal. ✅
Fail-closed posture (security-positive)
The design is fail-closed in the safe direction: any missing file, IO error, parse error, or absent status record ⇒ false ⇒ the worktree stays on the ordinary investigate-before-reap (HeartbeatStale) path. A Completed short-circuit-reclaim is only taken when positively proven from the engineer's own terminal record, AND still gated by the same idle stale_secs threshold (T3d). This prevents a forged/partial/absent record from causing a premature reclaim — the worst an attacker controlling sessions.jsonl could do is suppress the optimization (force the slower, evidence-preserving path), not trigger destructive early cleanup of a live claim. ✅
Findings
S1 — LOW / informational — unbounded read_to_string on sessions.jsonl.
engineer_session_completed() calls std::fs::read_to_string(&sessions), loading the entire file into memory. sessions.jsonl is written by the trusted local agent runtime inside our own state root, so this is not a realistic external DoS vector today. However, an unbounded append-only log read on every reaper tick is a latent memory/CPU cost if the file ever grows large (long-lived worktree). Only the last status record matters. Suggested (non-blocking) hardening: cap the read (e.g. read the tail / a bounded number of bytes) or stream line-by-line with a size guard, so a pathologically large log cannot balloon reaper memory. No action required for merge.
S2 — INFO — trust boundary of sessions.jsonl is intra-root.
The completion signal is sourced from a file inside the engineer's own worktree. This is the correct trust model (the engineer attests its own completion), and the fail-closed gating (above) means a corrupted record cannot escalate into an unsafe reclaim. Documenting for traceability: the security guarantee rests on stale_secs gating + positive-proof, not on the integrity of sessions.jsonl.
Verdict
No exploitable vulnerabilities found. No new injection, no path traversal, no sensitive-data exposure, no unsafe panics. Input handling is fail-closed and defensively parsed. Only a single LOW/informational hardening note (S1, bounded read) and one trust-model note (S2), neither blocking.
Security review submitted as evidence for Step 17c. Approve on security merits.
Step 17d — Philosophy Guardian Review (PR #4712)Scope: Compliance checklist
Notes (non-blocking)
Cross-reference to Step 17b (F1)The prior code review flagged that Step 17b's stated R1–R7 requirements target Compliance status: PASS. No philosophy-driven changes required before merge. |
Step 17e — Blocking-Issue Resolution (PR #4712)Reviewed all findings from the code (17b), security (17c), and philosophy (17d) reviews. Classified by the reviewers' own severities:
F1 resolved — scope decision (not a code defect)F1 flagged that the requirements framing (R1–R7) named a merged-PR-evidence fix touching
Conclusion: F1 is resolved as a scope decision. The claim-reaper approach is the chosen, targeted fix; the multi-file evidence-gate variant (R1–R7) is superseded by it for this defect. No code change required. Non-blocking items (deferred, tracked)
VerdictNo blocking issues. All three reviews approve on merits; the sole pre-merge item (F1) is resolved as a scope decision above. Validation on head |
Step 18b — Review Feedback Implementation (PR #4712)All three Step 17 reviews (Code / Security / Philosophy) approve on merits. Disposition of every feedback item below.
Blocking issues: 0 openF1 (the only pre-merge item) is closed as an intentional scope decision. Required code changes for merge: noneHead unchanged at Validation (head
|
…-1785025251, idle 31238s) Recurrence of #4437 for the same perpetual research goal. Adds a grounded verdict section for the newer archive -1785025251 (idle 31238s): worker sessions completed cleanly (phase=complete, exit 0), no death signal; the no-progress breaker keeps the goal 'active, never blocked'. Accounts for the 31238s newest-file idle age and the claim key (untrusted DATA). Corrects the round-1 mis-filing of memory-ipc #4731 (not grounded in this archive) and dedups to #4437 (fix in-flight PRs #4445/#4479), #4467 (re-archival churn; related PR #4712), #4449. Fail closed: claim + worktree preserved, nothing reaped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Problem
The stale-engineer claim-reaper classified a worktree purely by its
newest-file mtime, so a cleanly completed engineer — whose worktree
naturally stops writing files once its run ends — was indistinguishable from a
wedged one. It was labelled
HeartbeatStaleand driven into theinvestigate-before-reap path every tick, re-archiving evidence forever
without ever converging.
Concrete evidence — goal
advance-rysweet-agent-kgpacks-rs-to-full-parity-f29bb15c:~/.simard/reaped-engineers/…-f29bb15c-*over ~3.7 days (up from the 10× recorded in claim-reaper HeartbeatStale investigation never converges: recipe verdict discarded (reconcile_inflight_investigations write-back gap) → Pending-only, reap path unreachable, unbounded re-archival #4467), each with a
monotonically climbing
idle_ageand never reaped.…-1785015794/evidence.txt) shows the engineer's ownsession record was
{"session_id":"…","status":"completed","end_time":…}—a positively completed run, not a wedge (
idle_age 20520s= exactlyarchived_ts − end_time).This is the completed-vs-wedged diagnostic conflation of #4500 (closed but
demonstrably recurring — a regression), compounded by the
leaked-claim-on-completion path of #4464, and it is the concrete churn
tracked by #4467 ("unbounded re-archival").
Fix
WorktreeClaimLivenessProbe::assessnow reads the engineer's terminal sessionrecord (
.claude/runtime/sessions.jsonl). When the laststatusiscompleted, the claim is classifiedDeadReason::Completed— a positiveterminal signal, distinct from
HeartbeatStale.The reaper reclaims a
Completedworktree directly — releasing the leakedclaim and removing the residual worktree exactly once, with no agentic
investigation — which terminates the unbounded re-archival churn. Safety is
preserved:
claim-release / cleanup is still in flight is never raced.
active)stays on the ordinary path.
not completed → the safe investigate-before-reap path is preserved. A
completedverdict must be positively proven from the engineer's own record.This layer is complementary to the existing in-flight fixes: it operates at the
liveness probe (before classification), distinct from the reconcile
write-back (#4489/#4467) and the perpetual-goal exemption (#4479/#4437).
Tests
t3c_completed_worktree_is_reclaimed_without_investigation— a completed +long-idle worktree is reclaimed once and never investigated (the
churn-breaker invariant).
t3d_fresh_completed_worktree_is_not_reclaimed— completed but under the idlethreshold ⇒ protected.
probe_reports_completed_when_session_terminal_status_completed— probereturns
Completedfor the exact archived two-lineactive→completedshape.probe_not_completed_when_last_status_is_active— re-entered worktree staysHeartbeatStale.probe_not_completed_when_no_sessions_file— absent record fails closed.All 41
overseer::claim_reapertests pass;cargo fmt+cargo clippy --release -D warningsclean (commit + push gates green).Refs #4467, #4500, #4464.