Band: time left on the warm cache, hit rate, context as weather with its trend over 12 turns, plan limits, compact and keep-warm buttons; stops one send to a…

Band: time left on the warm cache, hit rate, context as weather with its trend over 12 turns, plan limits, compact and keep-warm buttons; stops one send to a cold cache and names its cost (Cache Tax runs inside).
Part of the Mod Box, which bundles it with other mods and one window to switch and configure them all. This is the same mod on its own.
Contains a fork. The cold-send guard and keep-warm are Cache Tax by Karan Bansal (MIT), embedded and modified (guard and auto-warm options, a compact-first hint, token usage); see
mods/cache-tax/and its LICENSE.
In a terminal Claude Code session (2.1.287 or newer):
/plugin install cache-meter --marketplace LupusMalusDeviant/claude-mods
| Command | What it does | ||||
|---|---|---|---|---|---|
| `/keepwarm <6h\ | 90m\ | always\ | off\ | status>` | keep the cache warm |
/cache-tax | cache costs of this session |
Set them in /config (or when installing).
| Option | Default | What it does |
|---|---|---|
ttlMinutes | 60 | 60 on a subscription (Pro/Max), 5 for the API's default cache. |
limits | on | Usage of the 5-hour and the weekly window. |
guard | refuse | refuse: stop one send to a cold cache with a large context and name the cost (send again to pay). warn: only show the cost. off. |
keepWarmHours | 3 | How long the keep-warm button keeps the cache warm (one small request over the cache about every 50 minutes of idle). |
autoWarm | off | on: after a paid cold write, keep the cache warm for 3 hours by itself (costs tokens). |
autoCompact | auto | Sets Claude Code's autoCompactWindow. On 1M-context models Claude only compacts by itself at about 967k tokens, and every turn re-reads the whole context until then. auto = Claude's default. |
language | en | Language of the texts this mod shows and writes. |
keep-warm requests, only when switched on
MIT, see LICENSE.
hooks/entry.tsx 30 lines1// Entry of the standalone Cache Meter. The mod hands its display to a band (in the Mod Box the
2// band manager draws it); here this hook, registered first and so the outermost, draws it below
3// whatever else is above the prompt.
4import type { Register } from 'claude-code'
5
6import { register as inner } from '../mods/cache-meter/hooks/register'
7import { openBand, takeBand } from '../mods/settings/hooks/band'
8
9export const register: Register = (on, options) => {
10 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
11 openBand(e)
12 const rest = await next(e)
13 const parts = takeBand(e)
14 if (e.props.hasSurvey || parts.length === 0) return rest
15 const { Box, Text } = $.ui.resolve(e)
16 return (
17 <Box flexDirection="column">
18 {rest}
19 {parts.map(p => (
20 <Box key={`band-${p.id}`} flexDirection="row" paddingX={1}>
21 <Box flexShrink={1}>{p.line as any}</Box>
22 {(p.actions as any[]).flatMap((a, i) => [<Text key={`gap-${i}`}>{' '}</Text>, a])}
23 </Box>
24 ))}
25 </Box>
26 )
27 })
28 ;(inner as any)(on, options)
29}
30mods/cache-meter/hooks/register.tsx 318 lines1// Cache Meter: one line above the prompt (through the Mod Box band manager) with
2// 1. how long the prompt cache stays warm (countdown from the last request),
3// 2. the last turn's hit rate (how much came from the cache),
4// 3. how full the context window is, as weather (sun to thunderstorm), with its trend over the
5// last 12 turns as a sparkline (idea from token-weather in anthropics/claude-code-playground),
6// 4. the plan's rate-limit windows (5 h / week),
7// 5. keep-warm and the cold-send guard of Cache Tax, which runs embedded here (no band of its own):
8// a "keep warm" button, the cost of a cold resend, and the guard that stops one cold send.
9// Plus a compact button that does what /compact does.
10import { atom, read, update } from 'claude-code'
11import type { EngineInterface, Register } from 'claude-code'
12
13import { register as cacheTax, taxInfo } from '../../cache-tax/hooks/register'
14import { offer as offerBand } from '../../settings/hooks/band'
15import type { BandPart } from '../../settings/hooks/band'
16import type { MeterInfo } from '../types'
17
18const info = atom({ plugin: 'cache-meter', key: 'info' } as const, {
19 lastAt: 0,
20 hit: null,
21 percent: null,
22 tokens: null,
23 window: null,
24 limits: [],
25 trend: [],
26} as MeterInfo)
27// A tick so the countdown moves without a new turn.
28const tick = atom({ plugin: 'cache-meter', key: 'tick' } as const, 0)
29
30const TICK_MS = 15_000
31const TREND_TURNS = 12
32
33// Display language and options (module level: band() runs from the band manager).
34let lang: 'de' | 'en' = 'en'
35const t = (de: string, en: string) => (lang === 'en' ? en : de)
36let ttlMs = 60 * 60_000
37let showLimits = true
38let keepWarmMs = 3 * 60 * 60_000
39
40const STORE_AUTOCOMPACT = 'autocompact-by-box'
41
42/** Tokens for Claude Code's autoCompactWindow setting, or null for its own default. */
43export function autoCompactTokens(choice: string): number | null {
44 const n = Number(choice.replace(/k$/i, '')) * 1000
45 return Number.isFinite(n) && n >= 100000 && n <= 1000000 ? n : null
46}
47
48// Writes the chosen window into ~/.claude/settings.json (Claude Code watches that file). Only a
49// value the box itself wrote is ever removed again, so a hand-set one stays.
50async function applyAutoCompact($: EngineInterface, choice: string) {
51 const home = ((await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? '').replace(/\\/g, '/')
52 if (!home) return
53 const path = `${home}/.claude/settings.json`
54 const s = JSON.parse(String(await $.fs.read(path))) as Record<string, unknown>
55 const want = autoCompactTokens(choice)
56 const ours = await $.store.get(STORE_AUTOCOMPACT)
57 if (want === null) {
58 if (ours === null || ours === undefined || s.autoCompactWindow !== ours) return
59 delete s.autoCompactWindow
60 await $.store.delete(STORE_AUTOCOMPACT)
61 } else {
62 if (s.autoCompactWindow === want) return
63 s.autoCompactWindow = want
64 await $.store.set(STORE_AUTOCOMPACT, want)
65 }
66 await $.fs.write(path, JSON.stringify(s, null, 2) + '\n')
67}
68
69export const register: Register = (on, options) => {
70 // Band manager: hand this mod’s display to the Mod Box (it draws, orders and places it).
71 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
72 const rest = await next(e)
73 if (!e.props.hasSurvey) {
74 const part = await band($, e).catch(() => null)
75 if (part) offerBand(e, part)
76 }
77 return rest
78 })
79
80 ttlMs = (Number(options.ttlMinutes) || 60) * 60_000
81 showLimits = options.limits !== 'off'
82 keepWarmMs = (Number(options.keepWarmHours) || 3) * 60 * 60_000
83 lang = options.language === 'de' ? 'de' : 'en'
84
85 // Cache Tax runs inside: the guard (stop|warn|off) and auto keep-warm after a paid cold write.
86 const guard = options.guard === 'warn' ? 'warn' : options.guard === 'off' ? 'off' : 'refuse'
87 cacheTax(on, { embedded: true, guard, autoWarm: options.autoWarm === 'on' })
88
89 on('session.start', {}, async ($, e, next) => {
90 const r = await next(e)
91 await refreshContext($)
92 await applyAutoCompact($, String(options.autoCompact ?? 'auto')).catch(() => {})
93 $.clock.every(TICK_MS, () => void update($, tick, n => (n ?? 0) + 1).catch(() => {}))
94 return r
95 })
96
97 // Every request of the main loop writes (or reads) the cache: the clock starts here.
98 on('turn.step', {}, async function* ($, e, next) {
99 if (!e.agentId) {
100 const now = await $.clock.now()
101 await update($, info, i => ({ ...i, lastAt: now }))
102 }
103 yield* next(e)
104 })
105
106 on('turn.complete', {}, async ($, e, next) => {
107 const r = await next(e)
108 if (e.agentId) return r
109 const u = e.usage
110 const now = await $.clock.now()
111 let hit: number | null = null
112 if (u) {
113 const total = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
114 hit = total > 0 ? Math.round((u.cache_read_input_tokens / total) * 100) : null
115 }
116 await update($, info, i => ({ ...i, hit, lastAt: i.lastAt || now }))
117 await refreshContext($)
118 await addTrendPoint($)
119 return r
120 })
121
122 // After a compact the old cache is worthless: clock back, re-read the context.
123 on('session.compact', {}, async ($, e, next) => {
124 const r = await next(e)
125 if (!e.agentId) {
126 await update($, info, i => ({ ...i, lastAt: 0, hit: null }))
127 await refreshContext($)
128 await addTrendPoint($)
129 }
130 return r
131 })
132}
133
134// For the Mod Box band manager (instead of an own AbovePrompt hook).
135export async function band($: EngineInterface, e: any): Promise<BandPart | null> {
136 const i = await read($, info)
137 await read($, tick) // subscribes to the tick
138 const now = await $.clock.now()
139 const tax = taxInfo(now)
140 const { Text, Button } = $.ui.resolve(e)
141
142 const left = i.lastAt ? i.lastAt + ttlMs - now : 0
143 const isCold = !!i.lastAt && left <= 0
144 const cacheColor = !i.lastAt ? undefined : isCold ? 'red' : left < 10 * 60_000 ? 'yellow' : 'green'
145 const cacheWord = !i.lastAt ? '—' : isCold ? t('kalt', 'cold') : `${fmtMin(left)}`
146 const coldCost = isCold && tax?.coldUsd != null && tax.ctx > 0 ? ` ≈ ${fmtUsd(tax.coldUsd)}` : ''
147
148 const cache = (
149 <Text>
150 {'Cache '}
151 <Text color={cacheColor} bold>{!i.lastAt ? '—' : isCold ? t('kalt', 'cold') : 'warm'}</Text>
152 {!i.lastAt ? null : isCold ? (
153 <Text dimColor>{t(` · nächste Nachricht liest alles neu${coldCost}`, ` · next message re-reads everything${coldCost}`)}</Text>
154 ) : (
155 <Text dimColor>{t(` noch ${fmtMin(left)}`, ` ${fmtMin(left)} left`)}</Text>
156 )}
157 </Text>
158 )
159 const warmKept = tax?.isWarmKept ? (
160 <Text>
161 {' · '}
162 <Text color="cyan">{t(`🔥 warm gehalten ${fmtMin(tax.keptLeftMs)}`, `🔥 kept warm ${fmtMin(tax.keptLeftMs)}`)}</Text>
163 {tax.nextPingMs != null ? <Text dimColor>{t(` · Ping in ${fmtMin(tax.nextPingMs)}`, ` · ping in ${fmtMin(tax.nextPingMs)}`)}</Text> : null}
164 </Text>
165 ) : tax?.stopped ? (
166 <Text dimColor>{t(` · Warmhalten gestoppt: ${tax.stopped}`, ` · keep-warm stopped: ${tax.stopped}`)}</Text>
167 ) : null
168 const hit = i.hit == null ? null : (
169 <Text>
170 {t(' · Treffer ', ' · hits ')}
171 <Text color={i.hit >= 80 ? 'green' : i.hit >= 40 ? 'yellow' : 'red'}>{`${i.hit}%`}</Text>
172 </Text>
173 )
174 const pct = i.percent
175 const ctx = pct == null ? null : (
176 <Text>
177 {` · ${weather(pct)} ${t('Kontext', 'context')} `}
178 <Text color={pct < 50 ? 'green' : pct < 75 ? 'yellow' : 'red'} bold>{`${pct}%`}</Text>
179 {i.tokens != null && i.window ? <Text dimColor>{` (${fmtTok(i.tokens)}/${fmtTok(i.window)})`}</Text> : null}
180 {(i.trend ?? []).length >= 2 ? <Text color={pct < 50 ? 'green' : pct < 75 ? 'yellow' : 'red'} dimColor>{` ${sparkline(i.trend)}`}</Text> : null}
181 {trendText(i.trend ?? []) ? <Text dimColor>{` ${trendText(i.trend ?? [])}`}</Text> : null}
182 </Text>
183 )
184 const limits = showLimits && i.limits?.length ? (
185 <Text>
186 {t(' · Abo ', ' · plan ')}
187 {i.limits.map((l, n) => (
188 <Text key={`cm-l-${n}`}>
189 {n > 0 ? ' ' : ''}
190 <Text dimColor>{`${limitName(l.kind)} `}</Text>
191 <Text color={l.percentUsed < 60 ? 'green' : l.percentUsed < 85 ? 'yellow' : 'red'}>{`${Math.round(l.percentUsed)}%`}</Text>
192 </Text>
193 ))}
194 </Text>
195 ) : null
196
197 const actions: unknown[] = []
198 if (!e.props?.isWorking && (pct ?? 0) >= 30) actions.push(<Button key="cm-compact" label="Compact" plain onPress={() => void compact($)} />)
199 if (tax && !e.props?.isWorking && i.lastAt) {
200 actions.push(
201 tax.isWarmKept ? (
202 <Button key="cm-warm-off" label={t('Warmhalten aus', 'Stop keep-warm')} plain onPress={() => void keepWarm($, 'off')} />
203 ) : (
204 <Button key="cm-warm" label={t(`Warm halten ${fmtMin(keepWarmMs)}`, `Keep warm ${fmtMin(keepWarmMs)}`)} plain onPress={() => void keepWarm($, `${Math.round(keepWarmMs / 3_600_000)}h`)} />
205 ),
206 )
207 }
208
209 const shortWeather = pct == null ? '' : ` ${weather(pct)} ${pct}%`
210 return {
211 id: 'cacheMeter',
212 urgent: false,
213 short: (
214 <Text>
215 <Text color={cacheColor} bold>{`⏱ ${cacheWord}`}</Text>
216 {tax?.isWarmKept ? <Text color="cyan">{' 🔥'}</Text> : null}
217 <Text>{shortWeather}</Text>
218 </Text>
219 ),
220 line: (
221 <Text>
222 {cache}
223 {warmKept}
224 {hit}
225 {ctx}
226 {limits}
227 </Text>
228 ),
229 actions,
230 status: `Cache ${cacheWord}${tax?.isWarmKept ? ' 🔥' : ''}${shortWeather}`,
231 }
232}
233
234// Keep-warm runs in the embedded Cache Tax; its /keepwarm command switches it ($ may not cross an import).
235async function keepWarm($: EngineInterface, args: string) {
236 await $.command.run({ command: 'keepwarm', args }).catch(() => $.ui.toast(t(`Bitte /keepwarm ${args} eingeben.`, `Please type /keepwarm ${args}.`)))
237}
238
239async function refreshContext($: EngineInterface) {
240 const usage = await $.session.usage().catch(() => null)
241 if (!usage) return
242 const { context, rateLimits } = usage
243 await update($, info, i => ({
244 ...i,
245 percent: context.percent ?? null,
246 tokens: context.tokens ?? null,
247 window: context.window || null,
248 limits: (rateLimits ?? []).map(l => ({ kind: l.kind, percentUsed: l.percentUsed, ...(l.resetsAt ? { resetsAt: l.resetsAt } : {}) })),
249 }))
250}
251
252async function addTrendPoint($: EngineInterface) {
253 await update($, info, i => (i.percent == null ? i : { ...i, trend: [...(i.trend ?? []), i.percent].slice(-TREND_TURNS) }))
254}
255
256const BARS = '▁▂▃▄▅▆▇█'
257/** The context fill of the last turns as one bar each, on a fixed 0–100 % scale. */
258export function sparkline(points: number[]): string {
259 return points.map(p => BARS[Math.min(BARS.length - 1, Math.max(0, Math.floor((p / 100) * BARS.length)))]).join('')
260}
261
262/** Average growth per turn over the trend, and how many turns are left at that pace (no compact in between). */
263export function trendText(points: number[]): string {
264 const steps = points.slice(1).map((p, n) => p - points[n]!).filter(d => d >= 0)
265 if (steps.length < 2) return ''
266 const per = steps.reduce((a, b) => a + b, 0) / steps.length
267 if (per < 0.5) return t('· stabil', '· steady')
268 const left = Math.floor((100 - points[points.length - 1]!) / per)
269 return t(`· +${per.toFixed(1)}%/Runde, voll in ~${left}`, `· +${per.toFixed(1)}%/turn, full in ~${left}`)
270}
271
272// five_hour -> 5h, seven_day -> week, everything else stays readable.
273export function limitName(kind: string): string {
274 if (/five_hour|5h/i.test(kind)) return '5h'
275 if (/seven_day_opus/i.test(kind)) return t('Woche Opus', 'week Opus')
276 if (/seven_day_sonnet/i.test(kind)) return t('Woche Sonnet', 'week Sonnet')
277 if (/seven_day|week/i.test(kind)) return t('Woche', 'week')
278 return kind.replace(/_/g, ' ')
279}
280
281async function compact($: EngineInterface) {
282 $.ui.toast(t('Sitzung wird komprimiert …', 'Compacting the session …'))
283 try {
284 const before = (await read($, info)).tokens
285 const r = await $.session.compact()
286 if ('skip' in r && r.skip) {
287 $.ui.toast(t('Compact wurde von einem Hook abgelehnt.', 'A hook refused the compact.'))
288 return
289 }
290 await refreshContext($)
291 const after = (await read($, info)).tokens
292 $.ui.toast(
293 before != null && after != null
294 ? t(`Komprimiert: ${fmtTok(Math.max(0, before - after))} Token gespart.`, `Compacted: ${fmtTok(Math.max(0, before - after))} tokens saved.`)
295 : t('Komprimiert.', 'Compacted.'),
296 )
297 } catch {
298 $.ui.toast(t('Compact geht nur zwischen zwei Runden.', 'Compact only works between turns.'))
299 }
300}
301
302export function weather(pct: number): string {
303 return pct < 40 ? '☀' : pct < 60 ? '⛅' : pct < 80 ? '🌧' : '⛈'
304}
305
306export function fmtMin(ms: number): string {
307 const m = Math.max(0, Math.ceil(ms / 60_000))
308 return m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}` : `${m} min`
309}
310
311function fmtTok(n: number): string {
312 return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
313}
314
315function fmtUsd(usd: number): string {
316 return '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
317}
318mods/settings/hooks/band.ts 44 lines1// The band manager's mailbox. A band mod computes its display in its own AbovePrompt hook (with its
2// own $; the validator does not let $ cross an import) and drops it here with offer(). The settings
3// hook, registered first and so the outermost, runs after all of them, takes the parts and draws
4// them ordered, compact or one line each, or sends them to the status line.
5export type BandPart = {
6 /** The mod's switch key, e.g. "cacheMeter". */
7 id: string
8 /** Wants attention (CI red, sign offer …): always its own line with its buttons. */
9 urgent: boolean
10 /** One short element for the compact line. */
11 short: unknown
12 /** The full element for its own line. */
13 line: unknown
14 /** Buttons that belong to the part. */
15 actions: unknown[]
16 /** Plain text for the status line. */
17 status: string
18}
19
20const parts = new Map<string, BandPart[]>()
21
22function keyOf(e: { requestId?: string }): string {
23 return e.requestId ?? 'band'
24}
25
26/** Called by the manager before the inner hooks run. */
27export function openBand(e: { requestId?: string }) {
28 parts.set(keyOf(e), [])
29}
30
31/** Called by a band mod with its display for this render. */
32export function offer(e: { requestId?: string }, part: BandPart) {
33 const list = parts.get(keyOf(e))
34 if (list) list.push(part)
35 else parts.set(keyOf(e), [part])
36}
37
38/** Called by the manager after the inner hooks ran. */
39export function takeBand(e: { requestId?: string }): BandPart[] {
40 const list = parts.get(keyOf(e)) ?? []
41 parts.delete(keyOf(e))
42 return list
43}
44mods/cache-tax/hooks/register.ts 602 lines1// Modified for mod-box (https://github.com/LupusMalusDeviant/mod-box): embedded into Cache Meter (embed options, exported helpers, guard/auto-warm options, token usage), state/matchers rewritten.
2// Original: https://github.com/karanb192/cache-tax (commit 06cac47).
3import type { EngineInterface, Register } from 'claude-code'
4import { statusIcons } from './status-icons'
5import { update } from 'claude-code'
6import { addUsage, USAGE_STORE } from '../../settings/hooks/usage'
7import type { UsageBook } from '../../settings/hooks/usage'
8
9const TTL_MS = 60 * 60 * 1000
10const PING_AFTER_MS = 50 * 60 * 1000
11const MIN_PING_MS = 60 * 1000
12const AUTO_WARM_MS = 3 * 60 * 60 * 1000
13const DEFAULT_WINDOW_MS = 6 * 60 * 60 * 1000
14const BIG_TOKENS = 50000
15const PING_PROMPT = 'Reply with the single word: warm'
16const KEY_DEADLINE = 'deadline'
17const KEY_EVERY = 'every'
18const KEY_GUARD = 'guard'
19const KEY_ALWAYS = 'always'
20
21// $ per million tokens, [cache read, 1h cache write, output], list prices September 2026.
22// Longer family names first: a model id matches the first row it contains.
23const PRICES: Array<[string, number, number, number]> = [
24 ['fable-5-1', 0.25, 20, 50],
25 ['fable-5', 1, 20, 50],
26 ['opus-5', 0.5, 10, 25],
27 ['opus-4', 0.5, 10, 25],
28 ['sonnet-5', 0.2, 4, 10],
29 ['sonnet', 0.3, 6, 15],
30 ['haiku', 0.1, 2, 5],
31]
32
33type PingRecord = { at: number; read: number; write: number; usd: number | null; warm: boolean }
34type Miss = { at: number; tokens: number; usd: number | null }
35type GuardMode = 'refuse' | 'warn'
36
37export type State = {
38 hasBand: boolean
39 sid: string
40 deadline: number
41 every: number
42 always: boolean
43 lastRequestAt: number
44 lastModel: string | null
45 ctx: number
46 compacted: boolean
47 guard: GuardMode
48 ackedAt: number
49 coldWritePending: boolean
50 misses: Miss[]
51 pending: { cancel: () => void } | null
52 last: PingRecord | null
53 stopped: string | null
54}
55
56function priceOf(model: string | null): [number, number, number] | null {
57 const m = (model ?? '').toLowerCase().replace(/[\s.]+/g, '-')
58 for (const [family, read, write, output] of PRICES) if (m.includes(family)) return [read, write, output]
59 return null
60}
61
62export function parseDuration(text: string): number | null {
63 const m = /^(?:(\d+)h)?(?:(\d+)m)?$/.exec(text.trim())
64 if (!m || (m[1] === undefined && m[2] === undefined)) return null
65 return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 * 1000
66}
67
68export function fmtDuration(ms: number): string {
69 const total = Math.max(0, Math.round(ms / 60000))
70 const h = Math.floor(total / 60)
71 const m = total % 60
72 if (h >= 48) return `${Math.floor(h / 24)}d ${h % 24}h`
73 return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`
74}
75
76function fmtUsd(usd: number | null): string {
77 return usd == null ? 'n/a' : '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
78}
79
80function fmtTok(n: number): string {
81 return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
82}
83
84function coldUsd(s: State): number | null {
85 const price = priceOf(s.lastModel)
86 return price ? s.ctx * price[1] / 1e6 : null
87}
88
89function warmUsd(s: State): number | null {
90 const price = priceOf(s.lastModel)
91 return price ? s.ctx * price[0] / 1e6 : null
92}
93
94/** Everything a fork bills: the cache read, any cache write, uncached input at the base rate (half the 1h write rate), and the output. */
95function pingUsd(u: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }, price: [number, number, number]): number {
96 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
97}
98
99/** The read-only upper bound: pings at the cache-read rate that cost as much as one cold write of the same context. */
100function breakEvenPings(s: State): number | null {
101 const price = priceOf(s.lastModel)
102 if (!price || s.ctx <= 0) return null
103 return Math.floor(price[1] / price[0])
104}
105
106function isCold(s: State, now: number): boolean {
107 return s.lastRequestAt > 0 && !s.compacted && now - s.lastRequestAt >= TTL_MS
108}
109
110function guardText(s: State, now: number): string {
111 const price = priceOf(s.lastModel)
112 const rate = price ? `$${price[1]}/MTok` : 'the cache-write rate'
113 const warm = warmUsd(s)
114 return `the prompt cache went cold ${fmtDuration(now - s.lastRequestAt - TTL_MS)} ago. Sending this re-writes ` +
115 `up to ${s.ctx.toLocaleString('en-US')} tokens at ${rate} = ${fmtUsd(coldUsd(s))}` +
116 (warm == null ? '' : ` (a warm turn would have cost ${fmtUsd(warm)})`) + '.'
117}
118
119// Mod Box: the third way out of a cold send. Compacting cold writes about as much now, but every
120// later turn then carries a small context instead of the whole one.
121const COMPACT_AFTER = 20000
122const COMPACT_OUTPUT = 4000
123
124export function compactHint(ctx: number, model: string | null): string {
125 const price = priceOf(model)
126 if (!price || ctx < BIG_TOKENS) return ''
127 const now = (ctx * price[1] + COMPACT_OUTPUT * price[2]) / 1e6
128 const perTurn = ((ctx - COMPACT_AFTER) * price[0]) / 1e6
129 return ` Or /compact first: about ${fmtUsd(now)} now, then each later turn reads about ${fmtTok(COMPACT_AFTER)} instead of ${fmtTok(ctx)} tokens (about ${fmtUsd(perTurn)} less per turn; compaction loses detail).`
130}
131
132export type ResumeFields = {
133 source: string
134 model?: string
135 context_tokens?: number
136 seconds_since_last_response?: number
137 prompt_cache_likely_expired?: boolean
138 estimated_cache_write_usd?: number
139}
140
141/** /clear starts a new conversation in the same process; nothing priced before it still exists. */
142export function resetForClear(s: State) {
143 s.ctx = 0
144 s.lastRequestAt = 0
145 s.compacted = false
146 s.ackedAt = 0
147 s.coldWritePending = false
148 s.misses = []
149 disarm(s)
150}
151
152/** Applies a resumed session's fields to the state; returns the line to log, if any. */
153export function seedFromResume(s: State, e: ResumeFields, now: number): string | null {
154 if (e.source !== 'resume' && e.source !== 'fork') return null
155 if (typeof e.context_tokens === 'number' && e.context_tokens > 0) s.ctx = e.context_tokens
156 if (typeof e.seconds_since_last_response === 'number') s.lastRequestAt = now - e.seconds_since_last_response * 1000
157 if (typeof e.model === 'string') s.lastModel = e.model
158 s.compacted = false
159 if (e.prompt_cache_likely_expired !== true || s.ctx < BIG_TOKENS) return null
160 const usd = typeof e.estimated_cache_write_usd === 'number' ? fmtUsd(e.estimated_cache_write_usd) : fmtUsd(coldUsd(s))
161 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.`
162}
163
164function statusText(s: State, now: number): string | undefined {
165 if (s.stopped) return `keepwarm stopped: ${s.stopped}`
166 if (!s.deadline) return undefined
167 const pingText = s.last ? ` · last ping read ${fmtTok(s.last.read)} ${fmtUsd(s.last.usd)}` : ''
168 const nextText = !s.lastRequestAt ? ' · waiting for the first turn'
169 : s.compacted ? ' · waiting for the first turn after compaction'
170 : isCold(s, now) ? ` · cold now, first ping ${fmtDuration(s.every)} after the next turn`
171 : ` · ping in ${fmtDuration(s.lastRequestAt + s.every - now)}`
172 return `keepwarm ${fmtDuration(s.deadline - now)} left${nextText}${pingText}`
173}
174
175function updateStatus($: EngineInterface, s: State, now: number) {
176 if (s.hasBand) $.ui.invalidate('ui.render')
177 else $.ui.status(statusText(s, now))
178}
179
180function disarm(s: State) {
181 if (s.pending) s.pending.cancel()
182 s.pending = null
183}
184
185// The window and its ping period belong to the session that armed them, so a
186// second session, or one resumed from another transcript, never inherits them
187// and cannot turn them off. The always switch and the guard mode stay global.
188function deadlineKey(s: State): string {
189 return `${KEY_DEADLINE}:${s.sid}`
190}
191
192function everyKey(s: State): string {
193 return `${KEY_EVERY}:${s.sid}`
194}
195
196/** 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. */
197async function prune($: EngineInterface, s: State, now: number) {
198 for (const key of [KEY_DEADLINE, deadlineKey(s)]) {
199 const deadline = await $.store.get(key)
200 if (deadline === undefined) continue
201 if (typeof deadline === 'number' && deadline > now) continue
202 await $.store.delete(key)
203 await $.store.delete(KEY_EVERY + key.slice(KEY_DEADLINE.length))
204 }
205}
206
207async function stop($: EngineInterface, s: State, why: string | null, forgetAlways = false) {
208 s.deadline = 0
209 s.every = PING_AFTER_MS
210 s.stopped = why
211 disarm(s)
212 await $.store.delete(deadlineKey(s))
213 await $.store.delete(everyKey(s))
214 if (forgetAlways) {
215 s.always = false
216 await $.store.delete(KEY_ALWAYS)
217 }
218 updateStatus($, s, await $.clock.now())
219}
220
221async function arm($: EngineInterface, s: State) {
222 disarm(s)
223 if (!s.deadline) return
224 const now = await $.clock.now()
225 if (now >= s.deadline) return stop($, s, null)
226 // A cold window still needs expiry cleanup, but must not send a model request.
227 if (s.lastRequestAt && !s.compacted && !isCold(s, now)) {
228 const untilCold = s.lastRequestAt + TTL_MS - now
229 const delay = Math.min(s.deadline - now, untilCold, Math.max(1000, s.lastRequestAt + s.every - now))
230 s.pending = $.clock.after(delay, () => { void ping($, s) })
231 } else {
232 s.pending = $.clock.after(s.deadline - now, () => { void arm($, s) })
233 }
234 updateStatus($, s, now)
235}
236
237// Mod Box: token usage of this mod's own model calls, for the window (session in $.state,
238// total in $.store). $ stays in this file; usage.ts only does the arithmetic.
239const USAGE = { plugin: 'cache-meter', key: 'usage' } as const
240async function recordUsage($: EngineInterface, model: string, u: Parameters<typeof addUsage>[3] | undefined) {
241 if (!u) return
242 await update($, USAGE, b => addUsage(b, 'cacheMeter', model, u)).catch(() => {})
243 const total = await $.store.get(USAGE_STORE).catch(() => null)
244 await $.store.set(USAGE_STORE, addUsage(total as UsageBook | null, 'cacheMeter', model, u)).catch(() => {})
245}
246
247async function ping($: EngineInterface, s: State) {
248 s.pending = null
249 if (!s.deadline) return
250 const now = await $.clock.now()
251 if (now >= s.deadline) return arm($, s)
252 // A turn in the meantime re-armed the timer; this callback is stale.
253 if (isCold(s, now)) return arm($, s)
254 if (now - s.lastRequestAt < s.every - 1000) return
255 let reply
256 try {
257 reply = await $.model.fork({ prompt: PING_PROMPT })
258 } catch (err) {
259 return stop($, s, `the ping failed, ${err instanceof Error ? err.message : String(err)}`)
260 }
261 if (reply === null) return stop($, s, 'the engine did not send the ping, either the snapshot was cold or the API call failed')
262 if (reply.isAnswered === false) {
263 const reason = reply.reason === 'nothing-to-fork' ? 'no conversation to warm yet'
264 : reply.reason === 'api-error' ? `the API call failed${reply.status === null ? '' : ` (${reply.status})`}`
265 : reply.reason === 'aborted' ? 'the ping was interrupted'
266 : 'the ping returned no text'
267 return stop($, s, reason)
268 }
269 const u = reply.usage
270 await recordUsage($, s.lastModel ?? '', u)
271 const price = priceOf(s.lastModel)
272 // 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.
273 const warm = u.cache_read_input_tokens > 0 && u.cache_creation_input_tokens < 0.1 * u.cache_read_input_tokens
274 const usd = price ? pingUsd(u, price) : null
275 s.last = { at: now, read: u.cache_read_input_tokens, write: u.cache_creation_input_tokens, usd, warm }
276 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`)
277 s.lastRequestAt = now
278 await arm($, s)
279}
280
281/** The reply to an arming command; on a cold cache it says when the first ping can come. */
282function armedText(s: State, now: number, windowMs: number): string {
283 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`
284 return `keepwarm on for ${fmtDuration(windowMs)}, a ping ${fmtDuration(s.every)} after each idle stretch keeps the cache read, not re-written`
285}
286
287async function startWindow($: EngineInterface, s: State, windowMs: number, every: number) {
288 const now = await $.clock.now()
289 s.every = every
290 if (every === PING_AFTER_MS) await $.store.delete(everyKey(s))
291 else await $.store.set(everyKey(s), every)
292 s.deadline = now + windowMs
293 s.stopped = null
294 await $.store.set(deadlineKey(s), s.deadline)
295 await arm($, s)
296}
297
298function card(s: State, now: number): string {
299 const lines: string[] = []
300 lines.push(`${s.lastModel ?? 'model not seen yet'}`)
301 if (s.compacted) lines.push('state reset by compaction, waiting for the first turn')
302 else if (!s.lastRequestAt) lines.push('state no request yet this session')
303 else if (isCold(s, now)) lines.push(`state COLD, last request ${fmtDuration(now - s.lastRequestAt)} ago`)
304 else lines.push(`state warm, ${fmtDuration(s.lastRequestAt + TTL_MS - now)} left`)
305 lines.push(`context ${s.ctx.toLocaleString('en-US')} tokens`)
306 lines.push(`cold cost ${fmtUsd(coldUsd(s))} to re-write it (warm turn ${fmtUsd(warmUsd(s))})`)
307 const always = s.always ? ' (always)' : ''
308 const idle = s.always ? 'off until the next session start, which arms 6h00m (always)' : 'off (/keepwarm to arm it for 6h00m)'
309 lines.push(`keepwarm ${s.deadline ? (statusText(s, now) ?? '').replace(/^keepwarm /, 'on, ') + always : s.stopped ? `stopped, ${s.stopped}${always}` : idle}`)
310 const pings = breakEvenPings(s)
311 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)}`)
312 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)'}`)
313 const paid = s.misses.reduce((a, m) => a + (m.usd ?? 0), 0)
314 lines.push(`session ${s.misses.length} cold write${s.misses.length === 1 ? '' : 's'} paid, ${fmtUsd(paid)}`)
315 return lines.join('\n')
316}
317
318export function freshState(): State {
319 return {
320 hasBand: false, sid: '', deadline: 0, every: PING_AFTER_MS, always: false, lastRequestAt: 0, lastModel: null, ctx: 0, compacted: false,
321 guard: 'refuse', ackedAt: 0, coldWritePending: false, misses: [], pending: null, last: null, stopped: null,
322 }
323}
324
325// Mod Box: embedded in the Cache Meter. The Cache Meter shows the state (no band of its own),
326// sets the guard and whether to keep warm automatically after a paid cold write.
327type Embed = { embedded: boolean; guard: GuardMode | 'off' | null; autoWarm: boolean }
328let current: State | null = null
329let embed: Embed = { embedded: false, guard: null, autoWarm: true }
330
331export type TaxInfo = {
332 isWarmKept: boolean
333 keptLeftMs: number
334 nextPingMs: number | null
335 stopped: string | null
336 coldUsd: number | null
337 ctx: number
338 guard: GuardMode | 'off'
339 last: { read: number; usd: number | null } | null
340}
341
342/** State for the Cache Meter, null while Cache Tax is not loaded. */
343export function taxInfo(now: number): TaxInfo | null {
344 const s = current
345 if (!s) return null
346 const kept = s.deadline > now
347 return {
348 isWarmKept: kept,
349 keptLeftMs: kept ? s.deadline - now : 0,
350 nextPingMs: kept && s.lastRequestAt && !isCold(s, now) ? Math.max(0, s.lastRequestAt + s.every - now) : null,
351 stopped: s.stopped,
352 coldUsd: coldUsd(s),
353 ctx: s.ctx,
354 guard: embed.guard === 'off' ? 'off' : s.guard,
355 last: s.last ? { read: s.last.read, usd: s.last.usd } : null,
356 }
357}
358
359/** Start keeping warm for windowMs (like /keepwarm 3h). */
360export async function startKeepWarm($: EngineInterface, windowMs: number) {
361 if (current) await startWindow($, current, windowMs, PING_AFTER_MS)
362}
363
364/** Stop keeping warm (like /keepwarm off). */
365export async function stopKeepWarm($: EngineInterface) {
366 if (current) await stop($, current, null)
367}
368
369export const register: Register = (on, options) => {
370 const s = freshState()
371 current = s
372 embed = {
373 embedded: options?.embedded === true,
374 guard: options?.guard === 'off' ? 'off' : options?.guard === 'warn' ? 'warn' : options?.guard === 'refuse' ? 'refuse' : null,
375 autoWarm: options?.autoWarm !== false,
376 }
377 let light = false
378 let noColor = false
379
380 on('config.set', { key: 'theme' }, async ($, e, next) => {
381 const result = await next(e)
382 if (result.deny === undefined) {
383 light = typeof result.value === 'string' && result.value.startsWith('light')
384 if (s.hasBand) $.ui.invalidate('ui.render')
385 }
386 return result
387 })
388
389 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
390 const rest = await next(e)
391 if (embed.embedded || !s.hasBand || e.props.hasSurvey) return rest
392 const now = await $.clock.now()
393 const text = statusText(s, now)
394 if (!text || (s.deadline && now >= s.deadline)) return rest
395 const state = s.stopped ? 'stopped' : !s.lastRequestAt || s.compacted ? 'unknown' : isCold(s, now) ? 'cold' : 'warm'
396 const color = noColor ? undefined : state === 'warm' ? 'success' : state === 'cold' ? (light ? '#c15f3c' : '#d97757') : undefined
397 const { Box, Text } = $.ui.resolve(e)
398 const asset = statusIcons[light ? 'light' : 'dark'][state === 'warm' || state === 'cold' ? state : 'neutral']
399 const icon = noColor ? Text({ bold: true, children: ['[>]'] })
400 : e.surface === 'terminal' ? $.ui.resolve(e).Image({ source: { png: asset.png }, columns: 3, rows: 1, alt: '[>]' })
401 : e.surface === 'desktop' ? $.ui.resolve(e).Svg({ source: asset.svg, width: 18, height: 18, alt: 'Cache Tax' })
402 : Text({ color, bold: true, children: ['[>]'] })
403 return Box({ flexDirection: 'column', children: [
404 rest,
405 Box({ flexDirection: 'row', alignItems: 'center', children: [
406 Box({ flexShrink: 0, children: [icon] }),
407 Text({ children: [
408 ' cache-tax · ',
409 Text({ color, bold: true, children: [state] }),
410 ` · ${text}`,
411 ] }),
412 ] }),
413 ] })
414 })
415
416 on('session.start', {}, async ($, e, next) => {
417 const r = await next(e)
418 s.hasBand = e.surface === 'terminal' || e.surface === 'desktop'
419 if (s.hasBand) {
420 const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
421 light = typeof theme === 'string' && theme.startsWith('light')
422 noColor = Boolean(await $.env.get('NO_COLOR'))
423 $.ui.status(undefined)
424 }
425 s.sid = await $.session.id()
426 const now = await $.clock.now()
427 await prune($, s, now)
428 const saved = await $.store.get(deadlineKey(s))
429 const savedEvery = await $.store.get(everyKey(s))
430 const savedGuard = await $.store.get(KEY_GUARD)
431 s.deadline = typeof saved === 'number' && saved > now ? saved : 0
432 s.every = typeof savedEvery === 'number' && savedEvery >= MIN_PING_MS ? savedEvery : PING_AFTER_MS
433 s.guard = savedGuard === 'warn' ? 'warn' : 'refuse'
434 if (embed.guard === 'warn' || embed.guard === 'refuse') s.guard = embed.guard
435 s.always = (await $.store.get(KEY_ALWAYS)) === true
436 // Always means a fresh default window every session, whatever the last one left behind.
437 if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
438 const usage = await $.session.usage()
439 if (usage.context.tokens) s.ctx = usage.context.tokens
440 await $.command.register({
441 name: 'keepwarm',
442 description: 'Keep the prompt cache warm: bare for 6h, a window such as 90m, always, off, or status (cache-tax)',
443 argumentHint: '[6h | always | off | status]',
444 immediate: true,
445 })
446 await $.command.register({
447 name: 'cache-tax',
448 description: 'Prompt cache state, cold price and this session\'s cold writes; guard warn|refuse (cache-tax, the Mod)',
449 argumentHint: '[status | guard warn | guard refuse]',
450 immediate: true,
451 })
452 // The hook form of cache-tax ships a /cache-tax:status skill; both installed means two guards.
453 const commands = await $.command.list()
454 if (commands.some(c => c.name === 'cache-tax:status')) {
455 $.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.')
456 }
457 updateStatus($, s, now)
458 return r
459 })
460
461 // Resume fields seed the guard before any turn of the resumed session has run.
462 on('classic.SessionStart', {}, async ($, e, next) => {
463 const r = await next(e)
464 if (e.source === 'clear') {
465 await stop($, s, null)
466 s.stopped = null
467 resetForClear(s)
468 s.sid = await $.session.id()
469 updateStatus($, s, await $.clock.now())
470 return r
471 }
472 const line = seedFromResume(s, e, await $.clock.now())
473 // The resume payload may omit the model; without it the guard cannot price the cold write.
474 if (!s.lastModel) s.lastModel = await $.session.model()
475 if (line) $.ui.log(line)
476 // The seeded clock decides whether a restored or always window pings before the first turn: never when it is cold.
477 await arm($, s)
478 return r
479 })
480
481 on('command.run', { command: 'keepwarm' }, async ($, e) => {
482 const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
483 const now = await $.clock.now()
484 if (words[0] === 'off') {
485 const wasAlways = s.always
486 await stop($, s, null, true)
487 return { text: wasAlways ? 'keepwarm is off, and no longer arms itself at session start' : 'keepwarm is off' }
488 }
489 if (words[0] === 'always') {
490 s.always = true
491 await $.store.set(KEY_ALWAYS, true)
492 await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
493 const cold = isCold(s, now) ? `. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn` : ''
494 return { text: `keepwarm always on: every session starts with a ${fmtDuration(DEFAULT_WINDOW_MS)} window; /keepwarm off turns it off for good${cold}` }
495 }
496 if (!words.length) {
497 await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
498 return { text: armedText(s, now, DEFAULT_WINDOW_MS) }
499 }
500 if (words[0] !== 'status') {
501 const window = parseDuration(words[0]!)
502 if (window == null) return { text: 'keepwarm takes a window such as 6h or 90m, or always, off, or status' }
503 // "every 2m" is a testing knob and lasts only for the window it was given with.
504 let every = PING_AFTER_MS
505 if (words[1] === 'every') {
506 const period = parseDuration(words[2] ?? '')
507 if (period == null || period < MIN_PING_MS) return { text: 'every takes a period of at least 1m' }
508 every = period
509 }
510 await startWindow($, s, window, every)
511 return { text: armedText(s, now, window) }
512 }
513 return { text: statusText(s, now) ?? 'keepwarm is off' }
514 })
515
516 on('command.run', { command: 'cache-tax' }, async ($, e) => {
517 const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
518 const now = await $.clock.now()
519 if (words[0] === 'guard') {
520 if (words[1] !== 'warn' && words[1] !== 'refuse') return { text: '/cache-tax guard takes warn or refuse' }
521 s.guard = words[1]
522 await $.store.set(KEY_GUARD, s.guard)
523 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' }
524 }
525 return { text: card(s, now) }
526 })
527
528 // The message that pays. Only its first character is read.
529 on('prompt.submit', {}, async ($, e, next) => {
530 if (embed.guard === 'off') return next(e)
531 if (e.origin.kind === 'plugin') return next(e)
532 if (typeof e.text !== 'string' || e.text.trimStart().startsWith('/')) return next(e)
533 const now = await $.clock.now()
534 if (!isCold(s, now) || s.ctx < BIG_TOKENS) return next(e)
535 if (s.guard === 'warn') {
536 $.ui.log(`${guardText(s, now)} Sending anyway; keepwarm will hold the cache for ${fmtDuration(AUTO_WARM_MS)} once it lands.${compactHint(s.ctx, s.lastModel)}`)
537 s.coldWritePending = true
538 return next(e)
539 }
540 if (s.ackedAt === s.lastRequestAt) {
541 s.ackedAt = 0
542 s.coldWritePending = true
543 return next(e)
544 }
545 s.ackedAt = s.lastRequestAt
546 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)}.${compactHint(s.ctx, s.lastModel)} Or /clear and start from a note.` }
547 })
548
549 on('turn.step', {}, async function* ($, e, next) {
550 if (!e.agentId) s.lastRequestAt = await $.clock.now()
551 yield* next(e)
552 })
553
554 on('turn.complete', {}, async ($, e, next) => {
555 const r = await next(e)
556 if (e.agentId) return r
557 const now = await $.clock.now()
558 // A sleeping host may deliver this turn before the expired window's timer.
559 if (s.deadline && now >= s.deadline) await stop($, s, null)
560 // turn.step stamps the exact request time; when no step of this turn did, the turn's end is the floor.
561 if (now - s.lastRequestAt > e.durationMs) s.lastRequestAt = now
562 s.compacted = false
563 s.ackedAt = 0
564 const u = e.usage
565 if (u) {
566 if (u.model) s.lastModel = u.model
567 const prev = s.ctx
568 // A turn's usage is its responses summed, so a ten-step turn reports ten
569 // reads of the context. The live window is the engine's figure; the sum
570 // is only the fallback for a host that reports no tokens.
571 const write = u.cache_creation_input_tokens
572 const live = (await $.session.usage()).context.tokens
573 s.ctx = live && live > 0 ? live : u.input_tokens + u.cache_read_input_tokens + write
574 const full = prev > 20000 && write >= 0.5 * prev
575 if (full || s.coldWritePending) {
576 const price = priceOf(s.lastModel)
577 const usd = price ? write * price[1] / 1e6 : null
578 s.misses.push({ at: now, tokens: write, usd })
579 if (embed.autoWarm && s.deadline < now + AUTO_WARM_MS) {
580 await startWindow($, s, AUTO_WARM_MS, s.every)
581 $.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.`)
582 }
583 }
584 }
585 s.coldWritePending = false
586 await arm($, s)
587 return r
588 })
589
590 on('session.compact', {}, async ($, e, next) => {
591 const r = await next(e)
592 if (!e.agentId) {
593 s.compacted = true
594 s.ctx = 0
595 s.ackedAt = 0
596 disarm(s)
597 await arm($, s)
598 }
599 return r
600 })
601}
602mods/cache-tax/hooks/status-icons.ts 32 lines1// 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
32mods/settings/hooks/usage.ts 89 lines1// Token usage of the mods that call a model themselves (Next Steps, Heads-up, Catch Me Up,
2// Handoff Note, keep-warm in the Cache Meter). Pure data and arithmetic: each of those mods records
3// its calls with its own $ (session in $.state "mb_usage", total in $.store "usage-total"),
4// the window only reads. Costs are an estimate from list prices.
5
6export type ModUsage = {
7 calls: number
8 input: number
9 output: number
10 cacheRead: number
11 cacheWrite: number
12 /** Estimated US dollars of the calls whose model had a price. */
13 usd: number
14 /** Calls whose model had no price in the table. */
15 unpriced: number
16}
17
18export type UsageBook = Record<string, ModUsage>
19
20export const USAGE_STORE = 'usage-total'
21
22type Usage = {
23 input_tokens: number
24 output_tokens: number
25 cache_read_input_tokens?: number | null
26 cache_creation_input_tokens?: number | null
27}
28
29// $ per million tokens: [cache read, cache write (1 h), output]; base input is half the 1 h write.
30// List prices as Cache Tax keeps them (September 2026). Longer family names first.
31const PRICES: Array<[string, number, number, number]> = [
32 ['fable-5-1', 0.25, 20, 50],
33 ['fable-5', 1, 20, 50],
34 ['opus-5', 0.5, 10, 25],
35 ['opus-4', 0.5, 10, 25],
36 ['opus', 0.5, 10, 25],
37 ['sonnet-5', 0.2, 4, 10],
38 ['sonnet', 0.3, 6, 15],
39 ['haiku', 0.1, 2, 5],
40]
41
42export function priceOf(model: string): [number, number, number] | null {
43 const m = model.toLowerCase().replace(/[\s.]+/g, '-')
44 for (const [family, read, write, output] of PRICES) if (m.includes(family)) return [read, write, output]
45 return null
46}
47
48export function costOf(model: string, u: Usage): number | null {
49 const p = priceOf(model)
50 if (!p) return null
51 const [read, write, output] = p
52 return ((u.cache_read_input_tokens ?? 0) * read + (u.cache_creation_input_tokens ?? 0) * write + u.input_tokens * (write / 2) + u.output_tokens * output) / 1e6
53}
54
55export function addUsage(book: UsageBook | null | undefined, id: string, model: string, u: Usage): UsageBook {
56 const prev = book?.[id] ?? { calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, usd: 0, unpriced: 0 }
57 const usd = costOf(model, u)
58 return {
59 ...(book ?? {}),
60 [id]: {
61 calls: prev.calls + 1,
62 input: prev.input + u.input_tokens,
63 output: prev.output + u.output_tokens,
64 cacheRead: prev.cacheRead + (u.cache_read_input_tokens ?? 0),
65 cacheWrite: prev.cacheWrite + (u.cache_creation_input_tokens ?? 0),
66 usd: prev.usd + (usd ?? 0),
67 unpriced: prev.unpriced + (usd == null ? 1 : 0),
68 },
69 }
70}
71
72export function tokensOf(m: ModUsage): number {
73 return m.input + m.output + m.cacheRead + m.cacheWrite
74}
75
76export function sumBook(book: UsageBook | null | undefined): ModUsage {
77 const out = { calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, usd: 0, unpriced: 0 }
78 for (const m of Object.values(book ?? {})) {
79 out.calls += m.calls
80 out.input += m.input
81 out.output += m.output
82 out.cacheRead += m.cacheRead
83 out.cacheWrite += m.cacheWrite
84 out.usd += m.usd
85 out.unpriced += m.unpriced
86 }
87 return out
88}
89mods/cache-meter/types/contract.d.ts 31 lines1/** Model calls the mod made itself: counted per session, priced at list price. */
2export type ModUsage = {
3 calls: number
4 input: number
5 output: number
6 cacheRead: number
7 cacheWrite: number
8 usd: number
9 unpriced: number
10}
11
12export type MeterInfo = {
13 /** Time of the main loop's last request (ms), 0 = unknown/cold. */
14 lastAt: number
15 /** Share of the last turn served from the cache, in percent. */
16 hit: number | null
17 percent: number | null
18 tokens: number | null
19 window: number | null
20 /** The subscription's rate-limit windows (5 hours, week), as the status line has them. */
21 limits: Array<{ kind: string; percentUsed: number; resetsAt?: string }>
22 /** Context fill after each of the last turns (percent, oldest first), for the trend line. */
23 trend: number[]
24}
25
26declare module 'claude-code' {
27 interface PluginState {
28 'cache-meter': { usage: Record<string, ModUsage>; info: MeterInfo; tick: number }
29 }
30}
31