Skip to content

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

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

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

Conversation

@amiddavid

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 the substrate it depends on.

What's here

  • internal/coref — the tier-1 reference index: which identifiers each tool output
    introduced, and whether any later model turn carried them forward. No bifrost, no components,
    no tokenizer dependency, because it has to stay interchangeable with deploy/harbor/coref.py's
    definition. Its fixture is the twin of coref_fixture.py, negative control included and
    asserted.
  • components/offload/coref — the Offload component. The one component that mutates the
    cached prefix on purpose, so: batched cuts, a per-session rewrite_budget, latched decisions
    replayed byte-for-byte, repairLostFreeze deliberately not consulted, side-effect-free planning.
  • deploy/harbor/coref.py + two converters (cc_capture.py, runlog_capture.py) — the
    measurement pass, plus the plumbing to run it on Claude Code transcripts and benchmark harness
    logs without an eval-box run.
  • Docs — the proposal, measured results, a
    component reference, and a
    one-page cheat sheet for the vocabulary.

The measurement, and the finding

Run on three corpora (none of them the eval-box captures — those were unreachable). The headline is
that they disagree by a factor of three:

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%

Reference density is a property of the workload, not a constant. Interactive work on a coherent
codebase keeps returning to the same files and errors; benchmark tasks survey, extract, and move on.
The last row bounds the obvious tail bias and the ordering survives it.

Three more results:

  • Distance is not the discriminator; repetition is. Sweeping closed_dist over a 10× range
    moves the answer 2–3 points; sweeping open_reps 2→6 moves it 18. And 44% of mass was last
    referenced 40+ messages ago while 60% is open — most referenced mass is old and still hot. A
    distance-based A/B split would confidently cut repeatedly-referenced content.
  • A reference consumes a median 18.7% of what its output introduced — hypothesis A confirmed.
  • Break-even is workload-dependent: median required T is 95 turns on interactive traffic
    (15/30 sessions clear it) against 17 and 14 on the benchmarks. Batching moves it from unreachable
    to comfortable-on-benchmarks, marginal-on-interactive. Steps and deferred agent-compaction remain
    the load-bearing justification.

LOCA's 0% closed is the proposal's own §8 prediction landing: it argued LOCA would be a tier-2/3
stress test where references arrive transformed past what a substring match can see.

One bug worth calling out

The first measurement said 71% referenced. The rule deciding "identifier vs English word" accepted
any token of 10+ characters, so description, transparency, efficiency and conditions scored
as references. A manufactured reference makes an output look load-bearing, so this class of bug
fails by silently declining to compact — invisible to any metric that counts only what the
component did. Corrected to require interior structure, a digit, or camelCase; every false positive
is now a regression case. The residual is bounded at ~6 points of under-reporting.

Status

Opt-in, in no preset. cut_unreferenced is on by default and justified on every corpus.
cut_closed is off: its yield ranges 0–15% by workload, which is no basis for a default.

Next, in order: re-run on capture-swe/capture-tb at the eval box → enable cut_closed there →
observe-mode expand rate as the precision inner loop → only then the scored benchmarks.

Verification

gofmt clean · go vet ./... clean · go test ./... 24 packages, 0 failures · fixture reproduces
its documented ground truth.

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>
… bound is wide

The first live paired reward measurement, pre-registered before it ran. 75 runs per
arm, 15 tasks by 5 seeds, format against format+coref at 32k, $191.03 of the $340
approved.

No detectable reward difference in either direction. Task-clustered, four tasks net
harmed and four net gained, p=1.000; under intent-to-treat four against five. The
pre-registered <=10% harm bound was not achieved and could not have been, since that
figure assumed zero harm events and there were four, giving <=51% -- effectively no
constraint. Worth stating plainly: the experiment was sized to pay off only under a
clean result, and the result was not clean.

The most informative number is the churn rather than the totals. Sixteen of 69 pairs
flipped outcome, split 7 harm to 9 gain, so coref changes about a quarter of outcomes
with no consistent direction. That looks more like trajectory perturbation than
systematic help or harm, and it means a directional claim needs far more independent
tasks than the 15 LOCA has. It also means churn is itself a cost, invisible to every
earlier measurement here because none paired individual runs.

coref removed the transport failures, 6 to 0, matching the 64k direction, and under
intent-to-treat that is worth five extra solves. Recorded with the caveat that this
is a benefit against an unattributed rig defect rather than against the provider, so
it must not be quoted as a reason to run coref: if the defect is fixed the benefit
disappears.

Yield is about one point on top of lossless -- 511,721 tokens over 1,266 requests,
acting on 6.8% of them -- and removing more tokens did not cost less: $97.80 against
$93.23, 4.9% more for 0.9pp more removed. Cache-write economics and trajectory
divergence are both candidates and are not separated here.

Conclusions are marked provisional under the pre-registration's own rule that three
or more errors is a rig failure rather than a result.

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

Arm 1 completed 75 runs and 28 of them, 37%, failed with the provider rejecting
"messages.1: tool_use ids were found without tool_result blocks immediately after".
The pre-registration named this case and said to fix rather than interpret, so arms
2 and 3 were cancelled before spending, saving roughly $180.

This is a fourth message-shape defect in summarize after the three fixed in
iteration 005, and it survived both schema.ValidateShape and the test asserting all
11 presets emit shape-valid requests, so either the validator has a gap or the
fixtures do not reach the breaking shape.

