SLOPSHOPPER

cache-tax

A Claude Code mod that keeps the one-hour prompt cache warm during breaks and stops a cold send once with its estimated rewrite cost. Warming uses model…

newbandcommandstatuspromptmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-tax
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /keepwarm ⎿ cache-tax: keepwarm on for 6h00m, a ping 50m after each idle stretch keeps the cache read, not re-written ⟨Claude Code's own drawing⟩ ▣ [>] cache-tax · warm · keepwarm 6h00m left · ping in 50m ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ▣ [>] cache-tax · warm · keepwarm 6h00m left · ping in 50m
README

<img src="https://raw.githubusercontent.com/karanb192/cache-tax/432a08d/site/assets/cache-tax-light.svg" alt="Cache Tax" width="64" height="64">

Cache Tax for Claude Code

Keep Claude Code's prompt cache warm during breaks.

Cache Tax is a Claude Code mod that refreshes your prompt cache while you step away and shows the estimated rewrite cost before a cold send. Run /keepwarm 90m before a break. Pings cost tokens; choose a window you expect to return within.

Install · Website · How it works · Costs and limits · Commands

Real recording of cache-tax stopping a cold send with a $6.61 estimate

A two-line recap in a 330k-token session triggered a $6.61 estimate. After resending, the reported cache write was $6.28. Recorded on 2.1.1; current wording says “up to” the estimated token count. These are API-equivalent costs, not extra subscription charges.

Install

Needs Claude Code 2.1.287 or later, a one-hour cache and Claude Code left running. Mods are on by default. Pings cost tokens, including uncapped output.

claude plugin marketplace add karanb192/claude-code-mods
claude plugin install cache-tax@claude-code-mods

Restart Claude Code or run /reload-plugins in an open session. No early-access flag is needed. If you set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS previously, remove it; current Claude Code ignores it.

In a warm session, run /keepwarm to arm six hours. Use /keepwarm 90m for a shorter window, or /keepwarm off to stop.

Check the main cache lifetime. Included subscription usage defaults to one hour. API-key, usage-credit and cloud-provider sessions default to five minutes, so set promptCacheTtl to "1h" for those billing paths. A 50-minute timer cannot protect a five-minute main cache. See Limits for the fork evidence.

Prefer a settings hook? The hook version warns by default, refuses once with CACHE_TAX_BLOCK=1, and provides a standalone status-line countdown. Warming is part of this mod.

How it works

Before a break: /keepwarm arms a bounded window. After 50 idle minutes, a timer inside Claude Code sends one tool-less fork over the session's transcript. It reads usage after every ping and stops if reads are zero or writes reach 10% of reads. There is no background transcript-polling loop.

When you return cold: for a context of at least 50,000 tokens, the guard drops your ordinary message once when its one-hour clock says cold. It shows the estimated rewrite price. Resend to continue, or /clear and start from a note. A prompt no person sent, such as a background task's notification, a scheduled prompt or another session's message, is sent with the price logged, because nobody is there to resend it. Anything a person may have sent keeps the guard, including Remote Control, Slack and channel messages, SDK hosts and prompts Claude Code cannot classify.

A detected cold write automatically arms at least three hours of keepwarm, six with always on, and prints what it cost. /cache-tax shows the current cache estimate, warming window and this session's cold-write tally.

Windows belong to individual sessions. An already-cold session waits for your next turn before pinging, and so does a window restored after Claude Code restarts, until the new process has a turn to fork. A ping refused by a busy API (429, 529, 5xx) is retried after 1, 2 and 4 minutes while it can still land before the cache goes cold. With always on, each of your turns that reaches the cache leaves at least six hours of window at the default period, so any break up to six hours stays warm however long the session runs. The same turn brings back a window that stopped, stretches a shorter one to six hours and resets a testing period; a longer window you set stays. /keepwarm off in any session stops every session rolling from its next turn. A window ends at its deadline; /keepwarm off also cancels it and clears the always setting.

A recorded warming ping

Real keepwarm status showing a 75k-token cache read at $0.02

Sonnet 5, 16 September 2026: the status reported 75k tokens read at $0.02. Nothing appeared in the conversation during the live run. This capture used the one-minute testing interval on 2.0.0; its card's $0.45 rewrite estimate used an older Sonnet price row, versus $0.30 for the same context with the current row. The two-cent ping is a receipt for that session, not a fixed price.

Why a short message can cost so much

When the cached prefix expires, even “give me a recap” can trigger a rewrite of the old context before the answer. Cache Tax's Fable 5.1 price row compares a $20 one-hour write with a $0.25 cache read per million tokens: 80x per token, not 80x total savings. See the pricing assumptions and Anthropic's price table.

Watch the 23-second overview. Its designed scenes label Fable 5.1 list prices; every terminal and status-line pixel comes from the real refusal recording and keep-warm receipt above. The refusal was recorded on 2.1.1 and the keep-warm crop on 2.0.0.

New to caching? Anthropic explains how Claude Code uses it.

Commands

/keepwarm keep warm for six hours /keepwarm 90m keep warm for a window of your own (also 2h30m, 6h) /keepwarm always keep every session warm until six hours after its last turn, remembered across sessions /keepwarm 6h every 2m pinging every two minutes; a testing knob, 1m to 59m, forgotten after this window (with always on, at your next turn) /keepwarm status the warming window, next ping and last receipt /keepwarm off stop, forget the window, and turn always off /cache-tax the card /cache-tax guard warn show the price and send (the hook's default) /cache-tax guard refuse drop a cold send once, the resend goes through (this mod's default)

The refusal, verbatim:

cache-tax: the prompt cache went cold 2h00m ago. Sending this re-writes up to 200,502 tokens at $20/MTok = $4.01 (a warm turn would have cost $0.05). Send it again to pay it, and keepwarm will then hold the cache for 3h00m. Or /clear and start from a note.

The figure is an upper bound. The context count the engine reports for a resumed session is the last response's input, cache read, cache write and output together, and the resume payload carries no separate output count to take off; on one 15-day-old session the refusal said 330,316 tokens and $6.61 and the write that followed was 314k tokens, $6.28.

While keepwarm is armed, the terminal and Desktop Code tab show a row above the prompt with the Cache Tax cube. Its lid is green for warm, orange for cold, and grey before the first turn or after compaction. The bold state label matches; the timer and receipt text use normal weight. Ghostty and kitty can draw the image; other terminals show [>] in its place. A nonempty NO_COLOR selects a bold text icon and an uncolored state label. The row also says warm, cold or unknown, so color is never the only signal. These states use the same one-hour clock as /cache-tax; they are estimates, not a live server cache check.

The row includes the warming window, next ping and last readback. A stopped loop shows a neutral symbol and its reason. /keepwarm off or an expired window removes the row. Surveys temporarily take priority, and other mods' content stays in the band. Sessions without this band retain the plain status text.

The cube follows Claude's light or dark theme. Its terminal image also has a contrasting edge for terminal backgrounds that differ from that setting. The embedded assets come from the website's official SVGs; maintainers can regenerate them with python3 tools/generate-status-icons.py and rsvg-convert installed. No image files are read or fetched at runtime.

The mod reads the theme and NO_COLOR once when the session starts. Accepted theme changes update the icon immediately; ordinary redraws do not reread either setting. Set NO_COLOR before starting the session.

Both forms installed

The hook and the mod share a name and a job, so having both means two guards on every cold send. The mod checks at session start whether the hook's /cache-tax:status command exists and says so once. Keep one guard. The hook remains an option for people who prefer settings hooks. Before uninstalling it, check your status line: the 🧊 row wired to cache-tax.js comes from the hook's files. This mod draws above the prompt while keepwarm is armed; it does not replace your shell status line.

