feat(coref): co-reference-aware compaction - #80
Conversation
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>
| 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 |
There was a problem hiding this comment.
pls explain this sentence, perhaps add an example
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
pls explain this paragraph in more details, I find it hard to follow, especially for a proposal document
There was a problem hiding this comment.
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.
|
|
||
| | 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 | |
There was a problem hiding this comment.
add an example column
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
i.e. doesnt this contradicts case B?
There was a problem hiding this comment.
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:
- 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 — soclosedcan't rest on "referenced once, long ago" alone. That is now stated as the reasoncut_closedships off, rather than mere caution. It also inverts my §7 reading ofused_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. - The concrete one.
refs == 0conflated two opposite states — "introduced 200 identifiers, nobody touched one" (evidence of deadness) and "introduced nothing I can see" (absence of evidence). There's now anopaqueclass 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)). |
There was a problem hiding this comment.
I dont see improvement-plan in the docs
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
can u explain this more, I'm not following
There was a problem hiding this comment.
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.
|
|
||
| | 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`). | |
There was a problem hiding this comment.
if it a recent turn, it might not had the chance to be referenced, dont we need to guard from this ?
There was a problem hiding this comment.
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.
|
|
||
| | 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. | |
There was a problem hiding this comment.
dont tune it, but still matters?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| | **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. | |
There was a problem hiding this comment.
since I'm not familiar with this repo yet, I would appreciate if you can in a comment explain this more
There was a problem hiding this comment.
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>
ac3bdf2 to
48ffc1b
Compare
…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>
…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>
…rect two claims
Offline probe series, under $2 of gateway spend, no LOCA run. Iteration 018 left the
merged design's 94% keep rate, its ~1 trim per arm, and solves falling 16 to 8
unexplained. Probing the decision directly instead of buying another $234 arm shows
merged has never run in the configuration it was measured good in.
Corrections to claims this repo acted on:
* iteration 014's "negative answer" is unsafe. That arm made 2,078 decisions across
2,030 calls, 1.02 candidates per call, and merged_kept_whole_batch never fired,
which rules out empty-array replies. Bulk adjudication is comparative and was
measured at ~15 candidates; at 1.02 the arm ran the per-output design already
refuted at 6% live-kept. Corrected in place; its efficiency and deferral findings
are unaffected since they do not depend on batch size.
* AllowCachedPrefix's comment claimed the tail restriction "is not a safety property
of the model call, it is a cache-cost property". It is also an information
property: need is relevance minus what is already captured elsewhere, and that
term lives in the turns after the output, which the model is not shown. The tail
restriction is what made the local prompt sound. Comment corrected in place.
What the probes establish:
* Appending a trailing user message to an identical prefix lands a full cache read,
no write, so a model call can read the real context at ~10% of fresh input.
tool_choice is not part of the cache key; tools are; this route rejects assistant
prefill.
* Transport versus judgment. Opaque ids are hallucinated, integer labels are not
(0 bad in 40+ trials), short quotes are verbatim (0 of 59 wrong), and trim's
retained text is invented 8 times in 9. The model must never carry content.
* trim is dead weight: chosen zero times in 21 opportunities, and the keep/drop arm
is identical to keep/drop/trim on every metric.
* The Tier-2 false drop is a policy error, not a comprehension error. Asked the
factual question the model is right 3/3, then drops anyway, because
BuildBulkPrompt tells it to judge relevance toward the CURRENT step while an
outstanding instruction still needs the output.
* Stating a better criterion is inert (4/4 false drops). Requiring the model to name
and quote the obligation halves it. Pooled over four runs, 14/14 against 9/14,
Fisher p about 0.04, with no loss of correct drops. 9/14 is still 64%, so the veto
is not a capability to build on.
* Batch of one drops the output 4/4, replicating the selection experiment's
per-output refutation in a different harness.
* Methodology: the model's self-report was accurate about its beliefs and wrong
about the cause it named; removing the wording it blamed made the result worse.
Also gofmt: the goto-splice scope block from the merged integration was not
gofmt-clean.
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…n evidence, no trim
Iterations 014, 016 and 018 each measured a merged arm that was never configured the
way the design was measured good. The model was shown 1.02 then 2.63 candidates per
call against the ~15 that produced 58% live-kept, so every one of those arms was closer
to the per-output design already refuted at 6%. Offline probing (iteration 019) shows
batch size is a yield/safety trade-off rather than a detail: at batch 3-6 the model
dropped a genuinely-spent output only 2 times in 4, at batch 10 it dropped it 4 in 4
and cleared 100% of genuinely-spent candidates. Small batches do not make it wrong,
they make it unwilling to act, which is what a 94.6% keep rate looks like from inside.
Three changes, each with a measurement behind it:
* The contract states the SPENT criterion — spent only if needed by none of the
current step, an unfinished user instruction, or a next step the agent itself
stated — and REQUIRES the model to name which obligation applies and quote it
verbatim. Stating the criterion alone measured inert at 4/4 false drops; requiring
the evidence halved it. Instructions a model can skim past are inert; a required
output field is not.
* trim is removed. Chosen zero times in 21 probe opportunities, identical metrics
without it, and in production accepted once against eight rejected as invented. It
was the only verdict that asked the model to transport text, which is what it is
worst at. A model that answers "trim" anyway degrades to keep, counted, rather than
being discarded — an unjudged output is indistinguishable from silence otherwise.
* mergedMaxItems 15 to 12. Quote fidelity degraded with batch size: 4 of 37 quotes
non-verbatim at batch 16 against 0 of 16 at batch 10, so the transport ceiling sits
between them and this takes the conservative end.
New guards, each verified to FAIL when its subject is reverted:
* a drop that names an outstanding obligation is refused, not performed. This is the
one verification pointing the dangerous way.
* a fabricated obligation quote is counted. It argues for keeping so it is not
dangerous, but it is the signal that the batch exceeds the model's transport limit.
* an unanswered criterion field is tolerated and counted, because requiring it would
collapse yield against a model that omits it, while ignoring it would hide that the
forcing function never ran.
* batch truncation is counted rather than silent.
Also commits the arm config. The merged configs for iterations 014, 016 and 018 lived
only on the eval box, which made them unreproducible.
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…e mechanism run The first launch hit the pre-registered abort criterion at 213 requests: 1.93 candidates per call with acted=0. The counters named the cause and it was not min_tokens. The economic gate suppressed 1,497 candidates against 224 that reached the model, top reason "cache-aware, saving below call cost". That gate prices each candidate against the cost of a whole model call, which is correct for the per-output loop where candidate equals call, and wrong for a design that makes one call per request regardless of batch size. It also fights min_tokens: a lower floor produces smaller candidates, each looking even less worth a call, which is why 3000 to 800 did not help. Third instance of one defect class, after the prefix pre-filter and llm_max_per_request: cost machinery written for per-output calls, applied to a one-call design. Also records that below_output_floor at 11,036 is an occurrence count inflated by per-request rescanning, not evidence about the floor, and that merged_quote_not_verbatim ran 8.5% on haiku against 0 of 59 on sonnet in the probes, so the forced-evidence mechanism may not survive the cheap model. No endpoint or pre-registered reading changed. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…el over its cached transcript
A component that must decide whether a tool output is still needed cannot answer that
from the output alone. Need is relevance MINUS whatever has already been captured
elsewhere in the transcript, and that second term lives in the later turns, which the
merged adjudication was never shown. It was asked to veto an exact-match index on
transformed reuse while being withheld the turns where the reuse appears.
Sending those turns fresh costs about ten times a cache read, and on the cheap model the
required verbatim quoting degraded to 20.8% at the batch sizes the bulk mechanism needs,
against 0 of 59 on the request model. So the judgement wants the agent's own model AND
the whole transcript, and only a cache read makes that affordable.
Measured on the live route before building any of this (iteration 019, section 2):
appending a trailing user message to a byte-identical prefix reads the entire prefix from
cache and writes nothing, 19,595 read against 0 created. tool_choice is not part of the
cache key, so forcing it to none is free and necessary, since the prefix carries the
agent's tools and the model otherwise answers with a tool_use. tools ARE part of the key:
omitting them read a different, smaller entry. The route also rejects assistant prefill,
which the appended user message satisfies by construction.
The prefix is the previous turn's SENT body, not the incoming one. The cache upstream was
populated by what context-guru emitted, which is the compacted form; the incoming body is
uncompacted and diverges at the first thing any component removed, making everything past
that point a fresh charge. The consequence is that the ask sees the transcript as of the
previous turn, which is acceptable for this judgement — the missing part is the newest
tool output, tail content that has had no turns in which to be superseded — and it keeps
a large model call off the agent's critical path.
* components: PrefixAsker and PrefixUsage, plus Ctx.PrefixAsk. Usage is RETURNED and
not merely recorded, because a prefix ask whose whole justification is the cache read
must let its caller see that the read happened.
* cheapmodel: Anthropic.CompletePrefixed, which appends the ask and touches nothing
else except stream, since every byte before the appended message is prefix.
* proxy: a bounded per-session stash of the body actually forwarded, and the asker
built from it. Off by default (CONTEXT_GURU_PREFIX_ASK) because it holds request
bodies in memory and because a feature whose benefit is a cache hit should not be on
by default in a host that cannot verify the hit.
* extract: BuildPrefixAsk ships an inventory rather than the outputs. Paying fresh to
send truncated copies of content the model is reading from cache would defeat the
mechanism and show it an excerpt of something it could read in full. Labels are small
integers: asked for opaque tool_use ids the model regularised them, and with integers
it was 0 bad labels in 40+ trials.
* merged: prefers the prefix ask, falls back to a plain completion on the first turn of
a session or any error. Falling back rather than skipping matters — treating "no
prefix" as "no verdicts" would disable the component on every session's first turn
and read as a model that declined to act. A cache read of zero is counted.
Each new guard verified to FAIL when its subject is reverted: the samples-not-shipped
invariant, the zero-cache-read counter, the fallback, and the tool_choice construction.
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…iring Live symptom: prefix_ask_used = 0 with prefix_ask_failed = 0 over 34 requests. The asker was never built, so the mechanism silently never ran and was indistinguishable from a feature switched off. Cause: the stash is written under the pipeline resolved, tenant-scoped session id, while the asker was built from the caller x-context-guru-session header, which this workload never sends. Empty key on lookup, resolved key on write, no error anywhere. The resolved id only exists inside apply.BodyOpts, which is the same call that needs the asker, so the session now travels as an Ask parameter and the component supplies c.Session. Also removes the pre-flight stash check. A first turn with nothing stashed must surface as an error from Ask, counted and falling back to a plain completion, rather than as a nil asker, because nil is what "the feature is off" looks like. The component tests all injected a fake asker, so none of them touched this wiring. That is the gap this commit closes: a fake satisfying the interface proves nothing about who supplies the key. The new proxy test covers the first-turn error, the matching-key success, refusal to serve another session a prefix, and the opt-in and Anthropic-only preconditions. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Written to answer a reviewer question: does the tension between these two config items predate this branch? It does, by weeks. min_tokens on extract_llm arrived in 7f0379a on 2026-07-25 and economic_gate in 2adb476 on 2026-08-10, while selection_mode: merged is this PR and not yet in main. But it separates into two claims and only one is a defect. In the per-output design the pipeline makes one model call per candidate, so a candidate IS a call and pricing a candidate against a call is correct: the floor and the gate are two filters agreeing, and the gate is the stricter and better informed of the two. What pre-exists is therefore redundancy and poor observability, not incorrectness -- min_tokens is effectively advisory below the gate economic floor, and nothing reports that the operator floor was honoured and then overruled. What this PR changed is the assumption underneath. Merged makes one call per request regardless of batch size, and with prefix asks the outputs are read from the cached transcript rather than shipped, so a candidate marginal cost is one inventory line of about thirty tokens. The gate still prices each candidate against a whole call, which is wrong by two to three orders of magnitude in that configuration and starves the batch of the peers the comparative judgement needs. Includes the floor sweep showing candidates per call at 1.29, 2.22 and 5.97 for min_tokens 800, 300 and 120, with the explicit caveat that only that column is comparable across the three runs -- they diverged in trajectory, spreading total traffic over 7x and summarize firing on 57, 66 and 2 percent of requests. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ng on every request Two defects found by a reviewer question: was the merged call firing only when the context was large enough? It was not. It fired on 190 of 264 requests, 72 percent, regardless of context size, and request_trigger_not_fired never fired once. Three restraints were off at the same time. Pinning min_tokens sets the explicit flag, and shouldFire then returns "explicit min_tokens/trigger configured" unconditionally, bypassing the derived pressure trigger; any explicit floor does this, since min_tokens, trigger.min_request_tokens and trigger.min_output_tokens all mark it, so the per-output floor and the when-to-act decision cannot be configured independently. The explicit request trigger is only enforced when the backend is not cache-aware (extract_llm.go:761), so with caching on an operator context threshold is silently ignored. And economic_gate was disabled, removing the last thing refusing low-value work. Firing on a small context is not only wasted spend. It removes outputs that have had no turns in which to be superseded, which the contract explicitly says to keep, so it is a harm mechanism -- and it confounds deferral, because summarize firing less may simply mean extract removed early and often. The arm configuration now pins nothing, so the derived trigger governs: fire above 0.60 context pressure or above 0.25 with more than 10 percent growth, which engages before summarize at 0.78. Separately, verdicts divided by calls was being used as the batch size and is not one. It counts what the model chose to ANSWER. Live it read 2.80 while merged_batch_truncated fired 43 times in 162 calls, which is arithmetically impossible for offered batches: the model is shown bulk-sized batches and silently omits most labels. Report.GateN plus a merged_offered counter now record offered and answered separately, so a starved batch and a model answering for a third of a full batch can no longer produce the same number. That conflation is what three iterations read as the model declining to act. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ead as a refusal
The new merged_offered counter showed batches of 21.7 candidates offered per call,
capped to 12, with the model answering for 2.39 of them: 11 percent verdict coverage.
Batch size was never the constraint. The constraint is that merged_unparseable was
firing on 24 of 34 calls, about 70 percent.
Cause: proxy.go builds the incoming model client without MaxTokens, so CompletePrefixed
fell back to 2048. The request model runs adaptive thinking, which consumes that budget
before emitting any text -- a probe at max_tokens 900 returned thinking blocks and no
text whatsoever -- and a verdict array over a 12-item batch, each entry carrying an
obligation label and a verbatim quote, is long. The array was cut mid-flight with no
closing bracket, the parse failed, and the caller changed nothing. In the counters that
is indistinguishable from a model that declined to act, which is how it was misread for
three iterations.
Part of this is self-inflicted by this branch: forcing the obligation evidence lengthened
the replies, and prefix asks moved them from haiku onto sonnet with thinking. But
merged_unparseable was also visible at 19 and 22 in two earlier arms and dismissed as
under one percent, which compared it against the DECISION count when it is a share of
CALLS.
* CompletePrefixed defaults to 16000 output tokens rather than 2048. Output bills as
generated and not as budgeted, so the ceiling costs nothing until used.
* a reply that opened the array and never closed it is now counted as
merged_reply_truncated, separately from merged_unparseable. The two need opposite
fixes -- raise the budget versus fix the prompt -- so one name for both hid this.
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…a schema, not prose
The verdict-coverage problem was an envelope problem, not a judgement problem, and the
fix is mostly deleting something I added.
Measured against the live route, same prefix, only tool_choice varying:
tool_choice reply shape cache coverage
{"type":"none"} prose / thinking read (free) 0 of 6 labels
{"type":"tool",...} tool_use MISS + rewrite 6 of 6
(omitted) tool_use read (free) 6 of 6, on 4 of 4 trials
So setting tool_choice none -- added to stop the model answering with a tool_use -- is
what drove it into prose, and the prose was then scored as an unparseable failure. That
is a large part of what read as "the model declines to act" across three iterations. A
sampled reply shows the model reasoning correctly under the criterion and simply saying
so in sentences: the task is unfinished, and no summary of the raw data has been recorded
elsewhere, therefore keep. Which the contract already calls a valid and often correct
answer.
Forcing a named tool also turns out not to be free: it wrote a separate cache entry,
8,378 tokens against the 8,268 already cached, so tool_choice does participate in the
cache key when it names a tool even though "none" does not.
* internal/adjudicate declares context_guru_adjudicate with an integer-labelled verdict
schema, and injects it on EVERY request rather than only when the pipeline is about to
ask. tools hash before system and messages, so a tool that comes and goes invalidates
the prefix from position zero -- the flap expand's always mode exists to prevent.
* CompletePrefixed no longer sets tool_choice, and prefers a tool_use input over text.
The input arrives schema-shaped, which removes three failure modes the text path had:
prose instead of JSON, verdicts for part of the batch, and an array cut off by the
output budget.
* stray calls the AGENT makes to the tool are answered on the request path, the same
shape as expand's RestoreResults and for the same reason: the client cannot execute a
proxy-injected tool, so it answers "not found" and the agent loses a turn to a dead
end. Counted as adjudicate_stray, because models do call advertised tools they were
told to leave alone -- directly observed with expand at step 2 of a run.
One test's assertion was inverted rather than adjusted: it demanded tool_choice none on
the reasoning that a tool_use reply had to be suppressed, and measurement reversed that.
The /stats golden test caught the new field, as designed. All four new guards verified to
FAIL when their subject is reverted.
Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
… eight measurement defects Six launches, five aborted on the pre-registered criterion, each abort exposing a defect the previous configuration hid. The sixth completed. 22/75 accuracy-weighted solves at about 240 dollars total, against iteration 018 at 8/75 for about 243. This is NOT an effect estimate for the merged design: there was no concurrent baseline, and the binary, the configuration and seven defects all differ. The pre-registration scoped this as a mechanism run with solves as context only, and that limit binds. Recorded first because it is the easiest number to misquote: LOCA prints Overall Success 70/75, which counts runs that completed without erroring, not tasks solved. The comparable metric is accuracy-weighted and it is 22/75. Anyone reading the raw output sees 70/75 first and is off by a factor of three. The mechanism now works end to end. Pressure-gated trigger firing on 35 percent of requests rather than 72, prefix asks reading 37,336,778 tokens from cache with zero cache writes over 778 asks, schema-shaped verdicts through the injected tool, no truncated replies, zero stray tool calls, and expand restoring 866 with 5 unresolved. summarize fired on 41.3 percent against 56.1, and extract_llm removed 8,140,204 unique tokens against 318,955. Still short: verdict coverage 65 percent, 59 batches truncated at the cap, 133 unparseable replies, and fabricated obligation quotes on 6.8 percent of verdicts. The iteration documents seven defects in one component measurement path, all producing the identical misleading signal that the model declines to act: the coref pre-filter, llm_max_per_request, the economic gate, any pinned floor disabling the pressure trigger, verdicts-divided-by-calls used as batch size, a 2048 output ceiling truncating the reply, and a tool_choice of none driving the model into prose. Four predate the branch, three are mine. The judgement machinery was never the problem: a sampled reply shows the model reasoning correctly under the criterion and saying so in sentences. An eighth defect is in the rig. capture_hop tested for markers using the unescaped spelling, while Go HTML-escapes the angle brackets, so has_marker read zero percent on every arm ever run here -- which reads as removals not being reversible while expand was restoring 866 of them. expand/expand.go rawMarkerRe documents that exact trap. Every previous marker-present line in this series should be treated as unmeasured. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…ped pipeline Iteration 020 got the mechanism working but had no concurrent baseline, so its 22/75 cannot be attributed to the design. This is the paired comparison, and the question is narrowed to what the merged adjudicator ADDS on top of the pipeline the product already ships: the two arms are identical but for one inserted component, with coref in neither because it is new on this branch and not part of what we had beforehand. extract_llm is placed before extract, following the existing ns-full order: the deterministic extractor shrinks outputs below the model pass floor if it runs first, which starves the batch -- the failure mode iteration 020 spent five aborts on. Two assumptions are flagged rather than buried. The treatment arm runs with the economic gate disabled, which is not a shippable configuration, because with it on the arm does not run the design at all; making the gate batch-aware is the real fix and is deliberately not done first, since an untested cost model introduced just before freezing the binary is how a measurement gets distorted. And both arms carry the injected adjudication tool even though the baseline cannot use it, which keeps their tools arrays and cache behaviour comparable at the price of the baseline not being byte-identical to the shipped product. The binary is frozen for both arms with its commit and SHA-256 recorded before launch. That is the whole reason iterations 014, 016 and 018 cannot be compared to each other. Per-seed accuracy exists at tasks/<Task>/state<N>/eval.json, so the test is paired. The primary endpoint is task-clustered over 15 clusters by paired Wilcoxon signed-rank, two-sided, and the clustered test governs -- five seeds of one task are correlated, not five free observations. Per-pair over 75 is a sensitivity check only. A harm upper bound above 25 percent blocks any positive claim, declared in advance per iteration 007 failure. No minimum effect size is claimed as a win, because the honest reading of a null result at this n is underpowered rather than no effect. My cost prior is stated in advance: parity to about 20 percent worse. CG arms have historically sent 32 to 35 percent fewer tokens and billed 12 to 14 percent more, and CG spend has tripled now the adjudicator runs on the request model. The case for this design is reward, not cost. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Binary cg-proxy-v19, sha256 prefix ecc02f28417fe8d5edbcfaf3cc13505b, built from code at fcf78cd, serving BOTH arms. Recording this before launch is the control that iterations 014, 016 and 018 lacked, and the reason those three cannot be compared to each other. Also commits both arm configs. They differ by exactly one inserted component, and arm B carries no min_tokens or trigger on extract_llm on purpose: pinning either marks the config explicit and shouldFire then returns true unconditionally, firing the component on every request regardless of context size. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…d why Arm A produced 16 upstream 400s in 3,347 requests, all prompt-is-too-long, on bodies of 2.6 to 14.8 MB. Diagnosed while arm A was still running and before its solve count existed. The cause is a single tool output larger than anything in the configured pipeline can reduce. extract matched no noise pattern and acted zero times, cmdfilter matched nothing and acted zero times, toon needs a uniform object array and dedup needs an exact duplicate, so the only real compactor is summarize, which protects keep_last 3 -- and a fresh oversized output sits in exactly that protected tail. extract_llm would decline it too, by design: over_model_context leaves any output exceeding the compaction model context verbatim, on the reasoning that a program written against a truncated sample would run against the full input. The product already has the answer. collapse is the content-agnostic fallback for an oversized tool output no more specific component handled, keeping a head and tail window and stashing the original behind a marker. It is in the general and codesafe presets and not in codesmart, which is what these arm configs descend from. So this is a rig configuration error rather than a product defect -- with the caveat that a user of the shipped codesmart preset has the same gap, which is worth raising separately. The arms are not being restarted: both lack collapse, so both take the same class of failure and the comparison stays fair in expectation. What is fixed instead is the analysis plan, and it is fixed before any outcome is known, because errors are the one place the omission could bias the result. Arm B removes more so it may error less, and excluding errored runs would then compare arm B survivors against arm A. Iteration 014 hit this with 15 errors against 8 and concluded intent-to-treat is the reading that survives. Primary analysis is now intent-to-treat: a run that errored or has no eval.json scores accuracy zero, and all 75 pairs are scored. Per-protocol is a sensitivity check reported with per-arm error counts. The error counts are themselves a reported endpoint, since a large asymmetry is a finding about oversized-output handling either way. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
The original figure multiplied each task avg_accuracy by its 5 seeds, and avg_accuracy averages only the runs that COMPLETED, so any task with an errored seed was over-credited. Recomputed per seed from tasks/<Task>/state<N>/eval.json, which is what intent-to-treat requires and what iteration 021 amendment 1 mandates for both arms. 21.00 of 75. Found while computing iteration 021 arm A by the same flawed method, which returned 17 before the per-seed recomputation returned 14. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…e gate At 477 requests arm B verdict coverage was 34 percent against a pre-registered gate of 50 and an instruction to abort below it. Recorded here rather than in the results so it cannot be retrospectively smoothed over. Continued on the operator call. The gate existed to catch the case where the design never ran, and by every other measure it is running: zero prefix asks with a zero cache read, zero truncated replies, zero stray tool calls, 44 real drops and 686k unique tokens removed by that point. The failure mode is partial answering, the model returning verdicts for about a third of a full twelve-item batch, not a dead mechanism. Coverage also rose through iteration 020 from 46 to 51 to 65 percent, so 34 at a fifth of the run may be early. The cost of continuing is stated rather than hidden: arm B numbers are a FLOOR, because it acted on roughly a third of the candidates it identified, so a null result cannot be read as merged does not help, only as merged at 34 percent coverage does not help detectably. The honest alternative was to abort, which would have preserved the gate authority at the cost of re-running arm B, and there is no tested coverage fix to re-run it with: the leading candidate is a smaller batch, since a probe answered 6 of 6 on six items against about 34 percent on twelve, and that is untested. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
…table over the shipped pipeline Primary endpoint is null. Plus 1.00 solve on 75 pairs at p equal 1.0000 and plus 2 percent total cost. The pre-registered reading for this outcome was written before the run: clustered null with both directions flat means no detectable marginal value at this n and this cost, close merged, keep the deterministic pipeline, report the ceiling honestly. ITT 14.00 of 75 for the baseline against 15.00 of 75 with merged. Task-clustered over 15 clusters, which governs, gives 2 better, 2 worse, 11 tied. Per-pair sensitivity gives 7 gained, 6 harmed, 62 unchanged. Harm upper bound 15.2 percent, which does not block. Eleven of fifteen tasks score zero in BOTH arms, which is the dominant fact about this benchmark power: the comparison rests on four tasks with two moving each way, so no configuration change could have shown a difference here without a large effect. Secondary effects are real but modest. Seven fewer errored runs, 17 down to 10. Fifteen points less summarization, 71 down to 56 percent, far short of the 4x that comparing against iteration 020 had suggested. Twenty-eight percent fewer requests. And 6.5M unique tokens removed with recovery working at 717 restores and zero unresolved. The cost prior stated in advance, parity to about 20 percent worse, lands at plus 2 percent: LOCA spend fell 30 dollars while CG spend rose 35. Four limits are recorded, none of which rescue the result. Coverage ended at 61 percent so the numbers are a floor. Neither arm carried collapse or mask, so neither is a shipped preset -- and collapse would probably not have helped regardless, because it skips outputs of 40 lines or fewer and a JSON API result is often one line, so a single-line multi-megabyte payload falls through it too. summarize ran alongside in-place offloaders against the advice in config.go. And 146 replies were unparseable, so those calls changed nothing. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
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 outputintroduced, 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'sdefinition. Its fixture is the twin of
coref_fixture.py, negative control included andasserted.
components/offload/coref— the Offload component. The one component that mutates thecached prefix on purpose, so: batched cuts, a per-session
rewrite_budget, latched decisionsreplayed byte-for-byte,
repairLostFreezedeliberately not consulted, side-effect-free planning.deploy/harbor/coref.py+ two converters (cc_capture.py,runlog_capture.py) — themeasurement pass, plus the plumbing to run it on Claude Code transcripts and benchmark harness
logs without an eval-box run.
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:
unreferencedclosedopenReference 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:
closed_distover a 10× rangemoves the answer 2–3 points; sweeping
open_reps2→6 moves it 18. And 44% of mass was lastreferenced 40+ messages ago while 60% is
open— most referenced mass is old and still hot. Adistance-based A/B split would confidently cut repeatedly-referenced content.
Tis 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%
closedis the proposal's own §8 prediction landing: it argued LOCA would be a tier-2/3stress 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,efficiencyandconditionsscoredas 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_unreferencedis on by default and justified on every corpus.cut_closedis off: its yield ranges 0–15% by workload, which is no basis for a default.Next, in order: re-run on
capture-swe/capture-tbat the eval box → enablecut_closedthere →observe-modeexpandrate as the precision inner loop → only then the scored benchmarks.Verification
gofmtclean ·go vet ./...clean ·go test ./...24 packages, 0 failures · fixture reproducesits documented ground truth.