reap.py: no agent + no transcript is two unknowns agreeing, and a live tmux session does not block it nf-0e9 ← Beads

open priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/683 by tools/beads/import_github.py's reconcile pass.

claude agents --json was observed dropping a live session on nf-dev-sg — reported on #641 by the session reconciling worktrees by hand:

> One invocation returned 6 sessions, omitting nf-dev-reaper — which was alive the whole time (pid 186489, tmux dev-reaper present, PR #647 in its status bar, actively working). Five invocations ~90s later all returned 7 and included it. > > …I was mid-way through deciding whether ~/nf/devs/reaper was orphaned litter. Had I trusted that one sample, the by-hand version of reap.py would have killed a session that was working an open PR.

Observed once, not reproduced in five subsequent attempts. No diagnosis is claimed — only that absence from a single sample is not proof of absence.

What is already covered (verified against 8238cfd, not assumed)

nflib.sessions.check_liveness blocks on recent transcript activity before it reads the agent list at all (#647 F1), so the reported case is refused:

``` dropped from agents, transcript 20s old → BLOCK "active 0m ago, under the 30m quiet threshold … and claude agents lists nothing here, so the process list is not the whole story" ```

A working session writes transcript lines every few seconds, so the very property that makes a false-absent dangerous is what makes the second signal loud. That is the cross-check #641 asks for, via ~/.claude/projects/<slug>/*.jsonl rather than tmux.

The gap

One combination still returns PASS:

``` no agent listed + no transcript found → PASS "no session is running here" ```

last_active is None is permissive by design — a long-dead session has no transcript either, and blocking on that would make reap.py reap nothing, which is the failure mode that makes a reaper worthless. The measurement above changes the other half of the conjunction though: an empty agent list is no longer *proof* of absence, so this is now two unknowns agreeing rather than two facts.

⚠️ And tmux is consulted only as a kill lever, never as a liveness checktmux_gone() proves death *after* kill-session, but a live dev-<topic> session sitting there does not stop the verdict reading REAPABLE.

Reachable only with a false-absent and an unfindable transcript (a workspace slug that does not resolve, ~/.claude/projects moved, /tmp-style wipe of the projects dir). Narrow, unreproducible today, and the failure mode is the worst one this tool has: unrecoverable, and it looks like a successful cleanup.

Shape of the fix

In check_liveness, before the if not session.agents: PASS exit:

```python if session.last_active is None and session.tmux and not session.agents: return Check("live session", UNPROVEN, f"{session.tmux} is a live tmux session, nothing is listed in " "claude agents, and no transcript was found — three sources " "and not one of them can speak") ```

Session.tmux is already populated by survey() and tmux_gone() already exists for the post-kill proof, so the plumbing is there. UNPROVEN rather than BLOCK keeps it out of the reaper's automatic path while leaving the manual git worktree remove --force line available — the same treatment the missing-ledger case gets (#618's shape).

Two judgement calls deliberately left open
  1. Is the transcript a sufficient second signal, or should tmux/pid be consulted routinely rather than only in this corner? The transcript is per-*workspace* and survives a session missing from the process list entirely, which is why it was chosen; tmux is per-*session* and closer to the thing being killed. They fail differently and a reaper could reasonably want both.
  2. Does #635's audit_sessions.py want the same treatment? It samples the same source. It reports rather than kills, so its exposure is smaller — but a false-absent there prints GHOST against a live session, which is how a human decides to kill one by hand. That was the path the #641 reporter was on.

Filed by session:reaper at wrap-up, from another session's measurement. Unclaimed.

Dependencies

None.

Comments

No comments.

Add a comment