The aborted arm did establish two things. summarize works mechanically and the
data-driven trigger was right: 48 firings across 1,201 requests with 35 model calls,
removing 1.67M tokens and lifting total removal to 29.6% against format-alone's
24.2%. And the HTML 400 from iteration 010 is a different fault -- these 28 are
proper JSON Anthropic errors served by uvicorn, where iteration 010's were raw HTML
with no Anthropic body. Two faults were being conflated; they are now separated and
neither is solved.

Reasoning from the source did not converge, because both splice sites do align their
boundary and with headCount=1 index 1 is the summary message, which cannot carry
tool calls, yet all 28 orphans are at messages.1. So the emitted list is not the list
the code appears to produce, and diagnosis moved to instrumentation.

Adds deploy/harbor/capture_hop.py, which sits between the proxy and the gateway
rather than upstream of the proxy like the existing shim, repairs nothing so it
cannot mask the defect, and records a structural digest of the outgoing message list
on any 4xx: per message the index, role, block types, tool_use ids declared and
tool_result ids answered.

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

ValidateShape reported a violation on every ordinary PARALLEL tool exchange. One
assistant message may carry several tool_use blocks, answered on the wire by a single
user message with several tool_result blocks, which bifrost's schema represents as a
RUN of consecutive role=tool messages. The rule inspected only msgs[i+1], so the
second call always looked unanswered:

  messages.1 [answered-tool-use] ... without `tool_result` blocks immediately after: call_b

That is indistinguishable from the real defect under investigation, which made the
validator useless exactly where it was needed. It now scans the whole run of
consecutive tool messages. The invariant is unchanged: the run must still be
contiguous, since any other role ends it.

Adds three regression tests for shapes no existing fixture covered, and one that
FAILS and documents a genuine defect.

summarize is exonerated at the message-list level: given a valid transcript it
preserves pairing, for single and parallel calls, at every keep_last from 1 to 5.

apply.rebuildCountChanged is not. With Anthropic wire input carrying parallel calls,
[summarize] emits [user, summary, assistant(tool_use, tool_use), user] -- the body
message holding both tool_results is dropped, leaving the parallel call unanswered.
That is exactly the shape a capture hop recorded on live traffic, where it failed 28
of 75 runs.

The fix is deliberately not attempted here. The suspect area is the slot/body-index
mapping that lets several normalized messages share one body index, and a careless
change risks the byte-losslessness guarantee this package exists to provide. This
component family has produced four shape defects already, so the reproduction is the
deliverable rather than a blind fifth fix.

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

Iteration 011 is blocked because any arm containing summarize fails about 37% of runs
on the apply.rebuildCountChanged defect reproduced in 2ec2445, so this iteration tests
a question that does not touch the broken path: what does extract_llm with full-body
reach add on top of format and coref, and is the removal repaid?

The baseline is reused from iteration 010 rather than re-run. That saves about $93 and
the config is byte-identical, at the cost of the arms running at different times so
gateway drift is uncontrolled. The reuse is declared valid only if the baseline's
recorded error count and solve rate still match, and that check runs before any
conclusion.

Primary endpoint is deliberately not 'does it remove more tokens', which it will, but
whether the removal is repaid: iteration 010's central negative result was that
removing more tokens cost more money, and the fold spends model calls on top. Churn is
promoted to its own endpoint since the fold rewrites strictly more and iteration 010
showed 23% of pairs flipping with no direction.

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

The fold looked like the first money-saving configuration in this work: 26.0%
reported savings against the lossless baseline's 24.2%, and $89.52 against $93.23.
Both readings survive scrutiny poorly.

CG's own stats report unique savings alongside the headline, and they diverge sharply
for non-deterministic components: coref by 4.07x in the coref arm and 8.40x in the
fold arm, extract_llm by 2.80x, while format -- deterministic and in place -- sits at
exactly 1.00x. The same removed content is re-credited on every later turn that
replays a frozen rewrite. So every coref yield figure in this log is inflated,
including iteration 010's 511,721 tokens, and in the fold arm coref's real
contribution is 71,612 tokens of 23,775,292, or 0.3%.

Correcting that inverts the ranking. By unique tokens the fold removes 5.26M against
the lossless baseline's 6.98M -- it is the worst of the three, not the best. The
mechanism is cannibalisation: format's own unique saving falls from 6.98M to 4.97M as
components are added ahead of it, so total unique removal drops as the pipeline grows.
savings_pct cannot show this because it sums per-component credit and therefore
rewards double-counting.

The apparent cost saving is also withdrawn. The fold pays 927,245 more cache-write
tokens, removes 1.72M fewer unique tokens, and spends $0.96 on its own model calls, so
on compaction-attributable terms it is break-even at best. The $3.71 difference is
trajectory variance -- it solved more tasks, and a run that succeeds differs in length
from one that flails -- and iteration 010 showed the same magnitude in the opposite
direction. Per-arm benchmark cost cannot price a component.

Reward is 3 net-harmed against 6 net-gained tasks, p=0.508, bound <=44%: the most
favourable direction measured, and still only an absence of evidence of harm at a
resolution too coarse to be worth much.

