The problem
extract_llm and extract_llm_sweep share the cg:res: result namespace, and the key carries no component tag:
resultKey(session, id) = cg:res:<session>:<id> — session and content hash only (components/offload/state.go:46).
- Both components derive
id from extract.ContentKey(content) (extract_llm.go, extract_sweep.go).
- The shipped
housellm preset runs extract_llm immediately before extract_llm_sweep in one pipeline (config/config.go:436).
So a cg:res: record written by extract_llm for content M is indistinguishable, to the sweep, from one the sweep wrote itself — and vice versa.
Why that matters
Both components use a hit on that key to bypass the cache-tail depth gate. The reasoning is sound only for a record the component itself wrote: "this session already sent these bytes, so replaying them is byte-identical at any depth". On another component's record the premise does not hold — the sweep would splice its own shape descriptor at arbitrary depth on the strength of bytes it never sent, and the stored value is a different projection entirely.
Reachability: not established, in either direction
Worth being explicit, because the obvious mechanism turns out to be wrong. The suggested path was extract_llm's replay apply() failing for want of a reserve slot, leaving M verbatim for the sweep to find — but tryMark/markToken never consult the store, and the replay path's commitRefresh never refuses, so apply declines only on the marker-inclusive never-worse check.
Reaching the state needs M present verbatim, at cached depth, with a live cg:res: from extract_llm and no marker on it. That has not been constructed. It may be unreachable today.
What is certain is that the invariant the code relies on is not expressible by the key, so it holds by accident of the current pipeline shapes rather than by construction — and the next pipeline or ordering change re-opens the question without anything failing.
Mitigating factor
The replacement each component writes is a pure function of (content, config), so a replay emits deterministic bytes whichever component decided. The harm is therefore about which projection gets spliced and at what depth, not about non-determinism.
Options
- Namespace the record per component —
cg:res:<component>:<session>:<id>. Makes the claim true by construction. Costs cross-component reuse of session-scoped results, which nothing currently relies on (the cross-session reuse is a separate namespace, cg:xres:, which is already keyed with the model and config).
- Store a component tag in
cachedResult and check it on read. One field, backward-tolerant (a missing tag reads as empty), and preserves the option of deliberate sharing later.
- Establish unreachability and document it. Cheapest now, and the reviewer's argument against it is the strong one: it has to be re-established after every pipeline change, and nothing fails when it stops being true.
Option 2 looks best: one field, no behaviour change for records written after it, and the invariant becomes checkable.
How it was found
Reviewing #188, where a comment on the sweep's replay path asserted the stronger claim ("the hit says this session already sent these bytes"). The comment has been corrected to state what the hit actually proves; this issue is the underlying design point, which predates #188 — both components have always shared the namespace.
The problem
extract_llmandextract_llm_sweepshare thecg:res:result namespace, and the key carries no component tag:resultKey(session, id)=cg:res:<session>:<id>— session and content hash only (components/offload/state.go:46).idfromextract.ContentKey(content)(extract_llm.go,extract_sweep.go).housellmpreset runsextract_llmimmediately beforeextract_llm_sweepin one pipeline (config/config.go:436).So a
cg:res:record written byextract_llmfor content M is indistinguishable, to the sweep, from one the sweep wrote itself — and vice versa.Why that matters
Both components use a hit on that key to bypass the cache-tail depth gate. The reasoning is sound only for a record the component itself wrote: "this session already sent these bytes, so replaying them is byte-identical at any depth". On another component's record the premise does not hold — the sweep would splice its own shape descriptor at arbitrary depth on the strength of bytes it never sent, and the stored value is a different projection entirely.
Reachability: not established, in either direction
Worth being explicit, because the obvious mechanism turns out to be wrong. The suggested path was
extract_llm's replayapply()failing for want of a reserve slot, leaving M verbatim for the sweep to find — buttryMark/markTokennever consult the store, and the replay path'scommitRefreshnever refuses, soapplydeclines only on the marker-inclusive never-worse check.Reaching the state needs M present verbatim, at cached depth, with a live
cg:res:fromextract_llmand no marker on it. That has not been constructed. It may be unreachable today.What is certain is that the invariant the code relies on is not expressible by the key, so it holds by accident of the current pipeline shapes rather than by construction — and the next pipeline or ordering change re-opens the question without anything failing.
Mitigating factor
The replacement each component writes is a pure function of
(content, config), so a replay emits deterministic bytes whichever component decided. The harm is therefore about which projection gets spliced and at what depth, not about non-determinism.Options
cg:res:<component>:<session>:<id>. Makes the claim true by construction. Costs cross-component reuse of session-scoped results, which nothing currently relies on (the cross-session reuse is a separate namespace,cg:xres:, which is already keyed with the model and config).cachedResultand check it on read. One field, backward-tolerant (a missing tag reads as empty), and preserves the option of deliberate sharing later.Option 2 looks best: one field, no behaviour change for records written after it, and the invariant becomes checkable.
How it was found
Reviewing #188, where a comment on the sweep's replay path asserted the stronger claim ("the hit says this session already sent these bytes"). The comment has been corrected to state what the hit actually proves; this issue is the underlying design point, which predates #188 — both components have always shared the namespace.