session lifecycle: poll only while work is live, then wrap up and reset context nf-jc33 ← Beads

closed priority 2 task unassigned activesession:b

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

Sessions currently run until they are archived. They never wrap up, so context grows monotonically, the poll cron keeps ticking whether or not there is live work, and cost scales with session age rather than with work done. Measured over 24h (2026-07-31 13:20 → 08-01 13:20 UTC).

What the data showed

Two sessions, same output, 3.9× apart on cost:

| | tonapse-main | ninefold-a | |---|---|---| | PRs created | 6 | 6 | | model calls | 378 | 402 | | median context | 783k | 376k | | cost | $316.19 | $81.89 | | cost per PR | $52.70 | $13.65 |

The gap is two independent 2× multipliers and nothing else: Fable 5 is exactly 2× Opus 5 on every axis ($10/$50 + $1/M cache read vs $5/$25 + $0.50/M), and tonapse-main carries 2.08× the context. 2.0 × 2.08 = 4.16 predicted vs 3.86 actual. (Model half is already fixed — all sessions moved to Opus 5.)

Session age, not PR count, is the context driver. ninefold-a did all 6 PRs in one session and still sat at 376k. tonapse-main has run since 2026-07-18 — 14 days — and climbed 593k → 983k across this window, compacting once, at the 1M wall. Per-PR sessions are not needed; ninefold-a is the existence proof.

Idle polling is the sharpest waste

Hourly median context for tonapse-main, overnight:

``` 07-31 14 n= 6 med 593k ← 6 calls/hr, all night, nothing happening ... (15 hours) 08-01 04 n= 6 med 660k 08-01 05 n=130 med 751k ← Aedan wakes up, real work starts ```

That flat n=6 band is the 30-min poll cron. ~$56 of the $316 — 18% — went to 15 hours of overnight polling that found nothing, purely because each tick re-reads a bloated context. It scales with the bloat: at 983k, idle polling alone is ~$141/day; on a fresh ~150k session the same cron is ~$11/day.

Proposal

1. Poll on a predicate, not a timer. Poll iff (tracked PRs still open) OR (claimed issues still open). Both empty → the session has nothing live → wrap up. No human judgement about whether work has "chilled out", and it self-limits: the overnight case above had zero open work and would not have ticked at all.

2. Define wrap-up. A session is wrapped when: - every claimed issue is closed or explicitly RELEASEd back to the backlog, - every tracked PR is merged or closed, with the feedback ledger clean (merge_pr.sh already enforces this), - residual findings are filed as issues and left unclaimed — captured, not carried, - tracked-prs.txt is empty and the poll cron is deleted.

Then report "wrapped, ready for reset" and stop. Context reset is safe at exactly this point because nothing in the session's head is load-bearing any more — the state lives in GitHub.

3. Size a chunk from the data. ninefold-a's day is the reference unit: ~6 PRs, ~400 calls, ends under ~400k. Reset there. Beyond ~600k every call is paying more to re-read context than to think.

4. Termination has to be extrinsic. "I can't find more work" is never true and can't be the stop condition. The stop condition is *the claimed set is done* — fixed at claim time. Follow-up bugs and PRs needed to actually nail a claimed issue are in scope; anything else goes to the backlog unclaimed.

5. A follow-up spawned by review of your own PR does not join your chunk. It is filed inert like anything else. The chunk is the set of issues claimed *at claim time* — not the transitive closure of everything they spawn.

Without this, rule 4 has no floor. #381 documents the exact failure: fix the claimed issue → open a PR → review spawns a new issue → that new issue is arguably "nailing the work" → repeat, with #375#377 → PR #378#379 as a live instance. Every step is individually defensible and the chunk never closes. This rule keeps "follow-ups allowed" meaning *finish what you claimed* rather than *follow the thread wherever it goes*.

Note what this does not restrict: fixing a defect in your own PR before it merges is just doing the work. The rule bites only at the point where a *new issue* would be claimed by the session that caused it.

What's actually unbounded: intake, not generation

An earlier revision of this issue blamed value 4 (idle-time strategic uplifts) for the scope growth. The data does not support that — recording the correction here because it changes which rule to edit.

Zero machine-filed issues were created during the entire 15-hour overnight idle band: #351 at 07-30T15:08, then nothing until #356 at 08-01T05:30. Through every one of those idle poll ticks, value-4 prospecting produced nothing at all. Machine-filed issues cluster in active working hours — they are byproducts of contact with the code while doing claimed work, not idle scanning. By volume, 13 of the 28 machine-filed issues over three days are mechanical PR #NNN follow-ups: from the reviewer bot (see #381); value 4's own output is low-volume and high-value (#389 probe_all never runs so main's UI gate rots, #387 a regression on main, #380 probe.sh running a stale DLL, #381 itself).

The unbounded rule is the intake instruction:

> On each poll and at session start, run ./tools/pr/poll.sh and, for every open issue that is unclaimed and that no open PR already references: 1. Claim it first

Every open issue is every session's work by default, with nothing scoping it. That is the growth mechanism.

So: keep generation, gate intake. Value 4 keeps its licence to find systemic problems — the finds are exactly what Aedan is too far from the code to see. What changes is that filing stops implying claiming (#397's inert backlog). Note the inversion: value 4's "cap at 1–2 proposals per idle stretch" exists because filing currently costs review attention; once the backlog is inert, filing is free until triage activates it, so that cap can be relaxed rather than tightened.

Value 1's "a deep review queue is NOT a reason to stop" and "don't self-throttle on queue depth" still need a carve-out — they should bound to the claimed set rather than to the whole tracker.

Sections to edit

Related: #381 (reviewer follow-up arrival rate — the actual volume driver), #397 (triage / inert backlog — the intake gate).

Dependencies

None.

Comments

No comments.

Add a comment