Also records a pre-registration design error. The promised validity check for the
reused baseline was vacuous, since reusing the same run directory compares the data to
itself and cannot detect drift between arms run hours apart. The $93 saved bought an
unverifiable comparison.

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

Root cause of iteration 011's 28-of-75 live failures, found by instrumenting the
rebuild rather than reading it.

An Anthropic assistant turn carries its calls as tool_use CONTENT BLOCKS, and
bschemas.ChatContentBlock has no such type -- its enum is text, image_url,
input_audio, file, refusal -- so the ids are absent from the unmarshaled message.
Everything reasoning about tool pairing therefore saw zero calls on every Anthropic
request, and one cause produced two defects.

dropOrphanedToolResults builds its answerable set from ToolCalls alone. On Anthropic
traffic that set was always empty, so every tool_result looked orphaned and the
"repair" deleted all of them; the provider then rejected the request for the
unanswered calls left behind. The instrumented rebuild showed summarize's output
containing no tool messages at all, only [head, summary, assistant, final user].

schema.ValidateShape was blind to the same ids, which is why a test asserting all 11
presets emit shape-valid requests stayed green while live traffic failed 37% of runs.
The offload-level tests passed for the same reason: they hand-build messages with
ToolCalls populated, which is the OpenAI-shaped representation, so they never
exercised the Anthropic path.

normalize now recovers the ids where the dialect is still known, rather than teaching
each consumer about Anthropic. This deliberately makes the lossless round-trip check
false for such messages, which is honest since bifrost cannot round-trip them, and it
only makes the write-back guard more conservative.

Verified: both reproducing tests pass, and the full suite is 24 packages green with
zero failures.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Corrects iteration 011's attribution. summarize is not at fault; the defect is in
apply, and it is one cause with two faces.

bschemas.ChatContentBlock has no tool_use type -- its enum is text, image_url,
input_audio, file, refusal -- so an Anthropic assistant turn's calls, which arrive as
content blocks, are absent after unmarshaling. Everything reasoning about tool pairing
saw zero calls on every Anthropic request. dropOrphanedToolResults therefore found an
always-empty answerable set, treated every tool_result as orphaned, and deleted all of
them; ValidateShape was blind to the same ids and so could not see the breakage, which
is why a test over all 11 presets stayed green while 37% of live runs failed. The
offload tests passed for the same reason: they hand-build messages with ToolCalls
populated, the OpenAI-shaped representation, so they never exercised the Anthropic path.

Also records a near-miss. The first live verification of the fix reported zero captured
failures and looked clean, but the same output showed llm_calls=0 and no summarize
line, so the component never fired and the result said nothing. Re-verified with a
diagnostic trigger that forces it to act. That is the same failure as iteration 012's
baseline-reuse check, which compared the data to itself: both asked whether the run
came back clean rather than whether the check could have failed.

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

The tool-call-id fix in 62126f4 unmasked a second defect that had been hidden behind
it. With ids recovered, dropOrphanedToolResults correctly stops deleting tool
results -- and those surviving synthetic role=tool messages then reached the rebuild,
which had never had to serialize one before. Live re-run: 12 of 39 runs failed with

  400 messages: Unexpected role "tool"

Anthropic has no tool role; the synthetic role=tool message is an internal
representation of a tool_result content block. The rebuild emits a message verbatim
only when it byte-matches its pre-pipeline form, so once format rewrote a tool
message's text -- it rewrites hundreds per run -- the message fell through to a fresh
marshal and leaked the internal role onto the wire. One defect had been masking the
other: while all results were being deleted, none could reach this path.

Two changes. Tool-text rewrites are now written into the body's tool_result blocks
before the rebuild, using the same sjson mechanism the equal-count path already uses,
so the rebuild only decides which messages to keep and never how to serialize one.
And synthetic tool messages are matched by tool_call_id rather than by bytes, since
their text may legitimately differ from the pre-pipeline form.

The regression test took three attempts to become real, which is worth recording. Its
first version passed with the fix removed because the fixture's tool content was
plain prose, which format leaves alone. The second still passed because the content
was already-compact JSON, hitting format's already_compact gate. It now uses indented
JSON so format acts, and asserts the precondition -- that some case carried a
rewritten tool_result through a count change -- so it fails loudly rather than passing
vacuously. Verified by neutralising the fix: the test fails, then passes when restored.

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>
Fix 1 recovered Anthropic tool-call ids and stopped dropOrphanedToolResults deleting
every tool result. That unmasked fix 2: the surviving synthetic role=tool messages
reached a rebuild that had never had to serialize one, so the arm-1 failure rate went
from 28/75 on the original defect to 13/39 on a different one -- Unexpected role
"tool" -- before reaching 0/16 with both fixes in place. One defect had been masking
the other.

Records the evidence that the verification is not vacuous, since two earlier clean
verifications this session proved nothing. In the clean run summarize is firing hard,
316 component log entries with up to 39,313 tokens removed in a single request, and
zero role or pairing errors reach the wire. Also records why a 5-task probe cannot
verify this at all: its trajectories are too shallow for summarize's min_messages and
span floor, so it never fires even with the trigger lowered to 5,000 tokens, which is
why arm 1 of the real experiment is the verification and runs behind a gate.

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

The clean re-run shows 2 failures in 38 runs and neither is the pairing bug: one is
the still-unattributed HTML 400 transport fault, and one is new -- 'each thinking
block must contain thinking'.

