Found by live verification of #118 against the real gateway, with a verbatim wire recorder between
context-guru and the provider.
The defect
The prefix ask reads the previous turn's sent body — everything up to MaxCachedIdx. The
candidate set is the uncached tail — only messages past MaxCachedIdx.
Those two sets are disjoint by construction. So the model is asked to judge whether outputs are
spent, over a transcript that does not contain them.
Why: sweeping() implies !ColdCache, so TailOnlyCold(i, true) degrades to the ordinary tail
gate, and only post-boundary messages become candidates. The ask, meanwhile, is the pre-boundary
body. Nothing in between connects them.
What the model actually did
Live, with the provider's own usage off the wire:
- A/turn3 — two older outputs refused as
cached_prefix=2, the two new ones offered. The model
kept both and cited "Reply with the word ACK only." as the obligation for each. That is a real
transcript string, so sweep_quote_fabricated correctly stayed silent — but it is not a
judgement about the outputs. It is the only obligation the model could find, because the outputs
were not there to reason about.
- D/turn2 — dropped its output on
{"i":0,"needed_by":"none","quote":"","verdict":"drop"},
having seen only begins: # ledger_b and a token count.
So a drop is being decided from a 90-character head and a size. The comparative judgement this
design rests on did not happen on either prefix-ask turn.
The second half: depth and the cache read are mutually exclusive
Both derive from the same tracker record. A tracker hit gives you a stash to ask over and a
tail-only boundary. A tracker miss gives you depth and sweep_no_prefix. Every
deep-candidate firing observed (runs C and E, MaxCachedIdx=-1) was on the fallback path — the
content-carrying prompt — not on the prefix ask.
So the mechanism that reaches deep history is the one the prefix ask was built to replace, and the
prefix ask only ever sees the tail it cannot judge.
Consequence: the shipped default is the refuted shape
sweep_inventory_of_one fires on the ordinary agent turn, not an edge case. The candidate set
is the tail; the tail of a normal agent turn is one tool result. Run D — the plain case — produced
sweep_inventory_of_one=1 sweep_dropped=1. Run A needed two tool calls crammed into a single turn
to reach an inventory of two.
That is the per-output shape 4ca1f13 records as "refuted at 6% live-kept", reached by default,
and the component drops on it.
Related bias in the fallback: conversationGoal opens with the first user message, so on B/turn3
the goal read [r1] Read data/orders_2021.csv … tell me the order count in each and the model kept
all three outputs as needed for "(a) the step you are on right now".
Why no test caught it
All 20 sweep unit tests run with MaxCachedIdx: -1, which disables the tail gate entirely — so
the disjointness cannot appear. And extract_sweep_test.go:104 claims the depth restriction is
lifted "so the candidates reach the inventory: the tail gate is exercised separately below". There
is no such test, in that file or the repo. That comment asserts coverage that does not exist.
This is the fifth vacuous-coverage instance in this component family (after the three trim tests,
the tool_result-counting precondition in #110, and the economic_gate: false blind spot in this
same component).
What needs deciding
This is design-level, not a patch. The options are not equivalent and each gives something up:
- Ask over the whole transcript, not the previous sent body — put the candidates inside the
region the model reads. Loses the cache read for anything past the boundary, which is the
mechanism's whole economy.
- Make the candidates the cached region — judge deep history, which is what the proposal says
the component is for, and accept that acting on it invalidates the prefix. That is what the
original cold-turn gating was for, and the pre-expiry window was meant to make it cheap.
- Keep the tail candidates and stop pretending the model reads them — ship the outputs in the
ask, i.e. the fallback path, and drop the prefix-ask premise for this component.
(2) looks closest to the stated intent: the proposal's problem statement is deep history, and the
pre-expiry window exists precisely so that invalidating the prefix costs little. Under (2) the
prefix ask and the candidate set coincide instead of excluding each other.
Also stale, and load-bearing for this decision
docs/proposals/sweep-adjudicator.md frames the problem entirely as deep history and never states
that the tail gate confines the component to the uncached tail — it says only that "the newest tool
output is invisible" to the ask, which understates this by the whole transcript.
docs/hosted.md:645-653 is wrong on five counts for this branch: "Both call a cheap model
(claude-haiku-4-5…)", "when the economic gate says", "runs only on a turn whose prompt cache has
expired", "adjudicates at depth", and "a batch of outputs at a time".
Found by live verification of #118 against the real gateway, with a verbatim wire recorder between
context-guru and the provider.
The defect
The prefix ask reads the previous turn's sent body — everything up to
MaxCachedIdx. Thecandidate set is the uncached tail — only messages past
MaxCachedIdx.Those two sets are disjoint by construction. So the model is asked to judge whether outputs are
spent, over a transcript that does not contain them.
Why:
sweeping()implies!ColdCache, soTailOnlyCold(i, true)degrades to the ordinary tailgate, and only post-boundary messages become candidates. The ask, meanwhile, is the pre-boundary
body. Nothing in between connects them.
What the model actually did
Live, with the provider's own usage off the wire:
cached_prefix=2, the two new ones offered. The modelkept both and cited "Reply with the word ACK only." as the obligation for each. That is a real
transcript string, so
sweep_quote_fabricatedcorrectly stayed silent — but it is not ajudgement about the outputs. It is the only obligation the model could find, because the outputs
were not there to reason about.
{"i":0,"needed_by":"none","quote":"","verdict":"drop"},having seen only
begins: # ledger_band a token count.So a drop is being decided from a 90-character head and a size. The comparative judgement this
design rests on did not happen on either prefix-ask turn.
The second half: depth and the cache read are mutually exclusive
Both derive from the same tracker record. A tracker hit gives you a stash to ask over and a
tail-only boundary. A tracker miss gives you depth and
sweep_no_prefix. Everydeep-candidate firing observed (runs C and E,
MaxCachedIdx=-1) was on the fallback path — thecontent-carrying prompt — not on the prefix ask.
So the mechanism that reaches deep history is the one the prefix ask was built to replace, and the
prefix ask only ever sees the tail it cannot judge.
Consequence: the shipped default is the refuted shape
sweep_inventory_of_onefires on the ordinary agent turn, not an edge case. The candidate setis the tail; the tail of a normal agent turn is one tool result. Run D — the plain case — produced
sweep_inventory_of_one=1 sweep_dropped=1. Run A needed two tool calls crammed into a single turnto reach an inventory of two.
That is the per-output shape
4ca1f13records as "refuted at 6% live-kept", reached by default,and the component drops on it.
Related bias in the fallback:
conversationGoalopens with the first user message, so on B/turn3the goal read
[r1] Read data/orders_2021.csv … tell me the order count in eachand the model keptall three outputs as needed for "(a) the step you are on right now".
Why no test caught it
All 20 sweep unit tests run with
MaxCachedIdx: -1, which disables the tail gate entirely — sothe disjointness cannot appear. And
extract_sweep_test.go:104claims the depth restriction islifted "so the candidates reach the inventory: the tail gate is exercised separately below". There
is no such test, in that file or the repo. That comment asserts coverage that does not exist.
This is the fifth vacuous-coverage instance in this component family (after the three trim tests,
the
tool_result-counting precondition in #110, and theeconomic_gate: falseblind spot in thissame component).
What needs deciding
This is design-level, not a patch. The options are not equivalent and each gives something up:
region the model reads. Loses the cache read for anything past the boundary, which is the
mechanism's whole economy.
the component is for, and accept that acting on it invalidates the prefix. That is what the
original cold-turn gating was for, and the pre-expiry window was meant to make it cheap.
ask, i.e. the fallback path, and drop the prefix-ask premise for this component.
(2) looks closest to the stated intent: the proposal's problem statement is deep history, and the
pre-expiry window exists precisely so that invalidating the prefix costs little. Under (2) the
prefix ask and the candidate set coincide instead of excluding each other.
Also stale, and load-bearing for this decision
docs/proposals/sweep-adjudicator.mdframes the problem entirely as deep history and never statesthat the tail gate confines the component to the uncached tail — it says only that "the newest tool
output is invisible" to the ask, which understates this by the whole transcript.
docs/hosted.md:645-653is wrong on five counts for this branch: "Both call a cheap model(
claude-haiku-4-5…)", "when the economic gate says", "runs only on a turn whose prompt cache hasexpired", "adjudicates at depth", and "a batch of outputs at a time".