@maxiaochao/pi-toolkit 0.2.2 → 0.2.3
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 +9 -1
- package/README.md +18 -0
- package/extensions/cache-export/index.ts +1 -1
- package/extensions/cache-export/render.ts +150 -21
- package/extensions/cache-export/tests/run-tests.mjs +23 -1
- package/global/AGENTS.md +3 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2026-09-08 - v0.2.3
|
|
2
|
+
|
|
3
|
+
- add cache export Context segment views
|
|
4
|
+
|
|
1
5
|
## 2026-09-03 - v0.2.2
|
|
2
6
|
|
|
3
7
|
- let the focused `/btw` overlay scroll its full transcript with Up/Down and PageUp/PageDown
|
|
@@ -51,7 +55,11 @@
|
|
|
51
55
|
|
|
52
56
|
- tbd
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
## Unreleased
|
|
59
|
+
|
|
60
|
+
- let `/cache_export` switch between all history and individual compaction-defined Context segments, defaulting to the latest segment; cache-hit cumulative lines reset at compaction or model boundaries
|
|
61
|
+
- add the independent `@maxiaochao/pi-codex-edit` package with model-aware Codex-style `apply_patch`
|
|
62
|
+
- include the benchmark summary and interactive HTML architecture explainer
|
|
55
63
|
|
|
56
64
|
## 2026-08-18 - v0.1.0
|
|
57
65
|
|
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ bash scripts/install.sh
|
|
|
40
40
|
|
|
41
41
|
The `/btw` side chat is a separate agent session. It can inspect the main conversation and repository, and its thread stays out of the main context until you close it and choose to inject a summary. Use `/btw` to open it or `/btw <question>` to ask directly. While the BTW overlay has focus, use `Up` / `Down` to scroll its transcript one line at a time and `PageUp` / `PageDown` to move by one transcript page.
|
|
42
42
|
|
|
43
|
+
Use `/cache_export [path]` to write an interactive per-request cache dashboard. It defaults to the latest compaction-defined Context segment for current-state diagnosis, and the View selector can show any earlier segment or all session history. Compaction breaks context charts; cache-hit accumulation also resets at model changes, while TTL and other inferred interruptions remain diagnostic events rather than segment boundaries.
|
|
44
|
+
|
|
43
45
|
The bundled `nightowl` theme is available to Pi through the package manifest and is selected automatically on npm installation only when no theme is already configured.
|
|
44
46
|
|
|
45
47
|
The bundled `web-browser` skill provides reusable scripts for starting Chrome/Chromium with remote debugging, navigating tabs, evaluating JavaScript, emulating devices, taking screenshots, dismissing cookie dialogs, and inspecting browser logs. It auto-detects common Chrome/Chromium installations on macOS and Linux, including Windows Chrome when Pi runs under WSL; set `BROWSER_BIN` when the binary is elsewhere.
|
|
@@ -68,6 +70,22 @@ pi update npm:@maxiaochao/pi-tiny-subagent
|
|
|
68
70
|
|
|
69
71
|
See [packages/tiny-subagent/README.md](packages/tiny-subagent/README.md) for personas, model selection, child isolation, live transcript rendering, and diagnostics.
|
|
70
72
|
|
|
73
|
+
## Codex Edit
|
|
74
|
+
|
|
75
|
+
The Codex edit package is a separate npm package. Install it when you want GPT/Codex models to use the freeform `apply_patch` protocol while other models keep Pi's native `edit` tool:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pi install npm:@maxiaochao/pi-codex-edit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It includes the extension implementation plus the benchmark summary and interactive architecture explainer. Its versions, release workflow, and tag are independent from the root package and tiny subagent:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pi update npm:@maxiaochao/pi-codex-edit
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
See [packages/codex-edit/README.md](packages/codex-edit/README.md) for the routing rules and [packages/codex-edit/summary.md](packages/codex-edit/summary.md) for the measured tradeoffs.
|
|
88
|
+
|
|
71
89
|
## Development
|
|
72
90
|
|
|
73
91
|
Repository development and release rules are documented in [AGENTS.md](AGENTS.md).
|
|
@@ -61,7 +61,7 @@ function defaultOutputPath(ctx) {
|
|
|
61
61
|
|
|
62
62
|
export default function (pi) {
|
|
63
63
|
pi.registerCommand("cache_export", {
|
|
64
|
-
description: "Export
|
|
64
|
+
description: "Export an interactive cache dashboard (latest Context segment by default, all-history switch, per-request charts)",
|
|
65
65
|
handler: async (args, ctx) => {
|
|
66
66
|
const sessionManager = ctx.sessionManager;
|
|
67
67
|
const entries = typeof sessionManager.getBranch === "function"
|
|
@@ -71,10 +71,12 @@ export function parseEntries(entries) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// pi stores tool calls inside assistant content blocks
|
|
74
|
+
const calledTools = [];
|
|
74
75
|
for (const blk of m.content || []) {
|
|
75
76
|
if (blk && typeof blk === "object" && blk.type === "toolCall") {
|
|
76
77
|
const name = blk.tool || blk.name || "?";
|
|
77
78
|
toolCounts[name] = (toolCounts[name] || 0) + 1;
|
|
79
|
+
calledTools.push(name);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -129,6 +131,7 @@ export function parseEntries(entries) {
|
|
|
129
131
|
stopReason: m.stopReason || "",
|
|
130
132
|
estimatedCost: cost,
|
|
131
133
|
gapTools,
|
|
134
|
+
calledTools,
|
|
132
135
|
prevOutput: requests.length ? requests[requests.length - 1].output : 0,
|
|
133
136
|
});
|
|
134
137
|
gapTools = [];
|
|
@@ -184,11 +187,12 @@ const fmtPct1 = (x) => (x * 100).toFixed(1) + "%";
|
|
|
184
187
|
// ---------------------------------------------------------------------------
|
|
185
188
|
|
|
186
189
|
function lineChart(title, series, opts = {}) {
|
|
187
|
-
const { yMax = null, percent = false, timestamps = null, events = [] } = opts;
|
|
190
|
+
const { yMax = null, percent = false, timestamps = null, events = [], breakBefore = [], requestNumbers = null } = opts;
|
|
188
191
|
const width = 900, height = 330, left = 68, right = 20, top = 42, bottom = 52;
|
|
189
192
|
const plotWidth = width - left - right, plotHeight = height - top - bottom;
|
|
190
193
|
const count = Math.max(0, ...series.map(([, v]) => v.length));
|
|
191
|
-
const observedMax = Math.max(0, ...series.
|
|
194
|
+
const observedMax = Math.max(0, ...series.flatMap(([, values]) => values.filter(Number.isFinite)));
|
|
195
|
+
const breaks = new Set(breakBefore);
|
|
192
196
|
let maximum = yMax || Math.max(observedMax, 1);
|
|
193
197
|
if (!percent) {
|
|
194
198
|
const magnitude = 10 ** Math.max(0, String(Math.trunc(maximum)).length - 1);
|
|
@@ -217,7 +221,7 @@ function lineChart(title, series, opts = {}) {
|
|
|
217
221
|
for (let index = 0; index < count; index++) {
|
|
218
222
|
if (count <= 20 || index % Math.max(1, Math.trunc(count / 12)) === 0 || index === count - 1) {
|
|
219
223
|
const [x] = point(index, 0);
|
|
220
|
-
parts.push(`<text class="tick" x="${x.toFixed(1)}" y="${height - 22}" text-anchor="middle">${index + 1}</text>`);
|
|
224
|
+
parts.push(`<text class="tick" x="${x.toFixed(1)}" y="${height - 22}" text-anchor="middle">${requestNumbers?.[index] ?? index + 1}</text>`);
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
parts.push(
|
|
@@ -240,11 +244,23 @@ function lineChart(title, series, opts = {}) {
|
|
|
240
244
|
});
|
|
241
245
|
series.forEach(([name, values], seriesIndex) => {
|
|
242
246
|
const [dark, light] = seriesColor(name);
|
|
243
|
-
const
|
|
244
|
-
|
|
247
|
+
const runs = [];
|
|
248
|
+
let run = [];
|
|
249
|
+
values.forEach((value, i) => {
|
|
250
|
+
if (breaks.has(i) || !Number.isFinite(value)) {
|
|
251
|
+
if (run.length) runs.push(run);
|
|
252
|
+
run = [];
|
|
253
|
+
}
|
|
254
|
+
if (Number.isFinite(value)) run.push(point(i, value).map((c) => c.toFixed(1)).join(","));
|
|
255
|
+
});
|
|
256
|
+
if (run.length) runs.push(run);
|
|
257
|
+
const polylines = runs.map((points) =>
|
|
258
|
+
`<polyline class="series-line" data-dark="${dark}" data-light="${light}" points="${points.join(" ")}" fill="none" stroke="${dark}" stroke-width="3"/>`,
|
|
259
|
+
).join("");
|
|
260
|
+
const labels = values.map((v) => !Number.isFinite(v) ? "N/A" : (percent ? `${(v * 100).toFixed(1)}%` : fmtInt(Math.trunc(v)))).join("|");
|
|
245
261
|
parts.push(
|
|
246
262
|
`<g class="series" data-series-id="${seriesIndex}" data-name="${esc(name, true)}" data-labels="${esc(labels, true)}">` +
|
|
247
|
-
|
|
263
|
+
polylines +
|
|
248
264
|
`<circle class="hover-point" data-dark="${dark}" data-light="${light}" r="5" fill="${dark}" visibility="hidden"/></g>`,
|
|
249
265
|
);
|
|
250
266
|
});
|
|
@@ -488,60 +504,121 @@ function renderMissPanel(miss) {
|
|
|
488
504
|
// render_usage_dashboard port
|
|
489
505
|
// ---------------------------------------------------------------------------
|
|
490
506
|
|
|
491
|
-
export function
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
export function buildContextSegments(requests, events) {
|
|
508
|
+
const starts = new Set(events
|
|
509
|
+
.filter((event) => event.kind === "compaction" && event.requestNumber >= 1 && event.requestNumber <= requests.length)
|
|
510
|
+
.map((event) => event.requestNumber));
|
|
511
|
+
const segments = [];
|
|
512
|
+
let start = 0;
|
|
513
|
+
for (let index = 1; index < requests.length; index++) {
|
|
514
|
+
if (!starts.has(requests[index].number)) continue;
|
|
515
|
+
segments.push({ number: segments.length + 1, requests: requests.slice(start, index) });
|
|
516
|
+
start = index;
|
|
495
517
|
}
|
|
518
|
+
segments.push({ number: segments.length + 1, requests: requests.slice(start) });
|
|
519
|
+
return segments;
|
|
520
|
+
}
|
|
496
521
|
|
|
522
|
+
function countToolCalls(requests) {
|
|
523
|
+
const counts = new Map();
|
|
524
|
+
for (const request of requests) {
|
|
525
|
+
for (const name of request.calledTools || []) counts.set(name, (counts.get(name) || 0) + 1);
|
|
526
|
+
}
|
|
527
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function eventsForRequests(events, requests) {
|
|
531
|
+
if (!requests.length) return [];
|
|
532
|
+
const from = requests[0].number;
|
|
533
|
+
const to = requests[requests.length - 1].number;
|
|
534
|
+
return events.filter((event) => event.requestNumber >= from && event.requestNumber <= to);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function chartEvents(events, requests) {
|
|
538
|
+
if (!requests.length) return [];
|
|
539
|
+
const first = requests[0].number;
|
|
540
|
+
return events.map((event) => ({ ...event, requestNumber: event.requestNumber - first + 1 }));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function renderDashboardView(requests, events, toolCalls, compactions, scopeLabel) {
|
|
497
544
|
const timestamps = requests.map((r) => r.timestamp);
|
|
545
|
+
const requestNumbers = requests.map((r) => r.number);
|
|
498
546
|
const cached = requests.map((r) => r.cached);
|
|
499
547
|
const cacheWrites = requests.map((r) => r.cacheWrite);
|
|
500
548
|
const fresh = requests.map((r) => r.fresh);
|
|
501
549
|
const outputs = requests.map((r) => r.output);
|
|
502
550
|
const reasoning = requests.map((r) => r.reasoning);
|
|
503
551
|
const rates = requests.map((r) => (r.fresh + r.cached + r.cacheWrite > 0 ? r.cached / (r.fresh + r.cached + r.cacheWrite) : 0));
|
|
552
|
+
const contextBreakBefore = new Set();
|
|
553
|
+
const cacheBreakBefore = new Set();
|
|
554
|
+
const resetBefore = new Set(events
|
|
555
|
+
.filter((event) => event.kind === "compaction" || event.kind === "model")
|
|
556
|
+
.map((event) => event.requestNumber));
|
|
504
557
|
const cumulativeRates = [];
|
|
505
558
|
let runCached = 0, runPrompt = 0;
|
|
506
|
-
|
|
559
|
+
let previousKey = null;
|
|
560
|
+
let cacheWindows = 1;
|
|
561
|
+
let currentWindowStart = 0;
|
|
562
|
+
for (let index = 0; index < requests.length; index++) {
|
|
563
|
+
const r = requests[index];
|
|
564
|
+
const key = `${r.provider}/${r.model}`;
|
|
565
|
+
const contextReset = index > 0 && events.some((event) => event.kind === "compaction" && event.requestNumber === r.number);
|
|
566
|
+
const cacheReset = index > 0 && (resetBefore.has(r.number) || (previousKey !== null && key !== previousKey));
|
|
567
|
+
if (contextReset) contextBreakBefore.add(index);
|
|
568
|
+
if (cacheReset) {
|
|
569
|
+
cacheBreakBefore.add(index);
|
|
570
|
+
cacheWindows += 1;
|
|
571
|
+
currentWindowStart = index;
|
|
572
|
+
runCached = 0;
|
|
573
|
+
runPrompt = 0;
|
|
574
|
+
}
|
|
507
575
|
runCached += r.cached;
|
|
508
576
|
runPrompt += r.fresh + r.cached + r.cacheWrite;
|
|
509
577
|
cumulativeRates.push(runPrompt > 0 ? runCached / runPrompt : 0);
|
|
578
|
+
previousKey = key;
|
|
510
579
|
}
|
|
511
580
|
|
|
512
581
|
const tot = (f) => requests.reduce((s, r) => s + f(r), 0);
|
|
513
582
|
const totalFresh = tot((r) => r.fresh), totalCached = tot((r) => r.cached),
|
|
514
583
|
totalWrite = tot((r) => r.cacheWrite), totalPrompt = totalFresh + totalCached + totalWrite,
|
|
515
584
|
totalOutput = tot((r) => r.output);
|
|
516
|
-
|
|
517
585
|
const cacheHitRate = totalPrompt > 0 && (totalCached > 0 || totalWrite > 0) ? totalCached / totalPrompt : null;
|
|
586
|
+
const windowRequests = requests.slice(currentWindowStart);
|
|
587
|
+
const windowPrompt = windowRequests.reduce((sum, r) => sum + r.fresh + r.cached + r.cacheWrite, 0);
|
|
588
|
+
const windowCached = windowRequests.reduce((sum, r) => sum + r.cached, 0);
|
|
589
|
+
const windowHitRate = windowPrompt > 0 && (windowCached > 0 || windowRequests.some((r) => r.cacheWrite > 0))
|
|
590
|
+
? windowCached / windowPrompt
|
|
591
|
+
: null;
|
|
518
592
|
|
|
519
593
|
const cards = [
|
|
594
|
+
["Scope", scopeLabel],
|
|
520
595
|
["Model requests", fmtInt(requests.length)],
|
|
521
|
-
["
|
|
596
|
+
["View cache hit rate", cacheHitRate !== null ? fmtPct1(cacheHitRate) : "N/A"],
|
|
597
|
+
["Latest cache window", `#${cacheWindows} · ${windowHitRate !== null ? fmtPct1(windowHitRate) : "N/A"}`],
|
|
522
598
|
["Cached input", fmtInt(totalCached)],
|
|
523
599
|
["Reported cache writes", fmtInt(totalWrite)],
|
|
524
600
|
["Fresh input", fmtInt(totalFresh)],
|
|
525
601
|
["Total prompt input", fmtInt(totalPrompt)],
|
|
526
602
|
["Output tokens", fmtInt(totalOutput)],
|
|
527
|
-
["Compactions", String(compactions)],
|
|
603
|
+
["Compactions in view", String(compactions)],
|
|
528
604
|
];
|
|
529
605
|
const cardsHtml = cards
|
|
530
606
|
.map(([l, v]) => `<div class="usage-card"><span>${esc(l)}</span><strong>${esc(v)}</strong></div>`)
|
|
531
607
|
.join("");
|
|
532
608
|
|
|
609
|
+
const eventMarkers = chartEvents(events, requests);
|
|
533
610
|
const charts = [
|
|
534
611
|
figure(lineChart("Prompt input by request",
|
|
535
612
|
[["cached", cached], ["cache writes", cacheWrites], ["fresh", fresh]],
|
|
536
|
-
{ timestamps, events })),
|
|
613
|
+
{ timestamps, requestNumbers, events: eventMarkers, breakBefore: contextBreakBefore })),
|
|
537
614
|
];
|
|
538
615
|
if (cacheHitRate !== null) {
|
|
539
616
|
charts.push(figure(lineChart("Cache hit rate",
|
|
540
617
|
[["request", rates], ["cumulative", cumulativeRates]],
|
|
541
|
-
{ yMax: 1.0, percent: true, timestamps })));
|
|
618
|
+
{ yMax: 1.0, percent: true, timestamps, requestNumbers, events: eventMarkers, breakBefore: cacheBreakBefore })));
|
|
542
619
|
}
|
|
543
620
|
charts.push(figure(lineChart("Output and reasoning tokens",
|
|
544
|
-
[["output", outputs], ["reasoning", reasoning]], { timestamps })));
|
|
621
|
+
[["output", outputs], ["reasoning", reasoning]], { timestamps, requestNumbers, events: eventMarkers, breakBefore: contextBreakBefore })));
|
|
545
622
|
|
|
546
623
|
const showHit = cacheHitRate !== null;
|
|
547
624
|
const rows = requests
|
|
@@ -561,9 +638,8 @@ export function renderDashboard(data) {
|
|
|
561
638
|
|
|
562
639
|
return (
|
|
563
640
|
`<div class="usage-cards">${cardsHtml}</div>` +
|
|
564
|
-
`<p class="usage-note">
|
|
565
|
-
`
|
|
566
|
-
`compactions, model or thinking changes. Some providers report cache reads but not cache writes.</p>` +
|
|
641
|
+
`<p class="usage-note">Prompt and output lines break at a Context segment boundary (compaction). Cache-window cumulative hit rate also resets at a model change. ` +
|
|
642
|
+
`TTL and service interruptions are shown only when recorded or inferred by the miss analysis; they do not create a Context segment.</p>` +
|
|
567
643
|
`<div class="usage-charts">${charts.join("")}</div>` +
|
|
568
644
|
renderMissPanel(analyzeMisses(requests, events)) +
|
|
569
645
|
renderStreakPanel(analyzeStreaks(requests, events)) +
|
|
@@ -577,6 +653,46 @@ export function renderDashboard(data) {
|
|
|
577
653
|
);
|
|
578
654
|
}
|
|
579
655
|
|
|
656
|
+
export function renderDashboard(data) {
|
|
657
|
+
const { requests, events, toolCalls, compactions } = data;
|
|
658
|
+
if (!requests.length) {
|
|
659
|
+
return `<p class="empty">No assistant responses with token usage were found in this session.</p>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const segments = buildContextSegments(requests, events);
|
|
663
|
+
const latest = segments[segments.length - 1];
|
|
664
|
+
const allView = renderDashboardView(requests, events, toolCalls, compactions, `All ${segments.length} context segment${segments.length === 1 ? "" : "s"}`);
|
|
665
|
+
const segmentViews = segments.map((segment) => {
|
|
666
|
+
const segmentEvents = eventsForRequests(events, segment.requests);
|
|
667
|
+
const startsWithCompaction = segmentEvents.some((event) => event.kind === "compaction" && event.requestNumber === segment.requests[0].number);
|
|
668
|
+
const label = `Context segment ${segment.number}${segment.number === latest.number ? " (latest)" : ""}`;
|
|
669
|
+
return {
|
|
670
|
+
id: `segment-${segment.number}`,
|
|
671
|
+
label,
|
|
672
|
+
html: renderDashboardView(
|
|
673
|
+
segment.requests,
|
|
674
|
+
segmentEvents,
|
|
675
|
+
countToolCalls(segment.requests),
|
|
676
|
+
startsWithCompaction ? 1 : 0,
|
|
677
|
+
label,
|
|
678
|
+
),
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
const options = [
|
|
682
|
+
`<option value="all">All context segments</option>`,
|
|
683
|
+
...segmentViews.map((view) => `<option value="${view.id}"${view.id === `segment-${latest.number}` ? " selected" : ""}>${esc(view.label)}</option>`),
|
|
684
|
+
].join("");
|
|
685
|
+
const views = [
|
|
686
|
+
`<section class="usage-context-view" data-context-view="all" hidden>${allView}</section>`,
|
|
687
|
+
...segmentViews.map((view) => `<section class="usage-context-view" data-context-view="${view.id}"${view.id === `segment-${latest.number}` ? "" : " hidden"}>${view.html}</section>`),
|
|
688
|
+
].join("");
|
|
689
|
+
|
|
690
|
+
return `<div class="usage-view-controls"><label for="context-segment-select">View</label>` +
|
|
691
|
+
`<select id="context-segment-select" data-context-segment-select>${options}</select>` +
|
|
692
|
+
`<span>Default: latest Context segment. All-history values remain available; cache metrics reset where compaction rewrites the prefix or the model changes.</span></div>` +
|
|
693
|
+
views;
|
|
694
|
+
}
|
|
695
|
+
|
|
580
696
|
// ---------------------------------------------------------------------------
|
|
581
697
|
// Full page
|
|
582
698
|
// ---------------------------------------------------------------------------
|
|
@@ -602,6 +718,10 @@ main { max-width: 1080px; margin: 0 auto; padding: 40px 24px 80px; }
|
|
|
602
718
|
h1 { color: var(--bright); font-size: 1.6rem; font-weight: 500; margin: 6px 0 4px; }
|
|
603
719
|
.sub { color: var(--muted); font-size: .78rem; margin: 0 0 30px; }
|
|
604
720
|
.usage-shell { margin-top: 18px; }
|
|
721
|
+
.usage-view-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 9px 12px; margin-bottom: 18px; color: var(--muted); font-size: .78rem; }
|
|
722
|
+
.usage-view-controls label { color: var(--bright); }
|
|
723
|
+
.usage-view-controls select { background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text); font: inherit; padding: 5px 8px; }
|
|
724
|
+
.usage-context-view[hidden] { display: none; }
|
|
605
725
|
/* keep hover points visible on dark page chrome for both chart palettes */
|
|
606
726
|
.hover-point { stroke: #ffffff; stroke-width: 2; }
|
|
607
727
|
${USAGE_STYLES}
|
|
@@ -611,6 +731,15 @@ ${USAGE_STYLES}
|
|
|
611
731
|
<h1>${esc(title)}</h1>
|
|
612
732
|
<p class="sub">tau-style dashboard rendered by pi-cache-dashboard · ${n} requests · times in ${esc(tzLabel())}${opts.sessionId ? " · session " + esc(opts.sessionId) : ""}</p>
|
|
613
733
|
<section class="usage-shell" id="panel-usage">${renderDashboard(data)}</section>
|
|
614
|
-
<script>${USAGE_SCRIPT}
|
|
734
|
+
<script>${USAGE_SCRIPT}
|
|
735
|
+
for (const select of document.querySelectorAll("[data-context-segment-select]")) {
|
|
736
|
+
const shell = select.closest(".usage-shell");
|
|
737
|
+
const show = () => shell.querySelectorAll("[data-context-view]").forEach((view) => {
|
|
738
|
+
view.hidden = view.dataset.contextView !== select.value;
|
|
739
|
+
});
|
|
740
|
+
select.addEventListener("change", show);
|
|
741
|
+
show();
|
|
742
|
+
}
|
|
743
|
+
</script>
|
|
615
744
|
</main></body></html>`;
|
|
616
745
|
}
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import registerCacheExport from "../index.ts";
|
|
7
|
-
import { parseEntries, analyzeStreaks, analyzeMisses, lineChartForTest } from "../render.ts";
|
|
7
|
+
import { parseEntries, analyzeStreaks, analyzeMisses, buildContextSegments, lineChartForTest, renderDashboard } from "../render.ts";
|
|
8
8
|
|
|
9
9
|
let pass = 0, fail = 0;
|
|
10
10
|
function check(name, cond, detail = "") {
|
|
@@ -318,6 +318,28 @@ const hitOf = (r) => r.cached / (r.fresh + r.cached + r.cacheWrite);
|
|
|
318
318
|
s.length === 2 && s.every((x) => x.hits.length === 3), JSON.stringify(s.map((x) => [x.from, x.to])));
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// P2. Context segments are compaction-only; models reset cache windows -----
|
|
322
|
+
{
|
|
323
|
+
const entries = [
|
|
324
|
+
req({ cached: 90000, fresh: 10000, min: 1, model: "a" }),
|
|
325
|
+
{ type: "model_change", timestamp: "2026-01-01T00:01:30Z", provider: "p", modelId: "b" },
|
|
326
|
+
req({ cached: 0, fresh: 100000, min: 2, model: "b" }),
|
|
327
|
+
{ type: "compaction", timestamp: "2026-01-01T00:02:30Z" },
|
|
328
|
+
req({ cached: 0, fresh: 20000, min: 3, model: "b" }),
|
|
329
|
+
];
|
|
330
|
+
const d = parseEntries(entries);
|
|
331
|
+
const segments = buildContextSegments(d.requests, d.events);
|
|
332
|
+
check("P2 model switch does not create a Context segment",
|
|
333
|
+
segments.length === 2 && segments[0].requests.length === 2 && segments[1].requests.length === 1,
|
|
334
|
+
JSON.stringify(segments.map((segment) => segment.requests.map((request) => request.number))));
|
|
335
|
+
const chart = lineChartForTest("segmented", [["cached", [10, 20, 30]]], { breakBefore: new Set([2]) });
|
|
336
|
+
check("P2 compaction chart boundary breaks the polyline", (chart.match(/<polyline /g) || []).length === 2, chart);
|
|
337
|
+
const dashboard = renderDashboard(d);
|
|
338
|
+
check("P2 dashboard defaults to latest segment and keeps an all-history view",
|
|
339
|
+
dashboard.includes('value="all"') && dashboard.includes('value="segment-2" selected') && dashboard.includes('data-context-view="all" hidden'),
|
|
340
|
+
dashboard.slice(0, 500));
|
|
341
|
+
}
|
|
342
|
+
|
|
321
343
|
// Q. subagent results remain visible as main-session tool input -----------
|
|
322
344
|
{
|
|
323
345
|
const entries = [
|
package/global/AGENTS.md
CHANGED
|
@@ -74,17 +74,7 @@ Before adding code to any file:
|
|
|
74
74
|
- Check shared utilities in the same module.
|
|
75
75
|
- If you're unsure why code is structured a certain way, ask before writing.
|
|
76
76
|
|
|
77
|
-
## 6.
|
|
78
|
-
|
|
79
|
-
**Multi-step work needs checkpoints. Don't keep going from a state you can't describe.**
|
|
80
|
-
|
|
81
|
-
After each significant step:
|
|
82
|
-
- Summarize what was done.
|
|
83
|
-
- State what was verified.
|
|
84
|
-
- State what remains.
|
|
85
|
-
- If you can't describe the current state back, stop and restate instead of continuing.
|
|
86
|
-
|
|
87
|
-
## 7. Fail Visibly, Not Silently
|
|
77
|
+
## 6. Fail Visibly, Not Silently
|
|
88
78
|
|
|
89
79
|
**Silent skips are failures. Surface uncertainty instead of hiding it.**
|
|
90
80
|
|
|
@@ -93,14 +83,14 @@ After each significant step:
|
|
|
93
83
|
- Default to surfacing uncertainty. When in doubt, over-communicate what you're unsure about.
|
|
94
84
|
- If a required input, file, or tool is missing (or a step is impossible under the stated constraints), do NOT fabricate it to make a check pass. Report what is missing and what you need.
|
|
95
85
|
|
|
96
|
-
##
|
|
86
|
+
## 7. Reuse Before Writing
|
|
97
87
|
|
|
98
88
|
**Before writing new code, check in order:**
|
|
99
89
|
1. Does it already exist in this codebase? Reuse it - do not re-implement it yourself, even with the standard library. (If the existing helper is itself the bug, fix it once - see Rule 3.)
|
|
100
90
|
2. Does the standard library cover it? Use it.
|
|
101
91
|
3. Only then: write the minimum code that works.
|
|
102
92
|
|
|
103
|
-
##
|
|
93
|
+
## 8. Use Available Editing Tools
|
|
104
94
|
|
|
105
95
|
**Follow the current harness's tool surface; do not assume a shell patch helper exists.**
|
|
106
96
|
|
package/package.json
CHANGED