acpmux: show how long a Claude chat's prompt cache stays warm in the context card
Merged
TE
2d ago+475+475+012345678901234567890123456789-42-42-01234567890123456789
Leo (2026-11-03): "i heard claude's cache time was like 5min... idk if we maybe also wanna be showing that/opt-in autocompact around that 5min mark"
This is part 1 of 2: the context card shows the prompt cache's state. Part 2 adds the opt-in "Compact before the cache expires" setting.
What Claude Code exposes (2.1.310)
- What each call reports: every call's usage names the tier it wrote:
cache_creation.ephemeral_5m_input_tokens/ephemeral_1h_input_tokens. This is the real TTL, and acpmux uses it. - How Claude Code picks the TTL, in its own order:
FORCE_PROMPT_CACHING_5MCLAUDE_CODE_PROMPT_CACHE_TTL(5m|1h)- the
promptCacheTtlsetting ENABLE_PROMPT_CACHING_1H- with none of these, "automatic": 1 hour on a Claude subscription within its limits, 5 minutes on an API key, Bedrock, Vertex or Foundry.
- Subagents have their own
CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL/subagentPromptCacheTtl(5 min unlessENABLE_PROMPT_CACHING_1H).
Change
- Daemon (
claude_stdio/prompt_cache.rs, new):- The expiry is the main loop's last request start (
message_start) plus the TTL. - The TTL is the tier the call reported. A call that only read keeps the last tier seen.
- The env and settings are not read: every turn writes its new tail, so the first call already names the tier. In 1,263 local transcripts, 210 of 79,919 main-loop calls (0.26%) named none.
- The request time is the call's
message_start, whose usage already names what it read and wrote, so a call stopped before itsmessage_deltastill moves the expiry. - The expiry goes out as
_meta.acpmux.cacheExpiresAt(unix ms) onusage_update: on each main-loop call and on the turn's result. - No expiry is sent when the call used no cache, before any call named a tier, for a subagent's call, or after a compaction (its context is not cached yet).
- The per-call
usage_updateis now built inprompt_cache.rs(Translator::call_usage), unchanged in shape, soinbound.rsgets shorter rather than longer. - Codex sends none.
- The expiry is the main loop's last request start (
- Webview:
- The context card adds one muted line under the bar: "Cache warm · 3:12 left", in the warning color in its last minute, then "Cache expired".
- The UI only counts down to acpmux's timestamp. A finished compaction drops the line, even when its boundary named no size.
- Timer: one 1 s interval, only while the card is open and the expiry is ahead. It stops at the expiry, when the expiry changes, and when the card unmounts.
- Gallery:
agent-pane.context-ringgetscache-warm,cache-expiring-soonandcache-expired. Each is Leo's screenshot's card (79% · 316.9K / 399.1K, Messages / MCP tools / Agent setup) plus the cache line, on a pinned clock.
Tests
-
Rust:
claude_stdio/prompt_cache_tests.rscovers:- the tier from the writes
- expiry = request + TTL
- a read-only call keeping the 1 h tier
- the result carrying the expiry and a compaction clearing it
- no expiry without cache use, before a tier is named, or for a subagent
- a call stopped before its
message_deltastill moving the expiry
These are new behaviour, so they fail on base (the module does not exist there).
-
Webview:
bun test src/agent-session/acpmux/ContextCard.test.tsxpasses 20/20 locally. The new tests cover the countdown stopping at the expiry, no line without an expiry, and the m:ss format.
Observed / Not run
- Ran: the ContextCard bun tests (20 pass),
scripts/web-lint.sh(passed) and rustfmt. - Ran on the fleet at 6873f57:
cargo test -p acpmux --lib claude_stdio::, 147 passed, 0 failed. The panic ratchet (rust_panic_ratchet.py) shows no new acpmux hits. The god-file check shows no file grown past its ceiling (inbound.rsis shorter than on base). The agent-pane turn-tokens test for the compaction drop is red without its fix and green with it. - Not run: the gallery capture. The advisory gallery run renders the three variants.
Changelog
- Added: the agent pane's context card shows how long a Claude chat's prompt cache stays warm.
risk: normal (daemon telemetry plus one UI line; no behaviour change)
Agent session: agent session
🤖 Generated with Claude Code