SLOPSHOPPER

cache-hugger

Keeps the main conversation's one-hour prompt cache warm and prices it when it goes cold. A cache-reading fork 50 idle minutes after the last request, while…

bandcommandtoaststatuspromptmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-hugger
› 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 › /hug-cache ⎿ cache-hugger: Hugging the cache to keep it warm for 6h00m: a ping 50m into each idle stretch reads it instead of re-writing ⟨Claude Code's own drawing⟩ ▣ [>] warm · 6h00m left (manual) · ping in 50m ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ▣ [>] warm · 6h00m left (manual) · ping in 50m
README

claude-mods

Bushi's home-made Claude Code mods, as a directory marketplace named bushi-local.

Mods

  • cache-hugger: keeps the main conversation's one-hour prompt cache warm and prices it when it goes cold. Since 3.0.0 it does everything cache-tax 2.2.1 did, so uninstall cache-tax (it warns at session start while both are installed). Details below; it also adds a sub-agent wait rule to your CLAUDE.md, described after that.

cache-hugger: what it does

Pings. A ping is a tool-less fork of the conversation that reads the cache and so renews it, sent 50 idle minutes after the last request, then every 50 minutes. Pings run while either reason holds:

  • Background work: a turn ended with a shell, subagent, monitor or workflow still in flight. Pings run until a turn leaves no work, for at most 12 hours after the last turn.
  • A /hug-cache window: /hug-cache (6 hours), /hug-cache 90m, /hug-cache always (a 6-hour window at every session start), /hug-cache off. A paid cold write opens a 3-hour window by itself.

Both reasons share one timer, so the cache is never pinged twice. A ping that finds the cache already gone, or fails, stops the pings until the next turn.

Guard. A message sent after the cache went cold (over an hour idle, context of 50k tokens or more) is refused once with its re-write price and how much of the 5-hour limit is used; sending it again pays it. /hug-cache guard warn only shows the price.

Prices. List prices from the pricing page on 17 October 2026, per model (Opus 5.5 at $8 per million for a 1-hour write and $0.20 for a read, Haiku 5.5 by prompt length). A turn counts as a cold write only when it wrote more than its context grew by, so a long turn that adds a lot of context is not mistaken for one.

Where it shows. Nothing goes to the transcript except the cold-write line. A row above the prompt (the status line where there is no such row) shows warm or cold, the time left and why (manual for /hug-cache, auto bg for background work, auto for always or after a cold write), the next ping as a countdown redrawn every minute, and the last ping's read and price. Each ping shows in a toast, and pings stopping early in a longer toast. /hug-cache status shows the model, cache state, context, cold price, break-even, background work and this session's cold writes.

Windows are kept per session in the mod's own store. Windows armed under cache-tax are not carried over.

cache-hugger: the sub-agent wait rule in CLAUDE.md

The mod's pings only reach the main conversation: a mod can fork the main thread ($.model.fork), never a sub-agent's. A sub-agent's prompt cache lasts 5 minutes, so a sub-agent that waits longer than that on a background job re-writes its whole cache when it wakes. The workable fix is for sub-agents to wake themselves: each short wake-up reads the cache and so refreshes it.

So at every session start the mod keeps this section in ~/.claude/CLAUDE.md ($CLAUDE_CONFIG_DIR/CLAUDE.md when that is set), between <!-- KEEPWARM_BG_WAIT_RULE_START --> (the markers keep their old name so existing blocks update in place) and <!-- KEEPWARM_BG_WAIT_RULE_END -->:

Waiting in sub-agents (4-minute slices). A sub-agent that waits on a background job (a review trial, a check run, a test suite, a build, a server) waits in slices of at most 4 minutes: a Bash or Monitor wait with a timeout of at most 240 s, then a short status check, then the next slice; never one long blocking wait. A session that launches sub-agents puts the rule in every prompt for one that may wait on a background job. Waits in the main conversation are exempt: its cache lasts an hour.

  • When: the first session that starts after the mod is installed or updated. Sessions already running don't re-read CLAUDE.md; start a new one.
  • Updates: a newer mod version rewrites the marked block in place. A section with the same heading added by hand, without the markers, is taken over.
  • Opting out: put <!-- KEEPWARM_BG_WAIT_RULE: off --> anywhere in CLAUDE.md; the mod then never adds or changes the rule.
  • Uninstalling: Claude Code has no uninstall hook, so the block stays; delete it by hand, from the start marker to the end marker.

Why 4-minute wake-ups: the numbers

Measured on one real session on 17 October 2026: an orchestrator session (Opus 5.5, high effort) delivering one slice of a backend feature over 7.5 hours, with 19 background sub-agents, mostly Opus 5.5, that implemented and reviewed tasks and often waited 5–10 minutes on review trials and check runs. Costs below are list API prices; on a subscription the same tokens count against usage limits instead.

Cache prices ($ per million tokens, Anthropic pricing, 17 October 2026). A 5-minute write is 1.25× the input price, a 1-hour write 2×, and a read 0.1× (0.05× on Opus 5.5 and Sonnet 5.5, 0.025× on Fable 5.1). A read restarts the cache's timer at no extra cost; there are no other cache lengths.

Model5-min write1-hour writeReadA 5-min write costs
Opus 5.5$5$8$0.2025 reads
Sonnet 5.5$2.50$4$0.1025 reads
Fable 5.1$12.50$20$0.2550 reads
Haiku 5.5, prompt ≤ 100k$0.125$0.20$0.0112.5 reads
Haiku 5.5, prompt > 100k$0.625$1$0.0512.5 reads

On Opus 5.5 one avoided re-write pays for about 24 refreshes, so wake-ups win for any wait between 5 minutes and roughly an hour and a half.

What happened. The main conversation never went cold: its longest idle stretch was 59 minutes, inside its 1-hour cache. The sub-agents re-wrote their whole 5-minute cache 16 times, about 7.4 M tokens, half of everything they wrote. One sub-agent waited about 6 minutes three times and re-wrote 608k, 626k and 649k tokens.

Simulation 1: the three worst sub-agents with a refresh every 4 minutes (Opus 5.5, cache tokens only; a refresh reads the whole cached context).

Sub-agentAs runWith 4-min refreshesRefreshes costSaved
A (3 re-writes)$23.08$14.60$0.53$8.48
B (2 re-writes)$20.27$14.29$0.43$5.98
C (1 re-write)$19.65$16.52$0.28$3.13
Total$63.00$45.41$1.24$17.58 (28 %)

It took 11 refreshes. Across all 19 sub-agents: $174.87 → $141.12, saving $33.75 (19 %). The same token flow priced on other models: Sonnet 5.5 saves $8.79 of $31.50, Fable 5.1 $46.49 of $115.45.

Simulation 2: the same three sub-agents on other models (cache and output tokens; compaction leaves an assumed 12k-token summary).

