@iaziz786/cmd-footer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Aziz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # cmd-footer
2
+
3
+ [Command Code](https://commandcode.ai) mod that puts cache hit ratio and decode
4
+ throughput in the footer, next to the input panel.
5
+
6
+ The footer segment looks like:
7
+
8
+ ```text
9
+ cache last 99.5% | avg 97.8% | 250 t/s
10
+ ```
11
+
12
+ | Segment | Meaning |
13
+ |---|---|
14
+ | `cache last` | Cache hit ratio of the last request: cache reads over prompt tokens |
15
+ | `avg` | Session-wide ratio, weighted by tokens rather than averaged per request |
16
+ | `t/s` | Decode rate of the last reply: output tokens over the stream window |
17
+
18
+ `cache last` turns red when a request re-billed tokens that should have been
19
+ cache reads, even when the ratio still looks healthy. A 99% request that
20
+ re-reads 3k tokens shows red, because those tokens cost full price.
21
+
22
+ Type `/cache` for the detailed line: token counts, request count, decode rate,
23
+ and whether the last turn re-billed.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ cmd mods add npm:@iaziz786/cmd-footer
29
+ ```
30
+
31
+ Or try it without installing:
32
+
33
+ ```bash
34
+ cmd --mod ./src/cmd-footer.ts
35
+ ```
36
+
37
+ ## How the numbers are measured
38
+
39
+ **Cache ratio.** `cacheReadTokens / inputTokens`. The AI SDK reports
40
+ `inputTokens` as the whole prompt, with cache reads and writes already inside
41
+ it, so the ratio is the cached share of that total.
42
+
43
+ **Decode rate.** Output tokens over the span from the first streamed delta to
44
+ the end of the response, so the wait for the first token is excluded. Three
45
+ conditions make a measurement untrustworthy and the previous reading is kept
46
+ instead:
47
+
48
+ - fewer than 4 stream deltas, or a window shorter than 300ms: delivery could
49
+ not be observed as a stream
50
+ - a rate above 400 t/s: a proxy buffered the reply and flushed it in one burst,
51
+ which compresses the window and invents impossible throughput
52
+
53
+ **Miss detection.** A request re-billed when the prompt of the previous request
54
+ was not read back from cache. Re-bills at or below 1024 tokens are treated as
55
+ cache breakpoint granularity noise. A compaction clears the baseline, so
56
+ freshly compacted content is not mistaken for a re-bill. Providers that never
57
+ report caching never show a miss.
58
+
59
+ The flag covers the whole run rather than a single request, because one prompt
60
+ can take several model calls and an early call can miss while the last one
61
+ hits.
62
+
63
+ ## Tuning
64
+
65
+ The thresholds are exported constants in `src/cmd-footer.ts`:
66
+
67
+ | Constant | Default | Effect |
68
+ |---|---|---|
69
+ | `MAX_PLAUSIBLE_RATE` | 400 | Raise it on faster hardware, or the rate stops updating |
70
+ | `MISS_NOISE_FLOOR_TOKENS` | 1024 | Lower it to flag smaller re-bills |
71
+ | `MIN_RATE_SPAN_MS` | 300 | Shortest window that can resolve a rate |
72
+ | `MIN_RATE_CHUNKS` | 4 | Fewest deltas that count as a stream |
73
+
74
+ ## Persistence
75
+
76
+ Totals survive `/reload` and resuming a session. One entry per run is written
77
+ through the session API, so a restored session continues its average instead of
78
+ starting over. A new session starts at zero.
79
+
80
+ ## Develop
81
+
82
+ ```bash
83
+ bun test
84
+ ```
85
+
86
+ ## Publish
87
+
88
+ Publishing runs on version tags. The tag must match `package.json`.
89
+
90
+ ```bash
91
+ npm version patch
92
+ git push --follow-tags
93
+ ```
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@iaziz786/cmd-footer",
3
+ "version": "0.1.0",
4
+ "description": "Command Code mod that puts cache hit ratio and decode throughput in the footer.",
5
+ "keywords": ["command-code", "cmd", "mod", "footer", "cache", "tokens", "tps"],
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Mohammad Aziz",
9
+ "homepage": "https://github.com/iAziz786/cmd-footer#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+ssh://git@github.com/iAziz786/cmd-footer.git"
13
+ },
14
+ "files": ["src", "README.md", "LICENSE"],
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "scripts": {
19
+ "test": "bun test",
20
+ "prepublishOnly": "bun test"
21
+ },
22
+ "commandcode": {
23
+ "mods": ["./src/cmd-footer.ts"]
24
+ }
25
+ }
@@ -0,0 +1,449 @@
1
+ import type {ModApi} from '@commandcode/harness';
2
+
3
+ // Namespaced because session.getCustomEntries filters by customType alone, so
4
+ // the type is the identity - it is not scoped per mod.
5
+ export const CUSTOM_TYPE = 'cache-ratio/usage';
6
+
7
+ export const DIM = '\u001b[2m';
8
+ export const GREEN = '\u001b[32m';
9
+ export const YELLOW = '\u001b[33m';
10
+ export const RED = '\u001b[31m';
11
+ export const CYAN = '\u001b[36m';
12
+ export const RESET = '\u001b[0m';
13
+
14
+ /** Fewer deltas than this over the window means delivery was bursty. */
15
+ export const MIN_RATE_CHUNKS = 4;
16
+
17
+ /** Spans shorter than this cannot resolve a decode rate. */
18
+ export const MIN_RATE_SPAN_MS = 300;
19
+
20
+ /**
21
+ * Public decode ceilings run a few hundred t/s; anything above is suspect.
22
+ * Raise this if a genuinely fast model starts showing a stale rate.
23
+ */
24
+ export const MAX_PLAUSIBLE_RATE = 400;
25
+
26
+ export interface RateWindow {
27
+ /** model_request_start timestamp: post-headers, pre-first-chunk. */
28
+ readonly startTs: number;
29
+ /** model_request_end timestamp. */
30
+ readonly endTs: number;
31
+ /** First delta timestamp; falls back to startTs when absent. */
32
+ readonly firstDeltaTs?: number;
33
+ /** Streamed delta events observed (text + thinking). */
34
+ readonly chunkCount: number;
35
+ }
36
+
37
+ interface UsageLike {
38
+ // AI SDK semantics: inputTokens is the prompt TOTAL. Cache read and cache
39
+ // write tokens are subsets of it, not additions to it.
40
+ readonly inputTokens?: number;
41
+ readonly cacheReadTokens?: number;
42
+ readonly cacheWriteTokens?: number;
43
+ }
44
+
45
+ /**
46
+ * Re-billed prompt tokens at or below this are cache breakpoint granularity
47
+ * noise, not a miss. Same floor pi's footer uses.
48
+ */
49
+ export const MISS_NOISE_FLOOR_TOKENS = 1024;
50
+
51
+ /** Previous request, the baseline a miss is judged against. */
52
+ export interface PreviousRequest {
53
+ readonly promptTokens: number;
54
+ readonly reportedCache: boolean;
55
+ }
56
+
57
+ interface CacheTotals {
58
+ readonly cachedTokens: number;
59
+ readonly promptTokens: number;
60
+ }
61
+
62
+ interface CacheStat extends CacheTotals {
63
+ readonly ratio: number | null;
64
+ }
65
+
66
+ interface Snapshot {
67
+ readonly last: number | null;
68
+ readonly average: number | null;
69
+ readonly requests: number;
70
+ readonly cachedTokens: number;
71
+ readonly promptTokens: number;
72
+ }
73
+
74
+ interface CacheState {
75
+ readonly last: CacheStat | null;
76
+ readonly totals: CacheTotals;
77
+ readonly requests: number;
78
+ }
79
+
80
+ // The on-disk shape. Flat numbers only: the session file is append-only JSONL,
81
+ // so a snapshot must survive being read back by a later process.
82
+ interface PersistedState {
83
+ readonly cachedTokens: number;
84
+ readonly promptTokens: number;
85
+ readonly requests: number;
86
+ readonly lastCachedTokens: number;
87
+ readonly lastPromptTokens: number;
88
+ }
89
+
90
+ export function tokenCount(value: number | undefined): number {
91
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
92
+ }
93
+
94
+ export function ratioOf(totals: CacheTotals): number | null {
95
+ return totals.promptTokens > 0
96
+ ? totals.cachedTokens / totals.promptTokens
97
+ : null;
98
+ }
99
+
100
+ export function cacheStat(usage: UsageLike): CacheStat {
101
+ const cachedTokens = tokenCount(usage.cacheReadTokens);
102
+ const promptTokens = tokenCount(usage.inputTokens);
103
+ return {cachedTokens, promptTokens, ratio: ratioOf({cachedTokens, promptTokens})};
104
+ }
105
+
106
+ export function combine(totals: CacheTotals, next: CacheTotals): CacheTotals {
107
+ return {
108
+ cachedTokens: totals.cachedTokens + next.cachedTokens,
109
+ promptTokens: totals.promptTokens + next.promptTokens,
110
+ };
111
+ }
112
+
113
+ export function emptyState(): CacheState {
114
+ return {last: null, totals: {cachedTokens: 0, promptTokens: 0}, requests: 0};
115
+ }
116
+
117
+ export function applyRequest(state: CacheState, stat: CacheStat): CacheState {
118
+ return {
119
+ last: stat,
120
+ totals: combine(state.totals, stat),
121
+ requests: state.requests + 1,
122
+ };
123
+ }
124
+
125
+ export function snapshotOf(state: CacheState): Snapshot {
126
+ return {
127
+ last: state.last?.ratio ?? null,
128
+ average: ratioOf(state.totals),
129
+ requests: state.requests,
130
+ cachedTokens: state.last?.cachedTokens ?? 0,
131
+ promptTokens: state.last?.promptTokens ?? 0,
132
+ };
133
+ }
134
+
135
+ export function serializeState(state: CacheState): PersistedState {
136
+ return {
137
+ cachedTokens: state.totals.cachedTokens,
138
+ promptTokens: state.totals.promptTokens,
139
+ requests: state.requests,
140
+ lastCachedTokens: state.last?.cachedTokens ?? 0,
141
+ lastPromptTokens: state.last?.promptTokens ?? 0,
142
+ };
143
+ }
144
+
145
+ function nonNegative(value: unknown): number | null {
146
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
147
+ ? value
148
+ : null;
149
+ }
150
+
151
+ // Restore is the one place that reads data written by an older process, so it
152
+ // validates instead of trusting the shape.
153
+ export function parseState(data: unknown): CacheState | null {
154
+ if (typeof data !== 'object' || data === null) return null;
155
+ const raw = data as Record<string, unknown>;
156
+
157
+ const cachedTokens = nonNegative(raw.cachedTokens);
158
+ const promptTokens = nonNegative(raw.promptTokens);
159
+ const requests = nonNegative(raw.requests);
160
+ const lastCached = nonNegative(raw.lastCachedTokens);
161
+ const lastPrompt = nonNegative(raw.lastPromptTokens);
162
+ if (
163
+ cachedTokens === null ||
164
+ promptTokens === null ||
165
+ requests === null ||
166
+ lastCached === null ||
167
+ lastPrompt === null
168
+ ) {
169
+ return null;
170
+ }
171
+
172
+ const last: CacheStat | null =
173
+ lastPrompt > 0
174
+ ? {
175
+ cachedTokens: lastCached,
176
+ promptTokens: lastPrompt,
177
+ ratio: ratioOf({cachedTokens: lastCached, promptTokens: lastPrompt}),
178
+ }
179
+ : null;
180
+
181
+ return {last, totals: {cachedTokens, promptTokens}, requests};
182
+ }
183
+
184
+ export function formatPercent(ratio: number | null): string {
185
+ return ratio === null ? 'n/a' : `${(ratio * 100).toFixed(1)}%`;
186
+ }
187
+
188
+ export function formatTokens(count: number): string {
189
+ if (count < 1000) return String(count);
190
+ if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;
191
+ return `${(count / 1_000_000).toFixed(1)}M`;
192
+ }
193
+
194
+ export function colorFor(ratio: number | null): string {
195
+ if (ratio === null) return DIM;
196
+ if (ratio >= 0.8) return GREEN;
197
+ if (ratio >= 0.5) return YELLOW;
198
+ return RED;
199
+ }
200
+
201
+ /**
202
+ * Decode throughput: output tokens over the span from the first streamed delta
203
+ * to the end of the response, so the wait for the first token is excluded.
204
+ *
205
+ * The span is only trustworthy when delivery was genuinely incremental. A proxy
206
+ * that buffers the whole reply and flushes it in one burst compresses the span
207
+ * toward zero and reports impossible rates, so those measurements are rejected
208
+ * instead of shown.
209
+ */
210
+ export function computeTokenRate(
211
+ outputTokens: number,
212
+ window: RateWindow,
213
+ ): number | undefined {
214
+ if (outputTokens <= 0) return undefined;
215
+
216
+ const rateStartTs = window.firstDeltaTs ?? window.startTs;
217
+ const spanMs = window.endTs - rateStartTs;
218
+ if (window.chunkCount < MIN_RATE_CHUNKS || spanMs < MIN_RATE_SPAN_MS) {
219
+ return undefined;
220
+ }
221
+
222
+ const rate = outputTokens / (spanMs / 1000);
223
+ return Number.isFinite(rate) && rate <= MAX_PLAUSIBLE_RATE ? rate : undefined;
224
+ }
225
+
226
+ export function formatRate(rate: number | undefined): string | null {
227
+ return rate === undefined ? null : `${Math.round(rate)} t/s`;
228
+ }
229
+
230
+ /**
231
+ * A miss is previous-prompt tokens that should have been cache reads but were
232
+ * re-billed. Ported from pi's footer with one adaptation: there prompt tokens
233
+ * are `input + cacheRead + cacheWrite`, here `inputTokens` is already the total.
234
+ *
235
+ * Hits, the first request, and providers that never report caching are not
236
+ * misses. Small re-bills are noise, not misses.
237
+ */
238
+ export function detectMiss(
239
+ previous: PreviousRequest | undefined,
240
+ usage: UsageLike,
241
+ ): boolean {
242
+ const promptTokens = tokenCount(usage.inputTokens);
243
+ if (!previous || promptTokens <= 0) return false;
244
+
245
+ const cacheRead = tokenCount(usage.cacheReadTokens);
246
+ const cacheWrite = tokenCount(usage.cacheWriteTokens);
247
+ // A zero-cache request only counts when caching was seen before: on a
248
+ // cache-read-only provider that is a total miss, while on a provider that
249
+ // has never reported caching it means nothing.
250
+ if (cacheRead + cacheWrite === 0 && !previous.reportedCache) return false;
251
+
252
+ // min() guards a shrunk context: a prompt that got smaller cannot have been
253
+ // re-billed beyond its own size.
254
+ return Math.min(previous.promptTokens, promptTokens) - cacheRead > MISS_NOISE_FLOOR_TOKENS;
255
+ }
256
+
257
+ export function nextPrevious(
258
+ usage: UsageLike,
259
+ reportedCache: boolean,
260
+ ): PreviousRequest | undefined {
261
+ const promptTokens = tokenCount(usage.inputTokens);
262
+ if (promptTokens <= 0) return undefined;
263
+ return {
264
+ promptTokens,
265
+ reportedCache:
266
+ reportedCache ||
267
+ tokenCount(usage.cacheReadTokens) + tokenCount(usage.cacheWriteTokens) > 0,
268
+ };
269
+ }
270
+
271
+ /** A miss outranks the ratio: a healthy percentage can still hide re-billed tokens. */
272
+ export function lastColor(ratio: number | null, missed: boolean): string {
273
+ return missed ? RED : colorFor(ratio);
274
+ }
275
+
276
+ // The rate segment is omitted, not shown as 0, while no measurement exists:
277
+ // "0 t/s" would claim the model is stalled rather than unknown.
278
+ function rateSuffix(rate: number | undefined, color: boolean): string {
279
+ const text = formatRate(rate);
280
+ if (text === null) return '';
281
+ return color ? ` ${DIM}|${RESET} ${CYAN}${text}${RESET}` : ` | ${text}`;
282
+ }
283
+
284
+ export function plainStatus(
285
+ snapshot: Snapshot,
286
+ rate: number | undefined,
287
+ missed: boolean,
288
+ ): string {
289
+ return (
290
+ `cache last ${formatPercent(snapshot.last)} | avg ${formatPercent(snapshot.average)}` +
291
+ rateSuffix(rate, false) +
292
+ (missed ? ' | miss' : '')
293
+ );
294
+ }
295
+
296
+ // The footer renders through Ink's <Text>, which normalizes these codes instead
297
+ // of stripping them. Headless hosts print notices to a log, so they get the
298
+ // plain variant.
299
+ export function statusLine(
300
+ snapshot: Snapshot,
301
+ rate: number | undefined,
302
+ missed: boolean,
303
+ ): string {
304
+ return (
305
+ `${DIM}cache${RESET} last ${lastColor(snapshot.last, missed)}${formatPercent(snapshot.last)}${RESET}` +
306
+ ` ${DIM}| avg${RESET} ${colorFor(snapshot.average)}${formatPercent(snapshot.average)}${RESET}` +
307
+ rateSuffix(rate, true)
308
+ );
309
+ }
310
+
311
+ export function detailLine(
312
+ snapshot: Snapshot,
313
+ rate: number | undefined,
314
+ missed: boolean,
315
+ ): string {
316
+ if (snapshot.requests === 0) return 'No request yet this session.';
317
+ const speed = formatRate(rate);
318
+ return (
319
+ `cache hit last ${formatPercent(snapshot.last)} ` +
320
+ `(${formatTokens(snapshot.cachedTokens)}/${formatTokens(snapshot.promptTokens)} prompt tokens) | ` +
321
+ `avg ${formatPercent(snapshot.average)} over ${snapshot.requests} request(s)` +
322
+ (speed === null ? '' : ` | ${speed} decode`) +
323
+ (missed ? ' | cache miss in last turn' : '')
324
+ );
325
+ }
326
+
327
+ export default function (cmd: ModApi): void {
328
+ let state = emptyState();
329
+ let restored = false;
330
+ let dirty = false;
331
+
332
+ // Decode window for the request in flight. cmd streams text and thinking
333
+ // deltas; tool-call arguments arrive whole, so they contribute no deltas.
334
+ let stream: {startTs: number; firstDeltaTs?: number; chunkCount: number} | null = null;
335
+ let rate: number | undefined;
336
+
337
+ // Miss tracking. A run is cmd's user turn: one prompt, several requests. An
338
+ // early request can miss while the last one hits, so the flag lives for the
339
+ // run rather than per request.
340
+ let previous: PreviousRequest | undefined;
341
+ let missedThisRun = false;
342
+
343
+ const repaint = (): void => {
344
+ const snapshot = snapshotOf(state);
345
+ if (cmd.ui.capabilities.status) cmd.ui.setStatus(statusLine(snapshot, rate, missedThisRun));
346
+ else cmd.ui.notify(plainStatus(snapshot, rate, missedThisRun));
347
+ };
348
+
349
+ // Seeding is idempotent: session_start covers the normal path, and the first
350
+ // request covers a host that binds without firing it. Restoring must happen
351
+ // before any request is counted, or the totals restart from zero.
352
+ const restore = (): void => {
353
+ if (restored) return;
354
+ const store = cmd.session;
355
+ if (!store) return;
356
+ restored = true;
357
+
358
+ const entries = store.getCustomEntries({customType: CUSTOM_TYPE});
359
+ const parsed = parseState(entries.at(-1)?.data);
360
+ if (!parsed) return;
361
+
362
+ state = parsed;
363
+ // Rebuild the miss baseline so a reload can still judge the next request.
364
+ // reportedCache is approximated from the last ratio: a request with cache
365
+ // reads proves the provider reports caching.
366
+ if (state.last) {
367
+ previous = {
368
+ promptTokens: state.last.promptTokens,
369
+ reportedCache: state.last.cachedTokens > 0,
370
+ };
371
+ }
372
+ if (cmd.ui.capabilities.status) {
373
+ cmd.ui.setStatus(statusLine(snapshotOf(state), rate, missedThisRun));
374
+ }
375
+ };
376
+
377
+ cmd.on('session_start', restore);
378
+
379
+ cmd.on('run_start', () => {
380
+ missedThisRun = false;
381
+ });
382
+
383
+ // Context legitimately changed, so the next request is new content being
384
+ // billed, not the old prompt being re-billed.
385
+ cmd.on('compaction_done', () => {
386
+ previous = undefined;
387
+ });
388
+
389
+ cmd.on('model_request_start', () => {
390
+ stream = {startTs: Date.now(), chunkCount: 0};
391
+ });
392
+
393
+ const noteDelta = (): void => {
394
+ if (!stream) return;
395
+ stream.firstDeltaTs ??= Date.now();
396
+ stream.chunkCount += 1;
397
+ };
398
+
399
+ cmd.on('text_delta', noteDelta);
400
+ cmd.on('thinking_delta', noteDelta);
401
+
402
+ cmd.on('model_request_end', event => {
403
+ const usage = (event as {usage?: UsageLike}).usage;
404
+ if (!usage) return;
405
+ restore();
406
+
407
+ const open = stream;
408
+ stream = null;
409
+
410
+ if (detectMiss(previous, usage)) missedThisRun = true;
411
+ previous = nextPrevious(usage, previous?.reportedCache ?? false) ?? previous;
412
+
413
+ state = applyRequest(state, cacheStat(usage));
414
+ dirty = true;
415
+
416
+ // A window that cannot be trusted keeps the previous reading: dropping
417
+ // to nothing would read as a stall, when the truth is "unmeasurable".
418
+ if (open) {
419
+ const measured = computeTokenRate(tokenCount(usage.outputTokens), {
420
+ startTs: open.startTs,
421
+ endTs: Date.now(),
422
+ ...open.firstDeltaTs === undefined ? {} : {firstDeltaTs: open.firstDeltaTs},
423
+ chunkCount: open.chunkCount,
424
+ });
425
+ if (measured !== undefined) rate = measured;
426
+ }
427
+
428
+ repaint();
429
+ });
430
+
431
+ // One entry per run, not per request: the latest snapshot is all a restore
432
+ // needs, and the session tree grows one row per message instead of one row
433
+ // per model call.
434
+ cmd.on('run_end', () => {
435
+ const store = cmd.session;
436
+ if (!store || !dirty) return;
437
+ dirty = false;
438
+ store.appendCustomEntry({
439
+ customType: CUSTOM_TYPE,
440
+ data: serializeState(state),
441
+ });
442
+ });
443
+
444
+ cmd.addCommand({
445
+ name: 'cache',
446
+ description: 'Cache hit ratio and decode rate: last request and session average',
447
+ handler: () => ({message: detailLine(snapshotOf(state), rate, missedThisRun)}),
448
+ });
449
+ }