You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What the code actually does today, since it is documented nowhere
Two mechanisms were apparently considered when expand was designed. Only one is in the tree, and its cross-turn behaviour is not what the other's rationale assumed.
The in-turn path. The model calls context_guru_expand; the proxy intercepts it (proxy.go:~1650), and expand.Continuation (expand/response.go:69) appends two messages to the upstream request body — the assistant message carrying the tool_use, then a user message with one tool_result block per call holding the original content — then re-invokes upstream, up to maxExpandRounds (3). The content lands at the end, so the cached prefix is untouched during the turn. That part works as intended.
It does not persist as its own turn. On the intercepted path the client never sees the tool_use/tool_result pair — the proxy answers it and returns only the final assistant response. So the next turn the client re-sends its own transcript without the expanded content. The expansion is transient.
What persists is a flag, not the content.offload.MarkKeptVerbatim writes cg:keep:<content-hash>, and thereafter every offloader skips that content (skipReduce → isKeptVerbatim). So from the next turn the original message goes upstream in full, at its original position.
The consequence, measured
Turn N sent compacted bytes at that position; turn N+1 sends the full original there. That is a change inside the cached prefix, so it costs a cache-write of the suffix — at ~11.5x a cache read, which is the cost the tail gate exists to avoid everywhere else.
turn 1 sent 73 tokens (compacted)
turn 2 sent 4,200 tokens (full) gates=map[marker_or_kept_verbatim:1]
So "expand does not hurt the cache" is true within the turn and false across it. The flip happens once per expanded content, on the turn after the expand.
That is arguably correct behaviour — re-compacting would loop the agent straight back into another expand, which is exactly what cg:keep: exists to prevent, and one cache-write is cheaper than an unbounded expand loop. But it is a real cost that is currently undocumented, uncounted, and invisible: no counter distinguishes "prefix flipped because the agent expanded" from any other cache-write, so an operator cannot see it and a benchmark cannot attribute it.
The alternative that was considered and does not exist
replace the marker with the content in place, and delete the expand tool call from the earlier messages.
Nothing in the tree does this. I checked expand's whole exported surface — Inject, HasTool, ResponseCalls, Continuation, Marker, HasPlaceholder, ParseMarkers, Resolve, ToolDefRaw, RepairToolResults, Unavailable, AggregateSSE, Unresolved — and there is no in-place replacement and no stripping of prior expand calls.
There is no switch between the two.inject_expand (auto / always / never) controls only whether the tool is advertised, not how a call is answered.
The one path where content does persist
repairExpandErrors (proxy.go:1296). When interception structurally cannot work — a client tool batched alongside expand, maxExpandRounds exhausted, an event stream that will not reconstruct, a non-Anthropic stream, or a bypassed turn carrying older markers — the client did see the call and answered No such tool available. The proxy then repairs that tool_result on the request side on every later turn, so the content does live in the client's transcript.
Worth being explicit: that is a fallback for a failure, not a mode. It is also the only configuration in which the expanded content survives as its own turn.
What is worth deciding
Document the cross-turn behaviour, since neither docs/how-to/recover-context.md nor docs/design.md says that an expand permanently un-compacts that content or that it costs one prefix flip.
If in-place replacement is wanted, the gating signals already exist. It was suggested it should apply only on a cache miss or after a serious compaction — ColdCache / TailOnlyCold already detect a turn whose prefix cache has provably expired, and compaction_resets counts turns where the agent's own compaction moved the boundary. So the condition is expressible with machinery that ships; only the behaviour is missing.
How it was found
Answering a question about which of the two designs is live. The measurement above is from a scratch test, not committed. Related: #199 (a different per-content-versus-per-message mismatch in the replay bypass), #200 (the counter-splitting pattern).
What the code actually does today, since it is documented nowhere
Two mechanisms were apparently considered when expand was designed. Only one is in the tree, and its cross-turn behaviour is not what the other's rationale assumed.
The in-turn path. The model calls
context_guru_expand; the proxy intercepts it (proxy.go:~1650), andexpand.Continuation(expand/response.go:69) appends two messages to the upstream request body — the assistant message carrying the tool_use, then a user message with onetool_resultblock per call holding the original content — then re-invokes upstream, up tomaxExpandRounds(3). The content lands at the end, so the cached prefix is untouched during the turn. That part works as intended.It does not persist as its own turn. On the intercepted path the client never sees the tool_use/tool_result pair — the proxy answers it and returns only the final assistant response. So the next turn the client re-sends its own transcript without the expanded content. The expansion is transient.
What persists is a flag, not the content.
offload.MarkKeptVerbatimwritescg:keep:<content-hash>, and thereafter every offloader skips that content (skipReduce→isKeptVerbatim). So from the next turn the original message goes upstream in full, at its original position.The consequence, measured
Turn N sent compacted bytes at that position; turn N+1 sends the full original there. That is a change inside the cached prefix, so it costs a cache-write of the suffix — at ~11.5x a cache read, which is the cost the tail gate exists to avoid everywhere else.
Measured directly (
mask,keep_recent: 0,min_tokens: 20, one 4,200-token tool output):So "expand does not hurt the cache" is true within the turn and false across it. The flip happens once per expanded content, on the turn after the expand.
That is arguably correct behaviour — re-compacting would loop the agent straight back into another expand, which is exactly what
cg:keep:exists to prevent, and one cache-write is cheaper than an unbounded expand loop. But it is a real cost that is currently undocumented, uncounted, and invisible: no counter distinguishes "prefix flipped because the agent expanded" from any other cache-write, so an operator cannot see it and a benchmark cannot attribute it.The alternative that was considered and does not exist
Nothing in the tree does this. I checked
expand's whole exported surface —Inject,HasTool,ResponseCalls,Continuation,Marker,HasPlaceholder,ParseMarkers,Resolve,ToolDefRaw,RepairToolResults,Unavailable,AggregateSSE,Unresolved— and there is no in-place replacement and no stripping of prior expand calls.There is no switch between the two.
inject_expand(auto/always/never) controls only whether the tool is advertised, not how a call is answered.The one path where content does persist
repairExpandErrors(proxy.go:1296). When interception structurally cannot work — a client tool batched alongside expand,maxExpandRoundsexhausted, an event stream that will not reconstruct, a non-Anthropic stream, or a bypassed turn carrying older markers — the client did see the call and answeredNo such tool available. The proxy then repairs that tool_result on the request side on every later turn, so the content does live in the client's transcript.Worth being explicit: that is a fallback for a failure, not a mode. It is also the only configuration in which the expanded content survives as its own turn.
What is worth deciding
docs/how-to/recover-context.mdnordocs/design.mdsays that an expand permanently un-compacts that content or that it costs one prefix flip.ColdCache/TailOnlyColdalready detect a turn whose prefix cache has provably expired, andcompaction_resetscounts turns where the agent's own compaction moved the boundary. So the condition is expressible with machinery that ships; only the behaviour is missing.How it was found
Answering a question about which of the two designs is live. The measurement above is from a scratch test, not committed. Related: #199 (a different per-content-versus-per-message mismatch in the replay bypass), #200 (the counter-splitting pattern).