ScenarioCostWith 4-min refreshes
Opus 5.5, as run$66.40$46.21
Sonnet 5.5, no compaction change$33.20$23.11
Haiku 5.5, compacting at ~187k (autoCompactWindow 200000)$3.77 (4–5 compactions each)$3.40
Haiku 5.5, compacting at ~90k$1.00 (16–18 compactions each)$0.96

Refreshes save about 30 % on Opus and Sonnet and only pennies on Haiku, whose compaction keeps contexts small. Haiku's price rises 5× above 100k-token prompts, so compacting below 100k is 3–4× cheaper than at 200k, at the price of many more lossy summaries.

Why not the 1-hour cache instead? The 1-hour cache costs 60 % more on every write, and the choice turns on how much a conversation writes compared with how long it sits idle:

  • Sub-agents write a lot and wait briefly. Across all 19, counting cache writes plus the reads that replace avoided re-writes: about $74 as run, about $61 on the 1-hour cache (no re-writes, but every write at $8), and about $42 with 4-minute wake-ups (refreshes included).
  • The main conversation writes little and waits long. It wrote 0.30 M new tokens in 7.5 hours (the 1-hour premium cost about $0.90) but sat idle 447 minutes. On the 1-hour cache, as run: $8.83. On the 5-minute cache: $32.75 with no refreshes (19 cold re-writes of 250–330k tokens), $13.81 with a refresh every 4 minutes (105 refreshes).

Claude Code's split, the 1-hour cache for the main conversation and the 5-minute cache for sub-agents, fits this kind of work; the rule covers the one gap, sub-agent waits over 5 minutes.

Method and limits. The simulations replay each request's cache read and write counts from the session transcripts. A gap of more than 5 minutes between two of a sub-agent's requests counts as a re-write of the whole context; with refreshes, a refresh every 4 minutes of a gap reads the whole context. The model-swap rows assume every model would have used the same tokens and turns as Opus did, which won't hold exactly. A 4-minute wake-up costs a little more than a bare refresh, since it also sends a status check and its reply. One session's numbers, not a general benchmark.

Install on a machine

Clone into ~/.claude/local-marketplace, then in Claude Code:

/plugin marketplace add ~/.claude/local-marketplace
/plugin install cache-hugger@bushi-local

The marketplace is read in place, so after a git pull run /reload-plugins or start a new session.

Test

From a plugin's folder: claude plugin test .