Same root cause family as the tool-call defect. bschemas.ChatContentBlock's type enum
is text, image_url, input_audio, file, refusal, so it cannot represent a thinking
block any more than a tool_use one, and any path that re-marshals an assistant message
instead of emitting its original raw bytes silently drops the thinking content. In
rebuildCountChanged that is the matched-below-zero branch, which exists for genuinely
new messages and cannot currently distinguish them from a modified survivor.

All three defects reduce to one fact: bifrost's schema is a lossy model of an
Anthropic request, so correctness depends on never re-serialising a message that came
from the body. Two defects broke that rule; the third was the checker having the same
blind spot.

Not fixed now, deliberately. It is rare, it does not block the experiment, and two
substantive changes to this package already landed tonight without review; a third
unreviewed change to the byte-losslessness machinery would raise the risk of a fourth
defect more than it lowers the risk of this one.

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

Gate passed with 4 failures of 75 against 28 before the fixes, so the two apply fixes
hold on live traffic and arms 2 and 3 are proceeding.

format+summarize solved 33 of 71 clean runs, statistically the same as the lossless
arm's 33 of 69, while costing about $152 against $93.23 -- 63% more. summarize reports
removing 94.7% of all tokens, which makes this the sharpest demonstration so far that
removing tokens is not saving money.

The obvious mechanism is not the culprit: the expand loop, where the marker invites the
model to restore the span and regrow context, is unsupported since expand appears twice
in the entire run log. What the counters show instead is that trajectories diverged
enormously -- mean pre-compaction request size is 141k tokens against the lossless
arm's 12.2k on identical tasks, an 11.5x inflation. A plausible but unestablished
reading is that lossy summarisation makes the agent redo work it can no longer see, so
total context grows even as each request shrinks; the cache breakdown is consistent
with the prefix being invalidated repeatedly and billing fresh where the lossless arm
billed cache reads.

Two cautions recorded against the 94.7%: tokens_before is a counterfactual assuming an
unchanged trajectory, and the trajectory is what changed; and summarize's own overcount
ratio is 2.23x, so its unique contribution is 129M rather than 288M.

If this holds in the remaining arms it strengthens the case for deferring
summarisation, since a summary here looks not merely lossy but actively expensive.

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

The claim the project was premised on, measured live for the first time. Each layer of
selective compaction makes the blunt summariser fire less often, monotonically: 56.1%
then 42.3% then 36.9% of requests. Cost tracks it almost exactly: $152.08, $125.00,
$92.44. The fold arm is best on every axis at once -- 35 solved, cheapest, most
deferral -- and is the only configuration in this body of work that beats the lossless
baseline on cost and solves while removing substantially more.

Four things recorded against the headline. The margin over lossless is 0.8%, inside the
trajectory noise that iteration 012 showed makes per-arm cost unable to price a
component, so the ordering is believable but the final margin is not. No reward
difference is detectable: task-clustered comparisons give 3 harm against 2 gain and 4
against 6, p at or above 0.75, with all four configurations solving 32 to 35 of 75, so
deferral buys cost and not accuracy on this evidence. summarize is never necessary at
this band, since contexts run 12-44k against a 1M window and the trigger was chosen
specifically to force firing, so these arms measure summarising when it is not needed
and cannot answer whether deferral pays when summarisation is genuinely forced. And mean
request size is not monotonic, so deferring more and carrying smaller contexts are not
the same axis.

Also flags a leverage effect worth following. coref's own unique removal is only 233k to
415k tokens, yet its presence cut summarisation by 25% per request and cost by $27. If
causal, coref's value is the summarisation it prevents rather than the tokens it
removes, which no yield metric in this repo would have shown. Trajectory divergence is
an equally live explanation and separating them needs a design that holds trajectory
fixed, which no arm here does.

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

Two corrections raised in review.

The summarize configuration was unrealistic. resummarize_tokens was 6,000, which this
arm's own traffic shows is the p90 of a SINGLE tool output -- 12% of individual outputs
exceed it alone -- so the tail crossed it within a turn or two and arm 1 produced 10
fresh summaries per run. The first trigger, 30,000 tokens, was 3% of Sonnet-5's 1M
window where Claude Code compacts near 90%, so arm 1 is not the /compact analogue the
document called it. The trigger was chosen to force firing because 32k-band contexts
never approach a 1M window, which built a configuration nobody would deploy.

Retracted: the economic magnitude. The +63% cost penalty is largely an artifact of that
configuration rather than a property of summarisation, and the $152 to $125 to $92
ordering measures over-eager resummarisation rather than anything transferable. What
stands is the deferral mechanism and its fit: coref removes about 2,490 tokens per
request against a 6,000-token threshold, 41% of the rollforward budget, and produced
-42% fresh summaries against a predicted -41%.

Second, 'fold' has been used for two different designs and the collision caused a
misreport. In iter002-004, iter012 and iter011 arm 3 it means extract_llm added as its
own component alongside coref, each acting separately. In discussion it meant the
co-reference criterion carried inside extract_llm's own prompt so one model call makes
both judgements. Only the first has been measured. The merged single-call design remains
untested: iter009 refuted only its per-output variant and bulk adjudication is the
surviving shape.