What it can reach

Validated on Claude Code 2.1.289:

❯ ./register.ts hooks: config.set{key=theme}, ui.render{component=AbovePrompt}, session.start, classic.SessionStart, command.run{command=keepwarm}, command.run{command=cache-tax}, prompt.submit, turn.step, turn.complete, session.compact ❯ ./register.ts calls: $.clock.after (via arm), $.clock.now, $.command.list, $.command.register, $.config.list, $.env.get, $.model.fork (via ping), $.session.id, $.session.model, $.session.usage, $.store.delete (via prune, startWindow, stop), $.store.get, $.store.set, $.ui.invalidate, $.ui.log, $.ui.resolve, $.ui.status ❯ ./register.ts env reads: NO_COLOR

Reach L2, drives Claude. Sees every prompt you type, every model request's timing and every answer's token counts.

Threat model for cache-tax (reach L2, drives Claude)

  1. Reads: of each prompt, whether it starts with a slash and nothing else (the text is passed on untouched, never kept, never logged); the time; the token counts and model id the engine already holds on turn.complete and on the fork's reply; the resume fields Claude Code computes for settings hooks; the command list, the session id and, when a resumed session's fields carry no model id, the session's model once at start; from its own $.store, the keepwarm deadline and the ping period keyed by session id, plus the global always switch and guard mode
  2. Runs: one $.model.fork per idle stretch inside a keepwarm window, one per ping period (50 minutes unless the testing knob set it, floor 1 minute), never outside the window, never onto a cache the mod already knows is cold, never after a readback that read nothing or wrote at least a tenth of what it read
  3. Sends: nothing leaves the machine except the fork, an API request over the session's own transcript with a fixed one-line prompt
  4. Persists: in $.store, the keepwarm deadline and the ping period under this session's id (with always on, rewritten after each turn), and the always switch and the guard mode for every session; a window that has ended is deleted at stop and at this session's next start, together with the bare keys of a 2.1.0 store; another session's keys are never deleted here, because a read followed by a delete cannot be made atomic against that session renewing its window, so a session that armed keepwarm and never came back leaves two small keys behind; the session's cold-write tally lives in memory and dies with the session
  5. Hostile input: the only text it parses is the argument of its two commands, matched against a duration regex and five literals; of the prompt text only the first non-blank character is inspected, for a slash; tool results and files never reach a branch; the fork's prompt is a constant, so nothing crafted can be sent through it; a refusal only ever drops the user's own message, and the resend is unconditional; if a hook throws, the engine skips it and the message enters unguarded, with one dim line

Cost and the plan-limit question

Prices are the list table, where Fable 5.1 reads at $0.25, writes the 1h tier at $20 and answers at $50 per million tokens; Sonnet 5 has its own row ($0.20, $4, $10). On an API key the arithmetic is plain. A ping bills the cache read, its own few uncached tokens at the base rate, and whatever the model says back at the output rate; the displayed ping cost counts all of it, since a fork takes no output cap and a model at high effort may think before it says "warm". A comeback after the lapse is a write. The card prints the read-only upper bound for your model, 80 pings on Fable 5.1, with the idle that covers at the current ping period; a real ping costs a little more than a read, so the true break-even sits below that number. On a subscription the dollars are a yardstick, not the bill, and how a cache read weighs against the 5-hour and weekly limits is not documented anywhere I could find. Watch the rate-limit row of your status line during the first window.

Limits

  • Mods require Claude Code 2.1.287+. They are on by default, but local settings, safe mode or organization policy can prevent them from loading. See mod controls.
  • The 50-minute schedule needs a one-hour main cache. Included subscription usage has it by default. Other billing paths need promptCacheTtl set to "1h".
  • One transcript-verified return at the default interval, Fable 5.1 on a subscription, 19 September 2026: /keepwarm armed at minute 42 of a break, the ping fired at minute 50 and read 157k, and the message sent at minute 62 read 156,886 tokens from cache and wrote 2,255. Claude Code's own cache countdown reset to 58 minutes after the ping, so the fork carried the one-hour TTL. One run, one configuration: it shows the ping kept that cache alive past the hour, not net savings for every setup. Claude Code documents forks in its non-main request bucket, normally five minutes. In this run, the fork still read the one-hour session prefix and reset the native countdown. That is observed behavior in one configuration, not proof that every provider handles the buckets identically.
  • A ping that reads warm proves the cache was warm then. Prefix changes can invalidate the cache regardless of time. The exact effect depends on the model and when Claude Code applies the change; see Claude Code's cache behavior.
  • Resume fields let the guard check the first ordinary send after resuming. If those fields are absent, the first turn seeds its clock and context.
  • The refusal's token count is an upper bound: it is the context the engine reports for the last response, which on a resume includes that response's output, and the mod has no separate output count to subtract.
  • The cold-write tally is per session and in memory; /clear empties it.
  • Context size is the engine's live window figure. A turn's own usage is its responses summed, which on a ten-step turn is ten reads of the context, so it is only the fallback where the host reports no live figure.

Local development

From a local checkout, load it for one session:

claude --plugin-dir .

To keep it installed, use the marketplace commands.

Prove it on your own session

The mock-clock tests prove the timer, the guard and the scoring, not that a fork hits the main cache. One ping proves that, and it costs a cache read plus the answer. In any warm session:

Reply with one word: ready /keepwarm 1h every 1m

Watch the row above the prompt after a minute, or the plain status text on other surfaces. last ping read 20k $0.01 reports the fork's readback. keepwarm stopped: the ping read ... and wrote ... means the readback did not meet the warm threshold; the loop has stopped. API busy, ping retry 1 of 3 means a 429, 529 or 5xx is being retried; the loop stops if the API is still busy after three retries or a minute before the cache goes cold. nothing to fork yet means the process has no conversation to fork, as after a restart; the window waits for your next message. Any other API error, an interruption or an empty reply stops the loop with a reason. Older engines that return null show a combined cold-snapshot or API-failure message. Finish with /keepwarm off. This short check establishes a cache read, not retention across the default 50-minute interval.

Tests and typecheck

Run from the repository root:

claude plugin validate .claude-plugin/plugin.json
claude plugin test .

The 137 tests use a mock clock and engine. They cover:

  • Guard: refuse once and resend, warn mode, slash commands, small contexts, prompts nobody typed, resume seeding and cold-write scoring.
  • Warming: command defaults and refused windows and periods, always/off, always after /clear, always rolling from the last turn, idle resets, usage-based stopping, cold-window expiry and delayed timers after sleep.
  • Session state: isolated store keys, restored windows, legacy cleanup, clear/compaction resets and subagent isolation.
  • Pricing and display: a row for every listed model, the Haiku 5.5 long-prompt tier priced per request, Opus 4 and 4.1 apart from Opus 4.5, output and uncached input costs, context counts, duration formatting and the read-only break-even figure.
  • Indicator: terminal images and desktop SVGs, theme selection, warm/cold transitions, resume, compaction, clear, expiry, surveys, other mods' content, text alternatives and NO_COLOR fallback.
  • Preferences: one read per session, immediate theme updates, denied changes and the effective theme returned by the settings writer.
  • Fork failures: a busy API is retried while the cache is warm, a retry that wakes too late is dropped, nothing to fork waits for the next turn, one ping is out at a time, a reply that lands after a request, /clear or compaction is discarded, and other failures and the null result returned by older releases stop without retrying.

The render tests check the element tree, not native terminal or desktop pixels. They do not validate server-side cache retention. See the live check.

Typecheck

Load this checkout with claude --plugin-dir . to generate declarations beside the plugin. Then, from the repository root:

npx -p typescript tsc -p .

Generated declarations live in .claude-plugin/types/. Keep them untracked.