Source 2 files
hooks/register.ts 711 lines
1import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
2import { statusIcons } from './status-icons'
3
4// Keeps the main conversation's one-hour prompt cache warm and prices it when
5// it goes cold. Pings are cache-reading forks of the conversation (`$.model.fork`)
6// sent 50 idle minutes after the last request, so the next request reads the
7// cache instead of re-writing it. They run while either reason holds:
8//
9// - background work: a turn ended with a shell, subagent, monitor or workflow
10//   still in flight, which will wake the session later. Pings then run until a
11//   turn leaves no work, for at most 12 hours after the last turn.
12// - a /hug-cache window: a time the user picked (6 hours by default, `always` for
13//   every session), or 3 hours after a cold write was paid.
14//
15// A message sent on a cold cache is refused once with its re-write price (or
16// only warned about, with /hug-cache guard warn). Nothing goes to the
17// transcript: the state shows in a row above the prompt (the status line where
18// there is no such row), each ping in a toast, and pings stopping early in a
19// longer toast.
20//
21// A mod can't ping a subagent's cache (`$.model.fork` forks the main thread
22// only), and a subagent's cache lasts five minutes. So at session start the mod
23// also keeps a rule in the user's CLAUDE.md that tells subagents to wait in
24// slices of at most four minutes; each wake-up then refreshes their cache.
25//
26// The window, guard, pricing and indicator are ported from cache-tax 2.2.1 by
27// Karan Bansal (MIT); see NOTICE.
28
29const TTL_MS = 60 * 60 * 1000
30export const PING_AFTER_MS = 50 * 60 * 1000
31/** Pings for background work stop this long after the last turn ended, in case a job hangs. */
32export const CAP_MS = 12 * 60 * 60 * 1000
33const MIN_PING_MS = 60 * 1000
34/** How often the countdown is redrawn while pings are scheduled. */
35const TICK_MS = 60 * 1000
36const AUTO_WARM_MS = 3 * 60 * 60 * 1000
37const DEFAULT_WINDOW_MS = 6 * 60 * 60 * 1000
38const BIG_TOKENS = 50000
39export const PING_PROMPT = 'Reply with the single word: warm'
40const KEY_DEADLINE = 'deadline'
41const KEY_EVERY = 'every'
42const KEY_GUARD = 'guard'
43const KEY_ALWAYS = 'always'
44
45export const RULE_START = '<!-- KEEPWARM_BG_WAIT_RULE_START -->'
46export const RULE_END = '<!-- KEEPWARM_BG_WAIT_RULE_END -->'
47/** Anywhere in CLAUDE.md, keeps the mod from adding or updating the rule. */
48export const RULE_OFF = '<!-- KEEPWARM_BG_WAIT_RULE: off -->'
49const RULE_HEADING = '## Waiting in sub-agents (4-minute slices)'
50export const WAIT_RULE = `${RULE_START}
51${RULE_HEADING}
52
53A sub-agent's prompt cache lasts 5 minutes; one idle tool call longer than that makes it rewrite its whole cache when it wakes (measured 17 Oct 2026: 600–700k-token caches rewritten 16 times in one session; see the cache-hugger README). So:
54
55- **If you are a sub-agent** and wait on a background job (a review trial, a check run, a test suite, a build, a server), wait in slices of at most 4 minutes: a Bash or Monitor wait with a timeout of at most 240 s, then a short status check, then the next slice. Never make one long blocking wait.
56- **If you launch sub-agents** (Agent tool, workflows), put this rule in every prompt for one that may wait on a background job.
57- Waits in the main conversation are exempt: its cache lasts an hour.
58${RULE_END}`
59
60/**
61 * CLAUDE.md's text with the wait rule in it, or null when it needs no change:
62 * the marked block is replaced when it differs, an unmarked section under the
63 * rule's heading (added by hand) is taken over, and otherwise the rule is
64 * appended. RULE_OFF anywhere leaves the file alone.
65 */
66export function withWaitRule(text: string): string | null {
67  if (text.includes(RULE_OFF)) return null
68  const start = text.indexOf(RULE_START)
69  const end = text.indexOf(RULE_END, start)
70  if (start >= 0 && end > start) {
71    const next = text.slice(0, start) + WAIT_RULE + text.slice(end + RULE_END.length)
72    return next === text ? null : next
73  }
74  const heading = text.search(/^## Waiting in sub-agents \(4-minute slices\)[ \t]*$/m)
75  if (heading >= 0) {
76    const rest = text.slice(heading + RULE_HEADING.length)
77    const after = rest.search(/^(## |<!-- )/m)
78    const tail = after < 0 ? '\n' : '\n\n' + rest.slice(after)
79    return text.slice(0, heading) + WAIT_RULE + tail
80  }
81  const body = text.trimEnd()
82  return (body ? body + '\n\n' : '') + WAIT_RULE + '\n'
83}
84
85async function ensureWaitRule($: EngineInterface) {
86  try {
87    const config = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
88    const path = `${config}/CLAUDE.md`
89    const text = (await $.fs.exists(path)) ? await $.fs.read(path) : ''
90    const next = withWaitRule(text)
91    if (next !== null) await $.fs.write(path, next)
92  } catch (err) {
93    $.ui.log(`cache-hugger: could not update CLAUDE.md with the sub-agent wait rule: ${err instanceof Error ? err.message : String(err)}`, { to: 'debug' })
94  }
95}
96
97/** $ per million tokens: cache read, 1-hour cache write, output. Uncached input is half the 1-hour write. */
98export type Price = [number, number, number]
99
100// List prices from https://platform.claude.com/docs/en/about-claude/pricing, 17 October 2026.
101// A model id matches the first row whose family it contains, so longer names come first.
102// The second price applies to prompts over 100,000 tokens (Haiku 5.5 only).
103const PRICES: Array<[string, Price, Price?]> = [
104  ['fable-5-1', [0.25, 20, 50]],
105  ['mythos-5-1', [0.25, 20, 50]],
106  ['fable-5', [1, 20, 50]],
107  ['mythos-5', [1, 20, 50]],
108  ['opus-5-5', [0.2, 8, 20]],
109  ['opus-5', [0.5, 10, 25]],
110  ['opus-4-1', [1.5, 30, 75]],
111  ['opus-4-5', [0.5, 10, 25]],
112  ['opus-4-6', [0.5, 10, 25]],
113  ['opus-4-7', [0.5, 10, 25]],
114  ['opus-4-8', [0.5, 10, 25]],
115  ['opus-4', [1.5, 30, 75]],
116  ['sonnet-5-5', [0.1, 4, 10]],
117  ['sonnet-5', [0.2, 4, 10]],
118  ['sonnet', [0.3, 6, 15]],
119  ['haiku-5-5', [0.01, 0.2, 0.5], [0.05, 1, 2.5]],
120  ['haiku-3-5', [0.08, 1.6, 4]],
121  ['haiku', [0.1, 2, 5]],
122]
123const TIER_TOKENS = 100000
124
125/** The model's prices for a request whose prompt (cache read + write + input) is `prompt` tokens; null for a model not in the table. */
126export function priceOf(model: string | null, prompt = 0): Price | null {
127  const m = (model ?? '').toLowerCase().replace(/[\s.]+/g, '-')
128  for (const [family, base, above] of PRICES) if (m.includes(family)) return above && prompt > TIER_TOKENS ? above : base
129  return null
130}
131
132type PingRecord = { at: number; read: number; write: number; usd: number | null; warm: boolean }
133type Miss = { at: number; tokens: number; usd: number | null }
134type GuardMode = 'refuse' | 'warn'
135type Summary = { id: string; type: string; status: string }
136type Usage = { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }
137
138export type State = {
139  hasBand: boolean
140  sid: string
141  /** When the /hug-cache window ends; 0 when none is open. */
142  deadline: number
143  /** When pings for background work stop (12 h after the turn that left it); 0 when the last turn left none. */
144  bgUntil: number
145  every: number
146  /** The window was opened by the mod (always, or after a cold write), not by /hug-cache. */
147  auto: boolean
148  always: boolean
149  lastRequestAt: number
150  lastModel: string | null
151  ctx: number
152  compacted: boolean
153  guard: GuardMode
154  ackedAt: number
155  coldWritePending: boolean
156  misses: Miss[]
157  pending: { cancel: () => void } | null
158  /** Redraws the countdown while pings are scheduled; nothing else changes it between turns. */
159  tick: { cancel: () => void } | null
160  last: PingRecord | null
161  stopped: string | null
162}
163
164export function parseDuration(text: string): number | null {
165  const m = /^(?:(\d+)h)?(?:(\d+)m)?$/.exec(text.trim())
166  if (!m || (m[1] === undefined && m[2] === undefined)) return null
167  return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 * 1000
168}
169
170export function fmtDuration(ms: number): string {
171  const total = Math.max(0, Math.round(ms / 60000))
172  const h = Math.floor(total / 60)
173  const m = total % 60
174  if (h >= 48) return `${Math.floor(h / 24)}d ${h % 24}h`
175  return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`
176}
177
178function fmtUsd(usd: number | null): string {
179  return usd == null ? 'n/a' : '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
180}
181
182export function fmtTokens(n: number): string {
183  return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
184}
185
186/** `HH:MM` on the host's clock. */
187export function clockTime(ms: number): string {
188  const d = new Date(ms)
189  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
190}
191
192function coldUsd(s: State): number | null {
193  const price = priceOf(s.lastModel, s.ctx)
194  return price ? s.ctx * price[1] / 1e6 : null
195}
196
197function warmUsd(s: State): number | null {
198  const price = priceOf(s.lastModel, s.ctx)
199  return price ? s.ctx * price[0] / 1e6 : null
200}
201
202/** Everything a fork bills: the cache read, any cache write, uncached input at the base rate (half the 1h write rate), and the output. */
203function pingUsd(u: Usage, price: Price): number {
204  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
205}
206
207/** 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. */
208export function isWarm(u: Pick<Usage, 'cache_read_input_tokens' | 'cache_creation_input_tokens'>): boolean {
209  return u.cache_read_input_tokens > 0 && u.cache_creation_input_tokens < 0.1 * u.cache_read_input_tokens
210}
211
212/** The read-only upper bound: pings at the cache-read rate that cost as much as one cold write of the same context. */
213function breakEvenPings(s: State): number | null {
214  const price = priceOf(s.lastModel, s.ctx)
215  if (!price || s.ctx <= 0) return null
216  return Math.floor(price[1] / price[0])
217}
218
219function isCold(s: State, now: number): boolean {
220  return s.lastRequestAt > 0 && !s.compacted && now - s.lastRequestAt >= TTL_MS
221}
222
223/** When pings stop: the later of the window's end and background work's cap; 0 when neither holds. */
224function endOf(s: State): number {
225  return Math.max(s.deadline, s.bgUntil)
226}
227
228/** The 5-hour window's use, as the last API response reported it; null off a subscription. */
229async function fiveHour($: EngineInterface): Promise<SessionRateLimit | null> {
230  try {
231    return (await $.session.usage()).rateLimits.find(r => r.kind === 'five_hour') ?? null
232  } catch {
233    return null
234  }
235}
236
237/** The cold-cache notice: a heading, then the price and the 5-hour limit as aligned key-value rows. */
238export function guardText(s: State, now: number, limit: SessionRateLimit | null): string {
239  const price = priceOf(s.lastModel, s.ctx)
240  const rate = price ? `$${price[1]}/MTok` : 'write rate'
241  const warm = warmUsd(s)
242  const reset = limit?.resetsAt ? Date.parse(limit.resetsAt) : NaN
243  return [
244    `Cache cold for ${fmtDuration(now - s.lastRequestAt - TTL_MS)}`,
245    `  re-write   ${fmtUsd(coldUsd(s))}  (${fmtTokens(s.ctx)} tok × ${rate}${warm == null ? '' : `; warm turn ${fmtUsd(warm)}`})`,
246    `  5h limit   ${limit ? `${limit.percentUsed}% used${Number.isNaN(reset) ? '' : `, resets ${clockTime(reset)}`}` : 'n/a'}`,
247  ].join('\n')
248}
249
250export type ResumeFields = {
251  source: string
252  model?: string
253  context_tokens?: number
254  seconds_since_last_response?: number
255  prompt_cache_likely_expired?: boolean
256  estimated_cache_write_usd?: number
257}
258
259/** /clear starts a new conversation in the same process; nothing priced before it still exists. */
260export function resetForClear(s: State) {
261  s.ctx = 0
262  s.lastRequestAt = 0
263  s.compacted = false
264  s.ackedAt = 0
265  s.coldWritePending = false
266  s.misses = []
267  s.bgUntil = 0
268  disarm(s)
269}
270
271/** Applies a resumed session's fields to the state; returns the line to log, if any. */
272export function seedFromResume(s: State, e: ResumeFields, now: number): string | null {
273  if (e.source !== 'resume' && e.source !== 'fork') return null
274  if (typeof e.context_tokens === 'number' && e.context_tokens > 0) s.ctx = e.context_tokens
275  if (typeof e.seconds_since_last_response === 'number') s.lastRequestAt = now - e.seconds_since_last_response * 1000
276  if (typeof e.model === 'string') s.lastModel = e.model
277  s.compacted = false
278  if (e.prompt_cache_likely_expired !== true || s.ctx < BIG_TOKENS) return null
279  const usd = typeof e.estimated_cache_write_usd === 'number' ? fmtUsd(e.estimated_cache_write_usd) : fmtUsd(coldUsd(s))
280  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.`
281}
282
283function statusText(s: State, now: number): string | undefined {
284  if (s.stopped) return `pings stopped: ${s.stopped}`
285  const end = endOf(s)
286  if (!end) return undefined
287  const why = s.bgUntil > s.deadline ? 'auto bg' : s.auto ? 'auto' : 'manual'
288  const pingText = s.last ? ` · last ping read ${fmtTokens(s.last.read)} ${fmtUsd(s.last.usd)}` : ''
289  const nextText = !s.lastRequestAt ? ' · waiting for the first turn'
290    : s.compacted ? ' · waiting for the first turn after compaction'
291    : isCold(s, now) ? ` · cold now, first ping ${fmtDuration(s.every)} after the next turn`
292    : ` · ping in ${fmtDuration(s.lastRequestAt + s.every - now)}`
293  return `${fmtDuration(end - now)} left (${why})${nextText}${pingText}`
294}
295
296function updateStatus($: EngineInterface, s: State, now: number) {
297  if (s.hasBand) $.ui.invalidate('ui.render')
298  else $.ui.status(statusText(s, now))
299  syncTick($, s)
300}
301
302/** Keeps a one-minute redraw running exactly while pings are scheduled, so "left" and "ping in" count down. */
303function syncTick($: EngineInterface, s: State) {
304  if (endOf(s) && !s.tick) {
305    s.tick = $.clock.every(TICK_MS, () => {
306      void (async () => {
307        const now = await $.clock.now()
308        if (s.hasBand) $.ui.invalidate('ui.render')
309        else $.ui.status(statusText(s, now))
310      })()
311    })
312  } else if (!endOf(s) && s.tick) {
313    s.tick.cancel()
314    s.tick = null
315  }
316}
317
318function disarm(s: State) {
319  if (s.pending) s.pending.cancel()
320  s.pending = null
321}
322
323// The window and its ping period belong to the session that armed them, so a
324// second session, or one resumed from another transcript, never inherits them
325// and cannot turn them off. The always switch and the guard mode stay global.
326function deadlineKey(s: State): string {
327  return `${KEY_DEADLINE}:${s.sid}`
328}
329
330function everyKey(s: State): string {
331  return `${KEY_EVERY}:${s.sid}`
332}
333
334/** Clears this session's own dead window and bare keys from an old store. Other sessions' keys are never touched: a read followed by a delete cannot be made atomic against their renewal. */
335async function prune($: EngineInterface, s: State, now: number) {
336  for (const key of [KEY_DEADLINE, deadlineKey(s)]) {
337    const deadline = await $.store.get(key)
338    if (deadline === undefined) continue
339    if (typeof deadline === 'number' && deadline > now) continue
340    await $.store.delete(key)
341    await $.store.delete(KEY_EVERY + key.slice(KEY_DEADLINE.length))
342  }
343}
344
345/** Pings have stopped early: the whole reason in a long toast; the row keeps the short form. */
346function notifyStopped($: EngineInterface, now: number, why: string) {
347  $.ui.toast(`Pings stopped at ${clockTime(now)}: ${why}. A request after an hour idle will re-write the cache.`, { timeoutMs: 15000 })
348}
349
350async function stop($: EngineInterface, s: State, why: string | null, forgetAlways = false) {
351  s.deadline = 0
352  s.bgUntil = 0
353  s.every = PING_AFTER_MS
354  s.stopped = why
355  disarm(s)
356  await $.store.delete(deadlineKey(s))
357  await $.store.delete(everyKey(s))
358  if (forgetAlways) {
359    s.always = false
360    await $.store.delete(KEY_ALWAYS)
361  }
362  const now = await $.clock.now()
363  updateStatus($, s, now)
364  if (why) notifyStopped($, now, why)
365}
366
367async function arm($: EngineInterface, s: State) {
368  disarm(s)
369  const end = endOf(s)
370  if (!end) return
371  const now = await $.clock.now()
372  if (now >= end) {
373    // A window ends quietly; background work may still be running when its cap ends.
374    const capped = s.bgUntil > 0 && s.bgUntil >= s.deadline
375    return stop($, s, capped ? '12 h cap after the last turn, background work may still be running' : null)
376  }
377  // A cold cache still needs expiry cleanup, but must not send a model request.
378  if (s.lastRequestAt && !s.compacted && !isCold(s, now)) {
379    const untilCold = s.lastRequestAt + TTL_MS - now
380    const delay = Math.min(end - now, untilCold, Math.max(1000, s.lastRequestAt + s.every - now))
381    s.pending = $.clock.after(delay, () => { void ping($, s) })
382  } else {
383    s.pending = $.clock.after(end - now, () => { void arm($, s) })
384  }
385  updateStatus($, s, now)
386}
387
388async function ping($: EngineInterface, s: State) {
389  s.pending = null
390  const end = endOf(s)
391  if (!end) return
392  const now = await $.clock.now()
393  if (now >= end) return arm($, s)
394  // A turn in the meantime re-armed the timer; this callback is stale.
395  if (isCold(s, now)) return arm($, s)
396  if (now - s.lastRequestAt < s.every - 1000) return
397  const sent = s.lastRequestAt
398  let reply
399  try {
400    reply = await $.model.fork({ prompt: PING_PROMPT })
401  } catch (err) {
402    return stop($, s, `the ping failed, ${err instanceof Error ? err.message : String(err)}`)
403  }
404  // A turn while the ping was out stamped a newer request and re-armed; it decides what comes next.
405  if (s.lastRequestAt !== sent) return
406  // Older Claude Code releases returned null instead of a failure object.
407  if (reply === null) return stop($, s, 'the engine did not send the ping, either the snapshot was cold or the API call failed')
408  if (reply.isAnswered === false) {
409    const reason = reply.reason === 'nothing-to-fork' ? 'no conversation to warm yet'
410      : reply.reason === 'api-error' ? `the API call failed${reply.status === null ? '' : ` (${reply.status})`}`
411      : reply.reason === 'aborted' ? 'the ping was interrupted'
412      : 'the ping returned no text'
413    return stop($, s, reason)
414  }
415  const u = reply.usage
416  const price = priceOf(s.lastModel, u.cache_read_input_tokens + u.cache_creation_input_tokens + u.input_tokens)
417  const warm = isWarm(u)
418  const usd = price ? pingUsd(u, price) : null
419  s.last = { at: now, read: u.cache_read_input_tokens, write: u.cache_creation_input_tokens, usd, warm }
420  if (!warm) return stop($, s, `the ping read ${fmtTokens(u.cache_read_input_tokens)} and wrote ${fmtTokens(u.cache_creation_input_tokens)} tokens (${fmtUsd(usd)}), the cache was already gone`)
421  s.lastRequestAt = now
422  $.ui.toast(`Cache refreshed: the ping read ${fmtTokens(u.cache_read_input_tokens)} tokens (${fmtUsd(usd)}).`)
423  await arm($, s)
424}
425
426/** The reply to an arming command; on a cold cache it says when the first ping can come. */
427function armedText(s: State, now: number, windowMs: number): string {
428  if (isCold(s, now)) return `Hugging the cache to keep it warm for ${fmtDuration(windowMs)}. It is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn`
429  return `Hugging the cache to keep it warm for ${fmtDuration(windowMs)}: a ping ${fmtDuration(s.every)} into each idle stretch reads it instead of re-writing it`
430}
431
432async function startWindow($: EngineInterface, s: State, windowMs: number, every: number, auto = false) {
433  const now = await $.clock.now()
434  s.every = every
435  s.auto = auto
436  if (every === PING_AFTER_MS) await $.store.delete(everyKey(s))
437  else await $.store.set(everyKey(s), every)
438  s.deadline = now + windowMs
439  s.stopped = null
440  await $.store.set(deadlineKey(s), s.deadline)
441  await arm($, s)
442}
443
444function card(s: State, now: number): string {
445  const lines: string[] = []
446  lines.push(`${s.lastModel ?? 'model not seen yet'}`)
447  if (s.compacted) lines.push('state       reset by compaction, waiting for the first turn')
448  else if (!s.lastRequestAt) lines.push('state       no request yet this session')
449  else if (isCold(s, now)) lines.push(`state       COLD, last request ${fmtDuration(now - s.lastRequestAt)} ago`)
450  else lines.push(`state       warm, ${fmtDuration(s.lastRequestAt + TTL_MS - now)} left`)
451  lines.push(`context     ${s.ctx.toLocaleString('en-US')} tokens`)
452  lines.push(`cold cost   ${fmtUsd(coldUsd(s))} to re-write it (warm turn ${fmtUsd(warmUsd(s))})`)
453  const always = s.always ? ' (always)' : ''
454  const idle = s.always ? 'off until the next session start, which arms 6h00m (always)' : 'off (/hug-cache to arm it for 6h00m)'
455  lines.push(`hugging     ${endOf(s) ? 'on, ' + (statusText(s, now) ?? '') + always : s.stopped ? `stopped, ${s.stopped}${always}` : idle}`)
456  lines.push(`background  ${s.bgUntil ? `work in flight, pings until ${clockTime(s.bgUntil)} at the latest` : 'no work in flight at the last turn'}`)
457  const pings = breakEvenPings(s)
458  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)}`)
459  lines.push(`guard       ${s.guard === 'refuse' ? 'refuse once (/hug-cache guard warn to only show the price)' : 'warn only (/hug-cache guard refuse to be stopped once)'}`)
460  const paid = s.misses.reduce((a, m) => a + (m.usd ?? 0), 0)
461  lines.push(`session     ${s.misses.length} cold write${s.misses.length === 1 ? '' : 's'} paid, ${fmtUsd(paid)}`)
462  return lines.join('\n')
463}
464
465export function freshState(): State {
466  return {
467    hasBand: false, sid: '', deadline: 0, bgUntil: 0, every: PING_AFTER_MS, auto: false, always: false, lastRequestAt: 0, lastModel: null, ctx: 0, compacted: false,
468    guard: 'refuse', ackedAt: 0, coldWritePending: false, misses: [], pending: null, tick: null, last: null, stopped: null,
469  }
470}
471
472export const register: Register = on => {
473  const s = freshState()
474  let light = false
475  let noColor = false
476
477  on('config.set', { key: 'theme' }, async ($, e, next) => {
478    const result = await next(e)
479    if (result.deny === undefined) {
480      light = typeof result.value === 'string' && result.value.startsWith('light')
481      if (s.hasBand) $.ui.invalidate('ui.render')
482    }
483    return result
484  })
485
486  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
487    const rest = await next(e)
488    if (!s.hasBand || e.props.hasSurvey) return rest
489    const now = await $.clock.now()
490    const text = statusText(s, now)
491    if (!text || (endOf(s) && now >= endOf(s))) return rest
492    const state = s.stopped ? 'stopped' : !s.lastRequestAt || s.compacted ? 'unknown' : isCold(s, now) ? 'cold' : 'warm'
493    const color = noColor ? undefined : state === 'warm' ? 'success' : state === 'cold' ? (light ? '#c15f3c' : '#d97757') : undefined
494    const { Box, Text } = $.ui.resolve(e)
495    const asset = statusIcons[light ? 'light' : 'dark'][state === 'warm' || state === 'cold' ? state : 'neutral']
496    const icon = noColor ? Text({ bold: true, children: ['[>]'] })
497      : e.surface === 'terminal' ? $.ui.resolve(e).Image({ source: { png: asset.png }, columns: 3, rows: 1, alt: '[>]' })
498      : e.surface === 'desktop' ? $.ui.resolve(e).Svg({ source: asset.svg, width: 18, height: 18, alt: 'cache-hugger' })
499      : Text({ color, bold: true, children: ['[>]'] })
500    return Box({ flexDirection: 'column', children: [
501      rest,
502      Box({ flexDirection: 'row', alignItems: 'center', children: [
503        Box({ flexShrink: 0, children: [icon] }),
504        Text({ children: [
505          ' ',
506          Text({ color, bold: true, children: [state] }),
507          ` · ${text}`,
508        ] }),
509      ] }),
510    ] })
511  })
512
513  on('session.start', async ($, e, next) => {
514    const r = await next(e)
515    s.hasBand = e.surface === 'terminal' || e.surface === 'desktop'
516    if (s.hasBand) {
517      const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
518      light = typeof theme === 'string' && theme.startsWith('light')
519      noColor = Boolean(await $.env.get('NO_COLOR'))
520      $.ui.status(undefined)
521    }
522    await ensureWaitRule($)
523    s.sid = await $.session.id()
524    const now = await $.clock.now()
525    await prune($, s, now)
526    const saved = await $.store.get(deadlineKey(s))
527    const savedEvery = await $.store.get(everyKey(s))
528    const savedGuard = await $.store.get(KEY_GUARD)
529    s.deadline = typeof saved === 'number' && saved > now ? saved : 0
530    s.every = typeof savedEvery === 'number' && savedEvery >= MIN_PING_MS ? savedEvery : PING_AFTER_MS
531    s.guard = savedGuard === 'warn' ? 'warn' : 'refuse'
532    s.always = (await $.store.get(KEY_ALWAYS)) === true
533    // Always means a fresh default window every session, whatever the last one left behind.
534    if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS, true)
535    const usage = await $.session.usage()
536    if (usage.context.tokens) s.ctx = usage.context.tokens
537    await $.command.register({
538      name: 'hug-cache',
539      description: 'Hug the prompt cache to keep it warm: bare for 6h, a window such as 90m, always, off, status (cache state and cold price), or guard warn|refuse (cache-hugger)',
540      argumentHint: '[6h | always | off | status | guard warn | guard refuse]',
541      immediate: true,
542    })
543    // cache-tax, which this mod replaces, would ping and guard a second time.
544    const commands = await $.command.list()
545    if (commands.some(c => c.name === 'cache-tax' || c.name === 'cache-tax:status')) {
546      $.ui.log('cache-tax is also installed, so the cache is pinged and a cold send is guarded twice. cache-hugger does all of its work; uninstall cache-tax.')
547    }
548    updateStatus($, s, now)
549    return r
550  })
551
552  // Resume fields seed the guard before any turn of the resumed session has run.
553  on('classic.SessionStart', async ($, e, next) => {
554    const r = await next(e)
555    if (e.source === 'clear') {
556      await stop($, s, null)
557      s.stopped = null
558      resetForClear(s)
559      s.sid = await $.session.id()
560      updateStatus($, s, await $.clock.now())
561      return r
562    }
563    const line = seedFromResume(s, e, await $.clock.now())
564    // The resume payload may omit the model; without it the guard cannot price the cold write.
565    if (!s.lastModel) s.lastModel = await $.session.model()
566    if (line) $.ui.log(line)
567    // The seeded clock decides whether a restored or always window pings before the first turn: never when it is cold.
568    await arm($, s)
569    return r
570  })
571
572  on('command.run', { command: 'hug-cache' }, async ($, e) => {
573    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
574    const now = await $.clock.now()
575    if (words[0] === 'off') {
576      const wasAlways = s.always
577      await stop($, s, null, true)
578      return { text: wasAlways ? 'Stopped hugging the cache, and no longer starting at session start' : 'Stopped hugging the cache' }
579    }
580    if (words[0] === 'status') return { text: card(s, now) }
581    if (words[0] === 'guard') {
582      if (words[1] !== 'warn' && words[1] !== 'refuse') return { text: '/hug-cache guard takes warn or refuse' }
583      s.guard = words[1]
584      await $.store.set(KEY_GUARD, s.guard)
585      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' }
586    }
587    if (words[0] === 'always') {
588      s.always = true
589      await $.store.set(KEY_ALWAYS, true)
590      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
591      const cold = isCold(s, now) ? `. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn` : ''
592      return { text: `Hugging the cache to keep it warm, always: every session starts with a ${fmtDuration(DEFAULT_WINDOW_MS)} hug; /hug-cache off turns it off for good${cold}` }
593    }
594    if (!words.length) {
595      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
596      return { text: armedText(s, now, DEFAULT_WINDOW_MS) }
597    }
598    const window = parseDuration(words[0])
599    if (window == null) return { text: '/hug-cache takes a window such as 6h or 90m, or always, off, status, or guard warn|refuse' }
600    // "every 2m" is a testing knob and lasts only for the window it was given with.
601    let every = PING_AFTER_MS
602    if (words[1] === 'every') {
603      const period = parseDuration(words[2] ?? '')
604      if (period == null || period < MIN_PING_MS) return { text: 'every takes a period of at least 1m' }
605      every = period
606    }
607    await startWindow($, s, window, every)
608    return { text: armedText(s, now, window) }
609  })
610
611  // The message that pays. Only its first character is read.
612  on('prompt.submit', async ($, e, next) => {
613    if (e.origin.kind === 'plugin') return next(e)
614    if (typeof e.text !== 'string' || e.text.trimStart().startsWith('/')) return next(e)
615    const now = await $.clock.now()
616    if (!isCold(s, now) || s.ctx < BIG_TOKENS) return next(e)
617    const limit = await fiveHour($)
618    if (s.guard === 'warn') {
619      $.ui.log(`${guardText(s, now, limit)}\nSending anyway; the cache is then hugged for ${fmtDuration(AUTO_WARM_MS)}.`)
620      s.coldWritePending = true
621      return next(e)
622    }
623    if (s.ackedAt === s.lastRequestAt) {
624      s.ackedAt = 0
625      s.coldWritePending = true
626      return next(e)
627    }
628    s.ackedAt = s.lastRequestAt
629    return { drop: `${guardText(s, now, limit)}\nSend again to pay it (then hugged ${fmtDuration(AUTO_WARM_MS)}), or /clear and start from a note.` }
630  })
631
632  on('turn.step', async function* ($, e, next) {
633    if (!e.agentId) s.lastRequestAt = await $.clock.now()
634    yield* next(e)
635  })
636
637  // Stop fires when the main conversation's turn ends; background_tasks lists the
638  // work still in flight that will wake it later. turn.complete acts on what
639  // Stop saw, once the turn is over.
640  let seen: readonly Summary[] | undefined
641  on('classic.Stop', async ($, e, next) => {
642    const r = await next(e)
643    seen = e.background_tasks ?? []
644    return r
645  })
646
647  on('turn.complete', async ($, e, next) => {
648    const r = await next(e)
649    if (e.agentId) return r
650    const now = await $.clock.now()
651    // A sleeping host may deliver this turn before the expired window's timer.
652    if (s.deadline && now >= s.deadline) {
653      s.deadline = 0
654      s.every = PING_AFTER_MS
655      await $.store.delete(deadlineKey(s))
656      await $.store.delete(everyKey(s))
657    }
658    // turn.step stamps the exact request time; when no step of this turn did, the turn's end is the floor.
659    if (now - s.lastRequestAt > e.durationMs) s.lastRequestAt = now
660    s.compacted = false
661    s.ackedAt = 0
662    const u = e.usage
663    if (u) {
664      if (u.model) s.lastModel = u.model
665      const prev = s.ctx
666      // A turn's usage is its responses summed, so a ten-step turn reports ten
667      // reads of the context. The live window is the engine's figure; the sum
668      // is only the fallback for a host that reports no tokens.
669      const write = u.cache_creation_input_tokens
670      const live = (await $.session.usage()).context.tokens
671      s.ctx = live && live > 0 ? live : u.input_tokens + u.cache_read_input_tokens + write
672      // Each warm step writes only what it added, so a long turn's summed write
673      // can match the context's growth without anything going cold. Only the
674      // write beyond that growth re-wrote the prefix the turn started from.
675      const rewrote = Math.max(0, write - Math.max(0, s.ctx - prev))
676      const full = prev > 20000 && rewrote >= 0.5 * prev
677      if (full || s.coldWritePending) {
678        const price = priceOf(s.lastModel, s.ctx)
679        const usd = price ? rewrote * price[1] / 1e6 : null
680        s.misses.push({ at: now, tokens: rewrote, usd })
681        if (s.deadline < now + AUTO_WARM_MS) {
682          await startWindow($, s, AUTO_WARM_MS, s.every, true)
683          $.ui.log(`cold write of ${fmtTokens(rewrote)} tokens paid (${fmtUsd(usd)}). Keeping the cache warm for ${fmtDuration(AUTO_WARM_MS)} so it is not paid again today; /hug-cache off to stop.`)
684        }
685      }
686    }
687    s.coldWritePending = false
688    if (seen !== undefined) {
689      s.bgUntil = seen.length ? now + CAP_MS : 0
690      if (seen.length) s.stopped = null
691      seen = undefined
692    }
693    // arm draws nothing when no reason is left, so the row is cleared here.
694    if (!endOf(s)) updateStatus($, s, now)
695    await arm($, s)
696    return r
697  })
698
699  on('session.compact', async ($, e, next) => {
700    const r = await next(e)
701    if (!e.agentId) {
702      s.compacted = true
703      s.ctx = 0
704      s.ackedAt = 0
705      disarm(s)
706      await arm($, s)
707    }
708    return r
709  })
710}
711
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": "iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAYAAADlNHIOAAAABmJLR0QA/wD/AP+gvaeTAAAMwklEQVR4nO1ce3RUxR3+ZjePFZKQ3Zm7G5KIJg1BsbaFBkSKoDFBqohiTSPWA/FRxHCowQePEF8VQkRIBTynkacgRwEh1pZDFUIERVDURKhCQCEISzT7Ssj7sbu//pFcXbI35ME+2Oh3zv1jZ+bO/X6/787Mzm9mLvAL/ArmbwK9gRDi94zRAiKkAABjKCJiiywWyxf+5tZTBJQAWq12kFrNFgDsEQCqDtkEsG2MsWyz2fytP/j1Bh2NuCwRHh7OJYnnqdWqEwCbrtFoVDMzZ6C09HOUln6OmZkzoNFoGEBpRM6jQuhe0+v1Bn/zDnhER0f345zPFYJXC8FJr5coI2Manfz2GLU0119wfXf6W8rKmkUGg56E4CQEr5MknqfT6SL8bUcgQsU5nyoEr2h3Jt0z+S76svQzN8d3vL7+6jBlZEyTRSAhhFmSdI8DCPK3UQEBznkK5/yI7MDx41No3949XTq+47Vv7x4aPz7FRQheJkm6NATYuOcz6HS6UULo9skOGzEiid4p3ErNTXWKDm6uryZzcSGZiwupub5auUxTHb1TuJVGjEhyFeJTIcQ4f9srw+9vg06nG6pSscUAJrX/xowZj2L6Xx9GSEiI+w1EqD/6OWy7tsFeZQYAqCO00N48CeHDbwJU7v8r7HY7Nm/eimXL8lFpMsnJ/3Y6ab7NZjvqJdO6Bb8JwDmPYYy9AFAGAHW/fv3w0IPTMGvWLISHhyne03jqKGzvb0XL92cU84OlgdAm343+141QzG9oaMC69Rvw6quvoqamFgAcAHudiJ6zWq3nPGJYD+FzASRJCnM6nTMZQw6AsODgYKSnp+Gpp56EXpIU72k+V46qXW+jsbysW8/QDEqANvVeaK5KVMy32apQUFCAVavXoqWlBQAaGcOK1lZHXnV1dXXvLOsdfClAiBAiA6AXAegZY7jjjjswf97TiIuLU7zBXm1FVfE7qDt8ECDq8QOvGPI78AnpCObKUwKj0YgVK1/Fm29uhtPpBAAbEZaEh4cvP336dFOPH9gL+EIAJkm6e4mQB7B4AEhKSsIzOfMxYoRyV+Gor0PNx//F+U+KQPbWS3u4Wo2wYWMQmXw3gsIGKJY5fPgIFi3Kxf6PD8hJZwG20GKxrAXguCQCXfHzZuWc8xTG8BKA4QCQmDgYTz05GxMnTlQs72xpRs2hYpzf9x84mz37AqpCQhExMhkDxt0JVahGscyHH+3Hiy8uxNdf/zguf80YvWA22972KBkXeEUAvV6fSuTYRAQ9AAwcOBCzsx7HlCnpUKvVbuXJ4UBd6X5UF/8L9rrz3qD0I9T9wxDxhz9iwKgUsKBgt3yn04mdO3di4aI8nDnTNtgzBhNj6gdMJtNuT/PxuAB6vX680+l4DwCLjByAzMxMPPLwg9BolN+6huOHYXtvM1qtlZ6mclEERXJok+9B2G9HAczdDS0tLdi6dRuWLHkZFqsVAEilUk8wmUy7PMnD4wIIwc8BiH5ydhaeeDILjCnH+5rKy2DbvQ3NxlOeptAjhMbGQ5d6LzRx1yjmEzmRv+wVLPvHKwBQYbFYYzz5fG8I4ATAjGfLoVK5dzctpnOo2vU2Gk4c8fSjLwn9En8D7fg0hOjd/et0OhB7ZRwAkMVi9WgE2RvhaAZA0fmBChdbPP7C+jxCGKKPgeGBrIDpgrwNb3RBBAAV55TDBR3ReOoobDvfQovJt5GAYB4Fbcpk9B+apDgId0R0zCAAgMVi9ajPfLYitmnTm3A43Oc0V8QPRXTm8xCTpnU6UfIk1P3DoB2fhpiZL7TFjDo43+l0YseOHV7nIcNnLSA6ZhAGD07A00890fVE7MMdcDY1epRXTydiSvwBz7cAnwogIykpCTkL5mHkyJGKdTga6lCz33ehiC+/PIzc3MWuoYg+LYAJaJshp6am4PnnnrloMK76wx2o/eLDngfjGEP/oUnQptxz0WDcy0uXYdu2QlBb/TYAuovxD3gBGFOFt4ejFwAI7144+jSqdm3tWTh6fBo0gwYr5ttsNhQUvOYajm5gDCtbWx15QUHqqovxD3gBZAO8syATDW3yXV0uyKxcuRK1tXWAwoJMd/l7Cn4TQIYvliRbW1uxZcvb3VqS/NkJIEOn041SqfASwMYCQFxcHLLnz8Htt98OpvA/neytqPm0GAAQcUOyYmSTiLBz507kLl6C8vJyOfkQwOZYLJZ9nuTfW1w2AsjgnKcAyGcM1wPA8OHDsCB7Pm68cVSPeBw8+AkW5S5GSUmpnEQA3g0JCf1LRUVFg7f49xSX3dZEq9VaZLVahwNsBoDvS0pK8ad7/4ypUzNQVna8y/vLyo5j6tQMFBS8hrVrVqG05DNkZEyFSqViAO5uaWn6n16vu83rhvgL8v6bjnt05PSe1HUpWxPPfHfygvxPDu6nsWPHuOwP0r0pSVKUN/l3B5ddF6SE8PBwrtGEPE2ExwFoNBoNTp08cUGZ+F8loqnpp2XMki8OISrqQv/a7XasXrMOy5blo6GhAQCqATbPYrGsBuD0Fv+L4bLrgpRQW1trNZut8xwO5xAAFzhahpzGGHYBwNx52fLenx8RFBSEx2ZMx94PipByazIARAJUwDn/SAgxxMtmKCIgBJBRVVXVZYiViM0CULV7dxHGjr0Z27YVupWJjY3Fxo2vY8OG9YiOjgZjGA3QQSGE8kYiLyKgBOgOLBbLCYCNIsIBk9mMvz2ehalTM2A0Gt3KpqbciuI9u+XWoGWMFvuab58TAGgTwWq13tT+T6q6aE8xbr4lBf8sWAW73X5B2YiIcCxZkgcAkI88+RJ9UoBAQp88tCCESCSi9QCNBoCUW5ORm7sQsbGxbmVramoxZ848APJhP9zjS659rgW0DaT0CWMYrZckrFj+CjZufF3R+buL9iD51lQU7SkGgCoiNt/XfPtcC2CMFhNBm5qagpUrliMiItytjNFoRHZ2jux4EOEAY+whi8VyQgjuU759TgB5IH0pL9fN+Z1NxKzWnyZivkafE6AzlJSUYs7ceTh69Fh7Cr3FmPoJs9n8gz95+XwMMBgM8d6snzEUAcDcufNRWVmJyspKZC/IwaS7Jrc7n06pVDTBYrHd39H53uamBJ+1AI1Gg6amJjgc9jIhdOtVqqBnTSaTx3fktg2kdMvuoj3aYcMvWBlrZQzLQkP7/d1oNF6w5UKONTkc9iyZq6/gsxbw8f59eOCB+6FWq4MBNt3pdJz0xkFqeSbMGAoB1AKoZQyFDodzuNlsne/qfDnaGhoacpIIc1UqVejEiROxb2+RJyn5FkLw80rhYPk6criE7ktPcz02WiGEeBTdbI0eChcHCSEedT0Ifl96Gh05XKLI+cx3J+X6vXt4wROQJL5dCE5TpqSTqbKiJwepj3HOJ3dV/6UKwDmfLAQ/1t2D4KbKCpoyJZ2E4CRJfLtnvORFCCESheA2ITglJMRTfv5Sqjlv7dTAoqL3acyY0T86UJL4AUnSjum8/t4JIEnaMZLED8jlRo4cQYXbt3R6ELyutorWrV1NQ4YMluu2+SNa2iu0i1AkG3v99b+mdWtXU2NDjaKxDfXnad3a1XTd0GtdW8S7Op1uqHvdPRNAp9MNFYK/K+cnJg6m/PylVFdbpcilqbGWCrdvoWHDhrlyKQoY57uCc54iBP9CNuTGG2+gwu1bOm0N1VVmys9fSvHxV8uGO4QQW6OiIq+W6+yuAJzzGCF0rwnBW4XgNGjQlfT8czlktVRetDWOG3eTq+O/av++RECDSZIuTQj+rWzYhAm30f6PPujUET98b6Tnn8uh6OiBsiMaJInnRUZGRnYlgCRJYe3ryLVCcBo4MIqysmaR0Xi60+cd+vQA3X3Xna6OPyOEmA6g75wySUhICBVCN1uSuEU2dMqUdDpe9lWnjjl18jhlZc0ivV6SxwdL1wK0lZEkQRkZ07qs/7HHppMkCZd7dbMTEhJC/e0vr8HlDa3p7hv62aGDHd/QTgWQW9jH+/depIWd7djC6uUW5m//+AxtfbRYIwS396aPVhJg9OhR3Rpj4uKukh1vF0Ks4Zx79ORjQKHjv5RrrxlCq1cVUH2d8jeAmhpradMbG9zSN72xgZoaaxXvqa+rptWrCujaa4Z0+S/rZ4uefrCpO1cgfLDpssMvnyy7PNCjWI3rdamxpl/gAoPB0F+SeI5oD/AZDHrKzJxBp8u/cXP86fJvKDNzhutnK89LEs8xGAz9/W1HwCMqKkrinC8XgjcLwSk2NoaefSabzp45RWfPnKJnn8mm2NgY2fHNnPPlUVFRyuedLjMEVH9oMBjiHY7WhQC7D+7cCaDNanVwTmVlpX+P3/cAASWAjPaPd2cTIRUAGMNuIpYbiB/v/gV+xv8Bhtk7GIFOT8EAAAAASUVORK5CYII="
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": "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"
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