Records what a sound re-run needs, including the row that was missing entirely: nothing
enforced a context ceiling, so requests reached 770k. With LOCA's clearing active the
baseline becomes genuinely lossy, which iteration 010 could not test since its shim
reported repairs=0 and the baseline kept everything.

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

The idea in its original form: once you are already paying for an LLM call, that call
can also decide what has been referenced and is spent, so the backward-looking index
and the forward-looking model stop being two passes. The value would lie where an
exact matcher structurally cannot judge -- Tier-2/3 reuse, where a transformed value
leaves no substring, and the anchor-versus-payload ambiguity.

Built as BULK rather than per-output, because the per-output form is refuted. One call
shown a single output plus its evidence scored 6% live-kept on haiku and 14% on
sonnet, both inside the drop-everything null model's error bar; shown fifteen outputs
together it reached 58%, at the lowest cost per output, because comparative judgement
is a question a model can answer and absolute judgement is not. The prompt also
carries cost-honest framing, worth about 26 points of live-kept on its own: it states
that a wrong removal is usually not noticed and silently costs task quality, and it
never mentions recoverability, since the clause reassuring the model that removals
stay recoverable produced 91% removal at 6% live-kept.

The prior is negative and recorded in the code: the free deterministic index still
beat every model arm measured at 95% live-kept and 11% false-drop, and neither
floor-symmetry nor a widened Tier-2 ground truth closed that gap. This exists to
answer the one axis decision-quality experiments cannot speak to, which is reward.
Default behaviour is unchanged, and a mistyped selection_mode now fails at
construction rather than silently running the default shape.

Integration is deliberately narrow: it fills the same projected/summary slots the
per-output loop fills, so freezing, marker creation, the store, the never-worse check
and every counter are shared verbatim, and only the decision differs. That is what
makes the two arms comparable.

Four tests, each verified to fail when the thing it checks is removed. One asserts
exactly one model call, since per-candidate calls would silently be the refuted
design. One asserts the prompt carries both the evidence and the cost-honest framing
and does not mention recoverability. One refuses a trim whose text was not in the
original. And one covers plain-text output, which caught a real gap: corefStub returns
empty for anything that is not JSON, so drops of logs, file reads and tracebacks
recorded a gate counter while producing no projection -- the arm would have looked
like it was deciding and removing nothing. mergedResidue now falls back to a head
peek, which corefstub.go's own reasoning says is the right residue for unstructured
content.

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>
…ponents

The design originally proposed and never measured: folding the co-reference criterion
into extract_llm's own call, against running coref and extract_llm as separate
sequential components. coref is absent from the merged arm deliberately, since that is
the claim; its criterion is carried in the prompt as evidence including the index's own
verdict.

States what is and is not being tested. Whether an LLM beats the index is already
answered and negative, at 95% live-kept and 11% false-drop for free with no model arm
beating it after both known biases were corrected. What is untested is reward, which no
decision-quality experiment can speak to. The implementation is bulk rather than
per-output because the per-output form of exactly this design was refuted at 6%
live-kept, inside the null model's error bar.

Secondary endpoint is calls and cost, since the merged shape should make one call per
request instead of several: a merged arm that costs more than separate components has
no case. Yield is quoted in unique tokens only, never savings_pct, which overcounts
non-deterministic components 2.2 to 8.4 times and ordered the arms backwards in
iteration 012. The merged gate counters are declared as a diagnostic so that keeping
everything, never being asked, and returning junk stay distinguishable.

Threats include a reused baseline whose validity check must actually be capable of
failing this time, unlike iteration 012's, and the two arms running on different
binaries, which is recorded rather than waved at.

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

The pre-run gate checked that LOCA's clearing was firing by looking at the shim's
repair count, reasoning that clearing orphans tool_use/tool_result pairs. That
indicator does not work: it read 0 in every arm and nearly led to recording the
baseline as lossless a second time.

compaction_resets is the correct counter -- it counts turns whose cached-prefix
boundary restarted because the transcript shrank under a stable session id, meaning
the agent compacted its own transcript. The blunt-reset arm shows 319 of them with
llm_calls=0 and only lossless format acting, so the shrinking is LOCA's clearing and
not CG's. LOCA's clear_tool_uses evidently rewrites tool-output content rather than
deleting messages, so no pair is orphaned and the shim has nothing to repair.

The baseline in this iteration is therefore genuinely lossy, at about 4.3 agent-side
compactions per run, which is what iteration 010 could never achieve and what moving
to 128k was for. The design works; the instrumentation choice was wrong.

Recorded because it is the same class of error as the three vacuous checks already
logged: a signal chosen for its plausibility rather than verified to move when the
thing it measures moves.

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

The separate-components arm produced 15 errors against the blunt-reset baseline's 3,
and five of them are 'prompt is too long', at 1.58M up to 6.04M tokens against a 1M
maximum. The agent's transcript reached six times the model's real window and
compaction could not bring it back under.

Hypothesis, stated as such because this design cannot isolate it: CG compacts each
request, but the agent keeps its own full history, and LOCA decides whether to clear
from the usage the provider reports, which reflects the compacted request. So the
better the proxy is at shrinking what it sends, the less the agent believes it needs to
clear, and the raw history grows unchecked until it outruns what per-request compaction
can rescue. The blunt-reset arm avoids this precisely because CG removes nothing there:
319 resets with zero model calls and only lossless format acting, and a mean arriving
request of 86,125 tokens against the CG arm's 692,613 on identical tasks.

