Skip to content

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

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

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

Conversation

@amiddavid

Copy link
Copy Markdown
Collaborator

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

What's here

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

The measurement, and the finding

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

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

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

Three more results:

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

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

One bug worth calling out

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

Status

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

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

Verification

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

pls explain this sentence, perhaps add an example

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

add an example column

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

i.e. doesnt this contradicts case B?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

Two separate defects, both now fixed in e7a2623:

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I dont see improvement-plan in the docs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

can u explain this more, I'm not following

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

dont tune it, but still matters?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

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

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

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

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Expanded both from first principles rather than by name.

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

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

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

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

Two problems, one conceptual and one concrete.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Docs only; no Go changed.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Labels

None yet

Projects

Status: New/ToDo

Development

Successfully merging this pull request may close these issues.

2 participants