SWE-bench Verified, live, four arms: 13% cheaper than no compaction, and the highest reward. See the numbers

Budget keep-alive pings

Choose a cache TTL covers the tier — five minutes or an hour. Keep an idle prompt cache warm covers turning keep-alive pings on at all. This page is about the knob in between: how many pings a conversation is worth spending while it's idle, rather than a single fleet-wide number.

The short version

How to configure it

The practical knobs live under the cache keep-alive config (see Keep an idle prompt cache warm for the full picture):

cache:
  keepalive: true
  keepalive_idle_seconds: 270          # ping interval — keep this under the tier's TTL
  keepalive_max_pings: 2               # flat cap per idle span
  keepalive_min_prefix_tokens: 80000   # skip small prefixes — a ping's fixed cost isn't worth it below this
  keepalive_max_usd_per_ping: 0.50     # per-ping cost ceiling

keepalive_max_pings is a ceiling other policies (including the predictor-driven budget below) can only tighten, never raise.

Turning it on

Pick it on the KV-cache page: it is the keepalive-budget checkbox, in the default arm set, scored against the same baseline, rates and savings arithmetic as every other arm.

That is new. BudgetPolicy used to be unreachable from any page — it needed a Predictor, and no Predictor existed outside a test. kvcache.ReuseModelV1 is that predictor: a fixed table of coefficients compiled into the binary, so NewKeepAliveBudget builds the arm from nothing and the name resolves on every process.

Config.MaxPings still caps it, and the page defaults to 2

A budget can ask for fewer pings than the cap, never more. The arm reasons over 8 sweeps, so at the page's default k=2 it is bounded to a quarter of its own horizon. It still wins there (see below), but if you want to see the schedule it would actually choose, raise the keep-alive cap to 8.

The shipped model

ReuseModelV1 is a fixed discrete-time survival model: one coefficient per shipped feature, compiled in and never re-fitted at runtime. It is a logistic regression rather than the gradient-boosted model the offline tooling can also fit, traded for reviewability (thirty-odd coefficients fit in a diff) and for a rule cheap enough to run on the live request path.

Fitted on 18.4 days of the hosted capture (219,650 idle spans, 37,418 reaching the first sweep): holdout AUC 0.9065 by row, 0.7837 by dollar. Full feature list, provenance and the three specification pitfalls found while fitting it (collinear sweep features, a mislabeled last sweep, and a cache_ttl train/serve skew) are in the reuse model research.

What it is worth, measured

Replayed through the KV-cache page's own scorer, against the fixed-5m baseline:

arm pings off the bill net per ping
optimal (unreachable — reads the future) 1,041 8.054% 3.069
keepalive-budget, cap 5 8,790 2.065% 0.0932
keepalive-budget, cap 2 (page default) 7,573 1.850% 0.0969
keepalive-5m flat k=5 59,139 1.860% 0.0125
keepalive-5m flat k=2 (DefaultMaxPings) 25,586 1.808% 0.0280
stop-reason-gated 14,969 0.710% 0.0188

On this window the arm beats every reachable alternative on money, which an earlier measurement on a shorter window did not find — that measurement reported a flat constant winning by 0.89 pp. Read it as a differently-specified model on a shorter corpus, not as a contradiction; see the reuse model research for the full accounting.

The budget genuinely varies per conversation — 41.4% of conversations are worth no ping at all, and the arm declines them, which is where most of the reduction in ping volume over a flat cap comes from.

Tuning guidance

How to use it from Go

The arm is in the registry, so a name is enough:

s, _ := kvcache.NewStrategy(kvcache.StrategyKeepAliveBudget, nil, cfg)
result := kvcache.Simulate(reqs, s, cfg)

With your own predictor instead of the shipped one, construct it directly:

pol := kvcache.BudgetPolicy{
    Predictor: yourModel,              // anything implementing kvcache.Predictor
    Interval:  cfg.PingIdle,           // MUST match what the simulator will ping at
    MaxK:      kvcache.DefaultBudgetMaxK,
    MinPrefix: 10_000,                 // optional: a ping's fixed overhead is not worth it below this
    Semantics: cfg.Semantics,
}
result := kvcache.Simulate(reqs, pol, cfg)

Interval must match Config.PingIdle — otherwise the windows this policy prices are not the windows the simulator buys.

The offline side

deploy/harbor/kv_ttl_keepalive_policy.py holds the fit and the evaluation:

cd deploy/harbor
python3 kv_ttl_keepalive_policy.py --self-test        # asserts the module's claims, stdlib only
python3 kv_ttl_keepalive_policy.py --fixture f.json   # the decision arithmetic's guard
python3 kv_ttl_keepalive_policy.py --score f.json     # the shipped MODEL's guard

# re-fit the shipped model and regenerate both checked-in artifacts
python3 kv_ttl_keepalive_policy.py \
    --db /var/lib/context-guru/cg.db --prices /etc/context-guru/prices.yaml \
    --model-out reusemodel_v1.json --go-out ../../kvcache/reusemodel_v1_gen.go

--db/--model-out/--go-out re-derive the shipped coefficients from the corpus rather than requiring them to be hand-edited. Both output artifacts are checked in, and the pair being checked against each other is what kv_ttl_reusemodel_drift_test.go guards.

The research behind this

The break-even derivation, the backward-induction budget (why a ping's value depends on the pings after it, not just the next window), the shipped model's full feature list and provenance, and the measured comparison between a flat cap and the learned per-conversation budget all live in Results: