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
v2.3.0MITupdated 2026-10-15aecheman27-gif/claude-mods/cache-tax
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="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. Only a prompt you can send again is dropped: one you typed, one from Remote Control, or one a mod sends as you (a next-steps click). A background task's notification, a /loop or routine firing, another session's message or an SDK prompt goes through with the price shown, since nothing would resend it.

Other models: the clock and the price are your session model's (/model). If another mod sends some turns to another model (auto-model does), those turns neither keep your model's cache warm nor count as its cold write, and keepwarm sends no ping while the last turn ran on another model, since a fork repeats the last request's model.

A detected cold write automatically arms at least three hours of keepwarm. /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. 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 arm a six-hour window at every session start, remembered across sessions /keepwarm 6h every 2m pinging every two minutes; a testing knob, floor 1m, forgotten after this window /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, the session's own model (at start, at resume, and on each prompt and model request) and the model that answered each main-loop request; of each prompt, who sent it (its origin); 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, 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 (typed, Remote Control, or a mod's prompt sent as the user), 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; Opus 5.5 ($0.20, $8, $20), Sonnet 5.5 and Sonnet 5 ($0.20, $4, $10) and Haiku 5.5 ($0.01, $0.20, $0.50 for prompts up to 100K tokens; $0.05, $1, $2.50 past that) have their own rows, the most specific id matching first; older models keep theirs. 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. An API failure, interruption, empty reply or missing conversation also 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 56 tests use a mock clock and engine. They cover:

  • Guard: refuse once and resend, warn mode, slash commands, small contexts, resume seeding and cold-write scoring.
  • Warming: command defaults, always/off, 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: model matching, 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: structured failure results from current engines 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 598 lines
1import type { EngineInterface, PromptOrigin, 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 AUTO_WARM_MS = 3 * 60 * 60 * 1000
8const DEFAULT_WINDOW_MS = 6 * 60 * 60 * 1000
9const BIG_TOKENS = 50000
10const PING_PROMPT = 'Reply with the single word: warm'
11const KEY_DEADLINE = 'deadline'
12const KEY_EVERY = 'every'
13const KEY_GUARD = 'guard'
14const KEY_ALWAYS = 'always'
15
16// $ per million tokens, [cache read, 1h cache write, output], list prices October 2026.
17// More specific ids first: a model id matches the first row it contains.
18const PRICES: Array<[string, number, number, number]> = [
19  ['fable-5-1', 0.25, 20, 50],
20  ['fable-5', 1, 20, 50],
21  ['opus-5-5', 0.2, 8, 20],
22  ['opus-5', 0.5, 10, 25],
23  ['opus-4', 0.5, 10, 25],
24  ['sonnet-5-5', 0.2, 4, 10],
25  ['sonnet-5', 0.2, 4, 10],
26  ['sonnet', 0.3, 6, 15],
27  ['haiku-5-5', 0.01, 0.2, 0.5],
28  ['haiku', 0.1, 2, 5],
29]
30// Haiku 5.5 is priced by prompt size: past 100K tokens a request bills this row.
31const HAIKU_5_5_OVER: [number, number, number] = [0.05, 1, 2.5]
32const HAIKU_TIER_TOKENS = 100000
33const FAMILIES = ['fable', 'opus', 'sonnet', 'haiku']
34
35type PingRecord = { at: number; read: number; write: number; usd: number | null; warm: boolean }
36type Miss = { at: number; tokens: number; usd: number | null }
37type GuardMode = 'refuse' | 'warn'
38
39export type State = {
40  hasBand: boolean
41  sid: string
42  deadline: number
43  every: number
44  always: boolean
45  /** The last request to the session's own model family; another family's cache is not this one. */
46  lastRequestAt: number
47  /** The id the API last reported for the session's own model family, for its price. */
48  lastModel: string | null
49  /** The session's own model, as $.session.model() names it. */
50  model: string | null
51  /** The model that answered the main loop's last request: what a keepwarm fork re-sends to. */
52  lastStepModel: string | null
53  ctx: number
54  compacted: boolean
55  guard: GuardMode
56  ackedAt: number
57  coldWritePending: boolean
58  misses: Miss[]
59  pending: { cancel: () => void } | null
60  last: PingRecord | null
61  stopped: string | null
62}
63
64/** [cache read, 1h write, output] for `model` on a request of `tokens` prompt tokens. */
65function priceOf(model: string | null, tokens = 0): [number, number, number] | null {
66  const m = (model ?? '').toLowerCase().replace(/[\s.]+/g, '-')
67  for (const [family, read, write, output] of PRICES) {
68    if (!m.includes(family)) continue
69    return family === 'haiku-5-5' && tokens > HAIKU_TIER_TOKENS ? HAIKU_5_5_OVER : [read, write, output]
70  }
71  return null
72}
73
74/** The session's own model priced at its context: the id the API reported for it, else its name. */
75function ownPrice(s: State, tokens = s.ctx): [number, number, number] | null {
76  return priceOf(s.lastModel ?? s.model, tokens)
77}
78
79function familyOf(model: string | null | undefined): string | undefined {
80  const m = (model ?? '').toLowerCase()
81  return FAMILIES.find(f => m.includes(f))
82}
83
84/** Whether `model` is of the session's own family; while the session's model is unknown, every model counts. */
85function isOwn(s: State, model: string | null | undefined): boolean {
86  const own = familyOf(s.model)
87  return own === undefined || familyOf(model) === own
88}
89
90/** True when the main loop last asked another model, so a fork would warm that model's cache, not the session's. */
91function isSwitched(s: State): boolean {
92  return s.lastStepModel !== null && !isOwn(s, s.lastStepModel)
93}
94
95/** Reads the session's own model; when /model moved it to another family, that family's cache has no record yet. */
96async function readModel($: EngineInterface, s: State) {
97  const model = await $.session.model().catch(() => s.model)
98  if (s.model !== null && familyOf(model) !== familyOf(s.model)) {
99    s.lastRequestAt = 0
100    s.lastModel = null
101  }
102  s.model = model
103}
104
105/** A prompt the person sent and can send again: typed, sent by Remote Control, or a plugin's prompt in their name (a next-steps click). */
106function isPerson(origin: PromptOrigin): boolean {
107  return origin.kind === 'composer' || origin.kind === 'bridge' || (origin.kind === 'plugin' && origin.asUser === true)
108}
109
110export function parseDuration(text: string): number | null {
111  const m = /^(?:(\d+)h)?(?:(\d+)m)?$/.exec(text.trim())
112  if (!m || (m[1] === undefined && m[2] === undefined)) return null
113  return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 * 1000
114}
115
116export function fmtDuration(ms: number): string {
117  const total = Math.max(0, Math.round(ms / 60000))
118  const h = Math.floor(total / 60)
119  const m = total % 60
120  if (h >= 48) return `${Math.floor(h / 24)}d ${h % 24}h`
121  return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`
122}
123
124function fmtUsd(usd: number | null): string {
125  return usd == null ? 'n/a' : '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
126}
127
128function fmtTok(n: number): string {
129  return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
130}
131
132function coldUsd(s: State): number | null {
133  const price = ownPrice(s)
134  return price ? s.ctx * price[1] / 1e6 : null
135}
136
137function warmUsd(s: State): number | null {
138  const price = ownPrice(s)
139  return price ? s.ctx * price[0] / 1e6 : null
140}
141
142/** Everything a fork bills: the cache read, any cache write, uncached input at the base rate (half the 1h write rate), and the output. */
143function pingUsd(u: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }, price: [number, number, number]): number {
144  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
145}
146
147/** The read-only upper bound: pings at the cache-read rate that cost as much as one cold write of the same context. */
148function breakEvenPings(s: State): number | null {
149  const price = ownPrice(s)
150  if (!price || s.ctx <= 0) return null
151  return Math.floor(price[1] / price[0])
152}
153
154function isCold(s: State, now: number): boolean {
155  return s.lastRequestAt > 0 && !s.compacted && now - s.lastRequestAt >= TTL_MS
156}
157
158function guardText(s: State, now: number): string {
159  const price = ownPrice(s)
160  const rate = price ? `$${price[1]}/MTok` : 'the cache-write rate'
161  const warm = warmUsd(s)
162  return `the prompt cache went cold ${fmtDuration(now - s.lastRequestAt - TTL_MS)} ago. Sending this re-writes ` +
163    `up to ${s.ctx.toLocaleString('en-US')} tokens at ${rate} = ${fmtUsd(coldUsd(s))}` +
164    (warm == null ? '' : ` (a warm turn would have cost ${fmtUsd(warm)})`) + '.'
165}
166
167export type ResumeFields = {
168  source: string
169  model?: string
170  context_tokens?: number
171  seconds_since_last_response?: number
172  prompt_cache_likely_expired?: boolean
173  estimated_cache_write_usd?: number
174}
175
176/** /clear starts a new conversation in the same process; nothing priced before it still exists. */
177export function resetForClear(s: State) {
178  s.ctx = 0
179  s.lastRequestAt = 0
180  s.lastStepModel = null
181  s.compacted = false
182  s.ackedAt = 0
183  s.coldWritePending = false
184  s.misses = []
185  disarm(s)
186}
187
188/** Applies a resumed session's fields to the state; returns the line to log, if any. */
189export function seedFromResume(s: State, e: ResumeFields, now: number): string | null {
190  if (e.source !== 'resume' && e.source !== 'fork') return null
191  if (typeof e.context_tokens === 'number' && e.context_tokens > 0) s.ctx = e.context_tokens
192  if (typeof e.seconds_since_last_response === 'number') s.lastRequestAt = now - e.seconds_since_last_response * 1000
193  if (typeof e.model === 'string') s.lastModel = e.model
194  s.compacted = false
195  if (e.prompt_cache_likely_expired !== true || s.ctx < BIG_TOKENS) return null
196  const usd = typeof e.estimated_cache_write_usd === 'number' ? fmtUsd(e.estimated_cache_write_usd) : fmtUsd(coldUsd(s))
197  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.`
198}
199
200function statusText(s: State, now: number): string | undefined {
201  if (s.stopped) return `keepwarm stopped: ${s.stopped}`
202  if (!s.deadline) return undefined
203  const pingText = s.last ? ` · last ping read ${fmtTok(s.last.read)} ${fmtUsd(s.last.usd)}` : ''
204  const nextText = !s.lastRequestAt ? ' · waiting for the first turn'
205    : s.compacted ? ' · waiting for the first turn after compaction'
206    : isCold(s, now) ? ` · cold now, first ping ${fmtDuration(s.every)} after the next turn`
207    : isSwitched(s) ? ' · no ping while the last turn ran on another model'
208    : ` · ping in ${fmtDuration(s.lastRequestAt + s.every - now)}`
209  return `keepwarm ${fmtDuration(s.deadline - now)} left${nextText}${pingText}`
210}
211
212function updateStatus($: EngineInterface, s: State, now: number) {
213  if (s.hasBand) $.ui.invalidate('ui.render')
214  else $.ui.status(statusText(s, now))
215}
216
217function disarm(s: State) {
218  if (s.pending) s.pending.cancel()
219  s.pending = null
220}
221
222// The window and its ping period belong to the session that armed them, so a
223// second session, or one resumed from another transcript, never inherits them
224// and cannot turn them off. The always switch and the guard mode stay global.
225function deadlineKey(s: State): string {
226  return `${KEY_DEADLINE}:${s.sid}`
227}
228
229function everyKey(s: State): string {
230  return `${KEY_EVERY}:${s.sid}`
231}
232
233/** 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. */
234async function prune($: EngineInterface, s: State, now: number) {
235  for (const key of [KEY_DEADLINE, deadlineKey(s)]) {
236    const deadline = await $.store.get(key)
237    if (deadline === undefined) continue
238    if (typeof deadline === 'number' && deadline > now) continue
239    await $.store.delete(key)
240    await $.store.delete(KEY_EVERY + key.slice(KEY_DEADLINE.length))
241  }
242}
243
244async function stop($: EngineInterface, s: State, why: string | null, forgetAlways = false) {
245  s.deadline = 0
246  s.every = PING_AFTER_MS
247  s.stopped = why
248  disarm(s)
249  await $.store.delete(deadlineKey(s))
250  await $.store.delete(everyKey(s))
251  if (forgetAlways) {
252    s.always = false
253    await $.store.delete(KEY_ALWAYS)
254  }
255  updateStatus($, s, await $.clock.now())
256}
257
258async function arm($: EngineInterface, s: State) {
259  disarm(s)
260  if (!s.deadline) return
261  const now = await $.clock.now()
262  if (now >= s.deadline) return stop($, s, null)
263  // A cold window still needs expiry cleanup, but must not send a model request.
264  // Nor may a fork that would re-send another model's last request: it would
265  // warm that model's cache, not the session's.
266  if (s.lastRequestAt && !s.compacted && !isCold(s, now) && !isSwitched(s)) {
267    const untilCold = s.lastRequestAt + TTL_MS - now
268    const delay = Math.min(s.deadline - now, untilCold, Math.max(1000, s.lastRequestAt + s.every - now))
269    s.pending = $.clock.after(delay, () => { void ping($, s) })
270  } else {
271    s.pending = $.clock.after(s.deadline - now, () => { void arm($, s) })
272  }
273  updateStatus($, s, now)
274}
275
276async function ping($: EngineInterface, s: State) {
277  s.pending = null
278  if (!s.deadline) return
279  const now = await $.clock.now()
280  if (now >= s.deadline) return arm($, s)
281  // A turn in the meantime re-armed the timer; this callback is stale.
282  if (isCold(s, now) || isSwitched(s)) return arm($, s)
283  if (now - s.lastRequestAt < s.every - 1000) return
284  let reply
285  try {
286    reply = await $.model.fork({ prompt: PING_PROMPT })
287  } catch (err) {
288    return stop($, s, `the ping failed, ${err instanceof Error ? err.message : String(err)}`)
289  }
290  if (reply === null) return stop($, s, 'the engine did not send the ping, either the snapshot was cold or the API call failed')
291  if (reply.isAnswered === false) {
292    const reason = reply.reason === 'nothing-to-fork' ? 'no conversation to warm yet'
293      : reply.reason === 'api-error' ? `the API call failed${reply.status === null ? '' : ` (${reply.status})`}`
294      : reply.reason === 'aborted' ? 'the ping was interrupted'
295      : 'the ping returned no text'
296    return stop($, s, reason)
297  }
298  const u = reply.usage
299  const price = ownPrice(s, u.cache_read_input_tokens + u.cache_creation_input_tokens + u.input_tokens)
300  // 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.
301  const warm = u.cache_read_input_tokens > 0 && u.cache_creation_input_tokens < 0.1 * u.cache_read_input_tokens
302  const usd = price ? pingUsd(u, price) : null
303  s.last = { at: now, read: u.cache_read_input_tokens, write: u.cache_creation_input_tokens, usd, warm }
304  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`)
305  s.lastRequestAt = now
306  await arm($, s)
307}
308
309/** The reply to an arming command; on a cold cache it says when the first ping can come. */
310function armedText(s: State, now: number, windowMs: number): string {
311  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`
312  return `keepwarm on for ${fmtDuration(windowMs)}, a ping ${fmtDuration(s.every)} after each idle stretch keeps the cache read, not re-written`
313}
314
315async function startWindow($: EngineInterface, s: State, windowMs: number, every: number) {
316  const now = await $.clock.now()
317  s.every = every
318  if (every === PING_AFTER_MS) await $.store.delete(everyKey(s))
319  else await $.store.set(everyKey(s), every)
320  s.deadline = now + windowMs
321  s.stopped = null
322  await $.store.set(deadlineKey(s), s.deadline)
323  await arm($, s)
324}
325
326function card(s: State, now: number): string {
327  const lines: string[] = []
328  lines.push(`${s.lastModel ?? s.model ?? 'model not seen yet'}`)
329  if (s.compacted) lines.push('state       reset by compaction, waiting for the first turn')
330  else if (!s.lastRequestAt) lines.push('state       no request yet this session')
331  else if (isCold(s, now)) lines.push(`state       COLD, last request ${fmtDuration(now - s.lastRequestAt)} ago`)
332  else lines.push(`state       warm, ${fmtDuration(s.lastRequestAt + TTL_MS - now)} left`)
333  lines.push(`context     ${s.ctx.toLocaleString('en-US')} tokens`)
334  lines.push(`cold cost   ${fmtUsd(coldUsd(s))} to re-write it (warm turn ${fmtUsd(warmUsd(s))})`)
335  const always = s.always ? ' (always)' : ''
336  const idle = s.always ? 'off until the next session start, which arms 6h00m (always)' : 'off (/keepwarm to arm it for 6h00m)'
337  lines.push(`keepwarm    ${s.deadline ? (statusText(s, now) ?? '').replace(/^keepwarm /, 'on, ') + always : s.stopped ? `stopped, ${s.stopped}${always}` : idle}`)
338  const pings = breakEvenPings(s)
339  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)}`)
340  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)'}`)
341  const paid = s.misses.reduce((a, m) => a + (m.usd ?? 0), 0)
342  lines.push(`session     ${s.misses.length} cold write${s.misses.length === 1 ? '' : 's'} paid, ${fmtUsd(paid)}`)
343  return lines.join('\n')
344}
345
346export function freshState(): State {
347  return {
348    hasBand: false, sid: '', deadline: 0, every: PING_AFTER_MS, always: false, lastRequestAt: 0, lastModel: null, model: null, lastStepModel: null, ctx: 0, compacted: false,
349    guard: 'refuse', ackedAt: 0, coldWritePending: false, misses: [], pending: null, last: null, stopped: null,
350  }
351}
352
353export const register: Register = on => {
354  const s = freshState()
355  let light = false
356  let noColor = false
357
358  on('config.set', { key: 'theme' }, async ($, e, next) => {
359    const result = await next(e)
360    if (result.deny === undefined) {
361      light = typeof result.value === 'string' && result.value.startsWith('light')
362      if (s.hasBand) $.ui.invalidate('ui.render')
363    }
364    return result
365  })
366
367  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
368    const rest = await next(e)
369    if (!s.hasBand || e.props.hasSurvey) return rest
370    const now = await $.clock.now()
371    const text = statusText(s, now)
372    if (!text || (s.deadline && now >= s.deadline)) return rest
373    const state = s.stopped ? 'stopped' : !s.lastRequestAt || s.compacted ? 'unknown' : isCold(s, now) ? 'cold' : 'warm'
374    const color = noColor ? undefined : state === 'warm' ? 'success' : state === 'cold' ? (light ? '#c15f3c' : '#d97757') : undefined
375    const { Box, Text } = $.ui.resolve(e)
376    const asset = statusIcons[light ? 'light' : 'dark'][state === 'warm' || state === 'cold' ? state : 'neutral']
377    const icon = noColor ? Text({ bold: true, children: ['[>]'] })
378      : e.surface === 'terminal' ? $.ui.resolve(e).Image({ source: { png: asset.png }, columns: 3, rows: 1, alt: '[>]' })
379      : e.surface === 'desktop' ? $.ui.resolve(e).Svg({ source: asset.svg, width: 18, height: 18, alt: 'Cache Tax' })
380      : Text({ color, bold: true, children: ['[>]'] })
381    return Box({ flexDirection: 'column', children: [
382      rest,
383      Box({ flexDirection: 'row', alignItems: 'center', children: [
384        Box({ flexShrink: 0, children: [icon] }),
385        Text({ children: [
386          ' cache-tax · ',
387          Text({ color, bold: true, children: [state] }),
388          ` · ${text}`,
389        ] }),
390      ] }),
391    ] })
392  })
393
394  on('session.start', async ($, e, next) => {
395    const r = await next(e)
396    s.hasBand = e.surface === 'terminal' || e.surface === 'desktop'
397    if (s.hasBand) {
398      const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
399      light = typeof theme === 'string' && theme.startsWith('light')
400      noColor = Boolean(await $.env.get('NO_COLOR'))
401      $.ui.status(undefined)
402    }
403    s.sid = await $.session.id()
404    await readModel($, s)
405    const now = await $.clock.now()
406    await prune($, s, now)
407    const saved = await $.store.get(deadlineKey(s))
408    const savedEvery = await $.store.get(everyKey(s))
409    const savedGuard = await $.store.get(KEY_GUARD)
410    s.deadline = typeof saved === 'number' && saved > now ? saved : 0
411    s.every = typeof savedEvery === 'number' && savedEvery >= MIN_PING_MS ? savedEvery : PING_AFTER_MS
412    s.guard = savedGuard === 'warn' ? 'warn' : 'refuse'
413    s.always = (await $.store.get(KEY_ALWAYS)) === true
414    // Always means a fresh default window every session, whatever the last one left behind.
415    if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
416    const usage = await $.session.usage()
417    if (usage.context.tokens) s.ctx = usage.context.tokens
418    await $.command.register({
419      name: 'keepwarm',
420      description: 'Keep the prompt cache warm: bare for 6h, a window such as 90m, always, off, or status (cache-tax)',
421      argumentHint: '[6h | always | off | status]',
422      immediate: true,
423    })
424    await $.command.register({
425      name: 'cache-tax',
426      description: 'Prompt cache state, cold price and this session\'s cold writes; guard warn|refuse (cache-tax, the Mod)',
427      argumentHint: '[status | guard warn | guard refuse]',
428      immediate: true,
429    })
430    // The hook form of cache-tax ships a /cache-tax:status skill; both installed means two guards.
431    const commands = await $.command.list()
432    if (commands.some(c => c.name === 'cache-tax:status')) {
433      $.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.')
434    }
435    updateStatus($, s, now)
436    return r
437  })
438
439  // Resume fields seed the guard before any turn of the resumed session has run.
440  on('classic.SessionStart', async ($, e, next) => {
441    const r = await next(e)
442    if (e.source === 'clear') {
443      await stop($, s, null)
444      s.stopped = null
445      resetForClear(s)
446      s.sid = await $.session.id()
447      updateStatus($, s, await $.clock.now())
448      return r
449    }
450    const line = seedFromResume(s, e, await $.clock.now())
451    // The resume payload may omit the model; the session's own then prices the cold write.
452    await readModel($, s)
453    if (line) $.ui.log(line)
454    // The seeded clock decides whether a restored or always window pings before the first turn: never when it is cold.
455    await arm($, s)
456    return r
457  }).catch(($, e, next) => next(e))
458
459  on('command.run', { command: 'keepwarm' }, async ($, e) => {
460    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
461    const now = await $.clock.now()
462    if (words[0] === 'off') {
463      const wasAlways = s.always
464      await stop($, s, null, true)
465      return { text: wasAlways ? 'keepwarm is off, and no longer arms itself at session start' : 'keepwarm is off' }
466    }
467    if (words[0] === 'always') {
468      s.always = true
469      await $.store.set(KEY_ALWAYS, true)
470      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
471      const cold = isCold(s, now) ? `. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn` : ''
472      return { text: `keepwarm always on: every session starts with a ${fmtDuration(DEFAULT_WINDOW_MS)} window; /keepwarm off turns it off for good${cold}` }
473    }
474    if (!words.length) {
475      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
476      return { text: armedText(s, now, DEFAULT_WINDOW_MS) }
477    }
478    if (words[0] !== 'status') {
479      const window = parseDuration(words[0] ?? '')
480      if (window == null) return { text: 'keepwarm takes a window such as 6h or 90m, or always, off, or status' }
481      // "every 2m" is a testing knob and lasts only for the window it was given with.
482      let every = PING_AFTER_MS
483      if (words[1] === 'every') {
484        const period = parseDuration(words[2] ?? '')
485        if (period == null || period < MIN_PING_MS) return { text: 'every takes a period of at least 1m' }
486        every = period
487      }
488      await startWindow($, s, window, every)
489      return { text: armedText(s, now, window) }
490    }
491    return { text: statusText(s, now) ?? 'keepwarm is off' }
492  })
493
494  on('command.run', { command: 'cache-tax' }, async ($, e) => {
495    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
496    const now = await $.clock.now()
497    if (words[0] === 'guard') {
498      if (words[1] !== 'warn' && words[1] !== 'refuse') return { text: '/cache-tax guard takes warn or refuse' }
499      s.guard = words[1]
500      await $.store.set(KEY_GUARD, s.guard)
501      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' }
502    }
503    return { text: card(s, now) }
504  })
505
506  // The message that pays. Only its first character is read. Only the person
507  // can send a dropped prompt again, so only theirs is refused; any other (a
508  // background task's notification, a /loop or routine firing, a peer's
509  // message, an SDK prompt) is sent with the price shown, or it would be lost.
510  on('prompt.submit', async ($, e, next) => {
511    if (typeof e.text !== 'string' || e.text.trimStart().startsWith('/')) return next(e)
512    await readModel($, s)
513    const now = await $.clock.now()
514    if (!isCold(s, now) || s.ctx < BIG_TOKENS) return next(e)
515    if (s.guard === 'warn' || !isPerson(e.origin)) {
516      const whose = isPerson(e.origin) ? '' : ` (a ${e.origin.kind} prompt, which nobody could send again)`
517      $.ui.log(`${guardText(s, now)} Sending anyway${whose}; keepwarm will hold the cache for ${fmtDuration(AUTO_WARM_MS)} once it lands.`)
518      s.coldWritePending = true
519      return next(e)
520    }
521    if (s.ackedAt === s.lastRequestAt) {
522      s.ackedAt = 0
523      s.coldWritePending = true
524      return next(e)
525    }
526    s.ackedAt = s.lastRequestAt
527    return { drop: `cache-tax: ${guardText(s, now)} Send it again to pay it, and keepwarm will then hold the cache for ${fmtDuration(AUTO_WARM_MS)}. Or /clear and start from a note.` }
528  }).catch(($, e, next) => next(e)) // a guard that fails sends the prompt: dropping is the harm here
529
530  // Only a request to the session's own model keeps its cache warm: another
531  // plugin (auto-model) may send a step to another model.
532  on('turn.step', async function* ($, e, next) {
533    if (e.agentId) return yield* next(e)
534    const at = await $.clock.now()
535    await readModel($, s)
536    const before = s.lastRequestAt
537    if (isOwn(s, e.model)) s.lastRequestAt = at
538    const r = yield* next(e)
539    // The model that answered decides: a plugin beneath may have sent the step elsewhere.
540    s.lastStepModel = r.usage?.model || e.model
541    if (isOwn(s, s.lastStepModel)) s.lastRequestAt = Math.max(s.lastRequestAt, at)
542    else if (s.lastRequestAt === at) s.lastRequestAt = before
543    return r
544  })
545
546  on('turn.complete', async ($, e, next) => {
547    const r = await next(e)
548    if (e.agentId) return r
549    const now = await $.clock.now()
550    // A sleeping host may deliver this turn before the expired window's timer.
551    if (s.deadline && now >= s.deadline) await stop($, s, null)
552    const u = e.usage
553    // A turn another plugin sent to another model neither keeps the session
554    // model's cache warm nor pays for it.
555    const isOwnTurn = !u?.model || isOwn(s, u.model)
556    if (u?.model) s.lastStepModel = u.model
557    // turn.step stamps the exact request time; when no step of this turn did, the turn's end is the floor.
558    if (isOwnTurn && now - s.lastRequestAt > e.durationMs) s.lastRequestAt = now
559    s.compacted = false
560    s.ackedAt = 0
561    if (u) {
562      if (u.model && isOwnTurn) s.lastModel = u.model
563      const prev = s.ctx
564      // A turn's usage is its responses summed, so a ten-step turn reports ten
565      // reads of the context. The live window is the engine's figure; the sum
566      // is only the fallback for a host that reports no tokens.
567      const write = u.cache_creation_input_tokens
568      const live = (await $.session.usage()).context.tokens
569      s.ctx = live && live > 0 ? live : u.input_tokens + u.cache_read_input_tokens + write
570      const full = prev > 20000 && write >= 0.5 * prev
571      if (isOwnTurn && (full || s.coldWritePending)) {
572        const price = ownPrice(s)
573        const usd = price ? write * price[1] / 1e6 : null
574        s.misses.push({ at: now, tokens: write, usd })
575        if (s.deadline < now + AUTO_WARM_MS) {
576          await startWindow($, s, AUTO_WARM_MS, s.every)
577          $.ui.log(`cold write of ${fmtTok(write)} tokens paid (${fmtUsd(usd)}). Keeping the cache warm for ${fmtDuration(AUTO_WARM_MS)} so it is not paid again today; /keepwarm off to stop.`)
578        }
579      }
580    }
581    s.coldWritePending = false
582    await arm($, s)
583    return r
584  })
585
586  on('session.compact', async ($, e, next) => {
587    const r = await next(e)
588    if (!e.agentId) {
589      s.compacted = true
590      s.ctx = 0
591      s.ackedAt = 0
592      disarm(s)
593      await arm($, s)
594    }
595    return r
596  })
597}
598
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": "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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