Cache-aware compaction and a prompt-cache keep-alive

Sorcar, 2026-09-20. Model in production: claude-fable-5-1 (82 % of spend). Sources: projects/cost-levers-followup-2026-09-20/FINDINGS.md sections 2 and 4, the Anthropic prompt-caching, context-editing and preserved-thinking documentation, and the 72 h replay in results/compaction_sim_gated_2026-09-20.json.

1. The problem

Every step of an agent re-sends the whole conversation. With prompt caching that costs 0.025× the input price on Fable 5.1 ($0.25 per million tokens) as long as the prefix is unchanged and the cache is warm. Two things break the prefix:

2. What changed

Compaction gate (src/kiss/core/context_compaction.py)

The compaction is planned first (plan_compaction) and applied only when should_compact agrees:

drop_tokens = dropped_chars(plan) // CHARS_PER_TOKEN          # 4 chars per token
compact if drop_tokens >= 0.25 * context_tokens               # MIN_DROP_FRACTION
       and context_tokens < 0.8 * handoff_limit                # HANDOFF_PROXIMITY_FRACTION
                                                              # handoff_limit = 0.7 * window (350k on 500k)

Defaults moved from keep 20 / min 2,000 chars / re-trigger +50k to keep 6 / min 500 / +100k. A skipped compaction is re-planned on every following step, because more results age out of the kept window. context_tokens is the input size the last completed request reported, which is what the agent knows before sending the next one.

Keep-alive (src/kiss/core/prompt_cache_keepalive.py, AnthropicModel.keep_prompt_cache_warm)

A tool call qualifies when its timeout/timeout_seconds argument is at least 300 s or it is a fan-out (run_parallel, run_commands_parallel, run_agent). While it runs, a daemon thread sends a ping every 240 s, the first one 240 s after the model request that preceded the tool call. The ping is the same request the step sent (tools, system, thinking, messages, byte for byte) plus one user message answering each in-flight tool_use with a placeholder tool_result, with max_tokens lowered to 256 (not part of the cached prefix). Its reply is discarded; its cost is added to the agent's budget on the agent thread after the tool returns, also when the tool ends by an interrupt or budget error. At most 10 pings per call; none when caching is disabled for the model; retries off and a 30 s timeout so the agent never waits long for a ping in flight.

0 4 min 5 min 8 min 12 min 16 min Before model request starts → cache warm 5 min cache expired; the next step re-writes the whole context ($12.50/M) After ping 1 ping 2 ping 3 ping 4 each ping reads the cached prefix ($0.25/M on Fable 5.1) and restarts the 5-minute clock tool call running (Bash with timeout ≥ 300 s, run_parallel, run_commands_parallel, run_agent)
Timeline of one long tool call. The TTL clock starts at the model request, not when the tool starts, which is why the first ping is scheduled from the request timestamp.

3. Measurement

3.1 Live 72 h KPI (compare_kpis.py --hours 72, 07:30 UTC)

Unchanged from the 06:24 UTC run except for the window shift, because the running daemon still executes the pre-change code. These are therefore the before numbers for this lever (claude-fable-5-1, filtered to post-lever trees, 3,728 steps, $1,193):

step typestepscostshare$/stepavg context
normal step3,561$626.8652.5 %0.176129,686
fan-out step (children folded in)14$369.2530.9 %26.37217,255
cache miss: compaction56$93.927.9 %1.68132,112
cache miss: ≥ 5 min gap40$87.587.3 %2.19173,934
first step of a session56$14.461.2 %0.2624,832

The scheduled cost-levers-72h-recheck cron job (2026-09-23 06:30 UTC) re-runs this table after the change has been running; the two cache-miss rows are what should move.

3.2 Replay of the last 72 h through the new code

213 Claude sessions of at least 20 steps were rebuilt from the event log and replayed through the real compaction functions (compaction_simulation.py). Input cost is priced at Fable 5.1 cache rates: the first step and every step after a compaction write the whole context, every other step reads it. Per-step growth is left out because it is the same in every policy.

policycompactionsinput billvs productionsteps ≥ 100ksessions reaching 350k
A: no compaction0$5420.6252.1 %12
B: production 09-19 (100k/50k, keep 20, min 2000)217$8741.0045.3 %6
H: new defaults, no gate164$7060.8142.4 %6
I: new defaults + gate (implemented)67$5750.6646.5 %11
J: drop gate only, no hand-off skip67$5750.6646.5 %11
A none B production H ungated I gated $542 $874, 217 compactions $706, 164 $575, 67 compactions Simulated input bill of 213 sessions / 72 h at Fable 5.1 cache prices (write $12.50/M after a compaction, read $0.25/M otherwise).
The gate removes 70 % of the compactions and a third of the compaction-related input bill, but on this model the cheapest input bill is still "never compact".

3.3 What the replay says

  1. The gate cuts the compaction-related input bill by 34 % against the 09-19 production policy (about $299 per 72 h at this volume) and the number of compactions from 217 to 67.
  2. At a 0.025× cache-read price a compaction that drops a quarter of the context needs about 196 further steps to pay back, so "no compaction" is still $32 cheaper than the gated policy on input alone. What compaction buys on Fable 5.1 is avoiding hand-offs, and the 25 % gate gives most of that back: 11 sessions reach the 350k hand-off limit against 6 with the old policy and 12 with none.
  3. The hand-off proximity skip changed no decision (I equals J): a 25 % drop is never available that late in a session, so the drop gate already covers it.

4. Risk found while researching

Anthropic's preserved-thinking prefix check (Claude Fable 5.1 and later) treats a shortened earlier tool_result as invalidating every later thinking block. Accounts created on or after 2026-08-31 get a 400 by default, and "later models will enforce the prefix check for all accounts". The client-side stubbing that compaction does today therefore works on this account but is a latent failure. Anthropic's replacement is server-side tool-result clearing (clear_tool_uses_20250919, beta header context-management-2025-06-27) with clear_at_least as the cache-worth gate. Not changed in this task.

5. Verification

6. Follow-ups

  1. Re-run compare_kpis.py --hours 72 after the deployment has been live for a few days (the cron job does this on 2026-09-23) and compare the two cache-miss rows.
  2. Make the gate price-aware: break-even steps = (write − read) / read × context / drop, against the steps left before hand-off, using the model's cache prices in MODEL_INFO. On 0.1× models this compacts about as now; on 0.025× models it compacts only to avoid a hand-off.
  3. Move the Anthropic path to server-side tool-result clearing (section 4).