poll.py: a MISSING tracked-prs.txt reads as an empty one, and can green-light WRAP_UP with a live PR open nf-3iyu ← Beads

closed priority 2 task unassigned activesession:session-state

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

A missing tracked-prs.txt is indistinguishable from an empty one, and the difference is the difference between "my chunk is done" and "I have lost track of a live PR".

Hit for real on 2026-08-06. My session's scratchpad under /tmp was wiped at 08:49 UTC while PR #603 was open, unmerged and awaiting approval. The next tick's digest opened with:

``` ════ TRACKED PRS ════ (no tracked file at /tmp/.../scratchpad/tracked-prs.txt) ```

I noticed. The concern is the paths where nobody would.

Mechanism

tracked_numbers() returns [] for a missing file — the same value as for an empty one:

```python def tracked_numbers(tracked: Path) -> list[str]: if not tracked.is_file(): return [] ```

Three things follow, in increasing severity:

1. --check never mentions it at all. emit_tracked_prs — the only thing that prints (no tracked file at …) — is called from run_digest, not run_check. CLAUDE.md tells every session to run --check first, every tick, precisely because it is ~5s against ~2min. On a quiet tick that path prints QUIET and nothing else. The cheap path that runs every tick is silent about having lost its subject.

2. The GAPS-ledger wake becomes unreachable. run_check's quiet path iterates tracked_numbers(tracked) to find unactioned ledgers. With no file it iterates nothing, so WAKE: N unactioned GAPS ledger(s) can never fire — and that wake exists specifically because "a ledger never perturbs the fingerprint, so quiet is where it hides". Losing the file makes the hiding place permanent.

3. WRAP_UP can fire with a live PR outstanding. The predicate is tracked_open == 0 and not mine. A wiped file makes tracked_open == 0 honestly-looking, so the only thing standing between a session and a false "the chunk is done, delete the cron, set your row to wrapped" is whether it also happens to have a claimed issue open. Mine did. A session whose issue closed first would have been told to wrap with an unmerged PR sitting in the queue.

Why this shape is already understood here

emit_idle already guards the mirror-image case, and correctly:

``` PREDICATE UNEVALUABLE: no sid given, so claimed issues could not be checked and WRAP_UP is suppressed. ```

That is exactly the right move — refuse to evaluate rather than evaluate on a degraded input — and the missing tracked file is the same hazard on the other half of the same and. It has no guard.

Shape of a fix (not prescribing)

tracked_numbers returning [] for two different situations is the root. Distinguishing absent from empty at that call site would let:

⚠️ Worth keeping the two apart deliberately: emptying tracked-prs.txt is a documented wrap-up step, so "empty" must stay a valid path to WRAP_UP. Only *absent* is the ambiguous one.

Adjacent, not folded in

The file lives in a session scratchpad under /tmp, which is not durable — that is what wiped here. Whether the poll's state belongs somewhere that survives a /tmp clean is a larger question than this issue; this one is only about the tool not degrading silently when it is gone.

Found while polling PR #603 (#601). Unclaimed.

Dependencies

None.

Comments

No comments.

Add a comment