If it holds, this is the most operationally important result here, and it cuts against
putting a compaction proxy in front of an agent that manages its own context: the two
mechanisms are substitutive rather than additive, and the proxy silently disables the
agent's safety net while taking on a job it cannot always finish. It also reframes
iteration 011's observation that summarisation drove contexts toward exhaustion.

Caveats recorded: compaction_resets cannot separate CG's own shrinking from LOCA's
clearing in the CG arms, so only the baseline's count is unambiguous, and the arms
differ by four components at once.

Yield at this band in unique tokens leaves the two components under study as rounding
error: coref contributes 874,969 of 2.46 billion tokens before, or 0.04%, with its
reported figure overstating it 19-fold, and extract_llm 0.08% at 11.8-fold.

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

The design originally proposed, measured at last: coref's criterion folded into
extract_llm's own bulk call, against running them as separate sequential components,
both arms on the same binary at the 128k band with 75 runs each.

Reward is indistinguishable. Intent-to-treat gives 16 solved against 16, with 6 harm
and 6 gain and p=1.000; task-clustered per-protocol gives 2 net-harmed against 0 and a
bound of 39%. Error counts are asymmetric at 15 against 8, so per-protocol exclusion is
confounded and ITT is the reading that survives.

The gate counters are the real answer, and the pre-registered reading for this outcome
was written before the run. Shown fifteen outputs with their co-reference evidence and
told the true cost of a wrong removal, the model keeps 88% of them and trims once in
2,074 decisions. That is the opposite failure from the per-output form, where a model
shown one output at a time dropped nearly everything at 6% live-kept. Both are the same
fact from opposite sides: the model is responding to how the question is framed rather
than discriminating between outputs. The single trim matters most, since trimming is
precisely the judgement an exact matcher cannot make and was where the design's value
was supposed to lie.

Merged does win on efficiency, the second pre-registered endpoint: 2,030 calls against
2,700 and $9.50 of CG model spend against $22.66, for the same reward, while deferring
summarize more and producing half the errors. Total cost is 2% apart and should not be
read, since per-arm LOCA cost cannot price a component; the CG-spend figure is
attributable because it counts this component's own calls.

It also carries the largest overcount ratio measured anywhere here, 31.7 times, so
anyone quoting its 573M saved tokens would overstate by thirty-fold; the unique figure
is 18.1M, which is still 9 times what separate extract_llm and coref manage together.

Combined with iteration 009, model-based selection has now failed in both directions --
reckless shown one output, inert shown many -- and the remaining case for merged is
purely economic rather than qualitative.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Found while reading the third arm's counters, which listed coref and extract_llm as
having acted when that arm's pipeline was format plus summarize and contained neither.

stage6.sh was generated with sed replacing cg-proxy-v7 by cg-proxy-v8, but the kill line
reads pkill -f "cg-proxy-v[7]" and the brackets mean sed never matched it. So the
generated script killed v7 while launching v8, and v8 proxies were never cleaned up
between arms. When a later arm reused a port the previous arm's proxy was still bound,
answered healthz identically, and the new proxy silently failed to bind; that arm then
ran the previous arm's pipeline and reported cumulative counters. s7-sum's 5,779 requests
are the earlier arm's 3,548 plus its own.

The headline comparison survives, and not by design: arms 1 and 2 alternated ports so
each got a fresh proxy, and their component sets match their configs exactly, with the
merged arm recording no coref -- the one thing that would betray a mix-up, since every
other arm has it. Had those two arms shared a port the merged result would have been
garbage and would have looked entirely plausible.

Fixed at the source rather than in the copy: the kill pattern now covers every version,
the generated copy is deleted, and a healthz 200 is no longer accepted as proof that the
intended proxy answered -- an arm now requires its own proxy log to show a pipeline line
and echoes which pipeline bound the port.

The two invalidated arms were expendable deferral context at about $460 for the pair,
and the question they served is already answered at 32k in iteration 011, so re-running
them is a budget decision rather than a correctness one.

Same failure family as the three vacuous checks and the wrong lossy-baseline signal:
healthz answered 'is something listening' when the question was 'is my process listening'.

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

A live arm reported 2,030 bulk calls and 2,074 verdicts, i.e. 1.02 verdicts per call,
so every "bulk" adjudication judged a single output. 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.

The cause was upstream gating. prefix_still_referenced removed 149,681 candidates in
that arm, leaving about one per request. That gate is self-defeating in merged mode
twice over: it starves the batch, and it means the model only ever sees what the index
has ALREADY judged spent, which destroys the veto on the index's Tier-1 blind spot that
merged mode exists to provide. The index's verdict still reaches the model, but as
evidence inside the prompt rather than as a gate that pre-decides the answer.

Merged mode now bypasses that pre-filter. The size floor still applies, so tiny outputs
are not offered.

Adds the assertion whose absence let this through: the prompt must offer more than one
output. Asserting a single call was not enough, since one call carrying one item is
exactly the refuted design. Verified at 8 items per call.

Also separates a deliberate keep-all from a parse failure. An empty verdict array is a
legitimate answer that the contract explicitly invites, and folding it into "no
verdicts" made it indistinguishable from junk in the counters -- which are the only way
to tell "the model declined to act" from "the model was never successfully asked", the
exact distinction this arm turns on. ParseBulkVerdicts now reports whether the reply
parsed, and keep-all is counted as merged_kept_whole_batch.