Source 2 files
hooks/register.ts 684 lines
1import type { EngineInterface, Register } from 'claude-code'
2import { statusIcons } from './status-icons'
3
4const TTL_MS = 60 * 60 * 1000
5const PING_AFTER_MS = 50 * 60 * 1000
6const MIN_PING_MS = 60 * 1000
7const MAX_RETRIES = 3
8const AUTO_WARM_MS = 3 * 60 * 60 * 1000
9const DEFAULT_WINDOW_MS = 6 * 60 * 60 * 1000
10const BIG_TOKENS = 50000
11const PING_PROMPT = 'Reply with the single word: warm'
12const KEY_DEADLINE = 'deadline'
13const KEY_EVERY = 'every'
14const KEY_GUARD = 'guard'
15const KEY_ALWAYS = 'always'
16
17// Prompts no person sent, so nobody could send them again after a refusal. Every
18// other origin, including sdk, unclassified and the owner's own Slack or channel
19// messages, may carry a person's words and keeps the guard.
20const UNATTENDED = new Set(['task-notification', 'scheduled-trigger', 'peer', 'peer-send-message', 'projects-relay', 'coordinator', 'observer', 'observer-activity'])
21
22const LONG_PROMPT_TOKENS = 100000
23
24// $ per million tokens, [cache read, 1h cache write, output], list prices October 2026.
25// More specific families first: a model id takes the first row it matches.
26const PRICES: Array<[string | RegExp, number, number, number]> = [
27  ['fable-5-1', 0.25, 20, 50],
28  ['mythos-5-1', 0.25, 20, 50],
29  ['fable-5', 1, 20, 50],
30  ['mythos-5', 1, 20, 50],
31  ['opus-5-5', 0.2, 8, 20],
32  ['opus-5', 0.5, 10, 25],
33  // Opus 4 and 4.1 in every spelling (claude-opus-4-1-20250805, claude-opus-4@20250514, the -0 and -1 aliases), never Opus 4.5 and later.
34  [/opus-4(?:-[01])?(?:$|[^-\d]|-20\d{6})/, 1.5, 30, 75],
35  ['opus-4', 0.5, 10, 25],
36  ['sonnet-5-5', 0.1, 4, 10],
37  ['sonnet-5', 0.2, 4, 10],
38  ['sonnet', 0.3, 6, 15],
39  ['haiku-5-5', 0.01, 0.2, 0.5],
40  ['3-5-haiku', 0.08, 1.6, 4],
41  ['haiku-3-5', 0.08, 1.6, 4],
42  ['haiku', 0.1, 2, 5],
43]
44
45// A request whose prompt, cache reads and writes included, is over 100k tokens pays these instead.
46const LONG_PRICES: Array<[string, number, number, number]> = [
47  ['haiku-5-5', 0.05, 1, 2.5],
48]
49
50type PingRecord = { at: number; read: number; write: number; usd: number | null; warm: boolean }
51type Miss = { at: number; tokens: number; usd: number | null }
52type GuardMode = 'refuse' | 'warn'
53
54export type State = {
55  hasBand: boolean
56  sid: string
57  deadline: number
58  every: number
59  always: boolean
60  lastRequestAt: number
61  lastModel: string | null
62  ctx: number
63  compacted: boolean
64  guard: GuardMode
65  ackedAt: number
66  coldWritePending: boolean
67  misses: Miss[]
68  pending: { cancel: () => void } | null
69  last: PingRecord | null
70  stopped: string | null
71  waitForTurn: boolean
72  retries: number
73  retryReason: string
74  gen: number
75  tick: number
76  offs: number
77  turnOffs: number | null
78  inFlight: boolean
79  stepWrites: { usd: number; priced: boolean } | null
80}
81
82function priceOf(model: string | null, promptTokens: number): [number, number, number] | null {
83  const m = (model ?? '').toLowerCase().replace(/[\s.]+/g, '-')
84  if (promptTokens > LONG_PROMPT_TOKENS) {
85    for (const [family, read, write, output] of LONG_PRICES) if (m.includes(family)) return [read, write, output]
86  }
87  for (const [family, read, write, output] of PRICES) {
88    if (typeof family === 'string' ? m.includes(family) : family.test(m)) return [read, write, output]
89  }
90  return null
91}
92
93export function parseDuration(text: string): number | null {
94  const m = /^(?:(\d+)h)?(?:(\d+)m)?$/.exec(text.trim())
95  if (!m || (m[1] === undefined && m[2] === undefined)) return null
96  return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 * 1000
97}
98
99export function fmtDuration(ms: number): string {
100  const total = Math.max(0, Math.round(ms / 60000))
101  const h = Math.floor(total / 60)
102  const m = total % 60
103  if (h >= 48) return `${Math.floor(h / 24)}d ${h % 24}h`
104  return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`
105}
106
107function fmtUsd(usd: number | null): string {
108  return usd == null ? 'n/a' : '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
109}
110
111function fmtTok(n: number): string {
112  return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
113}
114
115function coldUsd(s: State): number | null {
116  const price = priceOf(s.lastModel, s.ctx)
117  return price ? s.ctx * price[1] / 1e6 : null
118}
119
120function warmUsd(s: State): number | null {
121  const price = priceOf(s.lastModel, s.ctx)
122  return price ? s.ctx * price[0] / 1e6 : null
123}
124
125/** Everything a fork bills: the cache read, any cache write, uncached input at the base rate (half the 1h write rate), and the output. */
126function pingUsd(u: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }, price: [number, number, number]): number {
127  return (u.cache_read_input_tokens * price[0] + u.cache_creation_input_tokens * price[1] + u.input_tokens * price[1] / 2 + u.output_tokens * price[2]) / 1e6
128}
129
130/** The read-only upper bound: pings at the cache-read rate that cost as much as one cold write of the same context. */
131function breakEvenPings(s: State): number | null {
132  const price = priceOf(s.lastModel, s.ctx)
133  if (!price || s.ctx <= 0) return null
134  return Math.floor(price[1] / price[0])
135}
136
137function isCold(s: State, now: number): boolean {
138  return s.lastRequestAt > 0 && !s.compacted && now - s.lastRequestAt >= TTL_MS
139}
140
141function guardText(s: State, now: number): string {
142  const price = priceOf(s.lastModel, s.ctx)
143  const rate = price ? `$${price[1]}/MTok` : 'the cache-write rate'
144  const warm = warmUsd(s)
145  return `the prompt cache went cold ${fmtDuration(now - s.lastRequestAt - TTL_MS)} ago. Sending this re-writes ` +
146    `up to ${s.ctx.toLocaleString('en-US')} tokens at ${rate} = ${fmtUsd(coldUsd(s))}` +
147    (warm == null ? '' : ` (a warm turn would have cost ${fmtUsd(warm)})`) + '.'
148}
149
150export type ResumeFields = {
151  source: string
152  model?: string
153  context_tokens?: number
154  seconds_since_last_response?: number
155  prompt_cache_likely_expired?: boolean
156  estimated_cache_write_usd?: number
157}
158
159/** /clear starts a new conversation in the same process; nothing priced before it still exists. */
160export function resetForClear(s: State) {
161  s.ctx = 0
162  s.lastRequestAt = 0
163  s.compacted = false
164  s.ackedAt = 0
165  s.coldWritePending = false
166  s.misses = []
167  s.waitForTurn = false
168  s.stepWrites = null
169  outdate(s)
170  disarm(s)
171}
172
173/** Applies a resumed session's fields to the state; returns the line to log, if any. */
174export function seedFromResume(s: State, e: ResumeFields, now: number): string | null {
175  if (e.source !== 'resume' && e.source !== 'fork') return null
176  if (typeof e.context_tokens === 'number' && e.context_tokens > 0) s.ctx = e.context_tokens
177  if (typeof e.seconds_since_last_response === 'number') s.lastRequestAt = now - e.seconds_since_last_response * 1000
178  if (typeof e.model === 'string') s.lastModel = e.model
179  s.compacted = false
180  if (e.prompt_cache_likely_expired !== true || s.ctx < BIG_TOKENS) return null
181  const usd = typeof e.estimated_cache_write_usd === 'number' ? fmtUsd(e.estimated_cache_write_usd) : fmtUsd(coldUsd(s))
182  return `resuming cold. The first message re-writes ${s.ctx.toLocaleString('en-US')} tokens, about ${usd}. /clear and paste a summary if you only need the conclusions.`
183}
184
185function statusText(s: State, now: number): string | undefined {
186  if (s.stopped) return `keepwarm stopped: ${s.stopped}`
187  if (!s.deadline) return undefined
188  const pingText = s.last ? ` · last ping read ${fmtTok(s.last.read)} ${fmtUsd(s.last.usd)}` : ''
189  const nextText = !s.lastRequestAt ? ' · waiting for the first turn'
190    : s.compacted ? ' · waiting for the first turn after compaction'
191    : isCold(s, now) ? ` · cold now, first ping ${fmtDuration(s.every)} after the next turn`
192    : s.waitForTurn ? ' · nothing to fork yet, first ping after the next turn'
193    : s.retries ? ` · API busy, ping retry ${s.retries} of ${MAX_RETRIES}`
194    : ` · ping in ${fmtDuration(s.lastRequestAt + s.every - now)}`
195  return `keepwarm ${fmtDuration(s.deadline - now)} left${nextText}${pingText}`
196}
197
198function updateStatus($: EngineInterface, s: State, now: number) {
199  if (s.hasBand) $.ui.invalidate('ui.render')
200  else $.ui.status(statusText(s, now))
201}
202
203/** Cancels the timer. Every new timer is set right after this, so the tick also says the schedule changed. */
204function disarm(s: State) {
205  if (s.pending) s.pending.cancel()
206  s.pending = null
207  s.tick++
208}
209
210/** Outdates any ping in flight: /clear, a resume, compaction or stop replaced the state it was sent for. Re-arming or extending the window does not, so a ping out at that moment still counts; a main request outdates it through the request clock. */
211function outdate(s: State) {
212  s.gen++
213}
214
215// The window and its ping period belong to the session that armed them, so a
216// second session, or one resumed from another transcript, never inherits them
217// and cannot turn them off. The always switch and the guard mode stay global.
218function deadlineKey(s: State): string {
219  return `${KEY_DEADLINE}:${s.sid}`
220}
221
222function everyKey(s: State): string {
223  return `${KEY_EVERY}:${s.sid}`
224}
225
226/** Clears this session's own dead window and the bare keys a store written before 2.1.1 still holds. Other sessions' keys are never touched: a read followed by a delete cannot be made atomic against their renewal. */
227async function prune($: EngineInterface, s: State, now: number) {
228  for (const key of [KEY_DEADLINE, deadlineKey(s)]) {
229    const deadline = await $.store.get(key)
230    if (deadline === undefined) continue
231    if (typeof deadline === 'number' && deadline > now) continue
232    await $.store.delete(key)
233    await $.store.delete(KEY_EVERY + key.slice(KEY_DEADLINE.length))
234  }
235}
236
237async function stop($: EngineInterface, s: State, why: string | null, forgetAlways = false) {
238  // Set before any await, so a window being opened at the same moment sees the off and does not write itself back.
239  if (forgetAlways) {
240    s.always = false
241    s.offs++
242  }
243  s.deadline = 0
244  s.every = PING_AFTER_MS
245  s.stopped = why
246  s.retries = 0
247  outdate(s)
248  disarm(s)
249  await $.store.delete(deadlineKey(s))
250  await $.store.delete(everyKey(s))
251  if (forgetAlways) await $.store.delete(KEY_ALWAYS)
252  updateStatus($, s, await $.clock.now())
253}
254
255async function arm($: EngineInterface, s: State) {
256  disarm(s)
257  s.retries = 0
258  if (!s.deadline) return
259  const now = await $.clock.now()
260  // A timer set while the clock was read, such as a retry, is replaced here, never left running beside this one;
261  // from this line to the assignment below nothing awaits.
262  disarm(s)
263  s.retries = 0
264  if (now >= s.deadline) return stop($, s, null)
265  // A cold window still needs expiry cleanup, but must not send a model request.
266  if (s.lastRequestAt && !s.compacted && !isCold(s, now)) {
267    const untilCold = s.lastRequestAt + TTL_MS - now
268    // Waiting for a turn sends nothing, but wakes when the cache goes cold so the row says so.
269    if (s.waitForTurn) s.pending = $.clock.after(Math.min(s.deadline - now, untilCold), () => { void arm($, s) })
270    else {
271      const delay = Math.min(s.deadline - now, untilCold, Math.max(1000, s.lastRequestAt + s.every - now))
272      s.pending = $.clock.after(delay, () => { void ping($, s) })
273    }
274  } else {
275    s.pending = $.clock.after(s.deadline - now, () => { void arm($, s) })
276  }
277  updateStatus($, s, now)
278}
279
280async function ping($: EngineInterface, s: State) {
281  s.pending = null
282  if (!s.deadline) return
283  const tick = s.tick
284  const now = await $.clock.now()
285  // A timer set or cancelled while the clock was read (an extension, a retry, a turn) owns the window now;
286  // if that left no timer, a window still open is armed again rather than left without one.
287  if (s.tick !== tick) return s.pending || s.inFlight ? undefined : arm($, s)
288  // One fork at a time: the one already out re-arms when it settles. Checked after the await, and set
289  // below with no await in between, so two timers can never both send.
290  if (s.inFlight) return
291  if (now >= s.deadline) return arm($, s)
292  if (isCold(s, now)) return arm($, s)
293  if (s.compacted || s.waitForTurn || !s.lastRequestAt) return arm($, s)
294  // A request since this timer was set moved the clock; schedule from it.
295  if (now - s.lastRequestAt < s.every - 1000) return arm($, s)
296  // A host that slept past a retry's slot must not send it inside the last minute.
297  if (s.retries && now >= s.lastRequestAt + TTL_MS - MIN_PING_MS) return stop($, s, s.retryReason + afterRetries(s.retries))
298  const gen = s.gen
299  const sentAt = s.lastRequestAt
300  s.inFlight = true
301  const sent = await $.model.fork({ prompt: PING_PROMPT }).then(reply => ({ reply }), (err: unknown) => ({ err }))
302  const at = await $.clock.now()
303  s.inFlight = false
304  // Checked after the last await. A stop, /clear, resume or compaction, or a main request that moved the
305  // cache clock, replaced what this fork was sent for: its reply is dropped and the window armed afresh.
306  if (gen !== s.gen || s.lastRequestAt !== sentAt) return s.pending ? undefined : arm($, s)
307  if ('err' in sent) return stop($, s, `the ping failed, ${sent.err instanceof Error ? sent.err.message : String(sent.err)}`)
308  const reply = sent.reply
309  if (reply === null) return stop($, s, 'the engine did not send the ping, either the snapshot was cold or the API call failed')
310  if (reply.isAnswered === false) {
311    // A restarted process has no conversation to fork until its first turn; the window waits for one instead of ending.
312    if (reply.reason === 'nothing-to-fork') {
313      s.waitForTurn = true
314      return arm($, s)
315    }
316    const reason = reply.reason === 'api-error' ? `the API call failed${reply.status === null ? '' : ` (${reply.status})`}`
317      : reply.reason === 'aborted' ? 'the ping was interrupted'
318      : 'the ping returned no text'
319    // A busy or failing API is usually brief; retry while a ping can still land a minute before the cache goes cold.
320    if (reply.reason === 'api-error' && (reply.status === null || reply.status === 429 || reply.status >= 500)) {
321      const wait = MIN_PING_MS * 2 ** s.retries
322      if (s.retries < MAX_RETRIES && at + wait < Math.min(s.deadline, s.lastRequestAt + TTL_MS - MIN_PING_MS)) {
323        s.retries++
324        s.retryReason = reason
325        // Replaces any timer set while the fork was out, such as a window extension's, so only the retry fires.
326        disarm(s)
327        s.pending = $.clock.after(wait, () => { void ping($, s) })
328        updateStatus($, s, at)
329        return
330      }
331    }
332    return stop($, s, reason + afterRetries(s.retries))
333  }
334  const u = reply.usage
335  const price = priceOf(s.lastModel, u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens)
336  // A warm ping reads the prefix and writes only its own message; a write of a tenth of the read or more means the prefix broke.
337  const warm = u.cache_read_input_tokens > 0 && u.cache_creation_input_tokens < 0.1 * u.cache_read_input_tokens
338  const usd = price ? pingUsd(u, price) : null
339  s.last = { at: now, read: u.cache_read_input_tokens, write: u.cache_creation_input_tokens, usd, warm }
340  if (!warm) return stop($, s, `the ping read ${fmtTok(u.cache_read_input_tokens)} and wrote ${fmtTok(u.cache_creation_input_tokens)} tokens (${fmtUsd(usd)}), the cache was already gone`)
341  s.lastRequestAt = now
342  await arm($, s)
343}
344
345function afterRetries(n: number): string {
346  return n ? ` after ${n} ${n === 1 ? 'retry' : 'retries'}` : ''
347}
348
349/** The reply to an arming command; on a cold cache it says when the first ping can come. */
350/** How long keepwarm holds the cache after a paid cold write: the rolling window when always is on. */
351function holdMs(s: State): number {
352  return s.always ? DEFAULT_WINDOW_MS : AUTO_WARM_MS
353}
354
355function armedText(s: State, now: number, windowMs: number): string {
356  if (isCold(s, now)) return `keepwarm on for ${fmtDuration(windowMs)}. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn`
357  return `keepwarm on for ${fmtDuration(windowMs)}, a ping ${fmtDuration(s.every)} after each idle stretch keeps the cache read, not re-written`
358}
359
360// A /keepwarm off after `offs` was read wins: the window it cancelled is not written back. A caller that awaited
361// before deciding to open a window passes the count it read first.
362async function startWindow($: EngineInterface, s: State, windowMs: number, every: number, offs = s.offs) {
363  if (s.offs !== offs) return
364  const now = await $.clock.now()
365  if (s.offs !== offs) return
366  const deadline = now + windowMs
367  s.every = every
368  s.deadline = deadline
369  s.stopped = null
370  if (every === PING_AFTER_MS) await $.store.delete(everyKey(s))
371  else await $.store.set(everyKey(s), every)
372  await $.store.set(deadlineKey(s), deadline)
373  if (s.offs !== offs) {
374    // Remove only what this call wrote; a window armed after the off is left alone.
375    if ((await $.store.get(deadlineKey(s))) === deadline) {
376      await $.store.delete(deadlineKey(s))
377      if (!s.deadline) await $.store.delete(everyKey(s))
378    }
379    return
380  }
381  await arm($, s)
382}
383
384function card(s: State, now: number): string {
385  const lines: string[] = []
386  lines.push(`${s.lastModel ?? 'model not seen yet'}`)
387  if (s.compacted) lines.push('state       reset by compaction, waiting for the first turn')
388  else if (!s.lastRequestAt) lines.push('state       no request yet this session')
389  else if (isCold(s, now)) lines.push(`state       COLD, last request ${fmtDuration(now - s.lastRequestAt)} ago`)
390  else lines.push(`state       warm, ${fmtDuration(s.lastRequestAt + TTL_MS - now)} left`)
391  lines.push(`context     ${s.ctx.toLocaleString('en-US')} tokens`)
392  lines.push(`cold cost   ${fmtUsd(coldUsd(s))} to re-write it (warm turn ${fmtUsd(warmUsd(s))})`)
393  const always = s.always ? ' (always)' : ''
394  const idle = s.always ? 'off until the next turn, which arms 6h00m (always)' : 'off (/keepwarm to arm it for 6h00m)'
395  lines.push(`keepwarm    ${s.deadline ? (statusText(s, now) ?? '').replace(/^keepwarm /, 'on, ') + always : s.stopped ? `stopped, ${s.stopped}${always}` : idle}`)
396  const pings = breakEvenPings(s)
397  if (pings != null) lines.push(`break-even  up to ${pings} pings at the read rate cost one cold write, about ${fmtDuration(pings * s.every)} of idle at one ping per ${fmtDuration(s.every)}`)
398  lines.push(`guard       ${s.guard === 'refuse' ? 'refuse once (/cache-tax guard warn to only show the price)' : 'warn only (/cache-tax guard refuse to be stopped once)'}`)
399  const paid = s.misses.reduce((a, m) => a + (m.usd ?? 0), 0)
400  lines.push(`session     ${s.misses.length} cold write${s.misses.length === 1 ? '' : 's'} paid, ${fmtUsd(paid)}`)
401  return lines.join('\n')
402}
403
404export function freshState(): State {
405  return {
406    hasBand: false, sid: '', deadline: 0, every: PING_AFTER_MS, always: false, lastRequestAt: 0, lastModel: null, ctx: 0, compacted: false,
407    guard: 'refuse', ackedAt: 0, coldWritePending: false, misses: [], pending: null, last: null, stopped: null,
408    waitForTurn: false, retries: 0, retryReason: '', gen: 0, tick: 0, offs: 0, turnOffs: null, inFlight: false, stepWrites: null,
409  }
410}
411
412export const register: Register = on => {
413  const s = freshState()
414  let light = false
415  let noColor = false
416
417  on('config.set', { key: 'theme' }, async ($, e, next) => {
418    const result = await next(e)
419    if (result.deny === undefined) {
420      light = typeof result.value === 'string' && result.value.startsWith('light')
421      if (s.hasBand) $.ui.invalidate('ui.render')
422    }
423    return result
424  })
425
426  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
427    const rest = await next(e)
428    if (!s.hasBand || e.props.hasSurvey) return rest
429    const now = await $.clock.now()
430    const text = statusText(s, now)
431    if (!text || (s.deadline && now >= s.deadline)) return rest
432    const state = s.stopped ? 'stopped' : !s.lastRequestAt || s.compacted ? 'unknown' : isCold(s, now) ? 'cold' : 'warm'
433    const color = noColor ? undefined : state === 'warm' ? 'success' : state === 'cold' ? (light ? '#c15f3c' : '#d97757') : undefined
434    const { Box, Text } = $.ui.resolve(e)
435    const asset = statusIcons[light ? 'light' : 'dark'][state === 'warm' || state === 'cold' ? state : 'neutral']
436    const icon = noColor ? Text({ bold: true, children: ['[>]'] })
437      : e.surface === 'terminal' ? $.ui.resolve(e).Image({ source: { png: asset.png }, columns: 3, rows: 1, alt: '[>]' })
438      : e.surface === 'desktop' ? $.ui.resolve(e).Svg({ source: asset.svg, width: 18, height: 18, alt: 'Cache Tax' })
439      : Text({ color, bold: true, children: ['[>]'] })
440    return Box({ flexDirection: 'column', children: [
441      rest,
442      Box({ flexDirection: 'row', alignItems: 'center', children: [
443        Box({ flexShrink: 0, children: [icon] }),
444        Text({ children: [
445          ' cache-tax · ',
446          Text({ color, bold: true, children: [state] }),
447          ` · ${text}`,
448        ] }),
449      ] }),
450    ] })
451  })
452
453  on('session.start', async ($, e, next) => {
454    const r = await next(e)
455    s.hasBand = e.surface === 'terminal' || e.surface === 'desktop'
456    if (s.hasBand) {
457      const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
458      light = typeof theme === 'string' && theme.startsWith('light')
459      noColor = Boolean(await $.env.get('NO_COLOR'))
460      $.ui.status(undefined)
461    }
462    s.sid = await $.session.id()
463    const now = await $.clock.now()
464    await prune($, s, now)
465    const saved = await $.store.get(deadlineKey(s))
466    const savedEvery = await $.store.get(everyKey(s))
467    const savedGuard = await $.store.get(KEY_GUARD)
468    s.deadline = typeof saved === 'number' && saved > now ? saved : 0
469    s.every = typeof savedEvery === 'number' && savedEvery >= MIN_PING_MS && savedEvery < TTL_MS ? savedEvery : PING_AFTER_MS
470    s.guard = savedGuard === 'warn' ? 'warn' : 'refuse'
471    s.always = (await $.store.get(KEY_ALWAYS)) === true
472    // Always means a fresh default window every session, whatever the last one left behind.
473    if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
474    const usage = await $.session.usage()
475    if (usage.context.tokens) s.ctx = usage.context.tokens
476    await $.command.register({
477      name: 'keepwarm',
478      description: 'Keep the prompt cache warm: bare for 6h, a window such as 90m, always, off, or status (cache-tax)',
479      argumentHint: '[6h | always | off | status]',
480      immediate: true,
481    })
482    await $.command.register({
483      name: 'cache-tax',
484      description: 'Prompt cache state, cold price and this session\'s cold writes; guard warn|refuse (cache-tax, the Mod)',
485      argumentHint: '[status | guard warn | guard refuse]',
486      immediate: true,
487    })
488    // The hook form of cache-tax ships a /cache-tax:status skill; both installed means two guards.
489    const commands = await $.command.list()
490    if (commands.some(c => c.name === 'cache-tax:status')) {
491      $.ui.log('the hook form (cache-tax@claude-code-hooks) is also installed, so a cold send is warned about or refused twice. Uninstall it, or /cache-tax guard warn here.')
492    }
493    updateStatus($, s, now)
494    return r
495  })
496
497  // Resume fields seed the guard before any turn of the resumed session has run.
498  on('classic.SessionStart', async ($, e, next) => {
499    const r = await next(e)
500    // A resumed or cleared conversation is not the one a ping in flight was sent for.
501    outdate(s)
502    if (e.source === 'clear') {
503      await stop($, s, null)
504      s.stopped = null
505      resetForClear(s)
506      s.sid = await $.session.id()
507      // No session.start fires after /clear, so this is where always arms the new conversation.
508      if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
509      // A window armed while this handler awaited lost its timer to resetForClear; arm it again.
510      else if (s.deadline) await arm($, s)
511      else updateStatus($, s, await $.clock.now())
512      return r
513    }
514    const line = seedFromResume(s, e, await $.clock.now())
515    // The resume payload may omit the model; without it the guard cannot price the cold write.
516    if (!s.lastModel) s.lastModel = await $.session.model()
517    if (line) $.ui.log(line)
518    // The seeded clock decides whether a restored or always window pings before the first turn: never when it is cold.
519    await arm($, s)
520    return r
521  })
522
523  on('command.run', { command: 'keepwarm' }, async ($, e) => {
524    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
525    const now = await $.clock.now()
526    if (words[0] === 'off') {
527      const wasAlways = s.always
528      await stop($, s, null, true)
529      return { text: wasAlways ? 'keepwarm is off, and always is off for every session from its next turn' : 'keepwarm is off' }
530    }
531    if (words[0] === 'always') {
532      s.always = true
533      await $.store.set(KEY_ALWAYS, true)
534      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
535      const cold = isCold(s, now) ? `. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn` : ''
536      return { text: `keepwarm always on: every session stays warm until ${fmtDuration(DEFAULT_WINDOW_MS)} after its last turn; /keepwarm off turns it off for good${cold}` }
537    }
538    if (!words.length) {
539      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
540      return { text: armedText(s, now, DEFAULT_WINDOW_MS) }
541    }
542    if (words[0] !== 'status') {
543      const window = parseDuration(words[0])
544      if (!window) return { text: 'keepwarm takes a window such as 6h or 90m, or always, off, or status' }
545      // "every 2m" is a testing knob and lasts only for the window it was given with.
546      let every = PING_AFTER_MS
547      if (words[1] === 'every') {
548        const period = parseDuration(words[2] ?? '')
549        // A period of the cache's whole hour or more would fire only once the cache is already cold.
550        if (period == null || period < MIN_PING_MS || period >= TTL_MS) return { text: 'every takes a period from 1m to 59m' }
551        every = period
552      }
553      await startWindow($, s, window, every)
554      const rolls = s.always && (window < DEFAULT_WINDOW_MS || every !== PING_AFTER_MS)
555      return { text: armedText(s, now, window) + (rolls ? `. Always is on, so your next turn makes it at least ${fmtDuration(DEFAULT_WINDOW_MS)} at one ping per ${fmtDuration(PING_AFTER_MS)}` : '') }
556    }
557    return { text: statusText(s, now) ?? 'keepwarm is off' }
558  })
559
560  on('command.run', { command: 'cache-tax' }, async ($, e) => {
561    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
562    const now = await $.clock.now()
563    if (words[0] === 'guard') {
564      if (words[1] !== 'warn' && words[1] !== 'refuse') return { text: '/cache-tax guard takes warn or refuse' }
565      s.guard = words[1]
566      await $.store.set(KEY_GUARD, s.guard)
567      return { text: s.guard === 'refuse' ? 'guard set to refuse once: a cold send is dropped with its price, the resend goes through' : 'guard set to warn: a cold send goes through with its price shown' }
568    }
569    return { text: card(s, now) }
570  })
571
572  // The message that pays. Only its first character is read.
573  on('prompt.submit', async ($, e, next) => {
574    if (e.origin.kind === 'plugin') return next(e)
575    if (typeof e.text !== 'string' || e.text.trimStart().startsWith('/')) return next(e)
576    const now = await $.clock.now()
577    if (!isCold(s, now) || s.ctx < BIG_TOKENS) return next(e)
578    // A refused prompt that no person sent would be lost: nobody is there to send it again.
579    const unattended = UNATTENDED.has(e.origin.kind)
580    if (s.guard === 'warn' || unattended) {
581      $.ui.log(`${guardText(s, now)} Sending anyway${unattended ? `, since this ${e.origin.kind} prompt cannot be sent again` : ''}; keepwarm will hold the cache for ${fmtDuration(holdMs(s))} once it lands.`)
582      s.coldWritePending = true
583      return next(e)
584    }
585    if (s.ackedAt === s.lastRequestAt) {
586      s.ackedAt = 0
587      s.coldWritePending = true
588      return next(e)
589    }
590    s.ackedAt = s.lastRequestAt
591    return { drop: `cache-tax: ${guardText(s, now)} Send it again to pay it, and keepwarm will then hold the cache for ${fmtDuration(holdMs(s))}. Or /clear and start from a note.` }
592  })
593
594  on('turn.step', async function* ($, e, next) {
595    if (!e.agentId) {
596      // The off count when the turn's first request went out: an off after it wins over what the turn opens.
597      if (e.index === 0) s.turnOffs = s.offs
598      s.lastRequestAt = await $.clock.now()
599    }
600    const r = yield* next(e)
601    // Each request's write is priced at its own prompt length's tier (Haiku 5.5 changes price at 100k), not the turn's sum.
602    const u = r.usage
603    if (!e.agentId && u) {
604      const price = priceOf(u.model || e.model, u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens)
605      const was = s.stepWrites ?? { usd: 0, priced: true }
606      s.stepWrites = { usd: was.usd + (price ? u.cache_creation_input_tokens * price[1] / 1e6 : 0), priced: was.priced && price != null }
607      // A request that came back after a compaction or a restart cached the new prefix: the window pings from it now,
608      // not from the turn's end, so a long turn that stops at a permission prompt or a question stays warm.
609      if (s.compacted || s.waitForTurn) {
610        s.compacted = false
611        s.waitForTurn = false
612        await arm($, s)
613      }
614    }
615    return r
616  })
617
618  on('turn.complete', async ($, e, next) => {
619    const r = await next(e)
620    if (e.agentId) return r
621    // An off at any point in this turn wins over every window the turn would open; a host that raised no
622    // step falls back to the count at completion.
623    const offs = s.turnOffs ?? s.offs
624    s.turnOffs = null
625    const now = await $.clock.now()
626    // A sleeping host may deliver this turn before the expired window's timer.
627    if (s.deadline && now >= s.deadline) await stop($, s, null)
628    // turn.step stamps the exact request time; when no step of this turn did, the turn's end is the floor.
629    // A turn aborted before it sent anything touched no cache, so it leaves the clock alone.
630    if ((e.usage || !e.isAborted) && now - s.lastRequestAt > e.durationMs) s.lastRequestAt = now
631    s.compacted = false
632    s.waitForTurn = false
633    s.ackedAt = 0
634    // Always covers any break up to six hours after a turn that reached the cache (a turn that failed before any
635    // response touched nothing, and its cache may be cold): a window of at least six hours at the default period,
636    // which brings back a stopped window and replaces a shorter one or a testing period. Another session's
637    // /keepwarm off or /keepwarm always changes the shared switch, so every such turn reads it.
638    if (e.usage) {
639      const shared = (await $.store.get(KEY_ALWAYS)) === true
640      if (s.offs === offs) s.always = shared
641      if (s.always) await startWindow($, s, Math.max(DEFAULT_WINDOW_MS, s.deadline - now), PING_AFTER_MS, offs)
642    }
643    const u = e.usage
644    if (u) {
645      if (u.model) s.lastModel = u.model
646      const prev = s.ctx
647      // A turn's usage is its responses summed, so a ten-step turn reports ten
648      // reads of the context. The live window is the engine's figure; the sum
649      // is only the fallback for a host that reports no tokens.
650      const write = u.cache_creation_input_tokens
651      const live = (await $.session.usage()).context.tokens
652      s.ctx = live && live > 0 ? live : u.input_tokens + u.cache_read_input_tokens + write
653      const full = prev > 20000 && write >= 0.5 * prev
654      if (full || s.coldWritePending) {
655        // Priced per request when the steps reported usage; otherwise at the tier of the context the comeback re-sent.
656        const price = priceOf(s.lastModel, prev || s.ctx)
657        const usd = s.stepWrites?.priced ? s.stepWrites.usd : price ? write * price[1] / 1e6 : null
658        s.misses.push({ at: now, tokens: write, usd })
659        const extend = s.deadline < now + AUTO_WARM_MS && s.offs === offs
660        if (extend) await startWindow($, s, AUTO_WARM_MS, s.every, offs)
661        $.ui.log(`cold write of ${fmtTok(write)} tokens paid (${fmtUsd(usd)}).` +
662          (extend ? ` Keeping the cache warm for ${fmtDuration(AUTO_WARM_MS)} so it is not paid again today; /keepwarm off to stop.` : ''))
663      }
664    }
665    s.coldWritePending = false
666    s.stepWrites = null
667    await arm($, s)
668    return r
669  })
670
671  on('session.compact', async ($, e, next) => {
672    const r = await next(e)
673    if (!e.agentId) {
674      s.compacted = true
675      s.ctx = 0
676      s.ackedAt = 0
677      outdate(s)
678      disarm(s)
679      await arm($, s)
680    }
681    return r
682  })
683}
684
hooks/status-icons.ts 32 lines
1// Generated by tools/generate-status-icons.py from the official SVG assets.
2export const statusIcons = {
3  "dark": {
4    "warm": {
5      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#78b85a\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
6      "png": "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"
7    },
8    "cold": {
9      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#d97757\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
10      "png": "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"
11    },
12    "neutral": {
13      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#92928b\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
14      "png": "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"
15    }
16  },
17  "light": {
18    "warm": {
19      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#397527\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
20      "png": "iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAYAAADlNHIOAAAABmJLR0QA/wD/AP+gvaeTAAAMiUlEQVR4nO2ce3AV1R3Hv2fvMzchkpAHj0iLj4gJIIliFaNWCFYZp60a0KFOiFaRTo0GqAgxVq1IEgoJ1BmLIkopxUKBDjYwrYmIbcUOxKQWWhFR1GDAvHPv3b17zubur3+QhZC7IQ/ug0Q+M/vP7jlnf489j/2dB3CRi/QXIeRrNS7vEFxxC664NS7vEEK+NtJyDXl8Pt9YocqvCC77BZep26VrXN6qquoVkZZzyEHkHsG5UiK4rAouk7u9mcrLV1F62tWUnnY1lZevInd7s+EIIVT5FfJ6kyMt96CHiFwa9z4luNwmuEyqz0M7tm+hjIwMSkgYcdY1ceIEen39OvIpbsMRXs6VEqLm2EjrMeggIkkIby7ncr3RxFRV/Y1uuSUrwPDdrxtv/B7t2L7lTNOkyo1CVZ4gImuk9TKDRVqA7miqN5sYKwMwEQBqamrx4vJifPDBv/pVzo033oCnC5ciMzOj8w77hIBn7PaobYwxCrLYA+aCcYAQyg3Q9VIwdgsAHDt2DMuLV2D37t0gCrQXswDDr44CALR97AP5A8tkjGHmzJkoXLoY48aNM27vZ5K02GaLei9UuvSHiDuAc0+aBKmYgB8CQEtLC9aufQWvrlsPIURgBgbEfMeBxCkuWGMsAIAOWUfLRwrcR1SY+ApWqxX33z8bixYtRHJSklHMWzr0pQ7HsP+FTrveiZgDFEUZY5XoeTDkAbAoioLX3/gdXnrpJXg8XtM8rtE2JEyJgSPeYvpctPnRXKvA+wU3z+9y4aEH5+Kxxx5DbOwwAPCDsKFDZ8+6XK6vg6JYPwm7A4gopkPIPyewIgAxmqZhy5Y/YeXKVWhobDTN40ywYsR10XCNsvXpHWqDhsZqBeo3munz+Pg4zJ8/H/Me+SnsdjsA+AjsN3Y7L2Esrm1gmg2MsDmAiOyapuQBeAGEJCLCrl27UFzyaxw7dsw0jy3GgvgMF2KvcAzonXKdQON+GZrbpIMAkJKSgsfzH8OcOfdDkiQAaGGgFVZ79BrGmDqgl/aTkDuAiJgQvhwGKgFwGQBUV1fjhWXFOHDggGkeySEhbkIU4iZEgUnn+X6d4P6Uo7lWgd+nm6a55ppJePrpQmTdNNW4VQeGZTabaz1jzNx7QSKkDugcUpYCyASAI0c+xcpV5aioqDAXxsoQl+ZE3CQXJFtwRaMOQuv/VLT+R4GumY9Cb7k5C888U4T09DTj1n8J7HmHw/WnoArThZA4QFXVGRbJv4kISQBw4sQJlK9egzff3AK/P/CDYhJD7JUOxGe4YI06z0++F3Suo/WQD62HfCCTCiFJEmbOnImip5dg7Nixp+RjaPDrlgecTmdlsOUJugNUVb1dgv+vYGBtbe14+eWX8dr6N6Cq5k1q9KV2JF4fDVus+cgmVGheP1pqFbiPmo+Y7HY7Zs/OweLFTyJhxAiAQDosdzidzreDKUfQHaCp3q+JsdFl5atRVlYOXTev7lEjbUi4LhrOxMhGCNTGDjRVy/CdNB8xSRLDwoULsHBBARhRvc0ZMyaY7w+6AwRXdIDYpWPHmTY3jrhTQ8rolL4NKcOFfFxDc7UM3toR8MxisaDuq2MAGNkdrqC2kSFocIkBMDX+YOWMLhT0Dzbs9Z+3dqC+sn3QNEGhJmLa+05qqKtoOxVeuD4ajrjwiqK5/Wj6sOewRbgI7ZivCw88MAcWS+BIR6nXUPdWOxr2edHRw49SMNG5juYPZXz551ZT40uShLvuuivkchiEoBOWCQBGjxl71v36r7/Cp58exa9XlvX+IzbRBcke2R8xM/kBwO6IDqpgYXWAQXV1NZa9WIL9+/eblmFxShieHr5QxOTJ16CwcGnXUMQQdgBDAzr/kCsrq/Dc8y+cMxgXNykKl6Q6BySp9wuOpg+VcwbjnvzFIuTk3APGGAC0AIg/l/zBdkDY+gADm811OQMtAZhnxoxs7H23CitKS5CUmBiQVvP60bDPi7qKNign+j5KURs01O1qx4l3PabGj4+PR2HhUvzzH3sxa9a9YIwpBFZqs4vLz0u5ARD2GmB8QZGckMnPz8ewYTGAyYRMX+UPFhFzgMFApyQTrouGbdipCtzblKTNZsN9983q05Tkt84Bp/NdIJPy31oHGIRuWQqIwHba7VE/YYwpoZK/v4S9E+4NmzOmymZ3ZYJhPgEnMjMzsH3bVmzcuAHjx1/Va/7x46/Cxo0bMH/+o/jpw/OQkTkFGzZshK7rjIF+rAnloKZ6fxAGVSKDsSKt+4o1435/yjqfpYkTJqSf9fz227Pp37UHuizm9W4mr3dkKOXvCxdcE2QGkXuEENYnGegJAE5VVXHZ5alnpfn8syNwOp0gAhgDMq+9HidPnjwrjdVqxSMPP4RFixbC5XIBQBsYlthsrnWMMT1U8p+LC64JMoOx2GaHw7XEr0tXAYDT6QxIc+YevQ0ApSXLjbU/p+no6MBv176K79+Wjap39gDAcBDWakL5h6q6e2/fQsCgcIBBVFTUV72lIVjzAbTOmJGNv/99L3Jy7glIc/z4ceTm5mHu3AdRX18PAFMlZvlAVdXUgMQhZlA5oC84nc4jOlluALAvKTERv1mzGhs3bkBKSkpA2sqqdzBt+gyjNsRZmL843PIOOQcAp5xgs7tuBsN8AG3Z06dh77tV+Nn8ebBaz553cLs9WLx4CQCAgOxwyzokHTCYuCA3LZwvqqqmakJ5A8BUAKh6Zw8KC4tw/PjxgLSxscOwYkUJAIABVQQEdhohZMjVAFVVUyXm/xeAqQ2NjXj8iQLk5uaZGn9G9nTseacS2dOnAUCrnyxLwy3vkKsBFuYvJiCusrIK+Y8/AbfbE5AmJSUFy5cvMwwPAPt08j/kdEYfEVwOq7xDzgFGR/rUksIA4/flRyzcDDkH9ERmZgZWlBYjLc1YeEtv2jQsZDExJ8+ZMcSEvQ9QVfWyUJbPgCoAKC0tRnJyMpKTk7H8xWV4a+efDeN/zojusDti5nQ3fqhlMyNsNUBVVTidTkjMf1io8hu2Dvoli4n5Jtjv8ZNlqcT8t83Inh5XW3PW/gONwFbZ7VG/Yoz5uj44E2vyFxiyhouw1YCbsm7Fpk2b4ff7bWCYp9nYZ6HYSG38CTNgB8A8APMwYAdBz3Q4XEu7Gt+ItmrC8hkDPaXruqOiogK3fj/s/2PBQ3C53SwcbFxZWVNp9+6/nA7vci7XCyE/2teN1MEIFxORVQj50a4bwXfv/gtlZU01lXnChHSj/Pbzs04gQa8Bp9vgkuUYPvySgOeHD3+C3Nw83JszGzU1tWDAqFMRSd9BzpW7gy1PdzhX7taE7yAIaxkwqqamFvfmzEZubh4OH/4kIP3w4ZegtGQ5gDO6XdCoqpoquNwiuEwN39TTc88WUUrKmB6PFrjn7h/Rfz6q6XrqyT5N82X1VP5Aa4Cm+bIEl/cZ6Q5/fJDy8uZSYmKCqVyjR4+igoJ8OlFfZ5TdEolo6YBQVTVVCKXKUPbLL45SQUE+JScnmSo7cmQyFRTkU13dsdOG1Li8k3NPWvey++sAzj1pGpd3Gs9Pnqij554totGjR5nKkpSUSHl5c+mzox+f+SiEUjVojN8VTfVmCy5/aCjy30MfUV7e3B5rw9ixl9JzzxZRU+NJQ3m/xuWtPp/vu0aZfXWAoihjOs8W0gSXqa21kcrKVtK4cd85Z22srek6bSkf4lyZFRHjBQsiYpwrswSXjxqK7Xv/PZo5884eDZGaegWVla0kr6fVMIRyasTUOrw3BxBRTOc8skdwmWRvG72+fh2lXT2+x/dlZ0+jPXsquxr+KyHkeUQU3o1roYSIHEJVFgguNxmK7qrYSVOmXNejYSZPvoZeX7+OVJ/HMExTbw4w0nDVSzu2b+m1/D9s2khc9Z7JqyoLiGhgO8MHA2e+UMXd1y90+vTbun+h53IA7Xv/PbrzzjvOUcOu7F7DZKOGRdo+YaOzjX5NcLljIG20mQMOHfx3n/qY5qZvDMN3CFV+TVGUoO587A8X3HE1jU1NKCsrx+bNf4SmBa6IliQJOTn3YOvWbWfdnz07B9u27YCuBwY1bTYb5sy5HwsXLkBiQgKAi8fVBNDftaF94eKBTQPg23Zk2QVJf2M1wYw1XaQLRBStcbnICPD5FDdt+v0GmjRpYoDhJ02aSJt+v6HrsZXtGpeLiCg60noMeog8iUJ41wguc9F5cOvq1WWUnp5G6elptHp1WdeDW7kQ3jVEnsA9Txc5P1RVvUxw72bBZb3ruL/z0gX3bo7ErNa3js7Du7d3Obx7+8XDuy8yIP4Ps2m8hDMsk+cAAAAASUVORK5CYII="
21    },
22    "cold": {
23      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#c15f3c\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
24      "png": "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"
25    },
26    "neutral": {
27      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#686861\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
28      "png": "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"
29    }
30  }
31} as const
32