Cache Clock · GitHub Copilot CLI and VS Code
One idle gap, one 25× price.
Copilot bills tokens in AI credits. When the prompt cache expires, the next message rewrites your whole conversation at the cache-write price. Cache Clock shows the clock, prices the send, and asks before a costly cold send goes out.
A cache write costs far more than a cache read
On Claude and newer GPT models in Copilot, writing a prompt to cache is priced well above reading it back. The cache also expires. Step away longer than the TTL (time to live) and the next message, even “give me a recap”, pays the write price for everything.
The estimate (37.2k) is the last call’s input plus its answer. The real resend also carried your new message and what Copilot attaches, so it wrote 44,388.
Copilot reports the expiry, so nothing is guessed
Every main-agent model call emits an assistant.usage event. It carries the cache numbers and the prices. Cache Clock keeps the last call for each model.
assistant.usage
- cacheReadTokens
- cacheWriteTokens
- cacheExpiresAt
- cacheTtlSeconds
- per-token prices
- model
call per
model →
One entry per model
expiry · TTL · context size · read & write price. Answers: warm, cold, or unknown?
the next
send →
Cold vs warm cost
context × write price against context × read price. The difference is the “extra”.
cache-clock · Claude Sonnet 5.5
cache warm, 1m17s left (ttl 5m00s)
context 37.2k tokens
next send warm $0.003 · cold $0.08 (25× per token)
session 5 model calls · 0 cold after idle · 0 prefix breaks · $0 extra paid
guard refuse once above $0.10 · heads-up 5m before expiry
Prices come from the payload: nano-AI-credits per batch of tokens, and 1 credit is $0.01. Prices already include discounts. Through auto, Sonnet’s write was $2.25/M (list $2.50).
A costly cold send asks first
The hook onUserPromptSubmitted runs on every message you send. It checks four things, in this order. Only the last path can stop the message, and only if you say so.
-
Prompt submitted
onUserPromptSubmittedchecks the last model’s clock -
Starts with
/nameor!?Slash commands and shell escapes never call the model with your conversation.no ↓yes →● ALLOW · skippedNever guarded.
-
Guard off, or cache not cold?Warm, or unknown (no data yet, or right after a compaction).cold ↓yes →● ALLOW · silent
Compaction exemption: the context was rewritten anyway, so the next send is not an idle tax.
-
Extra cost ≥ threshold?Default $0.10 (
/cache-clock min). Extra = context × (write − read).yes ↓no →● ALLOW · silentSmall rewrites pass quietly. At the default, the $0.08 capture would pass.
-
Mode is warn, a turn is running, or this cold episode was already asked about?Episode = one model’s cache that expired at one instant. Refusals are remembered per session in
refused.jsonfor a week, across processes.none ↓yes →▲ SEND with a price noteWhile a turn runs, aborting would cancel that turn, so you only get the note. Your resend lands here.
-
Ask in the chat
session.ui.selectwith the price: Send anyway or Don't send. Don't send, dismissed, or no answer in 5 minutes:session.abort()without awaiting it (awaiting inside the hook deadlocks). No model call, 0 AI credits.■ ASK · once per cold cacheWhere Copilot cannot ask (
copilot -p), it refuses once instead: send the same message again to pay, or/clearand start fresh.
cache-clock: the Claude Sonnet 5.5 prompt cache went cold 2s ago. Sending this rewrites
about 37.2k tokens at $2.25/M = $0.08 (a warm send would read them for $0.003).
[ Send anyway ] [ Don't send ]
The refuse-once fallback, where Copilot cannot ask — a real capture (Copilot CLI 1.0.95, 11 October 2026, taken with /cache-clock min 0.01)
! cache-clock: the Claude Sonnet 5.5 prompt cache went cold 2s ago. Sending this rewrites
about 37.2k tokens at $2.25/M = $0.08 (a warm send would read them for $0.003).
Send it again to pay it, or /clear and start fresh.
The resend’s note — template, one line
cache-clock: sending cold, rewriting about <tokens> tokens for <cold $> (warm: <warm $>).
Switch modes with /cache-clock guard refuse (default: ask, or refuse once), warn (send and print the price) or off. /cache-clock help explains it all in the chat.
A warning one minute before a Claude cache expires
One ephemeral line appears before expiry. It fires only when a cold send’s extra cost (cold minus warm) is at least the threshold and no turn is running. The lead time is the smaller of your setting (5 min) and a fifth of the TTL. A 5-minute lead on a 5-minute cache would fire right after every call.
Change it with /cache-clock headsup 5, or headsup off. It keeps running under guard off until you turn it off.
A resumed session reads its clock from the log
assistant.usage is not replayed when you resume, and getEvents() omits usage records. So the extension reads the session’s own log, which keeps every record with its prices.
events.jsonl
~/.copilot/session-state/<id>/
one line per event
session.usage_record→
Seeded at start
Main-agent calls only. A session.compaction_complete line marks the clock unknown.
events
→
Works on call one
Even in copilot --resume … -p.
refused.json.See what cold sends already cost you
The extension only sees sessions it was loaded in. The report reads every session Copilot has persisted, CLI and VS Code alike. It sends nothing anywhere. It needs Node 18+.
Run itnode ~/.copilot/extensions/cache-clock/report.mjs # last 30 days
node ~/.copilot/extensions/cache-clock/report.mjs --since 7d # or 12h, all; --json for raw numbers
Sample output — the test machine after the README’s experiments
model calls cold after idle prefix breaks rewritten extra paid total spent idle before cold sends
Claude Sonnet 5.5 5 1 0 37.1k $0.08 $0.16 5m
GPT-6 Luna 201 1 2 44.9k $0.005 $0.14 31m
extra paid for cold rewrites: $0.08 of $0.29 spent · about $0.46 per week at this rate
The call came after the reported expiry. The fix is behavior: send sooner, or accept the price.
A cold call inside the TTL. Something changed the prompt prefix (tools, instructions, model routing), or the provider served it cold early.
“Extra paid” is what each cold call cost above a warm read of the same tokens. VS Code sessions persist only totals, not per-call records; the report counts them separately. In VS Code's Copilot chat the extension loads once "chat.canvases.enabled": true is set (measured 12 October 2026); the card and the cold-send question appear as question cards, and the heads-up reaches only VS Code's log.
Claude held to the second. Luna went cold early
Copilot’s reported expiry was tested on both models on 11 October 2026.
| Model (via Copilot) | Reported TTL | Write / read | Evidence |
|---|---|---|---|
| Claude Sonnet 5.5 | 300 s | 25× | Sent 20 s after cacheExpiresAt: read 0, wrote 44,388 |
| GPT-6 Luna | 1800 s | 12.5× | Every persisted call (201 on this machine) reports cacheTtlSeconds: 1800; gap test below |
Gap test, GPT-6 Luna
Two sessions were warmed at 14:55Z, then resumed with copilot --resume <id> -p, guard threshold at $0.
| Resumed | vs reported expiry | Cache read | Cache written | What Cache Clock did |
|---|---|---|---|---|
| after 29 min | 1 min before | 0 | 20,771 | allowed it (clock said warm) |
| after 31 min | 1 min after | (refused, no model call) | refused, 0 AI credits | |
| resend, new process | 1 min after | 10,157 | 10,653 | let it through with the cold note |
The refused-then-resent pair also shows the refusal memory working across processes. Separately, a brand-new session’s first call read 10,157 tokens from cache: sessions share the cached system-prompt prefix, while the conversation itself is per session.
Limits
The guard trusts cacheExpiresAt. When a provider serves the cache cold sooner (Luna at 29 of 30 min, once), the send is billed cold and the guard stays quiet. The report counts it as a prefix break.
The estimate is the last call’s input plus its answer. The real resend adds your message and attachments (44.4k actual vs 37.2k estimated). Copilot’s shared system prefix (~10k tokens) often stays cached, which pulls the other way.
cache-tax pings to hold the cache. An extension can only ping with session.send, which adds a user and an assistant turn each time. Run the report to see if pings would pay off.
With auto, the next prompt may route to another model; the guard prices the last model used. In -p mode, /cache-clock needs the interactive CLI. Claude was measured through auto only; each boundary result is a single run.