Also adds a test that Anthropic's context_management parameter survives a
count-changing rewrite byte-identically. LOCA's clearing is that server-side feature, so
if a rewrite dropped it the provider's clearing would be silently disabled while the
proxy also keeps requests under its trigger. It survives; five runs dying on "prompt is
too long" at up to 6M tokens are not caused by that.

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>
…anation

The claim was that CG's compaction keeps requests under the provider's clear_tool_uses
trigger, so the agent's own clearing never fires and its history grows unchecked. One
counter refutes it: context_management_events is zero across all 75 trajectories in
every arm, including the blunt baseline. Server-side context management never ran
anywhere, so the baseline was never protected by a trigger that fired.

The retracted claim had the shape of a good explanation and was asserted without
checking the counter that records the mechanism -- the same failure as the vacuous
checks already logged, in the direction of a more interesting story.

LOCA does request the feature, with context_management edits and the
context-management-2025-06-27 beta, and CG forwards the parameter byte-identically as a
new test verifies. So it is asked for and passed through yet never applied; whether the
gateway strips the beta or the Bedrock-routed model does not implement it is under
direct test.

The measurements stand: zero prompt-too-long errors in the baseline against five in the
CG arms, and mean arriving requests of 86,125 against 692,613 on identical tasks. The
causal story does not. With no server-side clearing anywhere the difference must come
from the trajectories, which is the same divergence confound iteration 012 established.
The honest position is narrower: pipelines including CG compaction reached contexts
exceeding the model's window while the lossless baseline did not, and why is not
established.

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

With server-side clearing inert everywhere, the size difference cannot be a reset.
Measured: the share of tool calls that are exact repeats is 0.4% in the lossless
baseline, 9.9% in the merged arm which keeps 94% of candidates, and 25.3% in the
separate arm with four components and the most removal. Dose-dependent in removal. And
cg_expand was called zero times across 225 runs.

The loop is that CG removes content and leaves a marker plus an expand tool, the agent
does not expand, it re-issues the same tool call with the same arguments, that returns
fresh output so the transcript grows rather than shrinking, which invites more
compaction and provokes more repeats. Hence 3x the steps and 8x the mean request,
ending in five requests that exceeded the model's window. The baseline stays small
because nothing is removed, so nothing is lost, so nothing is repeated.

This is a fourth outcome corefstub.go did not enumerate. It reasons about a wrong cut
being noticed and expanded, noticed but unattributable, or never noticed. The measured
outcome is that the model notices, does not expand, and re-runs the tool -- worse than
the first case, since it is a full tool execution plus fresh output rather than a cached
round-trip, and unlike the third it compounds because each repeat enlarges the
transcript that provoked it.

It also undercuts the reversibility argument justifying lossy compaction. The stash
makes a cut recoverable and expand makes recovery possible, but across 225 runs and
three pipelines the agent never chose it. Reversibility that is never exercised is not a
mitigation; on this evidence the marker functions as a signal to redo the work.

Caveats recorded: duplicate detection is regex-based so the monotone ordering carries
the argument rather than the absolute percentages; the figure is repeats as a share of
calls so it is not a length artifact; and it is not established that the specific
repeated calls are the ones whose output was compacted, which needs per-marker
correlation the capture does not record.

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

An earlier draft said the agent never called expand across 225 runs. That was a grep
error: it searched for cg_expand while the injected tool is named context_guru_expand.
The model calls it constantly -- 38 mentions in the separate arm, 108 in the merged one
-- and roughly half are refused outright with the client's own Tool

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

Adds per-request instrumentation to the capture hop recording how many tools were
advertised, whether expand was among them, and whether a marker was present. The tools
array flap had been argued from the advertise rule plus per-arm action rates and never
observed per request; this makes it a count. Transitions are measured in arrival order
across interleaved sessions, so the figure is an upper bound on per-session flapping,
which is stated where it is reported.

Pre-registers iteration 016: the merged arm with INJECT_EXPAND=always, by direction, with
a matched baseline to follow only if this shows improvement and no new defects. Six
declared criteria separate the fix working (expand no longer refused, no flap) from the
mechanism it was meant to break (cache-read share, repeat rate) from whether any of it
matters (reward, cost). If the first two pass and the rest do not move, the diagnosis was
right and the consequence was small, which is recorded in advance as a real answer rather
than a failure.

Verified before running: the advertise rule by unit test, and end-to-end that a
marker-free request now leaves the proxy carrying two tools with expand among them.

Threats recorded: always mode carries the hazard inject.go names, an unresolvable call
replayed to the client reading as an empty summary, which is the reason to watch criterion
one; and this arm differs from iteration 014's by both binary and inject mode, so a
difference cannot be attributed to the mode alone until the baseline arm runs.

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

Stopped at 70 of 75 once the pre-registered criteria were answered, since everything
remaining would have characterised a configuration already shown to be wrong.

Criterion 1 failed in the direction the pre-registration warned about. The flap is
genuinely fixed, with the tool advertised on 100% of requests and zero transitions, but
only 1.2% of requests carry anything to expand, so the model calls it 3.4 times more
often, CG cannot resolve it, and the raw tool_use is relayed to a client with no such
tool. Refusals went from 48 to 180 at 62 of 75. That is the hazard inject.go documents
and the pre-registration named. The inject mode was never the root cause: both modes fail
identically because CG replays an unresolvable call to a client that will always reject
it. The fix belongs in the proxy's expand loop, and always remains the right end state
once it lands since it is what removes the cache flap.

Corrects two earlier claims of mine.

Declines-to-act was misleading about yield. By verdict count the merged design keeps 94%,
but its few drops are the large ones, so it removes 18.1M unique tokens against the
separate arm's 2.83M -- 6.4 times more mass than deterministic coref plus separate
extract_llm combined. The accurate statement is that it removes a small fraction of
candidates but the biggest ones, while almost never trimming: merged_trim is 1 of 4,441
decisions, and trimming is precisely the judgement an exact matcher cannot make.

Repeated tool calls are not the main driver of context growth. They explain 11% of the
extra steps in the merged arm and 26% in the separate one. The dominant term is that the
agent takes 2.5 to 3 times more steps, and since every request carries all prior outputs,
context grows superlinearly, which accounts for 86k to 648k with no repeat loop required.
Differential success is ruled out at 15 against 16 solved; whether compaction causes more
non-identical exploration, or the baseline simply stops early, is not established.

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

Earlier sections quoted mean arriving requests of 86,125 against 692,613 and described
the CG arms as carrying about 8 times the context. That figure is tokens_before, the raw
history arriving at the proxy before compaction, and using it to imply cost was wrong.

Measured properly: the CG arms SENT 55.8M and 59.1M tokens upstream against the
baseline's 86.4M, so 32 to 35 percent fewer, and billed input was 164.5M and 166.2M
against 146.4M, so only 12 to 14 percent higher rather than 8x or the 19 to 24x the
arriving figures suggest. Compaction did its job.

The extra 75 to 85 dollars is a tier shift, not volume. The baseline billed 101.3M tokens
at cache-read rates and 37.7M fresh; the CG arms billed about 77M cached and about 80M
fresh. Fresh costs roughly ten times cache-read, so moving about 45M tokens between tiers
accounts for the whole difference, and reconstructing at Sonnet-5 rates gives about 114
against 192 to 198, tracking the observed 189.95, 204.94 and 223.22.

The mechanism is that compaction rewrites earlier messages and invalidates the provider's
cached prefix from that point on. The expand-tool flap made it worse by invalidating from
position zero, since tools precede system and messages in the cache hash, but the message
rewrites do it regardless, which is why cache-read share falls from 69.2% to 46-47.5%.

What survives is only the tail: five requests exceeded the 1M window, so at the extreme
compaction could not keep up. On the mean CG's output was smaller than the baseline's, so
the general claim is withdrawn.

This reframes what to fix. The target is not removing more tokens, since the arms already
send fewer, but cache-prefix stability -- and it reframes the expand-loop fix as keeping
the tools array stable and making recovery work, rather than reducing volume.

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

Root cause of the repeated-work loop measured at the 128k band. The expand loop replayed
the model's own tool_use to the client whenever NO id resolved. A client that does not
implement context_guru_expand -- which is most agent frameworks, since the tool is
injected by the proxy and exists nowhere in the client -- answers "Tool
'context_guru_expand' not found". The model loses its recovery path and re-runs the
original tool instead, paying a full tool execution plus fresh output and enlarging the
transcript that provoked the cut. Measured on LOCA: 17 of 38 attempts refused in one arm
and 48 of 108 in another, with exact-repeat tool calls at 9.9% and 25.3% against 0.4% in a
lossless baseline.

Three changes.

Nothing-resolved now sends the continuation with placeholders rather than replaying. The
resolved map already carries an explicit per-call placeholder, so the continuation is well
formed and tells the model the content is gone, which it can act on -- unlike a missing
tool. Capped at one such round, since a model that asks again gets the same placeholders
and continuing would burn a round-trip per attempt for no new information. The separate
!ok case, where the continuation could not be built at all, still replays: that is an
internal failure and fail-open is honest there.

Interception no longer requires that this request advertised the tool. It was gated on
that deliberately -- never declare a tool you will not handle -- but the unification
assumed a model only calls tools listed on the current request, and it does not. A session
that has ever been offered the tool is now remembered, and additionally every
NON-STREAMING response is inspected, which costs nothing because a JSON body is read in
full either way. The advertise gate is kept for SSE alone, where inspecting means
buffering the stream and the client really does lose incremental output, which is the cost
inject.go weighed and which applies only to streaming.

Unresolved calls are now counted, split by cause. A malformed id is the model inventing
one and needs no action; a well-formed id with nothing stashed behind it is a
context-guru defect, because a cut advertised as reversible is not. Both surface in
/stats. Neither existed before, which is how three experiment iterations ran while the
model was being refused recovery: the refusal was found by grepping the benchmark client's
transcripts, not from any counter here. The stats golden test caught the new fields and
they were added to the reviewed contract rather than the assertion loosened.

Two tests, each verified to fail when its subject is reverted: one asserts an unresolved
call is answered and counted rather than relayed, one asserts a marker-free turn's call is
still intercepted. Also adds expand.TestWhenIsExpandAdvertised, which pins the advertise
rule across all five input combinations, since that rule was previously only described in
comments.

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>
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>
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