@maxiaochao/pi-toolkit 0.13.6 → 0.13.8
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/CHANGELOG.md +17 -0
- package/README.md +5 -1
- package/extensions/btw/index.ts +7 -16
- package/extensions/btw/tests/context.mjs +78 -0
- package/package.json +4 -3
- package/packages/cache-export/CHANGELOG.md +10 -0
- package/packages/cache-export/README.md +14 -0
- package/packages/cache-export/TESTING.md +8 -2
- package/packages/cache-export/docs/demo.gif +0 -0
- package/packages/cache-export/docs/screenshot.png +0 -0
- package/packages/cache-export/extensions/cache-export.ts +44 -3
- package/packages/cache-export/package.json +1 -1
- package/packages/cache-export/src/render.ts +120 -59
- package/packages/cache-export/tests/e2e-browser.mjs +45 -5
- package/packages/cache-export/tests/evidence.mjs +113 -0
- package/packages/cache-export/tests/extension.mjs +52 -0
- package/packages/cache-export/tests/run-tests.mjs +12 -11
- package/packages/codex-edit/CHANGELOG.md +7 -0
- package/packages/codex-edit/README.md +4 -0
- package/packages/codex-edit/TESTING.md +4 -0
- package/packages/codex-edit/docs/test-report.html +19 -11
- package/packages/codex-edit/docs/test-report.md +22 -13
- package/packages/codex-edit/docs/test-summary.md +1 -1
- package/packages/codex-edit/extensions/codex-edit.ts +12 -4
- package/packages/codex-edit/tests/comparison-cases.mjs +35 -0
- package/packages/codex-edit/tests/comparison-harness.mjs +5 -6
- package/packages/codex-edit/tests/generate-test-report.mjs +12 -7
- package/packages/codex-edit/tests/native-vs-codex.ts +14 -14
- package/packages/codex-edit/tests/released-vs-local.test.mjs +4 -0
- package/packages/herdr-subagents/ARCHITECTURE.md +4 -1
- package/packages/herdr-subagents/CHANGELOG.md +12 -0
- package/packages/herdr-subagents/README.md +11 -2
- package/packages/herdr-subagents/TESTING.md +12 -0
- package/packages/herdr-subagents/docs/demo.webm +0 -0
- package/packages/herdr-subagents/docs/screenshot.png +0 -0
- package/packages/herdr-subagents/extensions/subagent-report.ts +84 -30
- package/packages/herdr-subagents/package.json +2 -2
- package/packages/herdr-subagents/tests/report-boundary.mjs +83 -0
- package/packages/herdr-subagents/tests/report-lifecycle.mjs +69 -0
- package/packages/herdr-subagents/tests/runtime.mjs +26 -0
- package/packages/herdr-subagents/tests/standalone-package.mjs +23 -0
- package/packages/worktree/CHANGELOG.md +4 -0
- package/packages/worktree/README.md +4 -0
- package/packages/worktree/TESTING.md +4 -0
- package/packages/worktree/bin/pi-worktree +1 -1
- package/packages/worktree/tests/run-tests.sh +10 -0
- package/skills/web-browser/SKILL.md +3 -1
- package/skills/web-browser/scripts/start.js +42 -8
|
@@ -9,8 +9,8 @@ import { USAGE_STYLES, USAGE_SCRIPT } from "./tau-assets.ts";
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
function hhmmss(ts) {
|
|
12
|
-
const iso =
|
|
13
|
-
if (
|
|
12
|
+
const iso = ts ?? "";
|
|
13
|
+
if (iso === "") return "";
|
|
14
14
|
const d = new Date(iso);
|
|
15
15
|
if (isNaN(d)) return "";
|
|
16
16
|
// render in the machine's local timezone (Asia/Shanghai here -> Beijing time)
|
|
@@ -26,13 +26,20 @@ export function tzLabel() {
|
|
|
26
26
|
return `${tz} (UTC${off})`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function timeMs(value) {
|
|
30
|
+
const result = typeof value === "number" ? value : typeof value === "string" && value ? Date.parse(value) : NaN;
|
|
31
|
+
return Number.isFinite(result) ? result : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function parseEntries(entries) {
|
|
30
35
|
const requests = [];
|
|
31
36
|
const events = [];
|
|
32
37
|
const toolCounts = {};
|
|
33
38
|
let compactions = 0;
|
|
34
39
|
let prevKey = null;
|
|
35
|
-
const
|
|
40
|
+
const auxiliary = [];
|
|
41
|
+
const warmByKey = new Map();
|
|
42
|
+
let observedPolicy;
|
|
36
43
|
let gapTools = []; // tool results accumulated since the previous request
|
|
37
44
|
|
|
38
45
|
const addEvent = (num, kind, label, ts) =>
|
|
@@ -41,7 +48,26 @@ export function parseEntries(entries) {
|
|
|
41
48
|
for (const e of entries || []) {
|
|
42
49
|
const t = e?.type;
|
|
43
50
|
const ts = e?.timestamp;
|
|
51
|
+
if (t === "custom" && e.customType === "cache-export-request") {
|
|
52
|
+
observedPolicy = e.data;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (t === "usage" || ((t === "compaction" || t === "branch_summary") && e.usage)) {
|
|
56
|
+
const kind = t === "usage" ? e.kind : t;
|
|
57
|
+
auxiliary.push({ kind, timestamp: hhmmss(ts), beforeRequest: requests.length + 1,
|
|
58
|
+
provider: e.provider ?? "Unknown", model: e.model ?? "Unknown", usage: e.usage ?? {} });
|
|
59
|
+
if (kind === "cache_warm" && ((e.usage?.cacheRead ?? 0) + (e.usage?.cacheWrite ?? 0)) > 0) {
|
|
60
|
+
warmByKey.set(`${e.provider}/${e.model}`, timeMs(ts));
|
|
61
|
+
}
|
|
62
|
+
if (t === "usage") continue;
|
|
63
|
+
}
|
|
64
|
+
if (t === "context_edit" || t === "branch_summary") {
|
|
65
|
+
addEvent(requests.length + 1, "context", t === "context_edit" ? "Context edit" : "Branch summary", ts);
|
|
66
|
+
warmByKey.clear();
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
44
69
|
if (t === "compaction") {
|
|
70
|
+
warmByKey.clear();
|
|
45
71
|
compactions += 1;
|
|
46
72
|
addEvent(requests.length + 1, "compaction", "Compaction", ts);
|
|
47
73
|
continue;
|
|
@@ -98,25 +124,33 @@ export function parseEntries(entries) {
|
|
|
98
124
|
}
|
|
99
125
|
prevKey = key;
|
|
100
126
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
const startMs = timeMs(m.timestamp);
|
|
128
|
+
const durationMs = Number.isFinite(m.durationMs) && m.durationMs >= 0 ? m.durationMs : null;
|
|
129
|
+
const previous = requests.at(-1);
|
|
130
|
+
const startGapMs = startMs !== null && previous?.startMs !== null && previous?.startMs !== undefined
|
|
131
|
+
? startMs - previous.startMs : null;
|
|
132
|
+
const idleMs = startMs !== null && previous?.endMs !== null && previous?.endMs !== undefined
|
|
133
|
+
? Math.max(0, startMs - previous.endMs) : null;
|
|
134
|
+
const policy = observedPolicy?.messageTimestamp === m.timestamp && observedPolicy?.provider === provider && observedPolicy?.model === model && observedPolicy?.api === api
|
|
135
|
+
? observedPolicy.policy : null;
|
|
136
|
+
observedPolicy = undefined;
|
|
137
|
+
const references = new Set((Array.isArray(policy?.explicitTtlsSeconds) ? policy.explicitTtlsSeconds : [])
|
|
138
|
+
.filter((seconds) => Number.isFinite(seconds) && seconds > 0));
|
|
139
|
+
// A reported one-hour write is a policy reference, not a server expiry time.
|
|
140
|
+
// Never propagate a guessed expiry or downgrade historical hits to five minutes.
|
|
141
|
+
if (u.cacheWrite1h > 0) references.add(3600);
|
|
142
|
+
const mixedWrites = u.cacheWrite1h > 0 && u.cacheWrite > u.cacheWrite1h;
|
|
143
|
+
const policyReferenceSeconds = !mixedWrites && references.size === 1 ? [...references][0] : null;
|
|
114
144
|
|
|
115
145
|
const cost = u.cost && typeof u.cost.total === "number" ? u.cost.total : null;
|
|
116
146
|
requests.push({
|
|
117
147
|
number: num,
|
|
118
148
|
isoTs: String(ts || ""),
|
|
119
|
-
timestamp: hhmmss(
|
|
149
|
+
timestamp: hhmmss(startMs) || "Unknown",
|
|
150
|
+
startMs, durationMs, startGapMs, idleMs,
|
|
151
|
+
endMs: startMs !== null && durationMs !== null ? startMs + durationMs : null,
|
|
152
|
+
lastWarmAt: warmByKey.get(key) ?? null,
|
|
153
|
+
policy, policyReferenceSeconds,
|
|
120
154
|
provider,
|
|
121
155
|
model,
|
|
122
156
|
api,
|
|
@@ -125,9 +159,8 @@ export function parseEntries(entries) {
|
|
|
125
159
|
cached,
|
|
126
160
|
cacheWrite,
|
|
127
161
|
cacheWrite1h: u.cacheWrite1h || 0,
|
|
128
|
-
cacheTtlMinutes,
|
|
129
162
|
output,
|
|
130
|
-
reasoning: u.reasoning
|
|
163
|
+
reasoning: Number.isFinite(u.reasoning) ? u.reasoning : null,
|
|
131
164
|
stopReason: m.stopReason || "",
|
|
132
165
|
estimatedCost: cost,
|
|
133
166
|
gapTools,
|
|
@@ -138,7 +171,7 @@ export function parseEntries(entries) {
|
|
|
138
171
|
}
|
|
139
172
|
|
|
140
173
|
const toolCalls = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
|
|
141
|
-
return { requests, events, toolCalls, compactions };
|
|
174
|
+
return { requests, events, toolCalls, compactions, auxiliary };
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
// ---------------------------------------------------------------------------
|
|
@@ -160,7 +193,7 @@ const SERIES_COLORS = {
|
|
|
160
193
|
"cache writes": ["#9cffb1", "#166534"],
|
|
161
194
|
fresh: ["#ff4f4f", "#b91c1c"],
|
|
162
195
|
request: ["#ff4f4f", "#b91c1c"],
|
|
163
|
-
cumulative: ["#93c5fd", "#2563eb"],
|
|
196
|
+
"segment cumulative": ["#93c5fd", "#2563eb"],
|
|
164
197
|
output: ["#c084fc", "#9333ea"],
|
|
165
198
|
reasoning: ["#9cffb1", "#166534"],
|
|
166
199
|
event: ["#a7f3f0", "#0f766e"],
|
|
@@ -181,6 +214,27 @@ function fmtCost(v) {
|
|
|
181
214
|
|
|
182
215
|
const fmtInt = (n) => n.toLocaleString("en-US");
|
|
183
216
|
const fmtPct1 = (x) => (x * 100).toFixed(1) + "%";
|
|
217
|
+
const fmtDuration = (ms) => Number.isFinite(ms) ? `${(ms / 1000).toFixed(1)}s` : "Unknown";
|
|
218
|
+
|
|
219
|
+
function policyLabel(request) {
|
|
220
|
+
const reference = request.policyReferenceSeconds;
|
|
221
|
+
const model = request.policy?.modelReference;
|
|
222
|
+
const modelText = model && Object.keys(model).length
|
|
223
|
+
? `; model reference ${Object.entries(model).map(([tier, seconds]) => `${tier}=${seconds}s`).join(", ")}` : "";
|
|
224
|
+
const retention = request.policy?.retention ? `; observed retention=${request.policy.retention}` : "";
|
|
225
|
+
return `${Number.isFinite(reference) ? `${reference}s recorded reference` : "Unknown"}${retention}${modelText}; server expiry Unknown`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderAuxiliaryUsage(auxiliary = []) {
|
|
229
|
+
const count = (value) => Number.isFinite(value) ? fmtInt(value) : "Unknown";
|
|
230
|
+
const rows = auxiliary.map((entry) => `<tr><td>${esc(entry.kind)}</td><td>${esc(entry.timestamp)}</td>` +
|
|
231
|
+
`<td>${esc(entry.provider)}</td><td>${esc(entry.model)}</td>` +
|
|
232
|
+
["input", "cacheRead", "cacheWrite", "output"].map((field) => `<td>${count(entry.usage[field])}</td>`).join("") + "</tr>").join("");
|
|
233
|
+
return `<div class="usage-panel"><h2>Auxiliary usage (whole branch)</h2>` +
|
|
234
|
+
`<p class="usage-note">Completed usage records only: cache warming, compaction and other auxiliary calls. Excluded from main-request charts and totals. A completed warm call is not proof that the next main prefix will hit. Planned warming is not counted.</p>` +
|
|
235
|
+
`<div class="usage-table-wrap"><table><thead><tr><th>Kind</th><th>Recorded completion</th><th>Provider</th><th>Model</th><th>Fresh</th><th>Cached</th><th>Written</th><th>Output</th></tr></thead>` +
|
|
236
|
+
`<tbody>${rows || '<tr><td colspan="8">No auxiliary usage recorded.</td></tr>'}</tbody></table></div></div>`;
|
|
237
|
+
}
|
|
184
238
|
|
|
185
239
|
// ---------------------------------------------------------------------------
|
|
186
240
|
// _line_chart port
|
|
@@ -271,7 +325,7 @@ function lineChart(title, series, opts = {}) {
|
|
|
271
325
|
// every index while the visible broken polylines still draw the gaps.
|
|
272
326
|
const full = `<polyline class="series-line" data-dark="${dark}" data-light="${light}" visibility="hidden" fill="none" stroke="${dark}" stroke-width="3" ` +
|
|
273
327
|
`points="${values.map((v, i) => point(i, Number.isFinite(v) ? v : 0).map((c) => c.toFixed(1)).join(",")).join(" ")}"/>`;
|
|
274
|
-
const labels = values.map((v) => !Number.isFinite(v) ? "
|
|
328
|
+
const labels = values.map((v) => !Number.isFinite(v) ? "Unknown" : (percent ? `${(v * 100).toFixed(1)}%` : fmtInt(Math.trunc(v)))).join("|");
|
|
275
329
|
parts.push(
|
|
276
330
|
`<g class="series" data-series-id="${seriesIndex}" data-name="${esc(name, true)}" data-labels="${esc(labels, true)}">` +
|
|
277
331
|
full +
|
|
@@ -291,7 +345,7 @@ function lineChart(title, series, opts = {}) {
|
|
|
291
345
|
);
|
|
292
346
|
});
|
|
293
347
|
parts.push(
|
|
294
|
-
`<text class="axis-label" x="${(left + plotWidth / 2).toFixed(1)}" y="${height - 4}" text-anchor="middle">
|
|
348
|
+
`<text class="axis-label" x="${(left + plotWidth / 2).toFixed(1)}" y="${height - 4}" text-anchor="middle">Main request sequence (not time)</text></svg>`,
|
|
295
349
|
);
|
|
296
350
|
return parts.join("");
|
|
297
351
|
}
|
|
@@ -309,8 +363,6 @@ const figure = (chart) =>
|
|
|
309
363
|
|
|
310
364
|
const MISS_THRESHOLD = 0.5; // hit rate below this gets an explanation row
|
|
311
365
|
const PREFIX_REUSE_THRESHOLD = 0.5;
|
|
312
|
-
const DEFAULT_TTL_MINUTES = 5;
|
|
313
|
-
const LONG_TTL_MINUTES = 60;
|
|
314
366
|
|
|
315
367
|
function reusablePrefix(request) {
|
|
316
368
|
const prompt = request.fresh + request.cached + request.cacheWrite;
|
|
@@ -319,10 +371,6 @@ function reusablePrefix(request) {
|
|
|
319
371
|
return explicitlyCached > 0 ? explicitlyCached : prompt;
|
|
320
372
|
}
|
|
321
373
|
|
|
322
|
-
function ttlMinutesAfter(request) {
|
|
323
|
-
return request.cacheTtlMinutes ?? (request.cacheWrite1h > 0 ? LONG_TTL_MINUTES : DEFAULT_TTL_MINUTES);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
374
|
export function analyzeMisses(requests, events) {
|
|
327
375
|
const evByReq = new Map();
|
|
328
376
|
for (const ev of events) {
|
|
@@ -347,30 +395,34 @@ export function analyzeMisses(requests, events) {
|
|
|
347
395
|
const prefixReuse = expectedPrefix > 0 ? Math.min(1, r.cached / expectedPrefix) : 0;
|
|
348
396
|
|
|
349
397
|
if (i === 0) {
|
|
350
|
-
causes.push(
|
|
398
|
+
causes.push(r.number === 1
|
|
399
|
+
? ["first request", "first recorded main request; earlier server cache state is unknown, so a cold start is not established"]
|
|
400
|
+
: ["segment boundary", "first request in this view, not the first request of the session"]);
|
|
351
401
|
}
|
|
352
402
|
if (prev && (prev.provider !== r.provider || prev.model !== r.model)) {
|
|
353
|
-
causes.push(["model switch", `${prev.provider}/${prev.model} -> ${r.provider}/${r.model}:
|
|
403
|
+
causes.push(["model switch", `${prev.provider}/${prev.model} -> ${r.provider}/${r.model}: provider/model changed; cache reuse across routes is unknown`]);
|
|
354
404
|
}
|
|
355
405
|
for (const ev of evByReq.get(r.number) || []) {
|
|
356
406
|
if (ev.kind === "compaction") {
|
|
357
|
-
causes.push(["compaction", "
|
|
407
|
+
causes.push(["compaction", "observed compaction rewrote context; a cache rebuild is possible, not proven"]);
|
|
408
|
+
} else if (ev.kind === "context") {
|
|
409
|
+
causes.push(["context edit", `${ev.label}: recorded context transformation; cache impact is unknown`]);
|
|
358
410
|
} else if (ev.kind === "model" && r.number > 1 && !causes.some(([c]) => c === "model switch")) {
|
|
359
|
-
causes.push(["model switch", `${ev.label}:
|
|
411
|
+
causes.push(["model switch", `${ev.label}: provider/model changed; cache reuse across routes is unknown`]);
|
|
360
412
|
} else if (ev.kind === "thinking" && r.number > 1) {
|
|
361
413
|
causes.push(["thinking change", `${ev.label}: request parameters changed`]);
|
|
362
414
|
}
|
|
363
415
|
}
|
|
364
|
-
if (prev && r.
|
|
365
|
-
const
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
368
|
-
causes.push(["possible TTL expiry", `
|
|
416
|
+
if (prev && r.startMs !== null && prev.startMs !== null && r.startMs > prev.startMs) {
|
|
417
|
+
const warmed = r.lastWarmAt !== null && r.lastWarmAt > prev.startMs && r.lastWarmAt <= r.startMs;
|
|
418
|
+
const reference = prev.policyReferenceSeconds;
|
|
419
|
+
if (!warmed && reference > 0 && (r.startMs - prev.startMs) / 1000 > reference && prefixReuse < PREFIX_REUSE_THRESHOLD && !causes.length) {
|
|
420
|
+
causes.push(["possible TTL expiry", `request-start gap ${((r.startMs - prev.startMs) / 60000).toFixed(1)}min crosses a recorded ${(reference / 60).toFixed(0)}min policy reference; not an idle gap or a server expiry measurement`]);
|
|
369
421
|
}
|
|
370
422
|
}
|
|
371
423
|
if (prev && prefixReuse < PREFIX_REUSE_THRESHOLD && !causes.length) {
|
|
372
|
-
const writeDetail = r.cacheWrite > 0 ? `; provider also reported ${r.cacheWrite.toLocaleString("en-US")} tokens written
|
|
373
|
-
causes.push(["cache rebuild", `
|
|
424
|
+
const writeDetail = r.cacheWrite > 0 ? `; provider also reported ${r.cacheWrite.toLocaleString("en-US")} tokens written (prefix identity unknown)` : "";
|
|
425
|
+
causes.push(["cache rebuild", `cache reads equal ${(prefixReuse * 100).toFixed(1)}% of the prior reported prompt reference; possible rebuild or changed prefix; actual server state and cause are unknown${writeDetail}`]);
|
|
374
426
|
}
|
|
375
427
|
if (r.cacheWrite > 0 && !causes.length) {
|
|
376
428
|
causes.push(["cache write", `provider reported ${r.cacheWrite.toLocaleString("en-US")} tokens written; a low read-hit rate is expected while a prefix is created or extended`]);
|
|
@@ -379,10 +431,10 @@ export function analyzeMisses(requests, events) {
|
|
|
379
431
|
const toolChars = (r.gapTools || []).reduce((sum, tool) => sum + tool.chars, 0);
|
|
380
432
|
const toolTokens = Math.round(toolChars / 4);
|
|
381
433
|
const toolDetail = toolTokens > 0 ? `, including ~${toolTokens.toLocaleString("en-US")} estimated tool-output tokens` : "";
|
|
382
|
-
causes.push(["new input", `
|
|
434
|
+
causes.push(["new input", `cache reads equal ${(prefixReuse * 100).toFixed(1)}% of the prior reported prompt reference; ${r.fresh.toLocaleString("en-US")} fresh tokens were reported; appended content is a possible explanation${toolDetail}`]);
|
|
383
435
|
}
|
|
384
436
|
if (!causes.length) {
|
|
385
|
-
causes.push(["unclassified", "no
|
|
437
|
+
causes.push(["unclassified", "no causal evidence matched; the server cache state is unknown"]);
|
|
386
438
|
}
|
|
387
439
|
|
|
388
440
|
for (const [cause] of causes) {
|
|
@@ -395,8 +447,7 @@ export function analyzeMisses(requests, events) {
|
|
|
395
447
|
}
|
|
396
448
|
|
|
397
449
|
const summaryArr = [...summary.entries()].sort((a, b) => b[1].fresh - a[1].fresh);
|
|
398
|
-
return { misses, summary: summaryArr, noCacheReporting, threshold: MISS_THRESHOLD
|
|
399
|
-
ttlMinutes: DEFAULT_TTL_MINUTES, longTtlMinutes: LONG_TTL_MINUTES };
|
|
450
|
+
return { misses, summary: summaryArr, noCacheReporting, threshold: MISS_THRESHOLD };
|
|
400
451
|
}
|
|
401
452
|
|
|
402
453
|
// Streak detection: many consecutive requests stuck in a mid hit-rate band.
|
|
@@ -419,7 +470,7 @@ export function analyzeStreaks(requests, events = []) {
|
|
|
419
470
|
let gaps = 0; // consecutive non-member requests inside the current run
|
|
420
471
|
let prevKey = null;
|
|
421
472
|
const resetBefore = new Set(events
|
|
422
|
-
.filter((event) => event.kind === "compaction" || event.kind === "model")
|
|
473
|
+
.filter((event) => event.kind === "compaction" || event.kind === "model" || event.kind === "context")
|
|
423
474
|
.map((event) => event.requestNumber));
|
|
424
475
|
const close = () => {
|
|
425
476
|
if (cur && cur.hits.length >= STREAK_MIN_LEN) streaks.push(cur);
|
|
@@ -479,13 +530,13 @@ function renderStreakPanel(streaks) {
|
|
|
479
530
|
const pct = (x) => (x * 100).toFixed(1) + "%";
|
|
480
531
|
return `<tr><td>#${s.from}–#${s.to}</td><td>${s.hits.length}</td><td>${pct(s.avgHit)}</td>` +
|
|
481
532
|
`<td>${fmtInt(s.fresh)}</td>` +
|
|
482
|
-
`<td style="white-space:normal">tool outputs: ${tools || "-"} · assistant replies: ${fmtInt(s.replyTokens)} · trace
|
|
533
|
+
`<td style="white-space:normal">tool outputs: ${tools || "-"} · assistant replies: ${fmtInt(s.replyTokens)} · trace estimate / fresh ~${pct(s.coverage)} of fresh</td></tr>`;
|
|
483
534
|
})
|
|
484
535
|
.join("");
|
|
485
536
|
return `<div class="usage-panel"><h2>Hit-rate streak analysis</h2>` +
|
|
486
537
|
`<p class="usage-note">Rule-based, no LLM. A streak is ${STREAK_MIN_LEN}+ requests (single healthy outlier tolerated) with hit rate ` +
|
|
487
538
|
`${(STREAK_LOW * 100).toFixed(0)}–${(STREAK_HIGH * 100).toFixed(1)}% (requests reporting cacheWrite break the streak; fresh ≥ ${STREAK_MIN_FRESH} tok, prefix ≥ ${STREAK_MIN_PROMPT}): ` +
|
|
488
|
-
`
|
|
539
|
+
`appended tool outputs or replies are possible contributors, not proof of cache health or failure. ` +
|
|
489
540
|
`Below ${(MISS_THRESHOLD * 100).toFixed(0)}% see the miss panel above. Tool tokens estimated at 4 chars/token.</p>` +
|
|
490
541
|
`<div class="usage-table-wrap" style="max-height:260px"><table>` +
|
|
491
542
|
`<thead><tr><th>Requests</th><th>Turns</th><th>Avg hit</th><th>Total fresh</th><th>Trace (fresh composition)</th></tr></thead>` +
|
|
@@ -508,9 +559,9 @@ function renderMissPanel(miss) {
|
|
|
508
559
|
.join("");
|
|
509
560
|
return `<div class="usage-panel"><h2>Cache miss analysis</h2>` +
|
|
510
561
|
`<p class="usage-note">Rule-based attribution, no LLM. Rows are requests below ${(miss.threshold * 100).toFixed(0)}% hit rate. ` +
|
|
511
|
-
`
|
|
562
|
+
`Observed usage and context events are facts; causes are possibilities. Policy references are not measured expiry times. Without historical request parameters, retention is unknown. No universal TTL is assumed.</p>` +
|
|
512
563
|
`<div class="usage-table-wrap" style="max-height:340px"><table>` +
|
|
513
|
-
`<thead><tr><th>#</th><th>Time</th><th>Hit rate</th><th>Fresh</th><th>
|
|
564
|
+
`<thead><tr><th>#</th><th>Time</th><th>Hit rate</th><th>Fresh</th><th>Evidence / possible cause</th></tr></thead>` +
|
|
514
565
|
`<tbody>${rows || `<tr><td colspan="5" class="empty">None</td></tr>`}</tbody></table></div>` +
|
|
515
566
|
`<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:12px">${summaryHtml}</div></div>`;
|
|
516
567
|
}
|
|
@@ -521,7 +572,7 @@ function renderMissPanel(miss) {
|
|
|
521
572
|
|
|
522
573
|
export function buildContextSegments(requests, events) {
|
|
523
574
|
const starts = new Set(events
|
|
524
|
-
.filter((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
575
|
+
.filter((event) => (event.kind === "compaction" || event.kind === "model" || event.kind === "context") && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
525
576
|
.map((event) => event.requestNumber));
|
|
526
577
|
const segments = [];
|
|
527
578
|
let start = 0;
|
|
@@ -571,7 +622,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
571
622
|
const contextBreakBefore = new Set();
|
|
572
623
|
const cacheBreakBefore = new Set();
|
|
573
624
|
const resetBefore = new Set(events
|
|
574
|
-
.filter((event) => event.kind === "compaction" || event.kind === "model")
|
|
625
|
+
.filter((event) => event.kind === "compaction" || event.kind === "model" || event.kind === "context")
|
|
575
626
|
.map((event) => event.requestNumber));
|
|
576
627
|
const cumulativeRates = [];
|
|
577
628
|
let runCached = 0, runPrompt = 0;
|
|
@@ -581,7 +632,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
581
632
|
for (let index = 0; index < requests.length; index++) {
|
|
582
633
|
const r = requests[index];
|
|
583
634
|
const key = `${r.provider}/${r.model}`;
|
|
584
|
-
const contextReset = index > 0 && events.some((event) => (event.kind === "compaction" || event.kind === "model") && event.requestNumber === r.number);
|
|
635
|
+
const contextReset = index > 0 && events.some((event) => (event.kind === "compaction" || event.kind === "model" || event.kind === "context") && event.requestNumber === r.number);
|
|
585
636
|
const cacheReset = index > 0 && (resetBefore.has(r.number) || (previousKey !== null && key !== previousKey));
|
|
586
637
|
if (contextReset) contextBreakBefore.add(index);
|
|
587
638
|
if (cacheReset) {
|
|
@@ -633,7 +684,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
633
684
|
];
|
|
634
685
|
if (cacheHitRate !== null) {
|
|
635
686
|
charts.push(figure(lineChart("Cache hit rate",
|
|
636
|
-
[["request", rates], ["cumulative", cumulativeRates]],
|
|
687
|
+
[["request", rates], ["segment cumulative", cumulativeRates]],
|
|
637
688
|
{ yMax: 1.0, percent: true, timestamps, requestNumbers, events: eventMarkers, breakBefore: cacheBreakBefore })));
|
|
638
689
|
}
|
|
639
690
|
charts.push(figure(lineChart("Output and reasoning tokens",
|
|
@@ -647,7 +698,10 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
647
698
|
`<td>${esc(r.model)}</td><td>${fmtInt(r.fresh)}</td><td>${fmtInt(r.cached)}</td>` +
|
|
648
699
|
`<td>${fmtInt(r.cacheWrite)}</td><td>${fmtInt(r.fresh + r.cached + r.cacheWrite)}</td>` +
|
|
649
700
|
`<td>${showHit ? fmtPct1(r.cached / Math.max(1, r.fresh + r.cached + r.cacheWrite)) : "N/A"}</td>` +
|
|
650
|
-
`<td>${fmtInt(r.output)}</td><td>${
|
|
701
|
+
`<td>${fmtInt(r.output)}</td><td>${Number.isFinite(r.reasoning) ? fmtInt(r.reasoning) : "Unknown"}</td>` +
|
|
702
|
+
`<td>${fmtDuration(r.durationMs)}</td><td>${fmtDuration(r.startGapMs)}</td><td>${fmtDuration(r.idleMs)}</td>` +
|
|
703
|
+
`<td>${esc(hhmmss(r.isoTs))}</td><td>${esc(policyLabel(r))}</td>` +
|
|
704
|
+
`<td>${r.lastWarmAt === null ? "Unknown" : esc(hhmmss(r.lastWarmAt))}</td><td>${esc(r.stopReason, true)}</td></tr>`,
|
|
651
705
|
)
|
|
652
706
|
.join("");
|
|
653
707
|
|
|
@@ -657,15 +711,16 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
657
711
|
|
|
658
712
|
return (
|
|
659
713
|
`<div class="usage-cards">${cardsHtml}</div>` +
|
|
660
|
-
`<p class="usage-note">Prompt and output lines break at a Context segment boundary (compaction or model change).
|
|
661
|
-
`
|
|
714
|
+
`<p class="usage-note">Prompt and output lines break at a Context segment boundary (compaction, context edit, or model change). Segment cumulative hit rate resets at each such boundary. ` +
|
|
715
|
+
`The x-axis is request sequence, not elapsed time. Cache hit rate = cacheRead / (input + cacheRead + cacheWrite). These are provider/Pi-reported usage values, not server cache telemetry. Auxiliary calls are separate; unreported reasoning is Unknown.</p>` +
|
|
662
716
|
`<div class="usage-charts">${charts.join("")}</div>` +
|
|
663
717
|
renderMissPanel(analyzeMisses(requests, events)) +
|
|
664
718
|
renderStreakPanel(analyzeStreaks(requests, events)) +
|
|
665
719
|
`<div class="usage-details">` +
|
|
666
720
|
`<div class="usage-panel"><h2>Requests</h2><div class="usage-table-wrap"><table>` +
|
|
667
|
-
`<thead><tr><th>#</th><th>
|
|
721
|
+
`<thead><tr><th>#</th><th>Request start</th><th>Provider</th><th>Response Provider</th><th>Model</th>` +
|
|
668
722
|
`<th>Fresh</th><th>Cached</th><th>Written</th><th>Prompt</th><th>Hit rate</th><th>Output</th>` +
|
|
723
|
+
`<th>Reasoning</th><th>Response duration</th><th>Start-to-start</th><th>Gap after response</th><th>Recorded at</th><th>Policy reference (not expiry)</th><th>Last completed warm with cache usage</th>` +
|
|
669
724
|
`<th>Stop</th></tr></thead><tbody>${rows}</tbody></table></div></div>` +
|
|
670
725
|
`<div class="usage-panel"><h2>Tool calls</h2>${toolRows}</div>` +
|
|
671
726
|
`</div>`
|
|
@@ -675,7 +730,7 @@ function renderDashboardView(requests, events, toolCalls, compactions, scopeLabe
|
|
|
675
730
|
export function renderDashboard(data) {
|
|
676
731
|
const { requests, events, toolCalls, compactions } = data;
|
|
677
732
|
if (!requests.length) {
|
|
678
|
-
return `<p class="empty">No assistant responses with token usage were found in this session.</p
|
|
733
|
+
return `<p class="empty">No main assistant responses with token usage were found in this session.</p>` + renderAuxiliaryUsage(data.auxiliary);
|
|
679
734
|
}
|
|
680
735
|
|
|
681
736
|
const segments = buildContextSegments(requests, events);
|
|
@@ -716,7 +771,7 @@ export function renderDashboard(data) {
|
|
|
716
771
|
return `<div class="usage-view-controls"><label for="context-segment-select">View</label>` +
|
|
717
772
|
`<select id="context-segment-select" data-context-segment-select>${options}</select>` +
|
|
718
773
|
`<span>Default: latest Context segment. All-history values remain available; cache metrics reset where compaction rewrites the prefix or the model changes.</span></div>` +
|
|
719
|
-
views +
|
|
774
|
+
views + renderAuxiliaryUsage(data.auxiliary) +
|
|
720
775
|
`<div class="usage-tooltip" role="status" aria-live="polite"></div>`;
|
|
721
776
|
}
|
|
722
777
|
|
|
@@ -778,6 +833,12 @@ document.querySelectorAll(".usage-chart").forEach((chart) => {
|
|
|
778
833
|
if (!tooltip) return;
|
|
779
834
|
const m = tooltip.textContent.match(/^Request (\\d+)/);
|
|
780
835
|
if (!m) return;
|
|
836
|
+
const index = Number(m[1]) - 1;
|
|
837
|
+
chart.querySelectorAll(".series").forEach((series) => {
|
|
838
|
+
if ((series.dataset.labels || "").split("|")[index] === "Unknown") {
|
|
839
|
+
series.querySelector(".hover-point")?.setAttribute("visibility", "hidden");
|
|
840
|
+
}
|
|
841
|
+
});
|
|
781
842
|
const global = nums[Number(m[1]) - 1];
|
|
782
843
|
if (global && global !== Number(m[1])) {
|
|
783
844
|
tooltip.textContent = tooltip.textContent.replace(/^Request \\d+/, "Request " + global);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Run: node tests/e2e-browser.mjs (from packages/cache-export)
|
|
12
12
|
|
|
13
13
|
import { spawn, execFileSync } from "node:child_process";
|
|
14
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
14
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
15
|
import net from "node:net";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
@@ -183,6 +183,8 @@ async function run() {
|
|
|
183
183
|
}
|
|
184
184
|
const usesWindowsChrome = IS_WSL && /\.exe$/i.test(chrome);
|
|
185
185
|
let windowsPid = 0;
|
|
186
|
+
let chromeProcess;
|
|
187
|
+
let cdp;
|
|
186
188
|
const port = await freePort();
|
|
187
189
|
const userDataDir = usesWindowsChrome
|
|
188
190
|
? commandOutput("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
@@ -202,7 +204,8 @@ async function run() {
|
|
|
202
204
|
`$p = Start-Process -FilePath '${winPath}' -ArgumentList @(${quoted}) -PassThru; $p.Id`]);
|
|
203
205
|
windowsPid = Number(out.trim());
|
|
204
206
|
} else {
|
|
205
|
-
spawn(chrome, chromeArgs, { stdio: "ignore", detached: true })
|
|
207
|
+
chromeProcess = spawn(chrome, chromeArgs, { stdio: "ignore", detached: true });
|
|
208
|
+
chromeProcess.unref();
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
try {
|
|
@@ -212,7 +215,7 @@ async function run() {
|
|
|
212
215
|
}
|
|
213
216
|
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
|
|
214
217
|
const page = list.find((t) => t.type === "page");
|
|
215
|
-
|
|
218
|
+
cdp = await cdpConnect(page.webSocketDebuggerUrl);
|
|
216
219
|
await cdp.send("Page.enable");
|
|
217
220
|
await cdp.send("Emulation.setDeviceMetricsOverride", { width: 1600, height: 1200, deviceScaleFactor: 1, mobile: false });
|
|
218
221
|
await cdp.send("Page.navigate", { url: "data:text/html;charset=utf-8," + encodeURIComponent(html) });
|
|
@@ -304,11 +307,48 @@ async function run() {
|
|
|
304
307
|
ev && ev.dataRequest === "1" && ev.info.includes("before request 25"),
|
|
305
308
|
JSON.stringify(ev));
|
|
306
309
|
|
|
307
|
-
|
|
310
|
+
// Exercise the actual native selector with CDP input, not a synthetic change.
|
|
311
|
+
const selectBox = await evalJs(`(() => {
|
|
312
|
+
const select = document.querySelector("[data-context-segment-select]");
|
|
313
|
+
select.scrollIntoView(); const r = select.getBoundingClientRect();
|
|
314
|
+
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
315
|
+
})()`);
|
|
316
|
+
for (const type of ["mousePressed", "mouseReleased"]) await cdp.send("Input.dispatchMouseEvent", { type, ...selectBox, button: "left", clickCount: 1 });
|
|
317
|
+
for (const [key, code] of [["Home", 36], ["Enter", 13]]) {
|
|
318
|
+
for (const type of ["keyDown", "keyUp"]) await cdp.send("Input.dispatchKeyEvent", { type, key, windowsVirtualKeyCode: code });
|
|
319
|
+
}
|
|
320
|
+
check("e2e real click/keyboard selects all-history", await evalJs(`document.querySelector("[data-context-segment-select]").value === "all"`));
|
|
321
|
+
check("e2e scope, formula, auxiliary and segment cumulative labels", await evalJs(`document.body.textContent.includes("cacheRead / (input + cacheRead + cacheWrite)") && document.body.textContent.includes("Auxiliary usage") && !!document.querySelector('[data-name="segment cumulative"]')`));
|
|
322
|
+
const plot = await evalJs(`(() => {
|
|
323
|
+
const chart = document.querySelector('[data-context-view="all"] [aria-label="Output and reasoning tokens"]');
|
|
324
|
+
chart.scrollIntoView({ block: "center" }); const r = chart.getBoundingClientRect();
|
|
325
|
+
return { x: r.x + r.width * 0.6, y: r.y + r.height * 0.6 };
|
|
326
|
+
})()`);
|
|
327
|
+
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...plot });
|
|
328
|
+
check("e2e unknown reasoning is neither a zero line nor a zero hover point", await evalJs(`(() => {
|
|
329
|
+
const series = document.querySelector('[data-context-view="all"] [data-name="reasoning"]');
|
|
330
|
+
return series.dataset.labels.split("|").every(value => value === "Unknown") &&
|
|
331
|
+
!series.querySelector(".series-line:not([visibility])") &&
|
|
332
|
+
series.querySelector(".hover-point").getAttribute("visibility") === "hidden" &&
|
|
333
|
+
document.querySelector(".usage-tooltip").textContent.includes("Unknown");
|
|
334
|
+
})()`));
|
|
335
|
+
if (process.env.CACHE_EXPORT_SCREENSHOT_DIR) {
|
|
336
|
+
mkdirSync(process.env.CACHE_EXPORT_SCREENSHOT_DIR, { recursive: true });
|
|
337
|
+
const { data } = await cdp.send("Page.captureScreenshot", { format: "png" });
|
|
338
|
+
writeFileSync(join(process.env.CACHE_EXPORT_SCREENSHOT_DIR, "unknown-reasoning.png"), Buffer.from(data, "base64"));
|
|
339
|
+
await evalJs("window.scrollTo(0, 0)");
|
|
340
|
+
const overview = await cdp.send("Page.captureScreenshot", { format: "png" });
|
|
341
|
+
writeFileSync(join(process.env.CACHE_EXPORT_SCREENSHOT_DIR, "screenshot.png"), Buffer.from(overview.data, "base64"));
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
if (cdp) { await cdp.send("Browser.close").catch(() => {}); cdp.close(); }
|
|
308
345
|
if (usesWindowsChrome && windowsPid) {
|
|
309
346
|
try { execFileSync("taskkill.exe", ["/PID", String(windowsPid), "/T", "/F"]); } catch { /* best effort */ }
|
|
310
347
|
}
|
|
311
|
-
|
|
348
|
+
if (chromeProcess?.pid) {
|
|
349
|
+
try { process.kill(-chromeProcess.pid, "SIGTERM"); } catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
350
|
+
}
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
312
352
|
if (!usesWindowsChrome) rmSync(userDataDir, { recursive: true, force: true });
|
|
313
353
|
}
|
|
314
354
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import register from "../extensions/cache-export.ts";
|
|
6
|
+
import { parseEntries, analyzeMisses, buildContextSegments, renderDashboard } from "../src/render.ts";
|
|
7
|
+
|
|
8
|
+
const origin = Date.parse("2026-01-01T00:00:00Z");
|
|
9
|
+
const iso = (seconds) => new Date(origin + seconds * 1000).toISOString();
|
|
10
|
+
const usage = (read = 0, write = 0) => ({ input: 10000, cacheRead: read, cacheWrite: write, output: 100 });
|
|
11
|
+
const request = (start, duration, read = 0, extraUsage = {}) => ({
|
|
12
|
+
type: "message", timestamp: iso(start + duration),
|
|
13
|
+
message: { role: "assistant", provider: "p", model: "m", api: "openai-codex-responses", timestamp: origin + start * 1000, durationMs: duration * 1000, content: [], usage: { ...usage(read), ...extraUsage } },
|
|
14
|
+
});
|
|
15
|
+
const misses = (data) => analyzeMisses(data.requests, data.events).misses;
|
|
16
|
+
const hasTtl = (data) => misses(data).some((miss) => miss.causes.some(([cause]) => cause === "possible TTL expiry"));
|
|
17
|
+
{
|
|
18
|
+
const data = parseEntries([request(0, 6, 50000), request(12, 408)]);
|
|
19
|
+
assert.equal(data.requests[1].startGapMs, 12000, "response persistence times are not request starts");
|
|
20
|
+
assert.equal(data.requests[1].idleMs, 6000);
|
|
21
|
+
assert.equal(data.requests[1].durationMs, 408000);
|
|
22
|
+
assert.equal(hasTtl(data), false);
|
|
23
|
+
assert.equal(data.requests[0].policyReferenceSeconds, null, "never assign Codex a generic five-minute TTL");
|
|
24
|
+
assert.equal(data.requests[0].reasoning, null, "unreported reasoning is unknown, not zero");
|
|
25
|
+
const html = renderDashboard(data);
|
|
26
|
+
assert.match(html, /Unknown/);
|
|
27
|
+
assert.match(html, /segment cumulative/);
|
|
28
|
+
assert.match(html, /request sequence/);
|
|
29
|
+
}
|
|
30
|
+
{
|
|
31
|
+
const data = parseEntries([
|
|
32
|
+
request(0, 1, 0, { cacheWrite: 50000, cacheWrite1h: 50000 }),
|
|
33
|
+
request(59 * 60, 1, 50000), request(61 * 60, 1, 50000), request(70 * 60, 1),
|
|
34
|
+
]);
|
|
35
|
+
assert.equal(data.requests[0].policyReferenceSeconds, 3600);
|
|
36
|
+
assert.equal(data.requests[2].policyReferenceSeconds, null, "do not invent a five-minute expiry after an old one-hour write");
|
|
37
|
+
assert.equal(hasTtl(data), false, "reported reads do not reveal the server's next expiry time");
|
|
38
|
+
}
|
|
39
|
+
{
|
|
40
|
+
const warm = { type: "usage", kind: "cache_warm", timestamp: iso(480), provider: "p", model: "m", usage: usage(50000) };
|
|
41
|
+
const data = parseEntries([request(0, 1, 50000), warm, { type: "compaction", timestamp: iso(540), usage: usage(10) }, request(600, 1)]);
|
|
42
|
+
assert.equal(data.requests.length, 2);
|
|
43
|
+
assert.equal(data.auxiliary.length, 2);
|
|
44
|
+
assert.match(renderDashboard(data), /Auxiliary usage/);
|
|
45
|
+
const warmed = parseEntries([request(0, 1, 50000), warm, request(600, 1)]);
|
|
46
|
+
assert.equal(warmed.requests[1].lastWarmAt, origin + 480000);
|
|
47
|
+
const planned = parseEntries([request(0, 1, 50000), { type: "custom", customType: "cache_warming_decision", timestamp: iso(480) }, request(600, 1)]);
|
|
48
|
+
assert.equal(planned.auxiliary.length, 0, "preparing a warm request is not completed usage");
|
|
49
|
+
assert.equal(planned.requests[1].lastWarmAt, null);
|
|
50
|
+
}
|
|
51
|
+
{
|
|
52
|
+
const data = parseEntries([request(0, 1, 50000), { type: "context_edit", timestamp: iso(30) }, request(60, 1)]);
|
|
53
|
+
const segments = buildContextSegments(data.requests, data.events);
|
|
54
|
+
assert.equal(segments.length, 2);
|
|
55
|
+
const segmentMiss = analyzeMisses(segments[1].requests, data.events).misses[0];
|
|
56
|
+
assert.ok(segmentMiss.causes.some(([cause]) => cause === "context edit"));
|
|
57
|
+
assert.ok(!segmentMiss.causes.some(([cause]) => cause === "first request"), "a segment is not the session's first request");
|
|
58
|
+
}
|
|
59
|
+
{
|
|
60
|
+
const root = mkdtempSync(join(tmpdir(), "cache-evidence-"));
|
|
61
|
+
try {
|
|
62
|
+
const handlers = new Map(), entries = [], saved = [];
|
|
63
|
+
let command;
|
|
64
|
+
register({
|
|
65
|
+
on: (name, handler) => handlers.set(name, handler),
|
|
66
|
+
appendEntry: (customType, data) => { saved.push(data); entries.push({ type: "custom", customType, data }); },
|
|
67
|
+
registerCommand: (_name, value) => { command = value; },
|
|
68
|
+
});
|
|
69
|
+
const ctx = { cwd: root, model: { provider: "p", id: "m", api: "openai-responses", promptCache: { short: 300, long: 86400 } }, sessionManager: { getBranch: () => entries }, ui: { notify() {} } };
|
|
70
|
+
const message = request(0, 1, 50000);
|
|
71
|
+
message.message.api = "openai-responses";
|
|
72
|
+
await handlers.get("turn_start")({}, ctx);
|
|
73
|
+
await handlers.get("before_provider_request")({ payload: { prompt_cache_retention: "24h", prompt_cache_key: "PRIVATE_KEY", input: "PRIVATE_PROMPT", headers: { Authorization: "PRIVATE_TOKEN" } } }, ctx);
|
|
74
|
+
await handlers.get("message_end")({ message: message.message }, ctx);
|
|
75
|
+
entries.push(message);
|
|
76
|
+
assert.equal(saved.length, 1);
|
|
77
|
+
assert.doesNotMatch(JSON.stringify(saved), /PRIVATE|Authorization|prompt_cache_key/);
|
|
78
|
+
assert.equal(parseEntries(entries).requests[0].policyReferenceSeconds, 86400);
|
|
79
|
+
await command.handler("report.html", ctx);
|
|
80
|
+
assert.ok(readFileSync(join(root, "report.html"), "utf8").includes("cache export"));
|
|
81
|
+
assert.equal(parseEntries([message]).requests[0].policyReferenceSeconds, null, "current model configuration must never backfill old entries");
|
|
82
|
+
const longGap = request(90000, 1);
|
|
83
|
+
const crossed = parseEntries([...entries, longGap]);
|
|
84
|
+
assert.equal(hasTtl(crossed), true, "crossing an observed reference may be labeled possible, never proven");
|
|
85
|
+
assert.match(JSON.stringify(misses(crossed)), /not an idle gap or a server expiry measurement/);
|
|
86
|
+
const warm = { type: "usage", kind: "cache_warm", timestamp: iso(89990), provider: "p", model: "m", usage: usage(10000) };
|
|
87
|
+
assert.equal(hasTtl(parseEntries([...entries, warm, longGap])), false, "do not use the main-request gap as if completed warming did not occur");
|
|
88
|
+
const mixedMessage = request(100000, 1, 50000, { cacheWrite: 10000, cacheWrite1h: 5000 });
|
|
89
|
+
mixedMessage.message.api = "anthropic-messages";
|
|
90
|
+
ctx.model.api = "anthropic-messages";
|
|
91
|
+
await handlers.get("turn_start")({}, ctx);
|
|
92
|
+
await handlers.get("before_provider_request")({ payload: { messages: [{ content: [
|
|
93
|
+
{ text: "PRIVATE_TEXT", cache_control: { type: "ephemeral", ttl: "5m" } },
|
|
94
|
+
{ cache_control: { type: "ephemeral", ttl: "1h" } },
|
|
95
|
+
] }] } }, ctx);
|
|
96
|
+
await handlers.get("message_end")({ message: mixedMessage.message }, ctx);
|
|
97
|
+
const mixedEvidence = entries.at(-1);
|
|
98
|
+
const later = request(104001, 1);
|
|
99
|
+
later.message.api = "anthropic-messages";
|
|
100
|
+
const mixedData = parseEntries([mixedEvidence, mixedMessage, later]);
|
|
101
|
+
assert.deepEqual(mixedData.requests[0].policy.explicitTtlsSeconds, [300, 3600]);
|
|
102
|
+
assert.equal(mixedData.requests[0].policyReferenceSeconds, null, "a partial one-hour write cannot override known mixed TTLs");
|
|
103
|
+
assert.equal(hasTtl(mixedData), false, "do not attribute the later miss to an invented whole-request TTL");
|
|
104
|
+
assert.equal(parseEntries([mixedMessage]).requests[0].policyReferenceSeconds, null, "mixed write counters alone also preclude a whole-request TTL");
|
|
105
|
+
for (const oneHourWrite of [0, 10000]) {
|
|
106
|
+
const variant = structuredClone(mixedMessage);
|
|
107
|
+
variant.message.usage.cacheWrite1h = oneHourWrite;
|
|
108
|
+
assert.equal(parseEntries([mixedEvidence, variant]).requests[0].policyReferenceSeconds, null, "known mixed policy remains unknown regardless of this response's write mix");
|
|
109
|
+
}
|
|
110
|
+
assert.doesNotMatch(JSON.stringify(saved), /PRIVATE/);
|
|
111
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
112
|
+
}
|
|
113
|
+
console.log("PASS cache evidence: timing, unknown retention/reasoning, auxiliary usage, context edits, safe history, session cwd");
|