Skip to content

feat(coref): co-reference-aware compaction - #80

Open
amiddavid wants to merge 98 commits into
mainfrom
feat/coref-compaction
Open

feat(coref): co-reference-aware compaction#80
amiddavid wants to merge 98 commits into
mainfrom
feat/coref-compaction

Conversation

@amiddavid

@amiddavid amiddavid commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Implements co-reference-aware compaction — picking what to drop
at a threshold crossing by looking at back-references rather than at content or age — and measures
it end to end on LOCA.

Status: awaiting rebase, not review-ready as a diff. Much of what this branch built has since
landed on main independently as extract_llm_sweep (#118) and the general-fixes PR (#110). This
branch will be re-cut from main once three follow-up PRs land (#136, #137, and a collapse fix),
leaving only the parts main does not have. Read this for the result and the argument, not the diff.


The claim

Selective, reference-aware removal defers the blunt summary, and that deferral pays in latency and
operational cost even when the provider's cache has not expired — the case a purely
expiry-triggered sweep structurally cannot reach.

The evidence — iteration 021, paired, pre-registered

Two arms differing by exactly one inserted component, same frozen binary (cg-proxy-v19, sha256
ecc02f28…, code at fcf78cd), same 15 tasks × 5 seeds, run sequentially.

A  [format, toon, dedup, failed_run, cmdfilter,              extract, cachesplit, summarize]
B  [format, toon, dedup, failed_run, cmdfilter, extract_llm, extract, cachesplit, summarize]
A (shipped pipeline) B (+ merged)
wall clock 8,640 s 7,062 s −18%
per task 115.2 s 94.2 s −18%
total cost $224.06 $228.66 +2% (parity)
errored runs 17 / 75 10 / 75 −7
summarize fired on 71% 56% −15 pts
steps / run 45.7 32.7 −28%
tool calls / run 83.5 61.4 −26%
exact repeats / run 38.6 (46.2% of calls) 20.7 (33.7%) −46%
requests 3,446 2,466 −28%
unique tokens removed 6,545,956
expand restored / unresolved 361 / 1 717 / 0
CG added latency / request 3,817 ms 6,170 ms +62%

Merged costs ~2.4 s more proxy time per request and the agent still finishes 18% faster, because it
makes 28% fewer requests and summarize runs 58% less.

The repeat result is the one that most directly supports the thesis, and it reverses a worry held
earlier in this work: removing content was expected to make the model re-fetch. It re-fetches less.
The likely mechanism is that blunt summarization on 71% of requests destroys detail the model then goes
back for, while selective removal preserves what is still live.

What iteration 021 did NOT establish

Reward. ITT over all 75 pairs: 14.00 → 15.00 solves, +1.00, p = 1.0000 (task-clustered over 15
clusters, which governs: 2 better, 2 worse, 11 tied; per-pair sensitivity: 7 gained, 6 harmed, 62
unchanged; harm 95% upper bound 15.2%, which does not block).

That is a null, not a refutation. Eleven of fifteen tasks score 0.00 in both arms, so the whole
comparison rests on four tasks with two moving each way — LOCA had almost no room to show a reward
difference for any configuration. Establishing reward needs benchmarks with headroom, which is the next
step rather than a claim made here.

Two further limits, both recorded in the pre-registration before the numbers existed:

  • Verdict coverage ended at 61% (2,114 answered of 3,461 offered), and arm B ran below its own
    pre-registered 50% checkpoint for the first third of the run (amendment 2). Arm B's numbers are a
    floor.
  • Neither arm carried collapse or mask, so neither is a shipped preset, and summarize ran
    alongside in-place offloaders against the advice in config.go.

Why this branch must keep prefix_econ, given extract_llm_sweep on main

Main's sweep fires only inside a pre-expiry windowremaining = CacheTTLMs − IdleMs, positive and
under preExpiry — so the removal is free: the cache entry was about to be discarded anyway. That is a
better idea than pricing the break-even, because it replaces an unknowable estimate (turns remaining)
with an observation.

But it cannot fire in the case this branch is about. Arm B fired on context pressurehigh context pressure on 1,747 requests, moderate pressure with fast growth on 109 — with prefix_econ pricing
each prefix mutation and refusing 385 of them. CacheTTLMs/IdleMs were not in the picture at all,
so sweeping() would have fired on none of iteration 021.

So the two triggers are complementary, and the rebased branch implements both:

trigger the removal is accuracy matters
pre-expiry window (on main) free — the entry was expiring barely
context pressure + S·T > 11.5·W (this branch) paid — it forces a suffix re-write a great deal

That sharpens what coref is for: on the free path a wrong removal costs an entry that was expiring
regardless; on the paid path you have spent a real suffix re-write, so the co-reference evidence is
what makes the paid trigger worth having at all.

What remains unique to this branch

  • internal/coref — the tier-1 reference index. main has no index; its sweep carries an
    explicitly empty Evidence seam documented as waiting for one.
  • components/offload/coref.go, corefstub.go — the component, and the residue reasoning.
  • components/offload/prefix_econ.go — the second trigger's economics (see above).
  • The experiment recorddocs/experiments/loca/iter007iter021 and docs/results/*, which is
    the evidence behind every claim here.

Superseded by main and being dropped: extract_llm_merged.go, internal/extract/bulk.go,
expand/restore.go (main's RepairToolResults is better — it has Unavailable() for aged-out stashes),
proxy/expandoffered.go.

The substrate measurement that started this

Reference density is a property of the workload, not a constant — three corpora disagree by a factor
of three, which is why the index is evidence for a decision rather than the decision itself:

Claude Code (interactive) UltraHorizon LOCA-bench
unreferenced 23% 78% 95%
closed 15% 8% 0%
open 60% 13% 4%
…restricted to ≥20 later turns 21% 70% 70%

Measurement defects found along the way

Eight, in one component's measurement path — recorded because every one produced the identical
misleading signal, "the model declines to act"
, and four of them predate this branch:

defect effect
coref prefix pre-filter removed 149,681 candidates, leaving ~1 per call
llm_max_per_request caps candidates in a one-call design
economic gate prices each candidate against a whole call; suppressed 1,497 vs 224 through
any pinned floor disables the pressure trigger fired on 72% of requests regardless of context (#134)
verdicts ÷ calls used as batch size counts what was ANSWERED, not OFFERED
2048 output-token ceiling reply cut mid-array on ~70% of calls
tool_choice: none drove the model into prose — 0 of 6 verdicts (#137)
rig: marker check used the unescaped spelling has_marker read 0.0% on every arm ever run

The judgement machinery was never the problem. A sampled reply shows the model reasoning correctly under
the criterion and simply saying so in sentences — which the contract itself calls a valid answer.

Next

  1. Land feat(schema): validate message SHAPE statically, so the fifth summarize defect is caught offline #136, fix(adjudicate): declare a verdict tool and stop forcing tool_choice:none #137 and the collapse fix on main.
  2. Re-cut this branch from main; port coref, fill the Evidence seam, implement the second trigger.
  3. Re-run on LOCA and at least one benchmark with reward headroom — the axis iteration 021 could not
    speak to.

Picks WHAT to drop at a threshold crossing by looking at back-references
rather than at content or age: if a later turn references an earlier tool
output, either the model already lifted the value it needed out of it (a
large cut is licensed) or it has marked the output as important (keep).

Three things the doc argues, two of which change the original idea:

- What a reference IS in our traffic, in three tiers, and the echo
  confound that decides whether tier 1 means anything at all: only
  tokens the output INTRODUCED can count, or the measurement trends
  toward "everything is referenced".
- Distance from the current turn is the wrong discriminator. A span
  referenced three times forty turns ago is a hot span that happens to
  be old. Open-vs-closed is the real axis, and it turns "certain enough"
  from a confidence score into a verifiable predicate.
- The cache arithmetic kills the naive version and specifies the real
  one. A cut at index i rewrites the suffix at 11.5x a cache-read, so a
  single early cut can never repay itself on tokens (T > 276 turns for
  5k cut at 20% depth). Batching, step reduction and deferring the
  agent's own compaction are what can pay, so the pass must be rare,
  batched and threshold-triggered.

Also records the constraints the codebase imposes on any such component:
decisions must be latched rather than re-derived (repairLostFreeze is
documented safe only for offloaders whose output is a pure function of
(content, config), which a history-dependent decision is not), cuts must
be one-way, and TailOnly is being violated on purpose so the cache-write
spend has to be budgeted and reported.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
internal/coref is the tier-1 reference index: which identifiers each tool
output INTRODUCED, and whether any later model turn carried them forward.
It depends on neither bifrost, the components package, nor the tokenizer,
which is deliberate — it has to stay interchangeable with the definition
in deploy/harbor/coref.py. If the two drift, the thresholds the offline
measurement produces are calibrated for a different algorithm than the
one that ships, silently. The Go fixture is the twin of coref_fixture.py
down to the four known answers AND the negative control: with the echo
guard disabled the src/config.py read must flip out of `unreferenced`,
and the test fails if it does not, so the control is asserted rather than
run once.

Prior-vocabulary exclusion is a firstSeen[token] -> index map rather than
a per-message snapshot of the running union: same answer, but
O(distinct tokens) instead of O(messages x tokens), which matters at the
transcript sizes this fires on.

components/offload/coref.go carries each of the design's constraints as a
tested behaviour rather than a comment:

- the index is built from the PRISTINE request, before any replay, so an
  earlier cut cannot remove identifiers from the exclusion sets and
  silently reclassify unrelated outputs;
- decisions are latched and replayed byte-for-byte even when fresh
  evidence would reclassify the span, and repairLostFreeze is
  deliberately NOT consulted (re-deriving a history-dependent decision at
  depth is the very byte-flip that repair exists to prevent);
- the prefix is mutated on purpose, under a per-session rewrite_budget,
  where an unreadable counter reads as EXHAUSTED rather than as zero —
  fail-open belongs on the request, not on an unbounded cache spend;
- planning is side-effect free, so a batch failing a gate leaves the
  request byte-identical;
- min_batch_frac and break_even implement the S*T > 11.5*W inequality,
  with T estimated from observed transcript growth and W bounded to the
  CACHED span, since content past the boundary would be written anyway.

cut_closed defaults to false and coref is in no preset: the closed cut
needs two calibrated thresholds, and calibration is the measurement's job.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
coref.py reports, per session, how much tool-output mass is never
referenced again, how far back references reach, how much of an output a
reference actually consumes, and what a batched cut would cost in
cache-writes against what it saves. coref_fixture.py pins four outputs
whose classification is fixed by construction, including the echo
confound and a negative control.

Two converters, because the eval-box captures were not reachable and both
of these cost zero API dollars — the runs already happened:

- cc_capture.py turns a Claude Code transcript (the agent's own
  append-only log of what it sent) into capture shape. It merges
  entry-per-block back into messages, since message COUNT is the axis
  recency is measured on, and segments at a token budget because these
  sessions span many context windows and no request ever held them whole.
- runlog_capture.py does the same for benchmark harness logs: loopb /
  UltraHorizon llm_calls.jsonl, litellm traces, and LOCA-bench
  all_trajectories.json. A DROP in message count is treated as a session
  boundary, because that is the harness clearing the agent's context, and
  measuring across a boundary the model cannot see would invent cuttable
  mass out of the reset.

Both emit only the largest body in full plus per-turn `turn_tokens`
records, and stamp an explicit `conv`. coref.py honours both fields when
present; a real capture sets neither. Without turn_tokens the Claude Code
transcripts alone expand to 47 GB of prefixes; without conv, segments
opening on a tool_result collided on the inferred key and 31 of them
grouped down to 24, discarding the rest.

Measured on three corpora (docs/results/coref-density.md), the headline is
that they disagree by a factor of three: unreferenced mass is 23% on
interactive Claude Code traffic, 78% on UltraHorizon and 95% on LOCA —
21%/70%/70% once restricted to outputs with at least 20 later turns, which
bounds the obvious tail bias. Reference density is a property of the
workload, not a constant. LOCA's 0% `closed` share is the design doc's own
prediction landing: it argued LOCA would be a tier-2/3 stress test where
references arrive transformed past what a substring match can see.

Also fixes the rule that decided the whole answer. An earlier version
accepted any token of 10+ characters, so `description`, `transparency`,
`efficiency` and `conditions` scored as references and referenced mass
came out at 71% instead of 60%. A manufactured reference makes an output
look load-bearing, so that class of bug fails by silently declining to
compact — invisible to any metric counting only what the component did.
Identifiers now need interior structure after trimming edge punctuation, a
digit, or camelCase; no bare length rule, and no stopword list, which
would not survive a change of domain or of language. The residual
(lowercase hyphenated compounds, indistinguishable from real names like
context-guru) is bounded at ~6 points of UNDER-reporting rather than
argued away.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
components/coref.md is the usual per-component page: how it works, why it
is batched, budgeted and rare, the full config table with what the
measurement already settles about each knob (closed_dist is nearly inert,
open_reps is the dial), and a section on what it deliberately does NOT do.

reference/coref-glossary.md is a one-page cheat sheet for the vocabulary
this work introduces — novel token, echo, open/closed/unreferenced,
closed_dist, open_reps, ref age vs consume lag, the three tiers, S/T/W and
break-even, latching, one-way, the rewrite budget — in the order you meet
them, each with why it exists rather than just what it means. The terms are
not guessable from their names and now appear across four documents, so
they need somewhere to be looked up.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Comment thread docs/proposals/coref-compaction.md Outdated
deliberately excluded — they are the mass being reduced, not the goal.

That signal is **forward-looking and position-free**. It answers "what is the agent trying to
do", never "which earlier span does this turn point back at". Co-reference is therefore not a

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls explain this sentence, perhaps add an example

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten with a worked example rather than the assertion. It now walks turn 4 reads src/auth.py / turn 5 says "the bug is TOKEN_GRACE_SECONDS" / thirty turns later the agent is on tests — and shows that asked "is the turn-4 output still needed?", conversationGoal can only answer "the task is still about auth", which is true of every output and so decides nothing. The fact that settles it (the one value taken sits in turn 5, and turn 5 isn't going anywhere) is positional and backward-looking, which that signal cannot represent at all.

Comment thread docs/proposals/coref-compaction.md Outdated
tuning change to an existing input; it is a new input, and it is the only input that can
justify dropping a *large*, *early* span rather than projecting a recent one.

The deterministic projector (`internal/extract/deterministic.go`) has the adjacent primitives

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls explain this paragraph in more details, I find it hard to follow, especially for a proposal document

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded into a bulleted walk-through of the two existing pieces and what each contributes: deterministic.go's important-key list is already an answer to "which parts of an output would a model carry forward?", and contain.go today checks a shrunken output is a subset of its original. The reusable idea is the second one run backwards — today it asks "is this compacted text contained in the original?", inverted it asks "is this span of the original contained in a later message?", and the same primitive becomes a reference detector. Same test, opposite direction: one validates a rewrite, the other measures reuse.

Comment thread docs/proposals/coref-compaction.md Outdated

| Tier | Signal | Detectable |
|---|---|---|
| **1** | `tool_use_id` ↔ `tool_result` pairing; and **literal carry-over** — a span introduced by tool result *i* reappearing verbatim in a later `tool_use` argument or assistant text (paths, symbols, line numbers, IDs, hashes, error strings) | exact, zero LLM |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an example column

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an EXAMPLE column. Same reference at each tier: Tier 1 TOKEN_GRACE_SECONDS = 0 reappearing verbatim in an Edit argument; Tier 2 [{"ms":1200},{"ms":1800}] → "total latency is 3 seconds" (the 3 appears nowhere — it was computed); Tier 3 a directory listing → "as I saw earlier, the tests live beside the source", which is unmistakable to a reader and shares no token at all.

"the model referred back to this" and "the value it took still exists in the request" are the
same fact. That is what makes the closed case cheap to establish rather than a second search.

Framed this way, `coref` is `dedup` generalized: from "this tool output is byte-identical to

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully agree. consider this scenario:
a tool output returned: { "name": "david", "id": 123, "address": "foobarbaz"}
, { "name": "osher", "id": 235, "address": "banana"} the agent said, I need to remember david 123 address.
the address itself wasn't coref, but the tool output is needed and cannot be removed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. doesnt this contradicts case B?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and this was the most valuable comment on the PR — it found a real bug, not just a wording problem.

I ran your exact example through the index rather than reasoning about it, and it's worse than you flagged. david, 123, foobarbaz are short lowercase words and a 3-digit number — precisely what the precision rules in §2 exclude — so the output yields zero trackable tokens. Zero novel tokens means zero references, which scored unreferenced, which is the class the default config cuts. So the shipped default would have deleted that output while the agent was still asking for the address.

Two separate defects, both now fixed in e7a2623:

  1. Your conceptual point. "Any reference is a surviving copy" is too strong. The model referenced an anchor (david, 123) in order to point at a payload (foobarbaz) it never restated. An exact matcher can't distinguish an anchor reference from a payload reference — so closed can't rest on "referenced once, long ago" alone. That is now stated as the reason cut_closed ships off, rather than mere caution. It also inverts my §7 reading of used_frac: a low value is ambiguous, not evidence for case A, because "took the value, rest is chaff" and "took an anchor, still needs the payload" look identical.
  2. The concrete one. refs == 0 conflated two opposite states — "introduced 200 identifiers, nobody touched one" (evidence of deadness) and "introduced nothing I can see" (absence of evidence). There's now an opaque class that is never cut at any setting. It is not a corner case: 8% of tool-output mass on interactive traffic, 20% on UltraHorizon, 40% on LOCA-bench — the last being 11 outputs averaging 22k tokens of exactly the record-dump shape you described.

Re-measuring dropped the headline unreferenced figures from 23/78/95% to 13/51/22%, and break-even from 15/30 to 9/30 sessions. Your counter-example is now a test case on both sides of the implementation.

## 4. The economics, and why they reshape the design

This is where the proposal has to survive contact with what the repo already measured
([improvement plan §0 and §C](../results/improvement-plan.md)).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see improvement-plan in the docs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/results/improvement-plan.md does exist on main (verified with git cat-file -e main:docs/results/improvement-plan.md) and is in the mkdocs nav, so the link resolves on the published site. It's just not in this PR's diff, so GitHub can't render it as a clickable target here.

Comment thread docs/proposals/coref-compaction.md Outdated
answered yes on every corpus.
- **A reference consumes a median 18.7% of what its output introduced** (11.5% on UltraHorizon).
Hypothesis A — "took one value, does not need the rest" — is confirmed rather than assumed.
- **Tier-2 leakage is 2%** of model turns (a stated numeric absent from all prior context) — real, and

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can u explain this more, I'm not following

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded. The short version: Tier 2 is a reference that arrived transformed, so by definition no substring match can find it — what's countable is a symptom. If a model turn states a numeric value appearing nowhere in any earlier message, it computed that number from something, and that something was almost certainly a tool output. 2% of turns look like that on interactive traffic, which is why a zero-LLM first version is viable.

Two caveats now stated, and the second is a self-inflicted one worth knowing: it's a lower bound (only numeric transformations leave this trace — reworded prose is invisible), and tightening the identifier rules also blinded the proxy, since bare numbers now need 5+ digits and most computed values are small. So its 0% on LOCA means "none among tokens the tokenizer still accepts", not "none" — on a corpus with 0% closed and 40% opaque, the honest reading is that Tier-2 references there are common and simply unmeasured.

Comment thread docs/reference/coref-glossary.md Outdated

| Verdict | Means | Cut it? |
|---|---|---|
| **`unreferenced`** | No later turn ever used anything this output introduced. | **Yes — the free cut.** No threshold needed, no model call. This is the shipped default (`cut_unreferenced`). |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it a recent turn, it might not had the chance to be referenced, dont we need to guard from this ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and no — there was no guard, which was a real gap. An output near the tail has had no chance to be referenced, so scoring it as unused would make a batched pass preferentially cut the most recent context, which is the worst possible choice. mask avoids this with keep_recent; coref had nothing.

Added min_later_turns (default 8): an output with fewer model turns after it is treated as open regardless of everything else. Worth noting what the state was before — the measurement had bounded this bias (LOCA's raw 95% fell to 70% when restricted to outputs with 20+ later turns) but nothing in the component guarded against it. Bounding a bias in a report is not the same as not having it in the code.

Comment thread docs/reference/coref-glossary.md Outdated

| Knob | Default | Means | Verdict from the data |
|---|---|---|---|
| **`closed_dist`** | 12 | How many messages **ago** the last reference must be before the output counts as `closed`. Newer than this ⇒ `open`. | **Nearly inert.** A 10× sweep (4→40) moves the answer 2–3 points. Don't tune it. |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont tune it, but still matters?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — that phrasing was self-contradictory. Rewritten to what's actually true: closed_dist is load-bearing but flat. Set it to 0 and the closed class stops existing, so it certainly matters; but anywhere in 4–40 gives the same answer within 2–3 points, so there's no return on tuning it. Leave it at the default and spend the effort on open_reps, which moves the answer 18 points across the same kind of range.

| **step reduction** | The real prize. `corr(Δsteps, Δcost) = +0.95`; unique token removal is ~0.02% of the bill. The objective is **steps and reward, not bytes**. |
| **deferring agent compaction** | Claude Code compacts itself at ~167k on a 200k model. Staying under that avoids a full-transcript summarization — a large cache event *and* a quality loss. Plausibly the biggest win. |

**The counter-intuitive consequence:** firing at 90% of the context window means `T` ≈ 0 — paying a

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this. 💌
though it depends how much is being cut isn't it? and for the determinsitic one, its cheap to calculate and can tell u how much deferring is happening.

I would also appreciate some thought on what it means for larger context windows that are now more and more frequent.... up to 1M

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — and both of your points landed in the doc.

On "it depends how much is being cut": yes, and more sharply than I'd written it. The agent-compaction prize is a step function, not a slope — you either drop below the threshold or you don't, and cutting 90% of what was needed to get there is worth nothing. Which argues for sizing the batch against the threshold distance, something min_batch_frac cannot currently express. Noted as a limitation.

On deterministic measurement: agreed, and it's the cheapest real metric available here — compare the API-reported usage against the documented compaction threshold and count the turns of headroom the cut bought. No benchmark scoring, no seeds, no LLM judge. It isn't in the metrics yet; it should be.

On 1M windows — I worked this through and the answer surprised me. Break-even is scale-invariant. Rearranged, S × T > 11.5 × W is T > 11.5 × (W/S) — it depends only on the ratio of rewritten suffix to cut mass, never on absolute size. A 1M transcript with the same density of cuttable mass needs the same T. So a bigger window neither rescues nor damns the token economics; it only moves when the trigger fires. What improves the ratio is cutting a larger share of what lies after the shallowest cut — an argument for cutting deep and rarely, not for cutting more.

Three things do genuinely change, now a table in §7 of the cheat sheet: cache-read becomes the entire bill (so coref is a cost play at 1M rather than a fit play — the strongest argument for it there); the agent's own compaction recedes to ~967k, making that prize rarer but much larger; and the index cost scales linearly, so an incremental per-session index stops being an optimization and becomes a requirement.

Comment thread docs/reference/coref-glossary.md Outdated
| **one-way / monotonic** | Keep → cut only. New evidence can never un-cut, because un-cutting is another rewrite. Monotonicity is a cost requirement, not tidiness. |
| **`freeze` / `reapplyFrozen`** | The mechanism that does it: record the replacement text against the original's content hash, and replay it on every later turn at any depth. |
| **`TailOnly`** | The rule every *other* age-based offloader follows: never touch the already-cached prefix. `coref` deliberately violates it — that's its purpose — which is why the spend is budgeted. |
| **`repairLostFreeze`** | A repair `mask`/`failed_run` may use: re-derive a lost decision at depth, safe because their output is a pure function of `(content, config)`. **`coref` must never use it** — its decision is history-dependent, so re-deriving is the very byte-flip the repair exists to prevent. |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since I'm not familiar with this repo yet, I would appreciate if you can in a comment explain this more

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded both from first principles rather than by name.

TailOnly is a helper on Ctx answering "may I safely modify the message at index i?" It returns false for anything the provider has already cached, because editing cached content breaks the prefix hash and forces a cache-write of everything after it. Every other age-based offloader (mask, failed_run, collapse) consults it and declines. coref deliberately ignores it — reaching into the cached prefix is the point, since by the time a session crosses the threshold all the mass is back there — which is exactly why its spend is budgeted rather than forbidden.

repairLostFreeze needs the background first: an offloader freezes its replacement text against the original's content hash and replays it every turn so the bytes stay stable. If the store drops that record (TTL, eviction), it would normally decline to act at depth — but then the message reverts to full text, which is itself a prefix change. So mask and failed_run may re-derive even deep in the prefix: their replacement is a pure function of (content, config), so re-deriving reproduces byte-for-byte what the provider already cached. coref must never do this, because its decision depends on the whole transcript — re-deriving against a longer one can yield a different class and different bytes, the precise flip the repair exists to prevent.

Review of #80 raised a counter-example that invalidated the measurement and
exposed a defect in the DEFAULT configuration:

    [{"name": "david", "id": 123, "address": "foobarbaz"},
     {"name": "osher", "id": 235, "address": "banana"}]
    model: "I need to remember david 123 address."

Two problems, one conceptual and one concrete.

The conceptual one: the design claimed that because coref only cuts tool outputs
and references live in model turns, any reference IS a surviving copy of the
value taken. It is not. Here the model references an ANCHOR (david, 123)
precisely in order to point at a payload (foobarbaz) it never restated. An exact
matcher cannot tell an anchor reference from a payload reference, so `closed`
cannot rest on "referenced once, long ago" alone — the substantive reason
cut_closed ships off, rather than mere caution. It also makes a LOW used_frac
ambiguous rather than evidence for case A: "took the value, rest is chaff" and
"took an anchor, still needs the payload" look identical.

The concrete one, and worse: run through the index, that output yields ZERO
trackable tokens. `david`, `123`, `foobarbaz` are short lowercase words and a
3-digit number, exactly what the precision rules exclude. No novel tokens means
no references, which scored `unreferenced` — the class the default config cuts.
Two states satisfy refs == 0 and they are opposites: "introduced 200
identifiers, nobody touched one" is evidence of deadness; "introduced nothing I
can see" is absence of evidence.

So `opaque` is its own class now, never cut at any setting. Not a corner case:
8% of tool-output mass on interactive traffic, 20% on UltraHorizon, 40% on
LOCA-bench — the last being 11 outputs averaging 22k tokens of record and
spreadsheet dumps. The first version would have deleted all of it on no
evidence.

The same review raised the mirror-image error: an output near the TAIL has had
no chance to be referenced, so scoring it unused makes a batched pass
preferentially cut the most RECENT context. min_later_turns (default 8) is
mask's keep_recent expressed in turns. The measurement had bounded this bias;
nothing guarded against it.

Aligning the two implementations exposed a third bug: the Go index counted a
"later turn" by whether it held distinctive tokens, while coref.py counted
model-authored surfaces. One definition now, asserted on both sides.

Re-measured, the numbers are materially lower and break-even materially worse,
since opaque and tail-protected mass left the cut set:

  unreferenced   23% -> 13%    78% -> 51%    95% -> 22%
  break-even    15/30 -> 9/30  7/10 -> 4/8   4/9 -> 2/6

which strengthens the conclusion that this must be justified on steps and
deferred agent-compaction, not on tokens.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Editorial pass from #80. The proposal was written as an argument and read as one
only if you already knew the vocabulary; these are the places review said it did
not.

- §1 shows what "forward-looking and position-free" costs in practice rather than
  asserting it, with a worked turn-4/turn-5 example, and explains the two
  existing extract primitives and what inverting containment buys.
- §2's tier table gains an EXAMPLE column: the same reference as a literal match,
  as a computed value (1200ms + 1800ms -> "3 seconds"), and as pure prose ("as I
  saw earlier").
- §7 names the echo-exclusion guard inline instead of assuming the glossary, so
  the document is self-contained from the top.
- §7 states that every decision rule in it is about COST, that reward is a gate
  rather than a metric, and that this measurement cannot speak to reward by
  construction — it reads traffic that already happened.
- §8 stops describing LOCA's orphaned tool_use/tool_result 400s abstractly and
  points at the fix to port: repair_tool_pairing() in forever's
  _anthropic_auth_hop.py, two phases, with a repair counter. Adds that coref
  cannot cause that bug — it rewrites text in place and never removes a message.
- Implementation status moves out to proposals/coref-implementation.md. It goes
  stale every commit while the argument does not, and a proposal doubling as a
  changelog stops being reviewable as a proposal. Cross-references are named
  links now rather than bare section numbers.
- The glossary gains opaque, min_later_turns and later-turns; replaces the
  self-contradictory "nearly inert, don't tune it" phrasing for closed_dist with
  what is true (load-bearing but flat, so leave it alone); and explains TailOnly
  and repairLostFreeze from first principles instead of name-dropping them.
- New glossary section on 1M-token windows. Break-even turns out to be
  SCALE-INVARIANT — T > 11.5*(W/S) depends on the ratio, not the size — so a
  bigger window moves only WHEN the trigger fires. What does change: cache-read
  becomes the whole bill, the agent's own compaction prize gets rarer but much
  larger and is cheap to measure deterministically, and index cost scales
  linearly. Also notes the prize is a step function, so a batch should be sized
  against the threshold distance, which min_batch_frac cannot express.
- Results doc carries the corrected numbers and a "what review changed" section
  recording the defect and the delta.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Review question: the claim "Tier-2 references there are common and unmeasured"
conflated two different scopes, and the answer is Tier 2 AND Tier 3.

derived_evidence is a Tier-2 proxy by construction — it looks for a numeric
value stated with no earlier occurrence, which catches a COMPUTED value. Tier 3
("as I noted earlier", "per the schema") carries no shared token and no novel
numeric, so that proxy could never see it. Tier 3 was therefore never measured
at all, at any point; it is not something the identifier-rule tightening broke.

But the inference about LOCA does span both. There a reference is either visible
to exact matching (the 36% open) or invisible, and invisible means Tier 2 or
Tier 3. So with 0% closed and 40% opaque, the defensible statement is that both
are common there and both unmeasured — for different reasons. Tier 2 has a
detector that is nearly blind; Tier 3 has none, by design rather than by
regression, which is why it sits in open questions instead of a measurement.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Follow-up from review. Two changes to what a cut leaves behind, and one
correction to the docs that were overstating the safety story.

The claim being corrected: "a wrong cut is not a wrong answer, it is an expand
round-trip plus a cache-write". That holds only when the model NOTICES.
Expansion is model-initiated — the tool is advertised and the host loop merely
answers a call — and nothing in the system detects a bad cut. So a wrong cut has
three outcomes, not one:

  1. the model notices and expands the right marker  -> a round-trip + a write
  2. it notices but cannot tell which marker holds it -> several expands, or not
  3. it never notices                                 -> answers from less, silently

Only (1) was priced. Reversibility is a CAPABILITY, not a guarantee: the stash
guarantees the bytes can be recovered, never that they are. Tier 3 is where (3)
lives — a missing semantic reference leaves nothing to look up, so nothing
prompts the expand call, and the result is a plausible answer built on less
evidence. Two consequences now stated wherever the claim was made: expand-rate
is a precision metric for NOTICED errors only and is blind to (3) by
construction (so a falling expand rate is ambiguous, not good news), and reward
is therefore the only instrument that sees the worst failure — which is why it
is a gate rather than one number among several.

What the design can actually influence is the 1-vs-2 gap, hence:

- The marker no longer asserts "no later turn referred back to it". That is
  precisely the claim that is FALSE whenever the reference was transformed or
  semantic, and it read as reassurance — a marker that talks the model out of
  recovering content is worse than an opaque one. It now states what was removed
  and never why removing it was safe, enforced by a test that greps the marker
  for safety claims.
- For structured content the residue describes the SHAPE rather than peeking at
  the first line: "200 records, fields: address, id, name". That is addressable —
  an agent hunting for an address can tell this is the output to expand — where a
  peek of one arbitrary row cannot. Key order is sorted because the marker text
  is replayed byte-for-byte every later turn, so a map-ordered descriptor would
  flip the prefix and pay for a cache-write. The peek is still used for
  unstructured output, where the head does identify the whole.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
0.15 came from the illustrative arithmetic in the proposal's §4 and was never
checked against how much cuttable mass exists. Measured on the 19 real sessions
that passed Claude Code's 167k compaction threshold, Tier-1 matching finds a mean
4.4% of the request as `unreferenced` and 9.6% including `closed` — so the gate
admitted 1/19 sessions with cut_closed on and 0/19 at the shipped cut set.

A gate no traffic can clear is not a conservative default, it is an off switch
that looks like a threshold. 0.05 admits 16/19.

Recorded as a starting point rather than a claim: the right value is an
experimental result, and min_batch_frac is a poor proxy for the question that
actually matters (whether this cut is the one that defers the agent's own
compaction, and by enough turns not to pay a second cache-write).

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
The proposal has claimed throughout that deferring the agent's own compaction is
plausibly the largest win, and never measured how often it is reachable. This
writes down the gap, the corrected arithmetic, and the order to close it in —
without building any of it.

Corrected arithmetic. Clearing the threshold is not enough: cutting to exactly
the line buys one turn, then the transcript grows past it and you either eat the
compaction or pay a SECOND cache-write at maximum W. So the requirement is
(usage - threshold) + growthPerTurn * headroomTurns. Measured on the 19 sessions
that passed 167k, as a share of the request: H=0 needs 7.3% (10/19 achievable),
H=20 needs 12.6% (5/19), H=40 needs 18% (0/19), H=60 needs 23.5% (0/19). Mean
available cut is 4.4% (unreferenced) / 9.6% (+closed). So a bar high enough to
avoid paying twice is a bar Tier-1 matching cannot clear. Flagged that the
deficit column is partly an artifact of segmenting transcripts at 180k, while the
availability column is not.

The design. min_batch_frac asks "is my cut large?"; the question is "does my cut
change the outcome?" coref is the only component paying a prefix rewrite, while
mask and friends take 12-27% from the cache-safe tail for free — so coref is a
marginal contributor paying the most, and should cut only when DECISIVE: not when
the pipeline is already under the threshold (prize won, rewrite buys nothing) and
not when even coref cannot get it under (agent compacts anyway, so we pay the
write and eat the compaction).

Why it is hard: it reduces to one scalar, tokens-until-compaction, and the
threshold is compared against the provider's reported usage — all four tiers plus
a local tail — which includes system, tool definitions and last turn's output,
none of which a component can see. schema.MessagesTokens is a systematic
undercount by an unknown amount.

Three routes in increasing cost, ordered so the first may make the others
unnecessary: (1) measure whether the prize is in play at all, using
modes.Tracker's existing reset detection — nothing new, and ground truth rather
than estimate; (2) let the host supply the distance, since the proxy holds the raw
body including system and tools; (3) only then calibrate the offset and learn
marginal growth per session in the Store, with a cross-session prior so turn one
is not cold, biased conservative because under-estimating growth is the disaster
case and over-estimating merely cuts less often.

And none of it touches reward, which remains the only detector for the silent
failure in §4.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…t it refutes

Ten arms scored against held-out ground truth over 885 real tool outputs
($43.88, 8105 decisions). Four results contradict claims already in these
docs, so the corrections travel with the report rather than trailing it.

New: docs/results/coref-selection-experiment.md — method (firing point,
evidence window, held-out future, null baseline), per-arm results, ten
findings, and the limitations section, with per-finding confidence labels.

Corrected:
- cut_unreferenced is not a free safe cut. 11% false-drop, not a boundary
  artifact (57% of errors land 51+ turns out), irreducible with the
  available features, and a lower bound since ground truth is Tier-1 only.
- min_later_turns does not buy accuracy. Kept for the structural reason
  (a batched pass must not prefer the newest context); the safety framing
  is removed.
- Break-even collapse was overstated ~3x. ~4.5x at a defensible operating
  point, not 10-15x.
- A model in the verdict path loses to the deterministic index on both
  axes, and no combination beats the index alone. The intermediate design
  is refuted, not merely unproven.
- The summarizer comparison is withdrawn: identifier matching scores
  verbatim survival and cannot score a paraphrase. Only the 11%
  turns-needing-lost-content figure survives from it.

Also recorded, all previously undocumented:
- mask is structurally inert on sequential caching traffic. TailOnly's
  maxCachedIdx = prevLen-1 makes its candidate and permitted sets disjoint
  for any keep_recent >= 1 (0/8 masked in a probe); repairLostFreeze
  maintains existing masks but cannot create the first at depth. The
  published 12.5%/27.5% figures straddle the tail-gate commit.
- skipReduce makes coref and extract_llm mutually exclusive per output,
  first-come. They cannot compose in a pipeline; combining the two ideas
  means combining them inside one component's decision.
- MarkKeptVerbatim keys by content hash with no session component, so one
  expand exempts that content in every future session, and the flag shares
  the payload LRU so it can be evicted. Now step 0 of the plan.
- W is bounded by the nearest live cache_control breakpoint, not the whole
  suffix, which strengthens the batching argument.
- Scope: the proposal is explicitly caching-regime only, and the two
  conventions that changes (TailOnly for backward-looking offloaders,
  allow_on_caching_backend) are noted as deliberate changes.
- The whole thing narrowed to one falsifiable hypothesis, with two of its
  four clauses already failing on measured traffic.

Docs only; no Go changed.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
MarkKeptVerbatim keyed on the content hash alone, with no session
component. The hash is global, so ONE expand in ONE session permanently
exempted that byte-identical content from compaction in EVERY session
thereafter.

The consequence runs the wrong way. Content that recurs byte-identically
across sessions is exactly the content most worth compacting -- a config
dump, a manifest, a schema, a file the agent re-reads every time. So the
guard preferentially and permanently disabled compaction on the highest-
value targets, nothing reported it, and the effect reads as yield decaying
for no reason.

Scope the key by session: the loop the guard prevents is intra-session by
construction (the agent expands, the next turn of THAT session re-sends the
restored original), so a session that never expanded anything cannot be in
a loop and needs no exemption. That is the smallest scope that still
prevents every loop the guard was built for.

The scoped id travels out of apply.Trace.Session and through to the proxy's
expand loop rather than being recomputed there, so the mark is always
written under the id the pipeline compacted under. An empty session is a
no-op, not a global mark -- unreachable on the live path (observe mode
compacts nothing, so there is no marker to expand), and recording globally
would reinstate exactly the leak this removes.

Second half: store.KeptPrefix joins DefaultPinPrefixes. The flag belongs
there by the namespace's own criterion, which is easy to miss because its
payload is one byte -- losing it does not lose data, it loses the FACT that
the agent already asked for this content back, so the next turn re-compacts
it and every turn thereafter pays a round-trip plus a cache-write. Before
this, a one-byte guard competed for LRU capacity against the multi-kilobyte
rewind stashes it guards, and lost.

Two new tests cover the half that was wrong: the exemption does not leak to
another session, and it still holds for the session that earned it. Full
suite passes.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
… how the corpus was read

Answers what coref-implementation.md called 'the largest unexamined claim in
the proposal', for $0 and with no eval box. Also finds a defect in how every
earlier measurement here read its corpus, and the first clause of the
hypothesis that does not fail.

Reachability, counted over real isCompactSummary events rather than
reconstructed boundaries: the agent compacts itself in 6/35 sessions (17%),
and 5/17 (29%) of sessions past 200 model turns. So every expected-value
argument in the proposal must be multiplied by ~0.17-0.29 -- a factor no
version of it carried. Subagent transcripts are excluded as separate
conversations.

The corpus defect: a Claude Code transcript is a TREE, not a linear
conversation. The compacted transcripts carry 25-51 forks and 338-632 leaves
each, and the parentUuid graph is too fragmented to walk (longest chain
collapses to 5-78 entries out of 1,486-5,217). A linear read therefore spans
multiple context windows -- it produced a '777,339-token request' on a 200k
model, which is what exposed it. Absolute request sizes are NOT recoverable
from this corpus.

Checked rather than assumed whether that invalidates the existing numbers:
exact-duplicate tool outputs are 16% by count but only 3% of mass pooled, 2%
median, 8% worst. The duplicates are small repeated reads, not the large
outputs the measurements turn on, so every SHARE-based result in the density
pass and the selection experiment stands. Absolute token figures are now
labelled indicative.

The positive finding: the density pass measured a required-cut deficit of
7.3% and concluded H=40 was unreachable (0/19). That deficit is an artifact
of firing LATE -- cc_capture.py segments at 180k, which places the
measurement past the threshold. At the moment the agent compacts, usage IS
the threshold by definition, so a pass firing at the crossing faces only
growth x headroom, which needs no absolute size measurement. On that basis
20-60 turns of headroom is affordable. This vindicates the proposal's claim
that the profitable moment to compact is earlier than the moment of maximum
pressure, now from the deferral side as well as the cache side.

Reported with its sensitivity rather than at face value: the two growth
estimators in this repo disagree 2x (239 vs 514 tok/turn) and the H=40
verdict flips between them, so 'can it buy 40 turns' is genuinely open.
cut_closed ships off, and the 11% false-drop applies to every yes.

One earlier claim weakened: the selection experiment called its 11%
false-drop a clean lower bound. Abandoned branches can supply a later
reference the live conversation never made, which inflates false-drop, so it
is bracketed by two opposing biases instead.

Adds deploy/harbor/coref_reachability.py and
docs/results/coref-reachability.md. Docs and one new script; no Go changed.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…on, and why

Characterised on an M-series Mac with Docker 29.1.3 while trying to run the
reported benchmarks locally. Three things worth writing down, because the
failure mode is a silent all-zero run rather than an error.

Both benchmarks are amd64-only. SWE-bench says so in its image names.
Terminal-Bench 2.0 looks portable -- its task Dockerfiles use multi-arch bases
-- but all 89 task.toml files pin a prebuilt alexgshaw/<task>:20251031 image
that overrides the Dockerfile, and those are single-arch amd64. So both
emulate under QEMU.

Emulation works; Claude Code does not run under it. It is a bun-compiled
single-file executable and segfaults on start (qemu: uncaught target signal
11). Installing from npm rather than the native bootstrap does not help --
same executable, so the install succeeds and then claude --version segfaults.

The reason this belongs in REPRODUCE.md rather than a note: Harbor surfaces
the segfault as NonZeroAgentExitCodeError, which is indistinguishable from an
agent failure without reading the container log. The run returns reward=0 on
every task and reads as a catastrophic preset. Same class of trap as the
CG_LAN and port-clash gotchas already documented.

Also corrects the Docker Hub quota claim to measured values: 100/hr anonymous
vs 200/hr authenticated per the registry's own RateLimit headers, not the
order of magnitude previously implied. Authenticating still matters -- the
anonymous limit is per-IP -- but for the right reason.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…mark capture

coref.py grouped requests into sessions by hashing the first 200 characters
of the first user message. That is sound for interactive traffic, where every
session opens on a different human sentence, and catastrophic on benchmark
traffic, where every task instruction opens with the same standard preamble.

Measured on capture-swebench.jsonl: the 200-char prefix has 19 distinct
values and the most common covers 1,771 of 1,795 requests. Since only the
largest member of each group is analyzed, 18 of 19 groups held nothing but
stray single-message calls and the run reported ONE session's worth of data.

The capture already carried the right key: the Anthropic clients pack
{device_id, account_uuid, session_id} into metadata.user_id. Preferring it
recovers 50 sessions, 433 tool outputs and 355,771 tokens from the same
bytes -- a 17x larger corpus. Same class of defect as the conv collision
already fixed for cc_capture.py, and it fails the same silent way: no error,
just less data measured and reported with full confidence.

With it fixed, step 1 of the implementation plan is done -- the eval-box
measurement the acceptance criteria are written against, blocked since the
project started, now in docs/results/coref-evalbox.md:

- unreferenced is 28% of tool-output mass on capture-swebench, double the
  interactive figure and the best of any corpus. +closed is 48%. Confirms
  proposal §8's claim that SWE-bench is the Tier-1-rich substrate.
- But peak request is 12,607 tokens against a 167,000 compaction threshold,
  so the deferral prize -- the largest claimed win -- cannot occur on this
  corpus at all. Not a small cut; no pressure.
- Break-even clears in 6/48 sessions at a window the traffic actually uses,
  0/48 at 200k (the window artifact the density pass warned about).
- cut_closed still stays off: 20% here against 0% on LOCA, so the workload
  spread that made it undefendable is unchanged.

And a caveat that inverts the framing of every earlier doc: capture-tb and
capture-swe are smoke captures (6 and 2 outputs above 300 tokens). The
interactive corpus those docs apologised for is larger and deeper than the
corpus they were deferring to.

Measured on the eval box itself; the captures already existed, so $0.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
The headline per-component number in these docs was credited to the wrong
component in eight places. It belongs to extract_llm. Some of the team call
its LLM trimming of large file reads the "programming masker", and that name
collision is how the figure got attached to mask.

Three independent lines settle it:

- The arm that produced the number contains no mask. codesmart is described
  in config.go as "the SWE-bench study's winning config" and is
  [format, toon, dedup, failed_run, cmdfilter, extract_llm, extract,
  cachesplit]. mask was never in it.
- docs/results/comparison.md, the primary results page, already attributes
  the savings to extract_llm + extract + cmdfilter/dedup and does not mention
  mask at all. The measurement never claimed it.
- mask is structurally incapable of it on caching traffic: behind the tail
  gate its candidate set (outputs older than keep_recent, all present last
  turn) and its permitted set (index > MaxCachedIdx) are disjoint for any
  keep_recent >= 1.

Sites corrected: components.md (x2), how-to/choose-a-preset.md,
how-to/measure-savings.md, reference/presets.md, components/mask.md,
reference/coref-glossary.md, proposals/coref-compaction.md.

Also walks back one of my own sentences added earlier in this branch. It said
the published 12.5% / 27.5% figures "straddle a behaviour change" (the tail
gate commit). That was too generous -- the figures were never mask's to
straddle. What mask actually saves on caching traffic has never been
measured, and the docs now say so instead of implying a number.

Each corrected site names the confusion explicitly so the misattribution does
not come back the next time someone reads "masker" and reaches for mask.

Docs only; no code changed.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…e-read

Raised in review: tail-only extract_llm has no break-even constraint since it
does not invalidate the cache. Correct in mechanism, and it exposed a real
mispricing.

savedTokenValue priced EVERY saved token at the cache-read rate whenever the
request was cache-aware, on the reasoning that content the agent re-sends is
already in the cached prefix. That is true of a REPLAY turn and false of the
turn the cut is made -- and when cache-aware, extract_llm is confined to the
TAIL, which by definition has never been cached. On that turn the content is
billed as a cache-write ($3.75/MTok, dearer than fresh input) or as plain
fresh input if it falls past the last cache_control breakpoint. Either way it
is 10-12.5x the rate it was assigned.

Confirmed from live usage rather than argued: a real SWE-bench trial reported
52,561 cache_creation tokens against 746,047 cache_read across 18 turns. New
tail content is cache-created every turn.

tokenValue now carries firstToken (the applied turn) alongside perToken (each
replay), and the gate computes removed x (firstToken + reuses x perToken).
The non-caching path is unchanged by construction -- one rate, so
first + r*rate is exactly (1+r)*rate -- and a test pins that so a future edit
cannot silently reprice the workloads the published numbers came from.

Directly recomputed break-evens:

  caching, recurring   30,397 -> 11,550 tok/output  (2.63x)
  caching, first sight 42,556 -> 12,900 tok/output  (3.30x)

The shipping VERDICT survives the correction even though the number did not:
SWE-bench's largest measured tool output is 2,760 tokens, still ~4x short, so
extract_llm stays off by default on caching backends. What changes is
large-output workloads -- on LOCA captures the eligible set goes from 7 to 31
of 1,639 outputs.

Two consequences recorded because they affect tuning: the cached/non-caching
break-even ratio falls from ~20x to ~6.4x, and recurrence becomes a much
weaker lever (x1.12 rather than x1.40) because the applied turn now dominates
the sum.

Three existing tests encoded the old arithmetic and were updated rather than
deleted, including the drift guard that ties these figures to
docs/components/extract_llm.md -- which is updated in step with them.

Also adds docs/results/component-gating.md, the replay pass that found this.
Its other results: the tail gate costs mask 93% of its effect (50.67% ->
3.33%) and failed_run all of it (1.29% -> 0%); extract_llm cannot fire on
SWE-bench at all because its output floor exceeds the largest tool output the
workload produces; codesmart therefore saves ~1% on caching traffic; and the
binding constraint across all of this is tool-output SIZE, not context length,
which makes LOCA-bench the only benchmark in the set where any of these
components can act. One unexplained observation is recorded as unexplained
rather than guessed at: extract_llm spends ~640ms/request while acting zero
times.

Full suite passes.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
The first measurement in which coref acts on real captured traffic through
the live pipeline rather than being scored offline. LOCA-bench because
component-gating.md established it is the only benchmark in the set whose
tool outputs clear these components' thresholds. $0 -- deterministic arms.

Substrate: the 9 deepest real request bodies from the LOCA capture, 3.34 MB,
tool-output mass 4,940-232,505 tokens each, replayed cache-aware.

  mask                 acted 9/9   402,135 tok   52.3% shrink
  coref (defaults)     acted 2/9    48,532 tok    6.5% shrink
  coref + cut_closed   acted 2/9    48,532 tok    6.5%  -- IDENTICAL

coref works and the result is not favourable: mask removes 8.3x more.

The reason is in the classification. Of the 148 outputs above the 300-token
floor, 142 (96%) are  -- referenced recently or three-plus times -- 6
are opaque, and ZERO are closed. The detector is working; LOCA's agents
reference their tool results immediately and repeatedly, so it correctly
reports that almost nothing is safe to remove. Same signature the density
pass found on LOCA trajectories, now reproduced through a different path.

cut_closed is byte-for-byte identical to the default because there are no
closed outputs at all. The knob held back for a corpus that could justify it
turns out to be structurally inert on the one corpus where the component can
otherwise act -- which settles what the density pass could only bound.

What this sharpens: mask removes 353,603 tokens that coref classifies as
still live. One question decides which component is right, and it has never
been asked -- does mask's extra cutting cost reward on LOCA? If mask is
reward-neutral there, coref's caution buys nothing on the only workload where
it can act. If mask loses reward, that 353,603-token gap is exactly the
damage coref exists to prevent. Cheaper and sharper than the SWE-bench
reward-parity arm originally planned, and well-posed because both arms are
deterministic.

Caveats recorded in full: n=9, no reward, deepest-request-only, and LOCA is
the adverse corpus for a Tier-1 detector by design -- so a poor result here
is not evidence about Tier-1-rich long-horizon traffic, which no benchmark in
the set provides.

Also records a measurement mistake of mine: an earlier probe ran
[mask, coref, extract] together and reported coref doing nothing. mask ran
first and replaced every output with a short marker, so coref saw only
sub-floor content. That is the skipReduce first-refusal interaction observed
live, and the reason these arms must run one component at a time.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
… pre-filtered

The tail restriction on extract_llm is a cache-COST property, not a safety
property of the model call: when cache-aware the component may only touch
messages the provider has not cached, because mutating the cached prefix
forces a cache-write of the suffix. That is why the measured mass sits where
it cannot reach it -- on LOCA captures, cached_prefix_above_floor showed large
outputs skipped for no reason other than being in the prefix.

allow_cached_prefix (default FALSE) lifts the restriction, and because the
cost is real, enabling it also switches on two gates the tail path does not
have:

1. The co-reference index as a free eligibility pre-filter. A prefix output is
   a candidate only if it introduced identifiers AND no later model turn
   carried any of them forward. Anything still referenced (open) or that the
   index cannot see into (opaque) is refused with no model call at all. This
   runs FIRST, ahead of the model and economic gates, because it is the
   cheapest check available and the whole point is not paying to look at
   content a deterministic pass can already clear.

2. The S*T > 11.5*W break-even, applied to the prefix BATCH -- one cache-write
   serves all of it, so it cannot be decided per candidate.

The division of labour is the design: the index looks BACKWARD (what has
already been referenced and is therefore spent) and the model looks FORWARD
(how much of what remains will still be needed). Neither sees what the other
sees, which is why they compose rather than duplicate.

Supporting changes:

- New components/offload/prefix_econ.go holds the economics of deliberately
  mutating the cached prefix -- cacheWriteX, prefixRewritePays,
  estimateTurnsRemaining, modelTurns -- lifted out of coref.go, which now
  delegates. Two components that pay the same cache-write must not price it
  differently in two places.
- The co-reference classifier defaults (closed_dist 12, open_reps 3,
  min_later_turns 8) become shared named constants for the same reason: a
  pre-filter that classified an output differently from coref would be
  answering a different question from the component whose measurements
  calibrated it.
- prefix_min_later_turns exposes the opportunity floor for prefix candidates.

Six tests, including the two that matter most: prefix reach is OFF by default
and makes no model call (the regression guard for every workload the published
numbers came from), and a declined prefix batch does NOT suppress tail work --
the tail costs no write, so dropping it would make enabling the feature
strictly worse than leaving it off.

Full suite passes.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…nd the fold is mis-wired

First end-to-end measurement of the proposal's largest claimed win: does
compacting the full request body defer the summarization an agent otherwise
runs when its context fills? Yes, by 72%. Five arms, ~$0.16 total.

Setup: 197 sequential turns across 9 LOCA conversations, reconstructed as
growing prefixes (LOCA is append-only) and replayed in order under a stable
per-conversation session id, so MaxCachedIdx advances turn by turn. summarize
is wired at a 60k context max and runs LAST, so it fires only when compaction
failed to keep the turn under the max -- which makes firings the deferral
measurement.

  S1  summarize alone                 71 firings   64.6% shrink
  S2  codesmart - extract_llm         46   (-35%)  58.2%
  S3  + tail extract_llm              46   (+0)    58.2%
  S4  + coref                         20   (-72%)  45.6%
  S4b + extract_llm prefix reach      20   (-72%)  45.6%

1. Deferral works and coref does it: 28 firings, 1,008,646 tokens, taking
   summarizations from 46 to 20. The deterministic pipeline gets a third of
   the way for free. The tail extract_llm lever adds exactly nothing -- S2 and
   S3 are byte-identical, consistent with every other measurement of it here.

2. allow_cached_prefix engages correctly and contributes nothing. The gates
   prove it engaged: cached_prefix 6,597 -> gone, replaced by
   prefix_still_referenced 6,519 (rejected for free) with economic_gate rising
   25 -> 103. Outcome byte-identical to S4. 98.8% of prefix candidates are
   still referenced, and what survives cannot clear break-even.

3. The useful result is a design error of mine: the pre-filter selects the
   WRONG CLASS. For UNREFERENCED content, dropping strictly dominates
   trimming -- a model call can at best preserve part of what is already
   spent, while paying a call and a cache-write, where coref removes it
   outright for free. There is no work for the model in the only class it is
   allowed to see. Trimming belongs to CLOSED: referenced once, long ago,
   value taken and remainder chaff -- still partly live, so what to keep needs
   judgement. Repointing the pre-filter is a one-line change and the obvious
   next experiment.

This rewrite also RETRACTS the earlier version of this page. That run sent one
request per conversation, so every request was a cold first turn with
MaxCachedIdx = -1 and the tail gate never engaged. It inflated mask to 52.3%
(its non-tail figure) and produced a "mask removes 8.3x more than coref"
comparison that was an artifact of the setup. It also reported zero CLOSED
outputs on LOCA, which is false -- the sequential replay surfaces 25, because
CLOSED needs a reference that has since gone stale and that cannot exist when
every request is turn 1.

And once more, the largest single lever is neither component under discussion:
format, a lossless JSON repack, recovers 1,266,088 tokens in 119 firings --
more than coref, for free. Every lossy component is competing for the
remainder.

Reward remains unmeasured and remains the gate.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
@amiddavid
amiddavid force-pushed the feat/coref-compaction branch from ac3bdf2 to 48ffc1b Compare August 20, 2026 21:23
…ction explicit

Review raised the objection that sinks the previous default: even UNREFERENCED
content may need model judgement, because coref matches exact identifiers only.
A value the model summed, converted or reworded leaves no substring behind
(tiers 2 and 3), so 'unreferenced' means 'no later exact reuse', not 'unused'.
That is exactly why the 11% false-drop measured against held-out ground truth
is a LOWER bound.

So handing that class to the model is not asking it to trim -- it is asking it
to VETO, to notice an implicit reference the index structurally cannot see. It
yields little when the index was right and is the only available mechanism for
catching when it was wrong. That is a real trade-off, not a tuning detail, so
it becomes configuration rather than a constant.

prefix_classes defaults to [unreferenced, closed]:

  closed       — referenced once or twice, long ago. Something WAS taken, and
                 an exact matcher cannot tell 'took the value, rest is chaff'
                 from 'took an ANCHOR and still needs the payload it points
                 at'. That ambiguity is why coref's cut_closed ships off, and
                 it is precisely a judgement call -- a model can read the
                 output, see the reference was a name or id, and keep the
                 payload a blind cut would lose.
  unreferenced — the veto case above.

open and opaque are REFUSED at construction rather than accepted: open is
content a later turn demonstrably still uses, opaque is content the index
cannot see into at all, and for neither is there evidence of being spent.
Admitting them would turn the pre-filter into 'consider everything' and lose
the one property that makes prefix reach affordable. An unknown entry is also
an error rather than silently ignored.

Note this changes the shipped default from unreferenced-only to both classes.
Deliberate: the LOCA replay showed unreferenced-only contributes nothing (the
model can only preserve part of what is already spent, while coref drops it
outright for free), so a default that admits only that class is a default that
cannot help.

Two tests: the refusals and the unknown-entry error, and that narrowing to
closed-only actually narrows -- the model is not consulted about unreferenced
content. Full suite passes.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Adopts the forever project's per-run iteration notation so the two projects
read side by side, and retro-fits the runs already made.

  docs/experiments/README.md              the log, its index, and conventions
  docs/experiments/captures/iter001/      component gating on capture-swebench
  docs/experiments/loca/iter001/          first LOCA replay -- RETRACTED
  docs/experiments/loca/iter002/          sequential replay, deferral 71 -> 20

The split is deliberate. An iteration page is a record of FACT: what was
executed, what came back, what it does and does not prove, and the artifact
paths so a number can be traced to the bytes that produced it. The
docs/results pages are the ARGUMENTS -- they synthesise across runs and get
rewritten as understanding changes. If the two disagree, the iteration page
wins.

Three conventions, each earned the hard way in this branch:

- Retractions stay. loca/iter001 keeps its wrong numbers behind a banner,
  because the cause (one request per conversation meant every request was a
  cold first turn, so the tail gate never engaged and mask looked 8.3x better
  than it is) is more instructive than the numbers were.
- Cost is always stated, even when it is $0, because free is a property worth
  knowing.
- Every arm names its binary. A binary built before allow_cached_prefix
  existed silently turned iter002's fold arm into coref-alone, and it was
  caught only because the gate counters came back byte-identical to the
  previous arm.

No code changed.

Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…er-claim

LOCA-bench reward is wired: its own ReAct agent and deterministic GEM scorer,
pointed straight at a context-guru proxy via LOCA_ANTHROPIC_BASE_URL, no
forever and no auth hop. Baseline at the 8K debug band scores 1.0, matching
forever's own iter001, with the proxy verifiably transparent on the off arm
(9 requests, 0 saved, no component acting).

Arms at 8K: all three score 1.0 (off / codesmart-minus-extract_llm / +coref),
input tokens -16% and -22%, steps 9 -> 8. Reward parity holds -- but only
format acted, so it confirms a LOSSLESS pipeline is harmless and says nothing
about coref. Recorded as such rather than as a result.

Two methodological findings from those arms:

- Back-to-back arms share the provider's prompt cache. cache_read was
  identical to the byte across all three (131,096 = 8 x 16,387) and
  cache_write fell to 0 after the first arm, which inherited the baseline's
  write. Cost comparisons across sequential arms are confounded by run order;
  only input/output tokens and steps are safe to compare.
- The 8K band is saturated, which for THIS question is a feature: a 1.0
  baseline is the ideal control for a regression test. forever needed headroom
  to show a lift; we need a ceiling to detect a loss.

Escalating to 128k produced three more failures, two of them mine, and the
page now records the sequence:

- EAGAIN on every band above 8K, chased through a full band bisect and
  attributed to LOCA's MCP transport. It was my own runner: a `| tail -25`
  made LOCA's stdout a pipe and Rich's band-scaled output overflowed it. The
  error names stdout as the writer; I read "write" and reached for the
  transport twice before checking my harness. Four runs and a bisect wasted.
- With the pipe gone, HTTP 400 with 42 orphaned tool_use ids -- exactly what
  the proposal's §8 predicts LOCA's trimmer does, including the instruction to
  port repair_tool_pairing() from forever rather than rediscover it. I
  rediscovered it first. Now a rig-side shim that lifts the function verbatim,
  sits BEFORE cg-proxy so compaction sees well-formed traffic, and counts
  repairs (354 across 42 requests, so orphaning is constant at this band).
- --max-tool-uses 100 is too small for the band: the run completed but scored
  0.0 with tool_use_counter 105, having hit the cap with 49 quizzes and 30
  assignments left to enumerate. trim_events 0 rules out context rot and the
  shim both.

And one correction to this page's own earlier claim: tool_success_counter is
NOT a general health signal. Passing runs emit a different feedback shape with
no such counter, and a 128k run showed it at 0 while the tools worked fine and
the real cause was the budget. In the first case the tools genuinely were
broken but the counter was incidental, not diagnostic. The durable lesson is
narrower -- read per-task eval.json rather than the summary, and confirm a
cause before naming one.

No code changed.

Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…the numbers

Reward at the 64k band, 12 tasks, three arms -- the first configuration in
this work with both real context pressure AND measurable headroom. Committed
BEFORE the results so the interpretation cannot be fitted to them.

iter003 ruled out the obvious bands: 8K is saturated at 1.0 (good regression
control, but only format fires so it says nothing about coref) and 128k gives
a genuine 0.0 (real context-rot collapse, but a zero floor at n=1 measures
nothing). A 3-task probe at 64k returned 1/3 -- partial, which is where signal
lives.

Two design points recorded because they are easy to get wrong later:

- Arm order puts the baseline LAST. Back-to-back arms share the provider's
  prompt cache, so whichever runs first pays the prefix write and the rest
  ride free. Running off last means the compaction arms cannot get a free ride
  from it. That does not remove the confound, it stops it flattering the arms
  under advocacy -- so cost is reported with the caveat, never as a clean
  saving.
- n=12 detects a gross effect only. A 1-2 task difference is noise at this
  size and will be reported as noise.

Pre-registered: arms >= baseline means reward-neutral-or-better under pressure
(which with iter002's 72% fewer summarizations is the first genuinely positive
case); arms < baseline means the cuts cost tasks and coref fails its own gate;
all three identical means the pipeline is not engaging even here and the
question moves to UltraHorizon.

No code changed.

Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…finally acts

Three arms at the 64k band over 12 tasks. Read naively the result says
compaction costs reward: baseline solves 4/12, det 2/12, full 3/12. It does
not say that, because the losses are a configuration bug of mine.

Every task error coincided exactly with a summarize firing -- det 1 and 1,
full 3 and 3, baseline 0 and 0 -- and all three errors are HTTP 400 SCHEMA
violations rather than model failures: role "tool" reaching the provider
(Anthropic takes tool results as user messages with tool_result blocks) and a
misplaced system message. components.md says plainly that summarize
restructures the transcript and must RUN ALONE so no other component's
in-place edits race apply's rebuild. I ran it with nine others. The count
correlation is exact, so the mechanism is not in doubt.

This also undermines iter002. That page reported 72% fewer summarizations
using the same summarize-in-a-pipeline configs, but it replayed through
/compact, which never forwards upstream -- so the malformed bodies were never
validated by a provider. The same pipeline 400s in production. The mechanism
(compaction reduces how often a context max is reached) still stands; the
specific configuration that produced 71 -> 20 is not shippable and the figure
must be re-earned with summarize isolated. More generally: a replay harness
that does not forward upstream cannot catch schema violations, which is a
structural blind spot in every /compact-based measurement here.

What did work is the fold. extract_llm acted for the FIRST time in this entire
investigation -- 27 firings, 584,125 tokens -- in the allow_cached_prefix arm,
taking total saving from 20.0% to 31.8%. Here extract_llm does the work and
coref contributes little, the reverse of iter002 where coref did everything
and the fold added nothing; the difference is the band, since 64k has prefix
content large enough to clear both the output floor and the break-even. That is
the first evidence the fold does something no other configuration achieves.

And once again format, a lossless JSON repack, is the largest single lever --
92% of the deterministic arm's total saving.

Two corrections to my own reporting: the mean accuracy I first computed
excluded errored tasks from the denominator, which flattered the compaction
arms, so the table now uses /12 throughout; and the arms' lower cost is not a
saving, since they errored out of tasks early and the prompt-cache confound
applies.

iteration 004b, rerunning without summarize, is in flight.

No code changed.

Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ot on average

Re-ran iter004's question with summarize removed, since every task error there
coincided exactly with a summarize firing. Same 12 tasks, same 64k band, same
deterministic GEM scorer. Removing summarize took errors to ZERO in both arms,
confirming the diagnosis.

  off (baseline)   4/12 solved  0 errors  $21.34   0%    010000101010
  ns-det           4/12 solved  0 errors  $14.67  17.1%  010000101010
  ns-full (fold)   5/12 solved  0 errors  $22.64  20.3%  010001101100

The parity result is stronger than a matching average: ns-det's per-task
outcome string is BYTE-IDENTICAL to the baseline -- the same four tasks solved
and the same eight failed, not merely the same mean. Removing 17.1% of content
changed nothing about which tasks succeeded, at 31% lower cost, with zero
model calls.

The fold arm ran extract_llm 38 times and coref 17 times, removed 20.3%, and
did not lose tasks. Its +1 task is reported as NOISE, per the reading
pre-registered before the run: it gained tasks 6 and 10 and lost task 11, and
net +1 at n=12 is sampling variation, not evidence that compaction helps.

Two things kept honest:

- Cost is only partly interpretable. ns-det ran after the baseline so it
  inherits some of the prompt-cache confound. The direction that IS safe to
  read is the uncomfortable one: ns-full cost MORE than baseline ($22.64 vs
  $21.34) despite removing 20.3% of tokens, because 11 model calls plus
  pipeline overhead outweighed the saving. Removing tokens is not saving money.
- format remains the dominant lever -- 99% of ns-det's saving from a lossless
  JSON repack. That has now held in every configuration measured, replay and
  live, at every band.

Also updates the experiment log index, including flagging iter002 as
config-invalid: its deferral figure came from a pipeline that 400s in
production, so the mechanism stands but the number must be re-earned with
summarize isolated.

No code changed.

Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Merged arm only per direction, baseline to follow only on improvement and no new
defects. Records what was wrong -- the proxy relayed the model's tool_use whenever no
expand id resolved, so a client without the injected tool answered not-found and the
model re-ran the original tool -- with the measurements: 17 of 38 and 48 of 108 attempts
refused, exact repeats at 9.9% and 25.3% against 0.4% lossless.

Six criteria separating the fix working from the new visibility from the loop it was meant
to break from the trade being accepted from whether it matters. INJECT_EXPAND returns to
auto, since decoupled interception no longer needs always, and that knowingly reaccepts
the tools-array flap, which criterion 5 measures.

Declared in advance that if the refusals go to zero and the repeat and step counts barely
move, the diagnosis was right and the consequence was small: iteration 016 established
repeats explain only 11 to 26 percent of the extra steps, so step count, whose cause
remains unestablished, is the dominant term. Also records that the volume story is already
withdrawn, so criterion 6 is about cache-tier mix and no large cost win is expected.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
… mixed-tool case

The response-side fix covered 5 of 107 cases. Live traffic showed why: the loop can only
satisfy an expand call that arrives alone, because when the model also calls a real tool
only the client can execute it, so the response is relayed and ResponseCalls sets
otherTools. Of 107 turns whose expand call was refused, 102 carried two or more tool_use
blocks and 5 carried one. The client then answers the proxy-injected tool itself with
"Tool 'context_guru_expand' not found", the model loses recovery, and it re-runs the
original tool -- a full tool execution plus fresh output, enlarging the transcript that
provoked the cut.

expand.RestoreResults works with the client's loop instead of against it: when the client
sends its results back, its own failed tool_result for the expand call is replaced with
the stashed original before the request goes upstream. No response splitting, nothing
required of the client, and the real tools are executed normally by whoever owns them.
Both dialects are handled -- Anthropic tool_result blocks and OpenAI role=tool messages.

Placed after the pipeline and before the forward, deliberately: restored content must not
be handed back to the components that just cut it, which would compact it into another
marker and another expand call. The kept-verbatim mark uses the pipeline's own session id,
for the same reason the response loop does -- written under any other id the guard sits
where nothing reads it. Unresolvable ids are left exactly as the client wrote them, since
the model is already reading a failure and a second invented failure string would only add
another story.

Adds an expand_restored counter, and a test asserting the model receives the content, the
client's failure text does not reach it, the real tool's result is untouched, and the
count increments. Verified to fail when the restore is reverted.

Also records a future-consideration note in the proposal: this rewrite touches a message
the model has already seen, so it is coherent only if applied deterministically on every
turn. Intermittent substitution both contradicts the model's own prior reasoning and
flaps the cached prefix. Determinism holds for as long as the stash lives, which
MarkKeptVerbatim and stash durability protect, and expand_unresolved_missing now counts
the cases where it did not. The note ends with the open question of whether reversibility
is worth this machinery at all, given the agent's measured fallback is to re-run the tool
rather than to give up.

Full suite: 24 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…aches the MODEL

Two observability gaps of my own making, both found by trying to verify the fix.

expand.Restored() was implemented but never wired into /stats, so there was no way to
tell a working restore from a silent no-op -- exactly the gap that let the expand
refusals run unnoticed for three iterations, recommitted while fixing them. Now
surfaced, and added to the stats golden contract rather than loosening the assertion.

And the metric I had been quoting was the wrong signal. The client cannot execute a
proxy-injected tool, so it ALWAYS refuses; counting refusals in the client's own log
therefore measures nothing about whether recovery works, and will stay non-zero by
design. What matters is whether that refusal text is still in the request the MODEL
receives, since the substitution happens on the way back upstream. The capture hop now
records refusal_reached_model per request, which must trend to zero if the restore is
working, and flapstats2 reports it.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…rect two claims

Offline probe series, under $2 of gateway spend, no LOCA run. Iteration 018 left the
merged design's 94% keep rate, its ~1 trim per arm, and solves falling 16 to 8
unexplained. Probing the decision directly instead of buying another $234 arm shows
merged has never run in the configuration it was measured good in.

Corrections to claims this repo acted on:

  * iteration 014's "negative answer" is unsafe. That arm made 2,078 decisions across
    2,030 calls, 1.02 candidates per call, and merged_kept_whole_batch never fired,
    which rules out empty-array replies. Bulk adjudication is comparative and was
    measured at ~15 candidates; at 1.02 the arm ran the per-output design already
    refuted at 6% live-kept. Corrected in place; its efficiency and deferral findings
    are unaffected since they do not depend on batch size.
  * AllowCachedPrefix's comment claimed the tail restriction "is not a safety property
    of the model call, it is a cache-cost property". It is also an information
    property: need is relevance minus what is already captured elsewhere, and that
    term lives in the turns after the output, which the model is not shown. The tail
    restriction is what made the local prompt sound. Comment corrected in place.

What the probes establish:

  * Appending a trailing user message to an identical prefix lands a full cache read,
    no write, so a model call can read the real context at ~10% of fresh input.
    tool_choice is not part of the cache key; tools are; this route rejects assistant
    prefill.
  * Transport versus judgment. Opaque ids are hallucinated, integer labels are not
    (0 bad in 40+ trials), short quotes are verbatim (0 of 59 wrong), and trim's
    retained text is invented 8 times in 9. The model must never carry content.
  * trim is dead weight: chosen zero times in 21 opportunities, and the keep/drop arm
    is identical to keep/drop/trim on every metric.
  * The Tier-2 false drop is a policy error, not a comprehension error. Asked the
    factual question the model is right 3/3, then drops anyway, because
    BuildBulkPrompt tells it to judge relevance toward the CURRENT step while an
    outstanding instruction still needs the output.
  * Stating a better criterion is inert (4/4 false drops). Requiring the model to name
    and quote the obligation halves it. Pooled over four runs, 14/14 against 9/14,
    Fisher p about 0.04, with no loss of correct drops. 9/14 is still 64%, so the veto
    is not a capability to build on.
  * Batch of one drops the output 4/4, replicating the selection experiment's
    per-output refutation in a different harness.
  * Methodology: the model's self-report was accurate about its beliefs and wrong
    about the cause it named; removing the wording it blamed made the result worse.

Also gofmt: the goto-splice scope block from the merged integration was not
gofmt-clean.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…n evidence, no trim

Iterations 014, 016 and 018 each measured a merged arm that was never configured the
way the design was measured good. The model was shown 1.02 then 2.63 candidates per
call against the ~15 that produced 58% live-kept, so every one of those arms was closer
to the per-output design already refuted at 6%. Offline probing (iteration 019) shows
batch size is a yield/safety trade-off rather than a detail: at batch 3-6 the model
dropped a genuinely-spent output only 2 times in 4, at batch 10 it dropped it 4 in 4
and cleared 100% of genuinely-spent candidates. Small batches do not make it wrong,
they make it unwilling to act, which is what a 94.6% keep rate looks like from inside.

Three changes, each with a measurement behind it:

  * The contract states the SPENT criterion — spent only if needed by none of the
    current step, an unfinished user instruction, or a next step the agent itself
    stated — and REQUIRES the model to name which obligation applies and quote it
    verbatim. Stating the criterion alone measured inert at 4/4 false drops; requiring
    the evidence halved it. Instructions a model can skim past are inert; a required
    output field is not.
  * trim is removed. Chosen zero times in 21 probe opportunities, identical metrics
    without it, and in production accepted once against eight rejected as invented. It
    was the only verdict that asked the model to transport text, which is what it is
    worst at. A model that answers "trim" anyway degrades to keep, counted, rather than
    being discarded — an unjudged output is indistinguishable from silence otherwise.
  * mergedMaxItems 15 to 12. Quote fidelity degraded with batch size: 4 of 37 quotes
    non-verbatim at batch 16 against 0 of 16 at batch 10, so the transport ceiling sits
    between them and this takes the conservative end.

New guards, each verified to FAIL when its subject is reverted:

  * a drop that names an outstanding obligation is refused, not performed. This is the
    one verification pointing the dangerous way.
  * a fabricated obligation quote is counted. It argues for keeping so it is not
    dangerous, but it is the signal that the batch exceeds the model's transport limit.
  * an unanswered criterion field is tolerated and counted, because requiring it would
    collapse yield against a model that omits it, while ignoring it would hide that the
    forcing function never ran.
  * batch truncation is counted rather than silent.

Also commits the arm config. The merged configs for iterations 014, 016 and 018 lived
only on the eval box, which made them unreproducible.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…e mechanism run

The first launch hit the pre-registered abort criterion at 213 requests: 1.93
candidates per call with acted=0. The counters named the cause and it was not
min_tokens. The economic gate suppressed 1,497 candidates against 224 that reached
the model, top reason "cache-aware, saving below call cost".

That gate prices each candidate against the cost of a whole model call, which is
correct for the per-output loop where candidate equals call, and wrong for a design
that makes one call per request regardless of batch size. It also fights min_tokens:
a lower floor produces smaller candidates, each looking even less worth a call, which
is why 3000 to 800 did not help. Third instance of one defect class, after the prefix
pre-filter and llm_max_per_request: cost machinery written for per-output calls,
applied to a one-call design.

Also records that below_output_floor at 11,036 is an occurrence count inflated by
per-request rescanning, not evidence about the floor, and that
merged_quote_not_verbatim ran 8.5% on haiku against 0 of 59 on sonnet in the probes,
so the forced-evidence mechanism may not survive the cheap model.

No endpoint or pre-registered reading changed.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…el over its cached transcript

A component that must decide whether a tool output is still needed cannot answer that
from the output alone. Need is relevance MINUS whatever has already been captured
elsewhere in the transcript, and that second term lives in the later turns, which the
merged adjudication was never shown. It was asked to veto an exact-match index on
transformed reuse while being withheld the turns where the reuse appears.

Sending those turns fresh costs about ten times a cache read, and on the cheap model the
required verbatim quoting degraded to 20.8% at the batch sizes the bulk mechanism needs,
against 0 of 59 on the request model. So the judgement wants the agent's own model AND
the whole transcript, and only a cache read makes that affordable.

Measured on the live route before building any of this (iteration 019, section 2):
appending a trailing user message to a byte-identical prefix reads the entire prefix from
cache and writes nothing, 19,595 read against 0 created. tool_choice is not part of the
cache key, so forcing it to none is free and necessary, since the prefix carries the
agent's tools and the model otherwise answers with a tool_use. tools ARE part of the key:
omitting them read a different, smaller entry. The route also rejects assistant prefill,
which the appended user message satisfies by construction.

The prefix is the previous turn's SENT body, not the incoming one. The cache upstream was
populated by what context-guru emitted, which is the compacted form; the incoming body is
uncompacted and diverges at the first thing any component removed, making everything past
that point a fresh charge. The consequence is that the ask sees the transcript as of the
previous turn, which is acceptable for this judgement — the missing part is the newest
tool output, tail content that has had no turns in which to be superseded — and it keeps
a large model call off the agent's critical path.

  * components: PrefixAsker and PrefixUsage, plus Ctx.PrefixAsk. Usage is RETURNED and
    not merely recorded, because a prefix ask whose whole justification is the cache read
    must let its caller see that the read happened.
  * cheapmodel: Anthropic.CompletePrefixed, which appends the ask and touches nothing
    else except stream, since every byte before the appended message is prefix.
  * proxy: a bounded per-session stash of the body actually forwarded, and the asker
    built from it. Off by default (CONTEXT_GURU_PREFIX_ASK) because it holds request
    bodies in memory and because a feature whose benefit is a cache hit should not be on
    by default in a host that cannot verify the hit.
  * extract: BuildPrefixAsk ships an inventory rather than the outputs. Paying fresh to
    send truncated copies of content the model is reading from cache would defeat the
    mechanism and show it an excerpt of something it could read in full. Labels are small
    integers: asked for opaque tool_use ids the model regularised them, and with integers
    it was 0 bad labels in 40+ trials.
  * merged: prefers the prefix ask, falls back to a plain completion on the first turn of
    a session or any error. Falling back rather than skipping matters — treating "no
    prefix" as "no verdicts" would disable the component on every session's first turn
    and read as a model that declined to act. A cache read of zero is counted.

Each new guard verified to FAIL when its subject is reverted: the samples-not-shipped
invariant, the zero-cache-read counter, the fallback, and the tool_choice construction.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…iring

Live symptom: prefix_ask_used = 0 with prefix_ask_failed = 0 over 34 requests. The
asker was never built, so the mechanism silently never ran and was indistinguishable
from a feature switched off.

Cause: the stash is written under the pipeline resolved, tenant-scoped session id,
while the asker was built from the caller x-context-guru-session header, which this
workload never sends. Empty key on lookup, resolved key on write, no error anywhere.
The resolved id only exists inside apply.BodyOpts, which is the same call that needs
the asker, so the session now travels as an Ask parameter and the component supplies
c.Session.

Also removes the pre-flight stash check. A first turn with nothing stashed must
surface as an error from Ask, counted and falling back to a plain completion, rather
than as a nil asker, because nil is what "the feature is off" looks like.

The component tests all injected a fake asker, so none of them touched this wiring.
That is the gap this commit closes: a fake satisfying the interface proves nothing
about who supplies the key. The new proxy test covers the first-turn error, the
matching-key success, refusal to serve another session a prefix, and the opt-in and
Anthropic-only preconditions.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Written to answer a reviewer question: does the tension between these two config
items predate this branch? It does, by weeks. min_tokens on extract_llm arrived in
7f0379a on 2026-07-25 and economic_gate in 2adb476 on 2026-08-10, while
selection_mode: merged is this PR and not yet in main.

But it separates into two claims and only one is a defect. In the per-output design
the pipeline makes one model call per candidate, so a candidate IS a call and pricing
a candidate against a call is correct: the floor and the gate are two filters
agreeing, and the gate is the stricter and better informed of the two. What
pre-exists is therefore redundancy and poor observability, not incorrectness --
min_tokens is effectively advisory below the gate economic floor, and nothing reports
that the operator floor was honoured and then overruled.

What this PR changed is the assumption underneath. Merged makes one call per request
regardless of batch size, and with prefix asks the outputs are read from the cached
transcript rather than shipped, so a candidate marginal cost is one inventory line of
about thirty tokens. The gate still prices each candidate against a whole call, which
is wrong by two to three orders of magnitude in that configuration and starves the
batch of the peers the comparative judgement needs.

Includes the floor sweep showing candidates per call at 1.29, 2.22 and 5.97 for
min_tokens 800, 300 and 120, with the explicit caveat that only that column is
comparable across the three runs -- they diverged in trajectory, spreading total
traffic over 7x and summarize firing on 57, 66 and 2 percent of requests.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ng on every request

Two defects found by a reviewer question: was the merged call firing only when the
context was large enough? It was not. It fired on 190 of 264 requests, 72 percent,
regardless of context size, and request_trigger_not_fired never fired once.

Three restraints were off at the same time. Pinning min_tokens sets the explicit flag,
and shouldFire then returns "explicit min_tokens/trigger configured" unconditionally,
bypassing the derived pressure trigger; any explicit floor does this, since min_tokens,
trigger.min_request_tokens and trigger.min_output_tokens all mark it, so the per-output
floor and the when-to-act decision cannot be configured independently. The explicit
request trigger is only enforced when the backend is not cache-aware
(extract_llm.go:761), so with caching on an operator context threshold is silently
ignored. And economic_gate was disabled, removing the last thing refusing low-value
work.

Firing on a small context is not only wasted spend. It removes outputs that have had no
turns in which to be superseded, which the contract explicitly says to keep, so it is a
harm mechanism -- and it confounds deferral, because summarize firing less may simply
mean extract removed early and often. The arm configuration now pins nothing, so the
derived trigger governs: fire above 0.60 context pressure or above 0.25 with more than
10 percent growth, which engages before summarize at 0.78.

Separately, verdicts divided by calls was being used as the batch size and is not one.
It counts what the model chose to ANSWER. Live it read 2.80 while merged_batch_truncated
fired 43 times in 162 calls, which is arithmetically impossible for offered batches: the
model is shown bulk-sized batches and silently omits most labels. Report.GateN plus a
merged_offered counter now record offered and answered separately, so a starved batch and
a model answering for a third of a full batch can no longer produce the same number.
That conflation is what three iterations read as the model declining to act.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ead as a refusal

The new merged_offered counter showed batches of 21.7 candidates offered per call,
capped to 12, with the model answering for 2.39 of them: 11 percent verdict coverage.
Batch size was never the constraint. The constraint is that merged_unparseable was
firing on 24 of 34 calls, about 70 percent.

Cause: proxy.go builds the incoming model client without MaxTokens, so CompletePrefixed
fell back to 2048. The request model runs adaptive thinking, which consumes that budget
before emitting any text -- a probe at max_tokens 900 returned thinking blocks and no
text whatsoever -- and a verdict array over a 12-item batch, each entry carrying an
obligation label and a verbatim quote, is long. The array was cut mid-flight with no
closing bracket, the parse failed, and the caller changed nothing. In the counters that
is indistinguishable from a model that declined to act, which is how it was misread for
three iterations.

Part of this is self-inflicted by this branch: forcing the obligation evidence lengthened
the replies, and prefix asks moved them from haiku onto sonnet with thinking. But
merged_unparseable was also visible at 19 and 22 in two earlier arms and dismissed as
under one percent, which compared it against the DECISION count when it is a share of
CALLS.

  * CompletePrefixed defaults to 16000 output tokens rather than 2048. Output bills as
    generated and not as budgeted, so the ceiling costs nothing until used.
  * a reply that opened the array and never closed it is now counted as
    merged_reply_truncated, separately from merged_unparseable. The two need opposite
    fixes -- raise the budget versus fix the prompt -- so one name for both hid this.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…a schema, not prose

The verdict-coverage problem was an envelope problem, not a judgement problem, and the
fix is mostly deleting something I added.

Measured against the live route, same prefix, only tool_choice varying:

  tool_choice          reply shape            cache             coverage
  {"type":"none"}      prose / thinking       read (free)       0 of 6 labels
  {"type":"tool",...}  tool_use               MISS + rewrite    6 of 6
  (omitted)            tool_use               read (free)       6 of 6, on 4 of 4 trials

So setting tool_choice none -- added to stop the model answering with a tool_use -- is
what drove it into prose, and the prose was then scored as an unparseable failure. That
is a large part of what read as "the model declines to act" across three iterations. A
sampled reply shows the model reasoning correctly under the criterion and simply saying
so in sentences: the task is unfinished, and no summary of the raw data has been recorded
elsewhere, therefore keep. Which the contract already calls a valid and often correct
answer.

Forcing a named tool also turns out not to be free: it wrote a separate cache entry,
8,378 tokens against the 8,268 already cached, so tool_choice does participate in the
cache key when it names a tool even though "none" does not.

  * internal/adjudicate declares context_guru_adjudicate with an integer-labelled verdict
    schema, and injects it on EVERY request rather than only when the pipeline is about to
    ask. tools hash before system and messages, so a tool that comes and goes invalidates
    the prefix from position zero -- the flap expand's always mode exists to prevent.
  * CompletePrefixed no longer sets tool_choice, and prefers a tool_use input over text.
    The input arrives schema-shaped, which removes three failure modes the text path had:
    prose instead of JSON, verdicts for part of the batch, and an array cut off by the
    output budget.
  * stray calls the AGENT makes to the tool are answered on the request path, the same
    shape as expand's RestoreResults and for the same reason: the client cannot execute a
    proxy-injected tool, so it answers "not found" and the agent loses a turn to a dead
    end. Counted as adjudicate_stray, because models do call advertised tools they were
    told to leave alone -- directly observed with expand at step 2 of a run.

One test's assertion was inverted rather than adjusted: it demanded tool_choice none on
the reasoning that a tool_use reply had to be suppressed, and measurement reversed that.
The /stats golden test caught the new field, as designed. All four new guards verified to
FAIL when their subject is reverted.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
… eight measurement defects

Six launches, five aborted on the pre-registered criterion, each abort exposing a defect
the previous configuration hid. The sixth completed.

22/75 accuracy-weighted solves at about 240 dollars total, against iteration 018 at 8/75
for about 243. This is NOT an effect estimate for the merged design: there was no
concurrent baseline, and the binary, the configuration and seven defects all differ. The
pre-registration scoped this as a mechanism run with solves as context only, and that
limit binds.

Recorded first because it is the easiest number to misquote: LOCA prints Overall Success
70/75, which counts runs that completed without erroring, not tasks solved. The
comparable metric is accuracy-weighted and it is 22/75. Anyone reading the raw output
sees 70/75 first and is off by a factor of three.

The mechanism now works end to end. Pressure-gated trigger firing on 35 percent of
requests rather than 72, prefix asks reading 37,336,778 tokens from cache with zero cache
writes over 778 asks, schema-shaped verdicts through the injected tool, no truncated
replies, zero stray tool calls, and expand restoring 866 with 5 unresolved. summarize
fired on 41.3 percent against 56.1, and extract_llm removed 8,140,204 unique tokens
against 318,955.

Still short: verdict coverage 65 percent, 59 batches truncated at the cap, 133
unparseable replies, and fabricated obligation quotes on 6.8 percent of verdicts.

The iteration documents seven defects in one component measurement path, all producing
the identical misleading signal that the model declines to act: the coref pre-filter,
llm_max_per_request, the economic gate, any pinned floor disabling the pressure trigger,
verdicts-divided-by-calls used as batch size, a 2048 output ceiling truncating the reply,
and a tool_choice of none driving the model into prose. Four predate the branch, three
are mine. The judgement machinery was never the problem: a sampled reply shows the model
reasoning correctly under the criterion and saying so in sentences.

An eighth defect is in the rig. capture_hop tested for markers using the unescaped
spelling, while Go HTML-escapes the angle brackets, so has_marker read zero percent on
every arm ever run here -- which reads as removals not being reversible while expand was
restoring 866 of them. expand/expand.go rawMarkerRe documents that exact trap. Every
previous marker-present line in this series should be treated as unmeasured.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ped pipeline

Iteration 020 got the mechanism working but had no concurrent baseline, so its 22/75
cannot be attributed to the design. This is the paired comparison, and the question is
narrowed to what the merged adjudicator ADDS on top of the pipeline the product already
ships: the two arms are identical but for one inserted component, with coref in neither
because it is new on this branch and not part of what we had beforehand.

extract_llm is placed before extract, following the existing ns-full order: the
deterministic extractor shrinks outputs below the model pass floor if it runs first,
which starves the batch -- the failure mode iteration 020 spent five aborts on.

Two assumptions are flagged rather than buried. The treatment arm runs with the economic
gate disabled, which is not a shippable configuration, because with it on the arm does
not run the design at all; making the gate batch-aware is the real fix and is
deliberately not done first, since an untested cost model introduced just before freezing
the binary is how a measurement gets distorted. And both arms carry the injected
adjudication tool even though the baseline cannot use it, which keeps their tools arrays
and cache behaviour comparable at the price of the baseline not being byte-identical to
the shipped product.

The binary is frozen for both arms with its commit and SHA-256 recorded before launch.
That is the whole reason iterations 014, 016 and 018 cannot be compared to each other.

Per-seed accuracy exists at tasks/<Task>/state<N>/eval.json, so the test is paired. The
primary endpoint is task-clustered over 15 clusters by paired Wilcoxon signed-rank,
two-sided, and the clustered test governs -- five seeds of one task are correlated, not
five free observations. Per-pair over 75 is a sensitivity check only. A harm upper bound
above 25 percent blocks any positive claim, declared in advance per iteration 007 failure.
No minimum effect size is claimed as a win, because the honest reading of a null result at
this n is underpowered rather than no effect.

My cost prior is stated in advance: parity to about 20 percent worse. CG arms have
historically sent 32 to 35 percent fewer tokens and billed 12 to 14 percent more, and CG
spend has tripled now the adjudicator runs on the request model. The case for this design
is reward, not cost.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Binary cg-proxy-v19, sha256 prefix ecc02f28417fe8d5edbcfaf3cc13505b, built from code at
fcf78cd, serving BOTH arms. Recording this before launch is the control that iterations
014, 016 and 018 lacked, and the reason those three cannot be compared to each other.

Also commits both arm configs. They differ by exactly one inserted component, and arm B
carries no min_tokens or trigger on extract_llm on purpose: pinning either marks the
config explicit and shouldFire then returns true unconditionally, firing the component on
every request regardless of context size.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…d why

Arm A produced 16 upstream 400s in 3,347 requests, all prompt-is-too-long, on bodies of
2.6 to 14.8 MB. Diagnosed while arm A was still running and before its solve count
existed.

The cause is a single tool output larger than anything in the configured pipeline can
reduce. extract matched no noise pattern and acted zero times, cmdfilter matched nothing
and acted zero times, toon needs a uniform object array and dedup needs an exact
duplicate, so the only real compactor is summarize, which protects keep_last 3 -- and a
fresh oversized output sits in exactly that protected tail. extract_llm would decline it
too, by design: over_model_context leaves any output exceeding the compaction model
context verbatim, on the reasoning that a program written against a truncated sample would
run against the full input.

The product already has the answer. collapse is the content-agnostic fallback for an
oversized tool output no more specific component handled, keeping a head and tail window
and stashing the original behind a marker. It is in the general and codesafe presets and
not in codesmart, which is what these arm configs descend from. So this is a rig
configuration error rather than a product defect -- with the caveat that a user of the
shipped codesmart preset has the same gap, which is worth raising separately.

The arms are not being restarted: both lack collapse, so both take the same class of
failure and the comparison stays fair in expectation. What is fixed instead is the
analysis plan, and it is fixed before any outcome is known, because errors are the one
place the omission could bias the result. Arm B removes more so it may error less, and
excluding errored runs would then compare arm B survivors against arm A. Iteration 014 hit
this with 15 errors against 8 and concluded intent-to-treat is the reading that survives.

Primary analysis is now intent-to-treat: a run that errored or has no eval.json scores
accuracy zero, and all 75 pairs are scored. Per-protocol is a sensitivity check reported
with per-arm error counts. The error counts are themselves a reported endpoint, since a
large asymmetry is a finding about oversized-output handling either way.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
The original figure multiplied each task avg_accuracy by its 5 seeds, and avg_accuracy
averages only the runs that COMPLETED, so any task with an errored seed was over-credited.
Recomputed per seed from tasks/<Task>/state<N>/eval.json, which is what intent-to-treat
requires and what iteration 021 amendment 1 mandates for both arms. 21.00 of 75.

Found while computing iteration 021 arm A by the same flawed method, which returned 17
before the per-seed recomputation returned 14.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…e gate

At 477 requests arm B verdict coverage was 34 percent against a pre-registered gate of 50
and an instruction to abort below it. Recorded here rather than in the results so it
cannot be retrospectively smoothed over.

Continued on the operator call. The gate existed to catch the case where the design never
ran, and by every other measure it is running: zero prefix asks with a zero cache read,
zero truncated replies, zero stray tool calls, 44 real drops and 686k unique tokens removed
by that point. The failure mode is partial answering, the model returning verdicts for
about a third of a full twelve-item batch, not a dead mechanism. Coverage also rose through
iteration 020 from 46 to 51 to 65 percent, so 34 at a fifth of the run may be early.

The cost of continuing is stated rather than hidden: arm B numbers are a FLOOR, because it
acted on roughly a third of the candidates it identified, so a null result cannot be read
as merged does not help, only as merged at 34 percent coverage does not help detectably.

The honest alternative was to abort, which would have preserved the gate authority at the
cost of re-running arm B, and there is no tested coverage fix to re-run it with: the
leading candidate is a smaller batch, since a probe answered 6 of 6 on six items against
about 34 percent on twelve, and that is untested.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…table over the shipped pipeline

Primary endpoint is null. Plus 1.00 solve on 75 pairs at p equal 1.0000 and plus 2 percent
total cost. The pre-registered reading for this outcome was written before the run: clustered
null with both directions flat means no detectable marginal value at this n and this cost,
close merged, keep the deterministic pipeline, report the ceiling honestly.

ITT 14.00 of 75 for the baseline against 15.00 of 75 with merged. Task-clustered over 15
clusters, which governs, gives 2 better, 2 worse, 11 tied. Per-pair sensitivity gives 7
gained, 6 harmed, 62 unchanged. Harm upper bound 15.2 percent, which does not block.

Eleven of fifteen tasks score zero in BOTH arms, which is the dominant fact about this
benchmark power: the comparison rests on four tasks with two moving each way, so no
configuration change could have shown a difference here without a large effect.

Secondary effects are real but modest. Seven fewer errored runs, 17 down to 10. Fifteen
points less summarization, 71 down to 56 percent, far short of the 4x that comparing against
iteration 020 had suggested. Twenty-eight percent fewer requests. And 6.5M unique tokens
removed with recovery working at 717 restores and zero unresolved. The cost prior stated in
advance, parity to about 20 percent worse, lands at plus 2 percent: LOCA spend fell 30 dollars
while CG spend rose 35.

Four limits are recorded, none of which rescue the result. Coverage ended at 61 percent so the
numbers are a floor. Neither arm carried collapse or mask, so neither is a shipped preset --
and collapse would probably not have helped regardless, because it skips outputs of 40 lines or
fewer and a JSON API result is often one line, so a single-line multi-megabyte payload falls
through it too. summarize ran alongside in-place offloaders against the advice in config.go.
And 146 replies were unparseable, so those calls changed nothing.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
amiddavid added a commit that referenced this pull request Aug 28, 2026
… coref seam

Two changes, both about what the next person needs rather than about behaviour today.

THE tool_use ID IS BACK IN THE INVENTORY, as a LOCATING field. Dropping it went one step past
the evidence: a9d666f establishes that the ANSWER key must be an integer -- asked to answer with
opaque ids the model regularised them, `toolu_01..07` for `toolu_probe_00..07`, against 0 bad
labels in 40+ trials with integers -- and says nothing about whether an id may appear in the
prompt. The echo risk was always about what the model RETURNS, not what it reads.

The cost of absence was the one EXACT anchor there is. The id is also in the transcript the model
reads from cache, so shipping it makes an inventory line and a tool result identifiable to each
other; without it head-plus-size is the only matching signal, which is shaky on a transcript
carrying a dozen near-identical `Read` results. The two aids now have separated roles in the code
and in the contract: the label is what the model answers with, the id and the head are what it
locates with, and the prompt says so outright ("ANSWER BY LABEL ... do not put it in your reply").

The unoffered-label guard's comment now records what it CANNOT catch: a label that is in range
but wrong removes the wrong content and looks valid from there, and nothing downstream can detect
it either. That is the failure the id exists to PREVENT rather than to detect, which is the only
defence available against a plausible-but-wrong label.

THE COREF SEAM IS LEFT CLEAN AND TRIPWIRED, because PR #80 rebases onto this branch and brings
the index's evidence signals and index-driven candidate selection with it.

  * AdjudicationItem.Evidence renders into the inventory line when non-empty and is omitted
    entirely when not, so the index can arrive without reshaping the contract -- and the contract
    is the part with measurements attached. Nothing populates it on main, and the prompt
    deliberately says nothing about how to read one: teaching the model to interpret counters the
    prompt never carries would be teaching it to read a field that does not exist.
  * sweep_inventory_thinned, AT the candidate-gathering site rather than only in the doc. 4ca1f13's
    real defect was a per-candidate PRE-FILTER sitting exactly there: prefix_still_referenced
    removed 149,681 candidates and left about one per request, silently turning a bulk arm into
    the per-output shape refuted at 6% live-kept while the arm reported itself as bulk throughout.
    It was self-defeating twice over -- it starved the comparison, and it meant the model only ever
    saw what the index had ALREADY judged spent, destroying the veto the mechanism exists to
    provide.

    main has no such thinner, so `eligible` and the inventory size are equal by construction and
    the counter cannot fire today. That is the point: a filter added between the eligible++ and
    the append trips it on its first request, and the comment says where the index's verdict
    belongs instead (as EVIDENCE for the model to weigh, never as a gate that pre-decides).
  * sweep_offered is restored -- it was lost in the one-call rewrite. A per-candidate loop cannot
    express "this many were SHOWN", and a live arm once reported 2.80 verdicts per call read as
    the batch size when it counted what the model ANSWERED. Without it, "the inventory is starved"
    and "the model answered for a third of it" are the same number.

Both docs gain the caveat the rebase makes necessary: the 58%-live-kept figure justifying
batch-style adjudication was measured WITH the index supplying evidence per candidate. On main the
model reasons from the transcript alone -- plausibly better, since it is not limited to exact
matches and the index's documented blind spot was transformed reuse, but UNTESTED. It is now
listed as the second thing a measurement should settle, after the pre-expiry window's width.

Tests, both directions on each property: the id appears in the ask and the answer key is stated;
evidence does NOT render empty and DOES render when populated; the tripwire reads zero on main and
its arithmetic counts a thinned inventory when one is simulated.

Suite: 27 packages, 0 failures. gofmt clean. Every changed file's sha256 verified against the
eval box before committing.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
amiddavid added a commit that referenced this pull request Aug 30, 2026
…cap the ask

Four blocking items from review, three of them defects.

CI WAS RED. `mkdocs build --strict` aborted on two pages absent from the nav.
components/extract_llm_sweep.md is added to the Offload list. docs/proposals/ is EXCLUDED
instead, following the pattern already there for docs/superpowers/ -- "working plans/specs for
the skill that authored them, not published site content". A proposal is the same category:
sweep-adjudicator.md argues against an earlier version of its own design and records which
measurements refuted it, which is useful in review and actively confusing on a page someone
reaches looking for how to configure something. Excluding rather than adding a nav section also
means the next proposal needs no nav edit, and it covers the coref proposal #80 will bring.
The component doc's link to it becomes a repository path rather than a site link, which is what
the strict build's remaining INFO line was pointing at -- a published 404 in waiting.

THE PARSER LOST EVERY VERDICT WITH PROSE IN THE REPLY, which is why live verification saw three
firings produce nothing. It took the FIRST `[` to the LAST `]` and unmarshalled the span, so any
bracket anywhere in the model's reasoning made the span unparseable -- and a model asked to
justify twelve verdicts writes reasoning. One of the three replies was 7,191 completion tokens
at 71.2s and NOT truncated; it simply had prose around and between the JSON. Reported as
`sweep_unparseable`, which reads as "the prompt is wrong".

Now it tries each `[` with a streaming decoder, which reads one value and ignores what follows,
and takes the first span that decodes. A decoded array is not automatically THE array, so it
must carry a verdict to be believed: `[{}]` decodes into []Verdict cleanly and would give a
phantom verdict for label 0 -- and label 0 is a real candidate, so acting on it would remove the
wrong output. An empty array stays a legitimate keep-all, because conflating that with junk is
what made "the model declined to act" and "the model was never successfully asked" one number
for three iterations (4ca1f13).

THE CALL'S COST LEDGER WAS STRUCTURALLY $0.00. The ModelCall record never set CostUSD at all, so
the per-call figure the dashboard shows for this component read zero on every firing -- measured
live at $0.00 against real cache reads of 449,304 and 449,376 tokens, while the request-level
rollup had the true $0.0940 and $0.1652. A component whose entire justification is cost looked
free, and two recorded totals disagreed with one of them structurally zero. Priced from the
REQUEST's model, which is what this component calls by construction, and from the same rates the
request-level figure uses so the two agree rather than being two independent guesses.

THE ASK WAS UNCAPPED, which risked losing every verdict rather than some. The reply carries a
verbatim quote per verdict against a 16,000-token budget, and live measurement puts a verdict at
~600 tokens including reasoning -- so ~26 candidates exhausts it, and truncation is
all-or-nothing: the array never closes, nothing parses, every verdict is discarded. A
50-candidate transcript would have swept nothing while paying for the call.

Capped at 12, largest first, and what the cap leaves unasked is counted. Twelve because two
independent arguments agree on it: the budget arithmetic puts the ceiling near 26, and cc1aa9f
measured quote fidelity degrading with size (4 of 37 non-verbatim at 16, 0 of 16 at 10) -- taken
when content was copied into the prompt, so it corroborates rather than proves. The cap
deliberately does NOT trip sweep_inventory_thinned: that alarm is for a pre-filter starving the
comparison, and a ceiling firing it would make it noise on exactly the transcripts where it
should be loudest. Whether the sweep should make a SECOND ask to cover the remainder is a real
coverage gap needing measurement, tracked in #132 rather than guessed at.

Also adds the fallback half of the keep-all split, which had no test -- only the prefix-ask
equivalent was asserted, and the fallback's is the one worth watching since it resolves toward
keep structurally.

A BOUNDARY THE FIRST VERSION OF THE PARSER FIX GOT WRONG, caught by the existing suite rather
than by me. Requiring the first element to carry a `verdict` field rejected
`[{"i":1,"needed_by":"none","quote":""}]` -- a REAL verdict object whose `verdict` the model
omitted -- and broke TestUnsureDefaultsToKeep. That reply must parse, so the caller can count it
as an unusable verdict and default to keep: a model that answered badly and a reply that could
not be read are different failures with different remedies, and they are separately counted
downstream for exactly that reason. So the guard now accepts ANY populated verdict field, which
still rejects `[{}]` and `[{"note":"x"}]` -- the objects carrying no information at all.

Every test revert-verified, output quoted:

  parser  -> three cases fail with "did not parse; a real reply of this shape produced zero
             compactions live" -- exactly the bracket-in-prose shapes, and the non-verdict
             arrays are then accepted, which is the other half of the fix
  cap     -> sweep_adjudicated = 12, want 20 before the cap existed; and the over-cap remainder
             uncounted

Full suite: 27 packages, 0 failures, gofmt clean, mkdocs --strict clean.

NOT fixed here, and reported back rather than silently accepted: the reviewer's floor
recommendations (cold 1000 kept, warm ~12,000 against the shipped 3000) rest on re-analysis of
published figures rather than new measurement, and the warm one contradicts the preset's own
committed comment -- that belongs on #120 with the author, not in this PR.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
OsherElhadad pushed a commit that referenced this pull request Aug 31, 2026
… over the model's cached transcript (#118)

* docs(proposals): the cold sweep should adjudicate, not compact

Specification only; no behaviour change. Committed ahead of the implementation because the
reasoning is the expensive part and it is currently spread across a closed PR, a commit
message on another branch, and a conversation.

The argument: extract_llm does one thing in two situations that want different things. On a
warm turn it trims a recent tool output, and rewriting is right there. On a cold sweep it does
the same to outputs deep in history, where the operation is wrong -- that content is either
still load-bearing, in which case rewriting corrupts what the model has already reasoned about,
or it is spent, in which case it should be removed rather than made smaller.

feat/coref-compaction reached this conclusion for the merged design and removed the `trim`
verdict (cc1aa9f): chosen zero times in 21 probe opportunities, metrics identical without it,
accepted once in production against eight rejected as invented, and the only verdict that asked
the model to transport text. What remained is binary -- keep verbatim, or drop with a shape
descriptor left in place -- with an obligation named and quoted verbatim, and unsure defaulting
to keep.

Applying that contract per-output removes the transporting OPERATION rather than just the
transporting strategies, which is the strongest form of the rule the measurement supports. It
also settles two things that were being carried as open questions: `rewrite` becomes moot on the
sweep rather than differently-defaulted, since nothing is rewritten; and the
extract_llm/extract_llm_sweep split stops being config hygiene, because strategy, rewrite,
aggressiveness and max_chars all cease to apply to a component that selects no compaction
strategy.

Records the config surface field by field, six safety invariants each stated as a test that must
fail when its subject is reverted, the counters and which two must be alertable, three open
questions that need measurement rather than argument, and an execution order whose first slice
is verifiable without any component wiring.

Supersedes #117, which restricted the sweep's strategies to the non-transporting ones. That
stopped the model RETURNING content but left `code` rewriting it, so it was a half-step toward
this and is closed rather than merged.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(extract): the cold-sweep adjudication contract, and the four rules that make a verdict actionable

The cold sweep and the warm tail want different operations. On a warm turn a rewrite is
right: the output is recent and a smaller version of it is more useful than none of it. On
a cold sweep the same operation runs against deep history, which is either still
load-bearing -- in which case rewriting corrupts content the model has already reasoned
about -- or spent, in which case the answer is removal, not a smaller version of something
nobody will read.

This is the contract for the second case, ported from the merged design's
internal/extract/bulk.go WITHOUT its batching. The contract is general; the batch is not.

The verdict is binary because cc1aa9f measured the alternative. trim was chosen ZERO times
in 21 probe opportunities, keep/drop scored identically to keep/drop/trim on every metric,
and in production it was accepted ONCE against EIGHT rejected as invented. It was the only
verdict that asked the model to transport text. Removing it removes the transporting
OPERATION rather than the transporting STRATEGIES: after this there is no reply field that
can carry output content at all.

The criterion is a required output field rather than an instruction because arms carrying
an identical criterion differed ONLY in whether the model had to name and quote the
obligation, and the arm that had to emit it halved the false-drop rate, 4/4 to 2/4. Stating
the criterion alone measured inert.

Two sections of bulk.go's prompt are deliberately absent. READING THE EVIDENCE taught the
model to interpret a co-reference index's counters, and no such index exists on this path.
JUDGE THEM AGAINST EACH OTHER describes comparative ranking, which a per-output call
structurally cannot do -- and that is a recorded risk, not an oversight: the merged
experiment measured comparative judgement as the difference between 6% and 58% live-kept.
The safety machinery is what carries this design, and every failure it detects resolves
toward keep.

Four guards, each verified to FAIL when its subject is reverted:

  * a drop that names an outstanding obligation is refused, not performed. Removing the
    refusal (keeping its counter) gives:

      --- FAIL: TestDropNamingAnObligationIsRefused (0.00s)
          adjudicate_test.go:38: needed_by="a": dropped an output the model itself said is still needed
          adjudicate_test.go:38: needed_by="b": dropped an output the model itself said is still needed
          adjudicate_test.go:38: needed_by="c": dropped an output the model itself said is still needed
          adjudicate_test.go:38: needed_by="A": dropped an output the model itself said is still needed
          adjudicate_test.go:38: needed_by=" b ": dropped an output the model itself said is still needed
          adjudicate_test.go:38: needed_by="in-progress-step": dropped an output the model itself said is still needed

  * unsure defaults to keep. Making an unparseable reply and an unusable verdict drop
    instead gives:

      --- FAIL: TestUnsureDefaultsToKeep (0.00s)
          adjudicate_test.go:81: empty reply: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: prose, no object: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: truncated object: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: not json inside braces: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: verdict absent: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: verdict empty: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: verdict is trim: dropped on an unusable verdict; unsure must default to keep
          adjudicate_test.go:81: verdict is prose: dropped on an unusable verdict; unsure must default to keep

  * a fabricated obligation quote is counted. It argues for KEEPING so it is not dangerous,
    but on this design it is the only remaining signal that the model is inventing, since
    nothing else it returns is content. Removing the containment check gives:

      --- FAIL: TestFabricatedObligationQuoteIsCounted (0.00s)
          adjudicate_test.go:106: a quote absent from the transcript was not counted as fabricated

  * an unanswered criterion field is tolerated and counted. Requiring it would collapse
    yield against a model that omits it; ignoring it would hide that the forcing function
    never ran. Removing the counter gives:

      --- FAIL: TestUnansweredCriterionIsToleratedAndCounted (0.00s)
          adjudicate_test.go:138: an unanswered criterion was not counted; the forcing function's absence would be invisible

Every test asserts a PRECONDITION before its subject -- that the reply parsed, and that the
verdict reached the branch under test. Without those, a parse regression turns each into a
vacuous pass: Judge would return the zero Adjudication, Drop would be false, and "the
refusal worked" would be indistinguishable from "the reply was never read".

The quote check retries once whitespace-insensitively. A model that re-wrapped a long line
copied faithfully and is not inventing, and this counter is meant to be alertable -- a
routine re-wrap firing it would train the operator to ignore the signal that matters.

No component wiring in this commit; the contract and its parser are verifiable alone.
Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(offload): the sweep's drop path -- a shape residue that transports nothing, and a drop that stays recoverable

Two properties carry the drop, and each is now a test.

THE RESIDUE TRANSPORTS NOTHING. It is computed from the output's SHAPE -- content class, line
count, record count, token count -- by this code, and contains no byte of the output. That is
stricter than the merged design's residue, which fell back to a 96-character head peek for
anything that was not a JSON array or object, i.e. for most real tool output. A head peek is
our code copying rather than the model copying, so it is not the failure cc1aa9f was about,
but it is still unverified content in the request -- and the point of an adjudicator over a
compactor is that no content moves at all. It also does not help where it looks like it
should: for a record set the first rows say nothing about whether the field you want is in
there, which is the argument the merged design already accepted for structured content and
then declined to apply to the rest.

The record count is there because a line count alone cannot answer "how much was here": a
multi-megabyte JSON API result is routinely a SINGLE line, which is the same shape that makes
collapse skip such payloads, and "1 line" is a useless thing to tell an agent about 200
records. Only a top-level array that parses gets a count; a partial payload gets the line
count rather than a guess, because the descriptor's whole value is that every number in it is
true.

THE DROP IS REVERSIBLE, AND THE MODEL IS NOT TOLD SO. It goes through tryMark/commitMark, so
the never-worse check is MARKER-INCLUSIVE -- the descriptor is small but the marker plus its
recovery hint is not free, and the pipeline's aggregate guard is per-request rather than
per-message. Note the asymmetry with the prompt, which never mentions recoverability:
measured, reassuring the model that removals stay recoverable produced 91% removal at 6%
live-kept. The operator gets the safety net; the model does not get to hear about it.

Guards, each verified to FAIL when its subject is reverted:

  * a dropped output stays recoverable. Removing the commitMark stash gives:

      --- FAIL: TestDroppedOutputStaysRecoverable (0.03s)
          extract_sweep_drop_test.go:56: the marker did not resolve — the drop would be unrecoverable
      --- FAIL: TestDroppedOutputWithoutAPersistingStoreLeavesNoDanglingMarker (0.00s)
          extract_sweep_drop_test.go:84: an unrecoverable drop must set Irreversible or the pipeline will revert it

    and omitting the marker token from the assembled text gives:

      --- FAIL: TestDroppedOutputStaysRecoverable (0.02s)
          extract_sweep_drop_test.go:52: expected exactly one resolvable marker, got 0 in "[context-guru removed a spent tool output — tool output, 7 lines, 109 tokens]"

  * the descriptor transports nothing. Restoring the merged design's head peek gives, on the
    first of four fixtures:

      --- FAIL: TestSweepDescriptorTransportsNothing (0.00s)
          extract_sweep_drop_test.go:117: descriptor transports content from the output: "KUBERNETES_NAMESPACE=quarantine-zebra" appears in "[context-guru removed a spent tool output — tool output, 7 lines, 109 tokens; began: KUBERNETES_NAMESPACE=quarantine-zebra\ndeployment/ingress-flamingo   READY 3/3   RESTARTS 0\ndeplo]"
          extract_sweep_drop_test.go:117: descriptor transports content from the output: "deployment/ingress-flamingo" appears in ...
          [27 further leaks across the remaining fixtures]

    and removing the record count gives:

      --- FAIL: TestSweepDescriptorCountsRecordsOnASingleLineArray (0.00s)
          extract_sweep_drop_test.go:138: a 200-record single-line array must report its record count, got "[context-guru removed a spent tool output — json_blob, 1 lines, 802 tokens]"

The recoverability test asserts a PRECONDITION that the drop actually happened before it
asserts recovery: "recoverable" is trivially true of content that was never removed, so a
helper that returned early would otherwise pass. The descriptor test asserts the shape facts
are present alongside the no-leak check, because a descriptor that came back empty would pass
the leak assertion while telling the agent nothing.

No component wiring yet -- the splice is a package helper the sweep component will call.
Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(offload): extract_llm_sweep -- the cold sweep adjudicates one output at a time

The component the previous two commits were building toward. On a turn whose prompt cache
has expired it asks a cheap model, ONE OUTPUT PER CALL, whether each candidate is still
needed, and either keeps it VERBATIM or removes it behind a shape descriptor and a
recoverable marker.

Not a batch, deliberately. A shared reply can be truncated mid-array, quote fidelity
degraded with batch size when measured (4 of 37 quotes non-verbatim at batch 16 against 0 of
16 at batch 10), and a batch-truncation counter has to exist to compensate. A per-output call
has none of those. The cost of the choice is that comparative judgement is unavailable, which
is recorded in internal/extract/adjudicate.go rather than glossed: the merged experiment
measured comparative ranking as the difference between 6% and 58% live-kept, so the prior on
per-output YIELD is negative and it is the safety machinery that carries this design.

Reused rather than forked, as specified: the model client, pricing, the economic gate, the
session result cache, the marker/stash machinery and the report/gate plumbing are all
extract_llm's. Only the decision is new.

The config surface is the split the proposal specifies. min_tokens, min_idle_seconds and
max_calls are the sweep's own; model, marker_mode, context, context_messages,
model_max_input_tokens and economic_gate are shared. strategy, rewrite, aggressiveness and
max_chars are REFUSED with a named reason rather than silently ignored -- an adjudicator
selects no strategy and produces no rewritten text, so a silently accepted `rewrite: false`
would read as "verified deletion-only is on" when nothing is being rewritten. Detected on a
separate probe struct, because a field on the config struct would have to be declared to the
settings form and would put a knob on the page whose only behaviour is to fail.

Three things the proposal did not cover, decided here:

  * A DROP MUST BE REPLAYED ON EVERY LATER TURN. The decision is frozen in the shared
    session-scoped result cache and re-applied at any depth on warm turns, because otherwise
    the next warm turn re-sends the removed output verbatim -- the saving evaporates AND the
    prefix the provider is caching stops being byte-stable, which costs more than the sweep
    saved. This is safe in the sense TailOnlyCold's doc requires: the DECISION came from a
    model, but the REPLACEMENT is sweepDescriptor(content), a pure function of the content,
    so a replay cannot emit different bytes than the turn that decided it. Session-scoped
    only, never global -- a drop is a judgement about THIS transcript's obligations.
  * The sweep carries its OWN ratioTracker. extract_llm's is calibrated on partial rewrites
    and this path is all-or-nothing, so pooling them would price each on the other's history.
    The gate's break-even for a drop is still the unmeasured number the proposal names as
    open question 3; rather than invent a prior, the existing exploration budget learns it.
  * The refusal is counted INSTEAD of a keep, not alongside it. Both leave the output
    verbatim, but folding "the model tried to remove something it had just said was needed"
    into the keep total is what would make the alertable counter invisible.

A REAL DEFECT FOUND WHILE VERIFYING THE CAP TEST. The gates were first raised from inside the
per-call goroutines. components.Report is copied by value across this codebase so its Gates
map carries no lock, and Go turns that into `fatal error: concurrent map writes` rather than
a wrong count -- it killed the test binary. Each call now accumulates its own gate names into
its own slot and the serial phase raises them, the same discipline the ModelCall records use.

Guards, each verified to FAIL when its subject is reverted:

  * a drop naming an outstanding obligation is refused end to end. Letting the component
    perform it gives:

      --- FAIL: TestSweepRefusesADropThatNamesAnObligation (0.08s)
          extract_sweep_test.go:157: the contradictory drop was PERFORMED (gates: map[below_output_floor:1 sweep_adjudicated:1 sweep_drop_refused_obligation:1 sweep_dropped:1])

  * the cold condition governs. Dropping the ColdCache check gives:

      --- FAIL: TestSweepDoesNothingOnAWarmTurn (0.05s)
          extract_sweep_test.go:179: a warm turn must say why it did nothing (gates: map[below_output_floor:1 cached_prefix:1])
      --- FAIL: TestSweepMinIdleRaisesTheBar/warm,_however_long_idle (0.00s)
          extract_sweep_test.go:202: sweeping=true, want false

  * a drop survives into later warm turns. Not freezing the decision gives:

      --- FAIL: TestSweepReplaysItsDropOnTheNextWarmTurn (0.08s)
          extract_sweep_test.go:236: the warm turn did not replay the frozen drop (gates: map[not_a_cold_sweep:1])

  * the cap binds. Disabling it gives:

      --- FAIL: TestSweepCapBinds (0.07s)
          extract_sweep_test.go:265: max_calls: 2 allowed 5 calls (gates: map[sweep_adjudicated:5 sweep_dropped:5])

  * the compaction-only keys are refused with a REASON. Removing the probe leaves the generic
    yaml message, which says the key is unknown rather than why it cannot apply:

      --- FAIL: TestSweepRejectsCompactionOnlyKeys/strategy (0.00s)
          extract_sweep_test.go:326: error does not say why the key cannot apply: yaml: unmarshal errors:
                line 1: field strategy not found in type offload.extractSweepConfig

  * every candidate is accounted for under concurrency. Raising the gates from the goroutines
    again gives, under -race:

      WARNING: DATA RACE
      Read at 0x00c000b92150 by goroutine 31:
        github.com/rossoctl/context-guru/components.(*Report).Gate()
            components/component.go:510
        github.com/rossoctl/context-guru/components/offload.(*ExtractSweep).Offload.func1()
            components/offload/extract_sweep.go:418

Every test asserts a PRECONDITION that the component acted -- the call count, and the gate
for the branch under test -- before asserting the outcome. A component that never ran leaves
the transcript in exactly the state a correct keep does, so without those the drop, keep and
refusal tests would all pass on a component that had stopped working.

extract_llm keeps its per_output and cold_cache surface for now; removing it is the next
commit, so this one is additive and the suite is green either way.
Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* test(sweep): pin the adjudicator's counter contract at both ends

The six counters the proposal names are what an operator's dashboard query and alert rule are
written against, so a rename breaks monitoring silently rather than loudly -- the harness and
the alert keep running and report zeros, which is the same failure mode statsGoldenTopLevel
exists to prevent for the /stats payload.

Two of the six must be ALERTABLE, and each names a distinct misbehaviour rather than a volume:

  * sweep_drop_refused_obligation -- the model tried to remove an output it had, in the same
    reply, just said was still needed. The removal does not happen; that is the invariant. But
    a non-zero rate means the contract is not holding and the only thing between the workload
    and silent context loss is our refusal.
  * sweep_quote_fabricated -- the model cited transcript text that is not in the transcript. It
    argues for KEEPING so it is not dangerous, but on this design it is the ONLY remaining
    signal that the model is inventing, because nothing else it returns is content. If the
    verdicts are to be trusted at all, this is the number that says whether they can be.

They reach an operator through the existing generic cg_component_gate_declines_total{component,
gate} series rather than through a bespoke metric each, so no new export surface is added.

Pinned at BOTH ends, with the literal strings written out twice on purpose:

  * components/offload drives the real component through five outcomes -- spent, still needed,
    a drop contradicting an obligation, an invented quote, an unanswered criterion -- and
    asserts it raises those exact names.
  * proxy asserts the same names survive to /stats under the component's own object AND to the
    Prometheus gate series, with distinct per-counter values so a mixed-up mapping shows up.

A single shared list would have been less duplication and a worse test: a rename that edited it
would leave every test passing while breaking every deployed alert rule.

Verified to FAIL when its subject is reverted. Renaming the two alertable counters in the
component only:

    --- FAIL: TestSweepRaisesTheContractedCounterNames/a_drop_contradicting_an_obligation (0.00s)
        extract_sweep_test.go:378: the component did not raise "sweep_drop_refused_obligation"; got map[below_output_floor:1 sweep_adjudicated:1 sweep_refused:1]
    --- FAIL: TestSweepRaisesTheContractedCounterNames/an_invented_obligation (0.00s)
        extract_sweep_test.go:378: the component did not raise "sweep_quote_fabricated"; got map[below_output_floor:1 sweep_adjudicated:1 sweep_fabricated:1 sweep_kept:1]

and dropping the gate histogram from the /stats rollup (metrics.addGates):

    --- FAIL: TestSweepCountersReachStatsAndMetrics (0.00s)
        sweep_counters_test.go:73: the component's gate histogram did not reach /stats, so the counter names cannot be checked

Both tests assert a PRECONDITION first -- that an adjudication was counted, and that the gate
histogram arrived at all. Without them a component that never called the model, or a rollup
that dropped `gates` entirely, would be indistinguishable from six renamed counters.

Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* fix(extract_llm): stop raising gates from the per-call goroutines (#119)

extract_llm raised two gates from inside its concurrent phase --
deduped_inflight_extraction and reply_truncated. components.Report is copied by value
throughout this codebase and its Gates map therefore carries no lock; the file says so
itself, six lines above, at the declaration of the per-slot ModelCall records that exist for
exactly this reason. The gates never got the same treatment.

THE SEVERITY IS WHY THIS IS ITS OWN COMMIT. An unsynchronised map write is not a wrong
counter and not a recoverable panic. The runtime aborts with `fatal error: concurrent map
writes`, which in a proxy kills the PROCESS -- every in-flight request of every session --
rather than letting one component fail open the way the rest of this file is careful to.
Found while building extract_llm_sweep, whose first draft made the same mistake and whose cap
test died on it.

The reachable path is the single-flight follower. extractInflight collapses byte-identical
content into one call and releases every waiter at the same instant, so N identical
candidates in one request give N-1 simultaneous raises. Four identical tool outputs is enough.

Fixed with the pattern already proved in the sweep component: each call appends its own gate
names to its own slot, and the existing serial phase raises them. The gates are raised
unconditionally there, before the `calls[k].Component != ""` guard, because a single-flight
follower returns BEFORE filling its ModelCall slot -- and its gate is precisely the one that
says so, so gating the raise on a filled record would drop it.

The broader question -- give Report.Gate a lock once so no component can reproduce this --
stays open on #119. This commit fixes the one live occurrence in the file it was already
being edited in.

Verified to FAIL when reverted. Raising the gates from the goroutines again gives, under
-race:

    WARNING: DATA RACE
    Read at 0x00c00015e158 by goroutine 28:
      github.com/rossoctl/context-guru/components.(*Report).Gate()
          components/component.go:507
      github.com/rossoctl/context-guru/components/offload.(*ExtractLLM).Offload.func2()
          components/offload/extract_llm.go:1356

    Previous write at 0x00c00015e158 by goroutine 27:
      github.com/rossoctl/context-guru/components.(*Report).Gate()
          components/component.go:508
      github.com/rossoctl/context-guru/components/offload.(*ExtractLLM).Offload.func2()
          components/offload/extract_llm.go:1356

and, WITHOUT -race, the quiet form -- a lost increment -- on 3 of 20 runs:

    --- FAIL: TestConcurrentCallsDoNotRaceOnTheGateHistogram (0.00s)
        extract_llm_gaterace_test.go:69: deduped_inflight_extraction = 2, want 3: a follower's gate was lost, which is the quiet form of the same unsynchronised write (gates: map[deduped_inflight_extraction:2])

That second form is why the test asserts an exact count rather than only relying on -race,
which the suite does not run by default.

The test asserts a PRECONDITION that a follower actually ran -- only a follower can raise
deduped_inflight_extraction -- because if the candidates never reached the concurrent phase
no two raises were ever simultaneous and the test would prove nothing.

Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(sweep)!: adjudicate a BATCH, not one output per call

The per-output shape this component shipped with was the one the experiments REFUTED, and the
proposal doc's argument for it was inverted. Corrected here, in the code and in the doc.

WHAT THE EVIDENCE ACTUALLY SAYS. docs/results/coref-selection-experiment.md, ten arms over
8,105 recorded decisions: shown ONE output, a model scored 6% live-kept on haiku and 14% on
sonnet, both inside the drop-everything null model's error bar -- shown a single output, a
model simply drops it. Shown ~15 together it reached 58% at the LOWEST cost per output, because
the overhead amortises and, more importantly, because comparative judgement beats absolute
judgement: ranking a dozen candidates against each other is a question a model can answer, "is
this one output expendable" is not.

`4ca1f13` was cited by the proposal as evidence that per-output "is a shape that has already
run". It says the opposite. It found a live arm at 1.02 verdicts per call and filed that as the
bug: "that is the per-output design refuted at 6% live-kept, not the bulk shape that measured
58%, so iteration 014 measured something other than what it claimed". It also added the
assertion whose absence let it through -- the prompt must offer more than one output, because
"asserting a single call was not enough, since one call carrying one item is exactly the
refuted design".

`cc1aa9f` gives the direction of the failure, and it is the one a SWEEP cannot tolerate: "at
batch 3-6 the model dropped a genuinely-spent output only 2 times in 4, at batch 10 it dropped
it 4 in 4 and cleared 100% of genuinely-spent candidates. Small batches do not make it wrong,
they make it UNWILLING TO ACT, which is what a 94.6% keep rate looks like from inside." A sweep
exists because the entire transcript is re-billing at the write rate; a timid adjudicator is an
expensive no-op exactly where the money is.

NEITHER OBJECTION TO BATCHING SURVIVES. Both were already solved on feat/coref-compaction:

  * truncated shared replies -- `659e7a6` traced 24 of 34 unparseable replies to a 2048-token
    output budget. Fixed here by a new optional components.Budgeter interface (same shape as
    Remodeler: same endpoint, same credential, larger reply budget), which the sweep uses to ask
    for extract.AdjudicationReplyTokens = 16000. Not a raise of cheapmodel.DefaultMaxTokens,
    because the budget a caller needs is a property of what it ASKS FOR: a one-output compaction
    reply and a twelve-verdict array are different lengths. Output bills as generated rather than
    as budgeted, so the ceiling costs nothing until used. A client without the knob still works
    and is counted (sweep_reply_budget_not_raised) rather than silently returning to truncation.
  * quote fidelity decaying with batch size -- measured, and the cap is set below the observed
    ceiling: 4 of 37 quotes non-verbatim at batch 16 against 0 of 16 at batch 10, so
    MaxAdjudicationItems is 12, the conservative end.

And the transport principle never distinguished the shapes at all: post-trim, no verdict carries
content either way. That argument rules out rewriting; it says nothing about how many verdicts
travel in one reply.

STARVATION, which is the failure that made this component's predecessor lie about its own shape.
`4ca1f13`'s other finding is that the arm's real defect was an upstream per-candidate filter --
prefix_still_referenced removed 149,681 candidates, leaving about one per request. There is no
coref index on main, but the ECONOMIC GATE was the same mechanism, so it now runs ONCE for the
batch rather than per candidate. That is also the correct arithmetic: it compares an expected
saving against one CALL's cost, and a call now covers up to twelve candidates, so charging each
of them a whole call priced the batch at ~12x its real cost. The old candidate-level max_calls
truncation is gone for the same reason -- max_calls: 4 leaving four candidates is a batch of
four, the size at which the model was measured unwilling to act. It now bounds BATCH calls.

Every safety guarantee is unchanged, because all of them are per-VERDICT properties: the refusal
of a drop naming an obligation, unsure-defaults-to-keep, fabricated-quote counting with the
whitespace-insensitive retry, the shape descriptor that transports nothing, reversibility, and
the drop-replay-on-warm-turns freeze. The #119 gate discipline matters more, not less: batches
still fan out, so each call accumulates its own gate names and the serial phase raises them.

New counters, all in the reviewed surface: sweep_offered (via a new Report.GateN, because a
per-candidate loop cannot express "this many were SHOWN" -- the distinction that cost three
iterations when 2.80 verdicts/call was read as the batch size), sweep_batch_truncated,
sweep_batch_of_one, sweep_kept_whole_batch, sweep_verdict_unknown_label,
sweep_verdict_duplicate_label, sweep_verdict_missing, sweep_reply_budget_not_raised.

docs/proposals/sweep-adjudicator.md carries the correction rather than the wrong reasoning.

Guards, each verified to FAIL when its subject is reverted:

  * the batch is offered as a batch. Chunking at 1 instead of MaxAdjudicationItems gives:

      --- FAIL: TestSweepOffersTheWholeBatchInOneCall (0.13s)
          extract_sweep_test.go:265: 12 candidates took 4 calls; the measured shape is ONE call over the batch
      --- FAIL: TestSweepAccountsForEveryCandidateUnderConcurrency (0.38s)
          extract_sweep_test.go:470: expected 5 concurrent batch calls, got 60 (gates: map[sweep_adjudicated:60 sweep_batch_of_one:60 ...])

  * the economic gate does not thin the batch. Putting it back per candidate reproduces the
    starvation exactly -- twelve candidates become a batch of two:

      --- FAIL: TestSweepEconomicGateDoesNotThinTheBatch (0.14s)
          extract_sweep_test.go:542: sweep_offered = 2, want 12: the gate thinned the batch one candidate at a time, which is how a bulk arm silently becomes the refuted per-output shape (gates: map[economic_gate:10 sweep_adjudicated:2 sweep_dropped:2 sweep_offered:2 ...])

  * the reply budget is raised. Skipping the Budgeter call gives:

      --- FAIL: TestSweepRaisesTheReplyBudget (0.08s)
          extract_sweep_test.go:422: the sweep asked for a 0-token reply budget, want 16000
      --- FAIL: TestSweepCountsAClientThatCannotRaiseItsBudget (0.00s)
          extract_sweep_test.go:443: a client without a budget knob was not counted (gates: map[... sweep_kept_whole_batch:1 ...])

  * a deliberate keep-all is not a failure. Folding the empty array back in gives:

      --- FAIL: TestSweepCountsADeliberateKeepAllSeparatelyFromAFailure (0.10s)
          extract_sweep_test.go:352: a deliberate keep-all was not counted as one (gates: map[... sweep_verdict_missing:1])

  * truncation is not a format failure. Making ReplyWasTruncated return false gives:

      --- FAIL: TestTruncationIsDistinguishedFromAFormatFailure (0.00s)
          adjudicate_test.go:140: a reply that opened the array and never closed it must read as truncated
      --- FAIL: TestSweepCountsADeliberateKeepAllSeparatelyFromAFailure (0.09s)
          extract_sweep_test.go:369: a cut-off array was not counted as truncation (gates: map[... sweep_unparseable:1])

  * a verdict for an unoffered label is never acted on. Removing the check does not merely act on
    the wrong content, it CRASHES -- a model-supplied label indexes the candidate slice directly:

      panic: runtime error: index out of range [99] with length 1
      github.com/rossoctl/context-guru/components/offload.(*ExtractSweep).adjudicateBatch(...)
          components/offload/extract_sweep.go:695

  * bounded coverage is visible. Dropping the truncation counter gives:

      --- FAIL: TestSweepBatchesPastTheItemCapAndCountsWhatItTruncated (0.30s)
          extract_sweep_test.go:326: sweep_batch_truncated = 0, want 6 — bounded coverage must be visible

  * a batch of one is counted. Removing the counter gives:

      --- FAIL: TestSweepCountsABatchOfOne (0.09s)
          extract_sweep_test.go:301: a batch of one was not counted; a starved batch would be invisible (gates: map[... sweep_kept:1 sweep_offered:1 ...])

  * the contract asks for comparative judgement. Removing the paragraph gives:

      --- FAIL: TestContractAsksForComparativeJudgementAndNoTransport (0.00s)
          adjudicate_test.go:246: prompt does not carry "JUDGE THEM AGAINST EACH OTHER"

The economic-gate tests run with the gate ON, which the rest of the file turns off. Without them
the whole gated path was untested and the per-candidate reversion passed the suite -- found by
running that reversion and watching nothing fail.

BREAKING: `max_calls` now bounds batch calls rather than per-output calls.

Suite: 27 packages, 0 failures. Also verified race-clean under -race.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* refactor(extract_llm)!: remove the cold-sweep surface, which is its own component now

Finishes the split. extract_llm is unconditionally the warm/tail compactor: `per_output` and
the whole `cold_cache` block are gone, along with every `sweeping` branch they governed --
the cadence and pressure exemptions, the sweep's own floor, the tail-gate lift, the
request-trigger carve-out, the separate cap arm, the one-writer-then-readers call ordering,
and the AUTO file-read exception. Roughly twenty conditionals that each existed to make one
component behave as two.

BREAKING, deliberately: there is one deployment and it is migrated by hand. But a removed key
must say where it went, so `per_output` and `cold_cache` are REFUSED with an error naming the
replacement rather than falling through to KnownFields' "field not found" -- which reads as a
typo rather than a relocation. `cold_cache: {enabled: true}` silently accepted would read as
"the sweep is on" while nothing swept, and that is the most expensive available misreading of
this config: the sweep exists for the turns measured at 4% of requests and 31% of spend.

MODEL ESCALATION MOVED RATHER THAN DYING. It was already guarded on `sweeping`, so removing
the sweep would have left it unreachable -- and it protects exactly the case that matters
more now: a batch's fixed prompt cost is larger than a single output's, so a small
adjudication model's window is easier to exceed, and when fitsModelContext declines every
candidate the sweep silently does nothing on the largest, most expensive transcripts. It now
lives in extract_llm_sweep, with its pricing and call-model re-derivation intact and a
sweep_escalated_to_agent_model counter.

MIGRATED, not just compiled:

  * the `housellm` preset -- the live deployment's configuration -- gains extract_llm_sweep
    after extract_llm, carrying cold_cache.min_tokens 1000 as its own min_tokens. Its comment
    keeps the measurements that justify the floor and now says plainly that they describe the
    value of sweeping this workload, not the yield of the adjudicator that now does it.
  * config/form.go loses its ONE per-component coupling. applyExtractLLMCoupling existed only
    because the constructor refused `per_output: false` with the sweep off, so the form had to
    translate two switches into pipeline membership; the sweep is a component now and follows
    the ordinary rule. The recommended prefill serves two blocks.
  * dash/ui/app.js loses XLLM_SWITCHES and the checkbox handler that flipped one switch back
    on -- a checkbox meaning two different things is what the split removed.
  * proxy/control_test.go, proxy/optionsfields_test.go, config/form_test.go,
    components/all/xglobal_test.go, apply/sweep_variants_test.go and the docs.

TWO TESTS WERE POINTED AT THE WRONG COMPONENT AND WOULD HAVE KEPT PASSING.
TestHousellmColdSweepActuallyFires read extract_llm's preset block; left alone it would have
asserted that a component which no longer sweeps still sweeps -- the same "configured into a
no-op while looking enabled" failure it was written to catch, one level up. It now reads the
extract_llm_sweep block, answers with a verdict array instead of a Starlark program, and also
asserts sweep_dropped, because a call that adjudicates and removes nothing is the same no-op
from the operator's side. The options-fields guard asserted that a nested key is served as one
dotted field using cold_cache.min_tokens; it now uses trigger.min_request_tokens and
additionally checks that the sweep's own fields reach the page at all and that the compaction
knobs do NOT -- a field for a key the constructor rejects is a control whose only behaviour is
to fail the save.

extract_cold_test.go is reduced to what is not about the sweep component: the pricing of a
cold turn, which extract_llm still sees, and the refusal of the moved keys. Everything else it
covered -- depth, the floor, the cap, min_idle_seconds, the context mode, not draining the hot
path's budget -- is tested against extract_llm_sweep, where it is exercised through the shape
that was measured good rather than through a compaction pass pointed at deep history.

Verified to FAIL when reverted. Removing the movedToSweep probe leaves the generic yaml
message:

    --- FAIL: TestKeysThatMovedToTheSweepAreRefusedByName/per_output (0.00s)
        extract_cold_test.go:68: error does not name the component the key moved to: yaml: unmarshal errors:
              line 1: field per_output not found in type offload.extractLLMConfig
        extract_cold_test.go:71: error does not say what per_output becomes (want mention of "warm/tail pass"): yaml: unmarshal errors:
              line 1: field per_output not found in type offload.extractLLMConfig
    --- FAIL: TestKeysThatMovedToTheSweepAreRefusedByName/cold_cache (0.00s)
        extract_cold_test.go:68: error does not name the component the key moved to: yaml: unmarshal errors:
              line 1: field cold_cache not found in type offload.extractLLMConfig

which is exactly the reading this guard exists to prevent: the key looks misspelled rather
than moved.

Also adds docs/components/extract_llm_sweep.md, including the three questions the design
records rather than answers.

Suite: 27 packages, 0 failures. gofmt clean.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* fix(sweep): the batches never shared their prompt prefix -- split it, and order the calls only where the write is earnable

Found in review. The sweep launched every batch concurrently under one semaphore with no cache
coordination, so each sent the same contract and conversation context as fresh input and none
could read another's write. Three separate things were wrong, and the first one made the other
two unreachable.

THERE WAS NO BREAKPOINT AT ALL. The component called Model.Complete, which routes to
CompleteSystem(ctx, "", prompt) and thence to CompleteBlocks(ctx, nil, prompt) -- no system
field, so systemBlocks placed no cache_control mark and there was nothing for a sibling batch
to read. Not a missed optimisation: a request field that was never sent.

The prompt is now SPLIT. The contract is a system block, invariant across every request and
tenant. The goal is a second system block when there is more than one batch to share it, and
stays in the user half otherwise -- with one batch there is nothing to read the entry and a
write costs 1.25x fresh, so marking it would be a 25% loss. THE CANDIDATES ARE NEVER IN THE
PREFIX: they differ per batch, so an entry containing them could never be read, which is
strictly worse than no breakpoint. Routed through extract.AskAdjudication so completeSplit
stays unexported and the decision about which half is cacheable lives next to the contract it
splits.

THE CALLS NOW EARN THE WRITE BEFORE READING IT. cheapmodel.claimCacheWrite deliberately
withholds the breakpoint from concurrent siblings -- an entry only ever written is worse than
none -- so with every batch in flight at once, sharing could not happen even in principle. The
first batch runs alone, then the rest concurrently, the same mechanism extract_llm uses. It
could not reuse extCfg.CacheContext/serialFirst directly, because the adjudication path builds
no extract.Cfg; the mechanism is mirrored explicitly instead.

AND IT IS CONDITIONAL, WHICH IS THE PART THE MEASUREMENT DECIDED. A cache_control below the
provider's minimum cacheable prefix is silently ignored -- no error,
cache_creation_input_tokens: 0 -- and the minimum is 4,096 provider tokens on haiku-class
against 1,024 on sonnet-class. MEASURED with internal/tokens:

    adjudication contract                      504 o200k
    contract + two-message `recent` context    ~537 o200k
    haiku-class floor                        3,413 o200k  (needs ~2,900 tokens of conversation)
    sonnet-class floor                         853 o200k  (needs ~349, which a real context supplies)
    unnameable gateway alias                 haiku-class, minCacheablePrefix's own default

So SHARING IS REACHABLE ON SONNET-CLASS AND PROVABLY CANNOT WORK ON HAIKU-CLASS at
`context: recent` -- and housellm pins claude-haiku-4-5. Serializing the first batch costs a
whole gateway queue round (~2-4 s p50, tail 12-16 s), so paying it for a write the provider
will refuse is strictly a loss. The component reads its own prefix size through
extract.AdjudicationPrefixTokens, asks cheapmodel.CacheablePrefix, and serializes only when the
answer is yes. On haiku it stays fully concurrent -- which is what it already did, but now for
a reason that is checked rather than by accident.

The asymmetry is recorded rather than resolved silently. `context: full` would clear haiku's
floor and is NOT adopted: that is proposal open question 2, unmeasured, and chasing a cache is
the wrong reason to change what the model is shown. Both outcomes are counted instead:
sweep_prefix_uncacheable when the floor cannot be cleared, and sweep_prefix_cache_read_ZERO when
a sibling read nothing despite the ordering -- that second failure is indistinguishable from a
working one except on the bill, which is why it needs a counter and not a comment.

max_calls DEFAULT STANDS, and now on stated arithmetic rather than an assumption I made and you
endorsed. Three extra batches duplicate ~1,600 input tokens: a fraction of a cent on haiku, and
about 4% of ONE batch's own body (twelve candidates bounded at 4,000 chars). The prefix is not
where this component's money is, by construction -- the transcript is deliberately not in it.
That is the difference from extract_llm's `context: full` sweep, where the prefix WAS the
transcript at ~138,000 tokens and duplicating it across five calls was the entire defect. The
comment on MaxCalls now says this in place of the bare extrapolation.

Guards, each verified to FAIL when its subject is reverted:

  * the contract is sent as a cacheable system prefix. Reverting to Model.Complete gives:

      --- FAIL: TestSweepSerializesTheFirstBatchWhenThePrefixIsCacheable (0.47s)
          extract_sweep_cache_test.go:127: only 0 system block(s): the goal did not join the cacheable prefix, so siblings have nothing to read
      --- FAIL: TestSweepDoesNotCacheTheGoalForASingleBatch (0.00s)
          extract_sweep_cache_test.go:178: a single-batch sweep sent 0 system blocks; the goal must stay in the user half so no 1.25x write is paid for an entry nothing reads

  * the candidates stay out of the prefix. Moving them into it gives:

      --- FAIL: TestAdjudicationSendsTheContractAsACacheableSystemPrefix (0.00s)
          adjudicate_cache_test.go:62: output 0 is inside the cacheable prefix; no sibling batch could ever read it
          adjudicate_cache_test.go:66: output 0 is not in the user half, so the model was never shown it

  * the first batch is serialized where the write is earnable. Removing the ordering gives:

      --- FAIL: TestSweepSerializesTheFirstBatchWhenThePrefixIsCacheable (0.46s)
          extract_sweep_cache_test.go:131: 3 calls had started when the first returned: the first batch did not run alone, so claimCacheWrite suppressed its breakpoint and no sibling can read a write

  * and NOT serialized where it is not. Serializing unconditionally gives:

      --- FAIL: TestSweepStaysConcurrentAndCountsItWhenThePrefixCannotBeCached (0.51s)
          extract_sweep_cache_test.go:157: the first batch was serialized on a model that cannot cache the prefix: the queue round bought nothing (started at first return = 1)

  * the uncacheable outcome is visible. Removing the counter gives:

      --- FAIL: TestSweepStaysConcurrentAndCountsItWhenThePrefixCannotBeCached (0.40s)
          extract_sweep_cache_test.go:153: haiku-class cannot cache this prefix and the component did not record it (gates: map[sweep_adjudicated:36 sweep_kept:36 sweep_offered:36 sweep_prefix_cache_read_ZERO:2 ...])

  * a sibling that read nothing is counted. Removing that counter gives:

      --- FAIL: TestSweepCountsASiblingThatReadNothingFromCache (0.39s)
          extract_sweep_cache_test.go:208: sweep_prefix_cache_read_ZERO = 0, want 2: a sibling paying fresh for the shared prefix would be invisible

  * a single batch pays for no cache write. Forcing cacheContext on gives:

      --- FAIL: TestSweepDoesNotCacheTheGoalForASingleBatch (0.06s)
          extract_sweep_cache_test.go:178: a single-batch sweep sent 2 system blocks; the goal must stay in the user half so no 1.25x write is paid for an entry nothing reads

The ordering test asserts three PRECONDITIONS before its subject -- that there were sibling
batches at all, that the prefix was judged cacheable, and that the split really produced two
system blocks -- because each of those failing would make "the first batch ran alone" true for
the wrong reason. The prefix-size test is written so that a contract which grows past a floor it
currently says it does not clear FAILS there, since that would be a real change in this
component's economics and this is where it should be noticed.

Suite: 27 packages, 0 failures. gofmt clean.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(prefixask): port prefix asks to main -- ask the request's model over its cached transcript

Groundwork for the sweep redesign, landed on its own because it is verifiable on its own and
because the sweep rewrite that consumes it is a separate change.

A component that must decide whether a tool output is still needed cannot answer that from the
output alone: need is relevance MINUS whatever has already been captured elsewhere in the
transcript, and that second term lives in the later turns. Sending those turns fresh costs ~10x
a cache read, and on a cheap model the required verbatim quoting degraded to 20.8% at bulk batch
sizes against 0 of 59 on the request model. So the judgement wants the agent's OWN model AND the
whole transcript, and only a cache read makes that affordable.

Ported from a9d666f on feat/coref-compaction rather than reimplemented:

  * components: PrefixUsage and PrefixAsker, plus Ctx.PrefixAsk. Usage is RETURNED and not
    merely recorded, because a caller whose whole justification is the cache read has to be able
    to gate on whether the read happened -- which a metrics counter cannot support.
  * cheapmodel: Anthropic.CompletePrefixed, which appends the ask as a trailing user message and
    touches nothing else except `stream`, since every byte before the appended message is prefix.
  * proxy: a bounded per-session stash of the body actually forwarded, and the asker built from it.
  * apply: Opts.PrefixAsk through to the Ctx.

THREE FACTS FROM THE ORIGINAL MEASUREMENT that are easy to lose and are now each a test:
appending to a byte-identical prefix reads the whole prefix and writes nothing (19,595 read / 0
created); `tool_choice` is NOT in the cache key so forcing it to none is free AND necessary, or
the prefix's tools make the model answer with a tool_use; `tools` ARE in the key, so stripping
them reads a different, smaller entry. The route also rejects assistant prefill, which the
appended user message satisfies by construction.

THE PREFIX IS THE PREVIOUS TURN'S SENT BODY, not the incoming one. The upstream cache was
populated by what context-guru emitted, i.e. the compacted form; the incoming body is
uncompacted and diverges at the first thing any component removed, making everything past that
point a fresh charge. Consequence, stated in the doc comment rather than left to be
rediscovered: the ask sees the transcript as of the previous turn, so the newest tool output is
invisible to it -- acceptable here, because tail content has had no turns in which to be
superseded and would be kept anyway.

Three deliberate differences from the branch this comes from:

  * ON BY DEFAULT, no CONTEXT_GURU_PREFIX_ASK gate. It was off there because "a feature whose
    benefit is a cache hit should not be on by default in a host that cannot verify the hit". We
    CAN verify it -- that is what returning PrefixUsage is for -- so there is nothing for the
    operator to opt in to. The other reason it was off, that it holds request bodies in memory,
    is a bound to state rather than a reason to disable: prefixask.go now documents what happens
    at each cap (an oversized body is not stashed; past the session or byte cap the whole stash
    is dropped), and the worst case is a declined sweep, never a wrong answer.
  * KEYED BY THE SCOPED SESSION ID. serve already receives tr.Session, which is exactly what a
    component reads as Ctx.Session. Keying the stash by the raw header instead would make every
    Ask miss while the mechanism looked switched on -- the failure mode the original's own
    comment warns about.
  * Ctx.CacheTTLMs carries the cache lifetime the cold decision already derives (5 minutes for a
    bare ephemeral mark, an hour for an explicit ttl, widened to the longest this prefix ever
    asked for). Added here because the sweep's trigger has to reason about where in the cache's
    LIFETIME a turn falls, not merely whether the entry is gone -- and re-deriving it in the
    component would be a second read of one fact, which is how the cold decision and the
    dashboard came to disagree once already.

CompletePrefixed also documents what the cached region IS here, because it is not what the rest
of that file deals with: systemBlocks places our own breakpoint and is bound by the provider's
minimum cacheable prefix (4,096 tokens on haiku-class), while this places none at all -- the
marks are the agent's own and the region is the transcript, which clears any floor by orders of
magnitude. The model-family asymmetry that governs systemBlocks does not apply.

Guards, each verified to FAIL when its subject is reverted:

  * tool_choice forced to none:

      --- FAIL: TestCompletePrefixedAppendsWithoutDisturbingThePrefix (0.00s)
          prefixask_test.go:127: tool_choice is not forced to none, so the model will answer with a tool_use: map[]

  * tools left exactly as the prefix had them:

          prefixask_test.go:117: tools were not preserved exactly; they are part of the cache key: []

  * the ask appended as a USER message:

          prefixask_test.go:105: the ask was appended as role "assistant"; this route rejects assistant prefill

  * usage returned so the caller can gate on it:

          prefixask_test.go:82: CacheRead = 0, want the provider's figure — the caller gates on it
      --- FAIL: TestAskUsesTheStashedBodyForThatSession (0.00s)
          prefixask_test.go:179: CacheRead = 0; the caller cannot gate on a figure that does not arrive

  * a missing prefix is an ERROR, not an empty answer:

      --- FAIL: TestAskWithoutAStashedBodyReportsNoPrefix (0.00s)
          prefixask_test.go:157: a missing prefix was not reported as an error
          prefixask_test.go:190: session 2 got session 1's prefix (err=<nil>)

  * Anthropic-only:

      --- FAIL: TestPrefixAskerIsAnthropicOnlyAndNeedsTheIncomingClient (0.00s)
          prefixask_test.go:219: an OpenAI route got an asker; its cache semantics are not the measured ones

  * the per-body cap binds:

      --- FAIL: TestAnOversizedBodyIsNotStashed (0.00s)
          prefixask_test.go:200: a 1500001-byte body was stashed past the 1500000 cap

No component consumes any of this yet, so behaviour is unchanged: the asker is built and handed
to the pipeline, and nothing asks. Suite: 27 packages, 0 failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(sweep)!: ask the request's model over its cached transcript, and drop the batching

Supersedes the batching. The sweep no longer copies candidate content into any prompt: it asks
the REQUEST's own model, over the transcript already in that model's prompt cache, and ships an
INVENTORY of candidates rather than their content. One call for all of them.

WHY. Three measurements, from a9d666f:

  * appending a trailing user message to a byte-identical prefix read 19,595 tokens from cache
    and created 0, on the live route.
  * verbatim quoting -- the only remaining signal that the model is inventing -- degraded to
    20.8% on the cheap model at bulk batch sizes, against 0 of 59 on the request model. Since a
    fabricated quote is the only check left on this design, that alone settles which model is
    asked.
  * need is relevance MINUS what has already been captured elsewhere, and that second term lives
    in the later turns, which a prompt carrying only the candidate cannot show.

REMOVED, because they existed only to bound copied content: the batch assembler, the 12-item
cap, per-batch concurrency, max_calls, sweep_batch_of_one and batch-truncation counting. Also
the sibling-ordering machinery from 316ead4 -- CacheablePrefix-conditional serialization,
AdjudicationPrefixTokens and sweep_prefix_uncacheable -- since with one call there are no
siblings to order and no floor to clear. The haiku/sonnet minimum-prefix asymmetry is moot: the
cached region is now the whole transcript, which clears any minimum by orders of magnitude, and
CompletePrefixed's comment records that explicitly.

THE TRIGGER IS PRE-EXPIRY, NOT COLD, and that resolves a real contradiction rather than tuning
one. The ask needs a WARM cache to read; the removal wants a COLD one so there is no live prefix
to invalidate. Both are cheap in the window where the entry still exists but has little life
left, so the sweep fires when 0 < remaining <= pre_expiry_seconds. The TTL is DERIVED, never
assumed: Ctx.CacheTTLMs is the same figure apply's cold decision uses, read out of the request
(5 minutes for a bare ephemeral mark, an hour for an explicit ttl, widened to the longest this
prefix ever asked for). Unknown does not fire.

THE WINDOW'S WIDTH IS THE ONE UNMEASURED NUMBER, and it is flagged as such in the code, the
component doc and the proposal. One minute is apply.coldMargin, the only figure here with a
stated purpose for clock uncertainty around cache expiry. Wider fires more often and invalidates
more remaining TTL; narrower fires rarely. Nothing measures either side, so it is configurable
and deliberately narrow rather than tuned.

THE CACHE READ IS VERIFIED AND ALWAYS COUNTED; the miss then FALLS BACK by default.
sweep_prefix_cache_read_ZERO fires whenever the read did not happen -- that part is not
optional, because a silent miss looks identical to a working call except on the bill. By default
the sweep then asks again with a bounded sample of each output, which is what a9d666f chose:
treating "no prefix" as "no verdicts" would disable the component on every session's FIRST turn
and read as a model that declined to act. `block_fallback: true` declines instead, for an
operator who would rather forgo the yield than pay for it. The fallback still asks the REQUEST's
model, because the measurement that chose that model is about faithful quoting rather than
caching, and that reason survives the loss of the cache read.

THE MODEL IS NOT A FREE CHOICE HERE, and the asymmetry with extract_llm is spelled out where a
reader will hit it. extract_llm may compact with any model because its prompt CARRIES the output;
this component's prompt carries an inventory and the outputs are read from the prompt cache of
the model being asked. Only the request's model has that cache, so `model.source: config` is
incoherent rather than merely suboptimal -- the ask would read nothing and pay fresh for the
entire transcript. A `model` block is refused with an error saying exactly that, not accepted and
silently corrected.

KEPT, all of it: the obligation refusal, unsure-defaults-to-keep, fabricated-quote counting with
the whitespace-insensitive retry, the zero-transport descriptor with its record count,
reversibility, decision freezing and replay, integer labels, the unoffered-label guard, and the
per-slot gate accumulation from #119. The reply budget is now cheapmodel.PrefixAskMaxTokens and
matters MORE than at batch 12, since one reply carries a verdict for every candidate.

Guards, each verified to FAIL when its subject is reverted:

  * the fallback is the default. Declining instead gives:

      --- FAIL: TestSweepFallsBackWhenTheCacheReadDidNotHappen (0.05s)
          extract_sweep_test.go:232: the default did not fall back, so the component stops working on a first turn (gates: map[below_output_floor:1 sweep_inventory_of_one:1 sweep_prefix_cache_read_ZERO:1])

  * block_fallback refuses it. Ignoring the switch gives:

      --- FAIL: TestBlockFallbackDeclinesInsteadOfPaying (0.05s)
          extract_sweep_test.go:269: block_fallback did not refuse the fallback (gates: map[... sweep_fallback_used:1 ... sweep_prefix_cache_read_ZERO:1])
      --- FAIL: TestSweepFallsBackWithNoAsker (0.00s)
          extract_sweep_test.go:311: block_fallback did not refuse the no-asker fallback (gates: map[... sweep_fallback_used:1 ...])

  * the missed read is counted in BOTH modes. Removing the counter gives:

      --- FAIL: TestSweepFallsBackWhenTheCacheReadDidNotHappen (0.05s)
          extract_sweep_test.go:228: a zero cache read was not counted; the mechanism's failure would be invisible
      --- FAIL: TestBlockFallbackDeclinesInsteadOfPaying (0.00s)
          extract_sweep_test.go:266: strict mode did not count the missed read (gates: map[below_output_floor:1 sweep_fallback_blocked:1 sweep_inventory_of_one:1])

  * a model block is refused with its reason. Letting it through leaves the generic yaml message:

      --- FAIL: TestSweepRefusesAModelBlockAndSaysWhy (0.00s)
          extract_sweep_test.go:329: the error does not mention "source: config", so the constraint reads as an oversight: yaml: unmarshal errors:
                line 1: field model not found in type offload.extractSweepConfig

  * the pre-expiry window governs. Reverting to the cold gate gives:

      --- FAIL: TestSweepFiresOnlyInThePreExpiryWindow/inside_the_window (0.00s)
          extract_sweep_test.go:293: sweeping = false, want true (idle=240000ms ttl=300000ms cold=false)
      --- FAIL: TestSweepFiresOnlyInThePreExpiryWindow/apply_already_called_it_cold (0.00s)
          extract_sweep_test.go:293: sweeping = true, want false (idle=240000ms ttl=300000ms cold=true)

  * the inventory ships no output content. Putting the body back in the ask gives:

      --- FAIL: TestSweepSendsTheInventoryAndNotTheOutputs (0.05s)
          extract_sweep_test.go:603: the whole output body was copied into the ask, defeating the mechanism
          extract_sweep_test.go:607: the ask is 38031 chars; it should be the contract plus one line per candidate

  * the unoffered-label guard prevents a crash on model input, not merely a wrong action.
    Removing it gives:

      panic: runtime error: index out of range [99] with length 1
        offload.(*ExtractSweep).adjudicate(...)  components/offload/extract_sweep.go:502

  * a nil asker is handled rather than dereferenced:

      panic: runtime error: invalid memory address or nil pointer dereference
      [signal SIGSEGV: segmentation violation code=0x1 addr=0x18]

ALSO FIXES A CORRUPTED FILE I COMMITTED IN 5d02669. config/form_test.go lost its first 60 bytes
to a bad base64 round-trip, so the config package did not compile on the pushed branch even
though the suite passed on the eval box -- I validated remotely and committed from a stale local
copy. The header is reconstructed and every changed file's sha256 is now checked against the box
before committing.

docs/proposals/sweep-adjudicator.md carries its second correction and now describes the code
rather than preceding it; docs/components/extract_llm_sweep.md is rewritten for the new
mechanism.

Suite: 27 packages, 0 failures. gofmt clean.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>

* feat(sweep): ship the tool_use id as a locating anchor, and guard the coref seam

Two changes, both about what the next person needs rather than about behaviour today.

THE tool_use ID IS BACK IN THE INVENTORY, as a LOCATING field. Dropping it went one step past
the evidence: a9d666f establishes that the ANSWER key must be an integer -- asked to answer with
opaque ids the model regularised them, `toolu_01..07` for `toolu_probe_00..07`, against 0 bad
labels in 40+ trials with integers -- and says nothing about whether an id may appear in the
prompt. The echo risk was always about what the model RETURNS, not what it reads.

The cost of absence was the one EXACT anchor there is. The id is also in the transcript the model
reads from cache, so shipping it makes an inventory line and a tool result identifiable to each
other; without it head-plus-size is the only matching signal, which is shaky on a transcript
carrying a dozen near-identical `Read` results. The two aids now have separated roles in the code
and in the contract: the label is what the model answers with, the id and the head are what it
locates with, and the prompt says so outright ("ANSWER BY LABEL ... do not put it in your reply").

The unoffered-label guard's comment now records what it CANNOT catch: a label that is in range
but wrong removes the wrong content and looks valid from there, and nothing downstream can detect
it either. That is the failure the id exists to PREVENT rather than to detect, which is the only
defence available against a plausible-but-wrong label.

THE COREF SEAM IS LEFT CLEAN AND TRIPWIRED, because PR #80 rebases onto this branch and brings
the index's evidence signals and index-driven candidate selection with it.

  * AdjudicationItem.Evidence renders into the inventory line when non-empty and is omitted
    entirely when not, so the index can arrive without reshaping the contract -- and the contract
    is the part with measurements attached. Nothing populates it on main, and the prompt
    deliberately says nothing about how to read one: teaching the model to interpret counters the
    prompt never carries would be teaching it to read a field that does not exist.
  * sweep_inventory_thinned, AT the candidate-gathering site rather than only in the doc. 4ca1f13's
    real defect was a per-candidate PRE-FILTER sitting exactly there: prefix_still_referenced
    removed 149,681 candidates and left about one per request, silently turning a bulk arm into
    the p…
Re-counting capfail-i21A.jsonl gives 19 records: 17 oversize-prompt 400s plus 2 transport
failures. The original 16 could not be reconstructed from any filter over that file.

More important than the count, and recorded alongside it: those digests hold per-message
STRUCTURE only, with no content, no per-message sizes and rig_seq null throughout. So the
related inference that the failing bodies had few enough lines for collapse line window to
miss them cannot be tested from this record in either direction. The earlier reasoning
treated a MESSAGE count of 4 as evidence about LINE count, which it is not.

Found while addressing review feedback on PR 138, whose body now carries the same
correction.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
amiddavid added a commit that referenced this pull request Sep 1, 2026
…shipped baseline

Written before the run, and before the binary is frozen, which is the control
iterations 014, 016 and 018 lacked and the reason none of them can be compared to
each other.

It opens by stating what iteration 021 did NOT establish, because that decides
what this one may claim: 021 was a null on its primary endpoint (clustered
p = 1.0000, 14.00 vs 15.00 solves, +2% cost), its pre-registered reading for that
outcome was "close merged", and `coref` was in neither of its arms. What 021 did
move was operational -- errors 17->10, summarize 71%->56%, requests -28%, 6.5M
tokens removed with 0 unresolved. So this iteration measures the operational
result and carries reward as a HARM GATE only, with the blocking bound declared in
advance per iteration 007's failure.

Three arms, alternatives rather than a stack. B turns on the sweep's evidence and
econ_trigger -- PR #80's merged design, where the index informs and the model keeps
the veto. C adds the coref component, the zero-LLM cutter. They are not combined
because coref leaves markers and the sweep skips marked content, so coref upstream
would hide its own cuts from the model: structurally the prefix_still_referenced
thinner that left about one candidate per request and silently turned a bulk arm
into the per-output shape refuted at 6% live-kept.

Band moves to 32k. At 128k, 11 of 15 tasks scored zero in BOTH arms, so nothing
could have been detected there whatever the configuration. Iteration 008 measured
52.7% at 32k against 33.3% at 64k, and signed-rank power peaks near a 50% base
rate. Stage 0 runs seed 42 only, 15 runs per arm, which 008 also validated (52.7%
over 75 configs against 53% on state0 alone).

The primary endpoint is turns-and-wall-clock SPLIT BY RUN OUTCOME. Iteration 021's
-28% requests is the strongest number it produced, but its own text attributes it
to fewer runaway sessions and its arm B also errored 7 fewer times -- so the drop
may be "failed less" rather than "solved faster", and an end-user latency claim
needs exactly that split. 021 could not make it.

Two shipped defects are carried deliberately and recorded so they are not mistaken
for errors in this file: #134 (pinning min_tokens/trigger makes shouldFire return
true unconditionally, so housellm's tail pass fires every request -- and since it
leaves markers the sweep skips, it can progressively starve arm B) and #120 (the
tail floor is 3000 where the comment above it derives 8000 and calls anything below
measurably a loss). Both are identical in all three arms, so neither can bias B-A
or C-A, and both become measured endpoints instead of config guesses.

The run does not start until a four-part pre-flight passes, including that the
sweep is actually offered candidates -- because the venv was rebuilt with --no-deps
and because the two rig failures this box has already produced "look like a result
rather than a broken rig: tasks run, requests flow, numbers come out."

Signed-off-by: David Amid <david.amid@il.ibm.com>
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New/ToDo

Development

Successfully merging this pull request may close these issues.

2 participants