pi-mega-compact 0.6.7 → 0.6.9
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/dist/extensions/dashboard-server.js +95 -8
- package/dist/src/compact.js +5 -1
- package/dist/src/supersede.js +8 -5
- package/extensions/dashboard-server.ts +108 -8
- package/package.json +1 -1
- package/src/compact.ts +5 -1
- package/src/supersede.ts +8 -5
|
@@ -83,7 +83,52 @@ function readIndex() {
|
|
|
83
83
|
inputRate: r.input_rate ?? null,
|
|
84
84
|
outputRate: r.output_rate ?? null,
|
|
85
85
|
lastSeen: Number(r.last_seen ?? 0),
|
|
86
|
+
// Defaults — enriched below from each repo's own store.
|
|
87
|
+
tokensKept: 0,
|
|
88
|
+
tokensDropped: 0,
|
|
89
|
+
sessions: 0,
|
|
90
|
+
contextWindow: null,
|
|
91
|
+
maxTokens: null,
|
|
92
|
+
reasoning: null,
|
|
86
93
|
}));
|
|
94
|
+
// Enrich each repo with per-store token + model detail read directly via
|
|
95
|
+
// node:sqlite (same zero-dependency invariant as readIndex; no store graph
|
|
96
|
+
// import). Best-effort: a missing/corrupt store degrades to the defaults
|
|
97
|
+
// above so the dashboard never fails to render.
|
|
98
|
+
for (const repo of mapped) {
|
|
99
|
+
try {
|
|
100
|
+
const storePath = join(repo.stateDir, "sqlite.db");
|
|
101
|
+
if (existsSync(storePath)) {
|
|
102
|
+
const sdb = new DatabaseSync(storePath, { readOnly: true });
|
|
103
|
+
try {
|
|
104
|
+
const tok = sdb
|
|
105
|
+
.prepare(`SELECT COALESCE(SUM(token_estimate),0) AS kept,
|
|
106
|
+
COALESCE(SUM(original_token_estimate),0) AS dropped,
|
|
107
|
+
COUNT(DISTINCT session_id) AS sess
|
|
108
|
+
FROM context_chunks WHERE dedup_status != 'removed'`)
|
|
109
|
+
.get();
|
|
110
|
+
repo.tokensKept = Number(tok.kept ?? 0);
|
|
111
|
+
repo.tokensDropped = Number(tok.dropped ?? 0);
|
|
112
|
+
repo.sessions = Number(tok.sess ?? 0);
|
|
113
|
+
const mrow = sdb
|
|
114
|
+
.prepare(`SELECT context_window, max_tokens, reasoning
|
|
115
|
+
FROM model_snapshots ORDER BY captured_at DESC LIMIT 1`)
|
|
116
|
+
.get();
|
|
117
|
+
if (mrow) {
|
|
118
|
+
repo.contextWindow = Number(mrow.context_window ?? 0) || null;
|
|
119
|
+
repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
|
|
120
|
+
repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
sdb.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* best-effort — keep the defaults */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
87
132
|
// Defensive display hygiene (belt-and-suspenders — the real fix is that
|
|
88
133
|
// tests now isolate via MEGACOMPACT_INDEX_DIR): drop transient test/temp
|
|
89
134
|
// paths that should never have been real repos, and collapse duplicate
|
|
@@ -443,15 +488,23 @@ function dashboardHtml(tierName) {
|
|
|
443
488
|
<thead>
|
|
444
489
|
<tr>
|
|
445
490
|
<th>Model</th><th>Provider</th>
|
|
446
|
-
<th style="text-align:right">
|
|
491
|
+
<th style="text-align:right" title="Tokens dropped from context by compaction (the input reclaimed)">Tokens In</th>
|
|
492
|
+
<th style="text-align:right" title="Tokens kept as compacted summaries still in context (the output retained)">Tokens Out</th>
|
|
493
|
+
<th style="text-align:right">Freed</th>
|
|
494
|
+
<th style="text-align:right" title="Model context window (max input tokens the model accepts)">Ctx Window</th>
|
|
495
|
+
<th style="text-align:right" title="Model max output tokens per turn">Max Out</th>
|
|
496
|
+
<th style="text-align:right" title="Reasoning-capable model">Reas.</th>
|
|
497
|
+
<th style="text-align:right" title="Distinct sessions with at least one checkpoint">Sessions</th>
|
|
447
498
|
<th style="text-align:right">Checkpoints</th>
|
|
448
|
-
<th style="text-align:right">
|
|
499
|
+
<th style="text-align:right" title="USD per input token">In $/tok</th>
|
|
500
|
+
<th style="text-align:right" title="USD per output token">Out $/tok</th>
|
|
449
501
|
<th style="text-align:right">$ Saved</th>
|
|
450
502
|
<th style="text-align:right">Last Used</th>
|
|
451
503
|
</tr>
|
|
452
504
|
</thead>
|
|
453
|
-
<tbody id="bm-rows"><tr><td colspan="
|
|
505
|
+
<tbody id="bm-rows"><tr><td colspan="14" class="repo-none">loading…</td></tr></tbody>
|
|
454
506
|
</table>
|
|
507
|
+
<p class="legend-note" style="margin-top:8px">Tokens In = Σ original region tokens dropped by compaction. Tokens Out = Σ compacted summary tokens still retained in context. Freed = Tokens In − Tokens Out (net context reclaimed). Ctx Window / Max Out / Reas. come from the latest captured model snapshot for each repo.</p>
|
|
455
508
|
|
|
456
509
|
<div class="updated" id="sm-updated"></div>
|
|
457
510
|
</div>
|
|
@@ -673,33 +726,67 @@ function dashboardHtml(tierName) {
|
|
|
673
726
|
var rows = document.getElementById('bm-rows');
|
|
674
727
|
if (!rows) return;
|
|
675
728
|
if (!repos || !repos.length) {
|
|
676
|
-
rows.innerHTML = '<tr><td colspan="
|
|
729
|
+
rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
|
|
677
730
|
return;
|
|
678
731
|
}
|
|
679
732
|
var groups = {};
|
|
680
733
|
for (var i = 0; i < repos.length; i++) {
|
|
681
734
|
var r = repos[i];
|
|
682
735
|
var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
|
|
683
|
-
if (!groups[key]) groups[key] = {
|
|
736
|
+
if (!groups[key]) groups[key] = {
|
|
737
|
+
model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0,
|
|
738
|
+
tokensSaved: 0, tokensIn: 0, tokensOut: 0, sessions: 0, usd: 0, lastAt: 0,
|
|
739
|
+
inRates: [], outRates: [], ctxWindows: [], maxTokens: [], reasoning: null,
|
|
740
|
+
};
|
|
684
741
|
var g = groups[key];
|
|
685
742
|
g.repos++;
|
|
686
743
|
g.checkpoints += (r.checkpointCount || 0);
|
|
687
744
|
g.tokensSaved += (r.tokensSaved || 0);
|
|
688
|
-
|
|
745
|
+
g.tokensIn += (r.tokensDropped || 0);
|
|
746
|
+
g.tokensOut += (r.tokensKept || 0);
|
|
747
|
+
g.sessions += (r.sessions || 0);
|
|
748
|
+
if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.inRates.push(r.inputRate); }
|
|
749
|
+
if (r.outputRate) g.outRates.push(r.outputRate);
|
|
750
|
+
if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
|
|
751
|
+
if (r.maxTokens) g.maxTokens.push(r.maxTokens);
|
|
752
|
+
if (r.reasoning != null) g.reasoning = r.reasoning;
|
|
689
753
|
if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
|
|
690
754
|
}
|
|
691
755
|
var arr = [];
|
|
692
756
|
for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
|
|
693
757
|
arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
|
|
758
|
+
// Helper: a set of numeric samples collapses to a single value when all
|
|
759
|
+
// repos in the group agree, otherwise shows the range (min–max) so the
|
|
760
|
+
// user can see mixed-config model groups at a glance.
|
|
761
|
+
function collapseNum(samples) {
|
|
762
|
+
if (!samples || !samples.length) return '—';
|
|
763
|
+
var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
|
|
764
|
+
return lo === hi ? lo.toLocaleString() : lo.toLocaleString() + '–' + hi.toLocaleString();
|
|
765
|
+
}
|
|
766
|
+
function collapseRate(samples) {
|
|
767
|
+
if (!samples || !samples.length) return '—';
|
|
768
|
+
var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
|
|
769
|
+
var fmt = function(v) { return '$' + v.toFixed(6); };
|
|
770
|
+
return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
|
|
771
|
+
}
|
|
694
772
|
rows.innerHTML = arr.map(function(g) {
|
|
773
|
+
var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
|
|
695
774
|
var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
|
|
696
775
|
var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
|
|
776
|
+
var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
|
|
697
777
|
return '<tr>' +
|
|
698
778
|
'<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
|
|
699
779
|
'<td>' + sanitize(g.provider) + '</td>' +
|
|
700
|
-
'<td class="num">' + g.
|
|
780
|
+
'<td class="num">' + (g.tokensIn || 0).toLocaleString() + '</td>' +
|
|
781
|
+
'<td class="num">' + (g.tokensOut || 0).toLocaleString() + '</td>' +
|
|
782
|
+
'<td class="num">' + freed.toLocaleString() + '</td>' +
|
|
783
|
+
'<td class="num">' + collapseNum(g.ctxWindows) + '</td>' +
|
|
784
|
+
'<td class="num">' + collapseNum(g.maxTokens) + '</td>' +
|
|
785
|
+
'<td class="num">' + reas + '</td>' +
|
|
786
|
+
'<td class="num">' + g.sessions.toLocaleString() + '</td>' +
|
|
701
787
|
'<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
|
|
702
|
-
'<td class="num">' + g.
|
|
788
|
+
'<td class="num">' + collapseRate(g.inRates) + '</td>' +
|
|
789
|
+
'<td class="num">' + collapseRate(g.outRates) + '</td>' +
|
|
703
790
|
'<td class="num">' + sanitize(usd) + '</td>' +
|
|
704
791
|
'<td class="num">' + sanitize(when) + '</td>' +
|
|
705
792
|
'</tr>';
|
package/dist/src/compact.js
CHANGED
|
@@ -16,7 +16,11 @@ function truncate(s, max) {
|
|
|
16
16
|
return s.length <= max ? s : `${s.slice(0, max)}…`;
|
|
17
17
|
}
|
|
18
18
|
function firstText(m) {
|
|
19
|
-
|
|
19
|
+
// PREVENT crash: pi can hand us a message with text: undefined (pure
|
|
20
|
+
// tool-call/tool-result). Guard the trim so the legacy summarizeMessages
|
|
21
|
+
// path can't throw the same undefined-text crash the extractive path did.
|
|
22
|
+
const raw = m.text ?? "";
|
|
23
|
+
const t = raw.trim();
|
|
20
24
|
return t.length > 0 ? t : undefined;
|
|
21
25
|
}
|
|
22
26
|
/** Heuristic: does this text look like chatty filler we can collapse? */
|
package/dist/src/supersede.js
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
import { extractFileCandidates } from "./compact.js";
|
|
10
10
|
/** Classify a message's relationship to a file path. */
|
|
11
11
|
function fileOps(msg) {
|
|
12
|
-
// msg.text may be undefined for pure tool-call/result
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
12
|
+
// PREVENT crash: msg.text may be undefined for pure tool-call/result
|
|
13
|
+
// messages. extractFileCandidates guards the split, but if it returned a
|
|
14
|
+
// hit we'd still call .toLowerCase() on the raw (possibly-undefined) text.
|
|
15
|
+
// Coerce once so both extractFileCandidates and the write-detection regex
|
|
16
|
+
// are safe, and the early return still skips empty messages.
|
|
17
|
+
const text = msg.text ?? "";
|
|
18
|
+
const paths = extractFileCandidates(text);
|
|
16
19
|
if (paths.length === 0)
|
|
17
20
|
return [];
|
|
18
|
-
const low =
|
|
21
|
+
const low = text.toLowerCase();
|
|
19
22
|
const isWrite = /\b(write|edit|create|save|append|overwrite|update|patch|modify)\b/.test(low);
|
|
20
23
|
return paths.map((p) => ({ path: p, op: isWrite ? "write" : "read" }));
|
|
21
24
|
}
|
|
@@ -70,6 +70,17 @@ interface IndexRepo {
|
|
|
70
70
|
inputRate: number | null;
|
|
71
71
|
outputRate: number | null;
|
|
72
72
|
lastSeen: number;
|
|
73
|
+
// Per-repo token + model detail (S25 dashboard enrichment), read directly
|
|
74
|
+
// from each repo's node:sqlite store at stateDir. tokensKept = Σ stored
|
|
75
|
+
// summary tokens ("out"); tokensDropped = Σ original region tokens ("in");
|
|
76
|
+
// sessions = distinct sessions with a checkpoint; contextWindow/maxTokens/
|
|
77
|
+
// reasoning come from the latest model_snapshots row for that repo.
|
|
78
|
+
tokensKept: number;
|
|
79
|
+
tokensDropped: number;
|
|
80
|
+
sessions: number;
|
|
81
|
+
contextWindow: number | null;
|
|
82
|
+
maxTokens: number | null;
|
|
83
|
+
reasoning: boolean | null;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
interface IndexSummary {
|
|
@@ -107,7 +118,54 @@ function readIndex(): IndexIndex | null {
|
|
|
107
118
|
inputRate: (r.input_rate as number | null) ?? null,
|
|
108
119
|
outputRate: (r.output_rate as number | null) ?? null,
|
|
109
120
|
lastSeen: Number(r.last_seen ?? 0),
|
|
121
|
+
// Defaults — enriched below from each repo's own store.
|
|
122
|
+
tokensKept: 0,
|
|
123
|
+
tokensDropped: 0,
|
|
124
|
+
sessions: 0,
|
|
125
|
+
contextWindow: null,
|
|
126
|
+
maxTokens: null,
|
|
127
|
+
reasoning: null,
|
|
110
128
|
}));
|
|
129
|
+
// Enrich each repo with per-store token + model detail read directly via
|
|
130
|
+
// node:sqlite (same zero-dependency invariant as readIndex; no store graph
|
|
131
|
+
// import). Best-effort: a missing/corrupt store degrades to the defaults
|
|
132
|
+
// above so the dashboard never fails to render.
|
|
133
|
+
for (const repo of mapped) {
|
|
134
|
+
try {
|
|
135
|
+
const storePath = join(repo.stateDir, "sqlite.db");
|
|
136
|
+
if (existsSync(storePath)) {
|
|
137
|
+
const sdb = new DatabaseSync(storePath, { readOnly: true });
|
|
138
|
+
try {
|
|
139
|
+
const tok = sdb
|
|
140
|
+
.prepare(
|
|
141
|
+
`SELECT COALESCE(SUM(token_estimate),0) AS kept,
|
|
142
|
+
COALESCE(SUM(original_token_estimate),0) AS dropped,
|
|
143
|
+
COUNT(DISTINCT session_id) AS sess
|
|
144
|
+
FROM context_chunks WHERE dedup_status != 'removed'`,
|
|
145
|
+
)
|
|
146
|
+
.get() as { kept: number; dropped: number; sess: number };
|
|
147
|
+
repo.tokensKept = Number(tok.kept ?? 0);
|
|
148
|
+
repo.tokensDropped = Number(tok.dropped ?? 0);
|
|
149
|
+
repo.sessions = Number(tok.sess ?? 0);
|
|
150
|
+
const mrow = sdb
|
|
151
|
+
.prepare(
|
|
152
|
+
`SELECT context_window, max_tokens, reasoning
|
|
153
|
+
FROM model_snapshots ORDER BY captured_at DESC LIMIT 1`,
|
|
154
|
+
)
|
|
155
|
+
.get() as { context_window: number; max_tokens: number; reasoning: number } | undefined;
|
|
156
|
+
if (mrow) {
|
|
157
|
+
repo.contextWindow = Number(mrow.context_window ?? 0) || null;
|
|
158
|
+
repo.maxTokens = Number(mrow.max_tokens ?? 0) || null;
|
|
159
|
+
repo.reasoning = Number(mrow.reasoning ?? 0) === 1;
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
sdb.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
/* best-effort — keep the defaults */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
111
169
|
// Defensive display hygiene (belt-and-suspenders — the real fix is that
|
|
112
170
|
// tests now isolate via MEGACOMPACT_INDEX_DIR): drop transient test/temp
|
|
113
171
|
// paths that should never have been real repos, and collapse duplicate
|
|
@@ -541,15 +599,23 @@ function dashboardHtml(tierName: string): string {
|
|
|
541
599
|
<thead>
|
|
542
600
|
<tr>
|
|
543
601
|
<th>Model</th><th>Provider</th>
|
|
544
|
-
<th style="text-align:right">
|
|
602
|
+
<th style="text-align:right" title="Tokens dropped from context by compaction (the input reclaimed)">Tokens In</th>
|
|
603
|
+
<th style="text-align:right" title="Tokens kept as compacted summaries still in context (the output retained)">Tokens Out</th>
|
|
604
|
+
<th style="text-align:right">Freed</th>
|
|
605
|
+
<th style="text-align:right" title="Model context window (max input tokens the model accepts)">Ctx Window</th>
|
|
606
|
+
<th style="text-align:right" title="Model max output tokens per turn">Max Out</th>
|
|
607
|
+
<th style="text-align:right" title="Reasoning-capable model">Reas.</th>
|
|
608
|
+
<th style="text-align:right" title="Distinct sessions with at least one checkpoint">Sessions</th>
|
|
545
609
|
<th style="text-align:right">Checkpoints</th>
|
|
546
|
-
<th style="text-align:right">
|
|
610
|
+
<th style="text-align:right" title="USD per input token">In $/tok</th>
|
|
611
|
+
<th style="text-align:right" title="USD per output token">Out $/tok</th>
|
|
547
612
|
<th style="text-align:right">$ Saved</th>
|
|
548
613
|
<th style="text-align:right">Last Used</th>
|
|
549
614
|
</tr>
|
|
550
615
|
</thead>
|
|
551
|
-
<tbody id="bm-rows"><tr><td colspan="
|
|
616
|
+
<tbody id="bm-rows"><tr><td colspan="14" class="repo-none">loading…</td></tr></tbody>
|
|
552
617
|
</table>
|
|
618
|
+
<p class="legend-note" style="margin-top:8px">Tokens In = Σ original region tokens dropped by compaction. Tokens Out = Σ compacted summary tokens still retained in context. Freed = Tokens In − Tokens Out (net context reclaimed). Ctx Window / Max Out / Reas. come from the latest captured model snapshot for each repo.</p>
|
|
553
619
|
|
|
554
620
|
<div class="updated" id="sm-updated"></div>
|
|
555
621
|
</div>
|
|
@@ -771,33 +837,67 @@ function dashboardHtml(tierName: string): string {
|
|
|
771
837
|
var rows = document.getElementById('bm-rows');
|
|
772
838
|
if (!rows) return;
|
|
773
839
|
if (!repos || !repos.length) {
|
|
774
|
-
rows.innerHTML = '<tr><td colspan="
|
|
840
|
+
rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
|
|
775
841
|
return;
|
|
776
842
|
}
|
|
777
843
|
var groups = {};
|
|
778
844
|
for (var i = 0; i < repos.length; i++) {
|
|
779
845
|
var r = repos[i];
|
|
780
846
|
var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
|
|
781
|
-
if (!groups[key]) groups[key] = {
|
|
847
|
+
if (!groups[key]) groups[key] = {
|
|
848
|
+
model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0,
|
|
849
|
+
tokensSaved: 0, tokensIn: 0, tokensOut: 0, sessions: 0, usd: 0, lastAt: 0,
|
|
850
|
+
inRates: [], outRates: [], ctxWindows: [], maxTokens: [], reasoning: null,
|
|
851
|
+
};
|
|
782
852
|
var g = groups[key];
|
|
783
853
|
g.repos++;
|
|
784
854
|
g.checkpoints += (r.checkpointCount || 0);
|
|
785
855
|
g.tokensSaved += (r.tokensSaved || 0);
|
|
786
|
-
|
|
856
|
+
g.tokensIn += (r.tokensDropped || 0);
|
|
857
|
+
g.tokensOut += (r.tokensKept || 0);
|
|
858
|
+
g.sessions += (r.sessions || 0);
|
|
859
|
+
if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.inRates.push(r.inputRate); }
|
|
860
|
+
if (r.outputRate) g.outRates.push(r.outputRate);
|
|
861
|
+
if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
|
|
862
|
+
if (r.maxTokens) g.maxTokens.push(r.maxTokens);
|
|
863
|
+
if (r.reasoning != null) g.reasoning = r.reasoning;
|
|
787
864
|
if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
|
|
788
865
|
}
|
|
789
866
|
var arr = [];
|
|
790
867
|
for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
|
|
791
868
|
arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
|
|
869
|
+
// Helper: a set of numeric samples collapses to a single value when all
|
|
870
|
+
// repos in the group agree, otherwise shows the range (min–max) so the
|
|
871
|
+
// user can see mixed-config model groups at a glance.
|
|
872
|
+
function collapseNum(samples) {
|
|
873
|
+
if (!samples || !samples.length) return '—';
|
|
874
|
+
var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
|
|
875
|
+
return lo === hi ? lo.toLocaleString() : lo.toLocaleString() + '–' + hi.toLocaleString();
|
|
876
|
+
}
|
|
877
|
+
function collapseRate(samples) {
|
|
878
|
+
if (!samples || !samples.length) return '—';
|
|
879
|
+
var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
|
|
880
|
+
var fmt = function(v) { return '$' + v.toFixed(6); };
|
|
881
|
+
return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
|
|
882
|
+
}
|
|
792
883
|
rows.innerHTML = arr.map(function(g) {
|
|
884
|
+
var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
|
|
793
885
|
var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
|
|
794
886
|
var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
|
|
887
|
+
var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
|
|
795
888
|
return '<tr>' +
|
|
796
889
|
'<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
|
|
797
890
|
'<td>' + sanitize(g.provider) + '</td>' +
|
|
798
|
-
'<td class="num">' + g.
|
|
891
|
+
'<td class="num">' + (g.tokensIn || 0).toLocaleString() + '</td>' +
|
|
892
|
+
'<td class="num">' + (g.tokensOut || 0).toLocaleString() + '</td>' +
|
|
893
|
+
'<td class="num">' + freed.toLocaleString() + '</td>' +
|
|
894
|
+
'<td class="num">' + collapseNum(g.ctxWindows) + '</td>' +
|
|
895
|
+
'<td class="num">' + collapseNum(g.maxTokens) + '</td>' +
|
|
896
|
+
'<td class="num">' + reas + '</td>' +
|
|
897
|
+
'<td class="num">' + g.sessions.toLocaleString() + '</td>' +
|
|
799
898
|
'<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
|
|
800
|
-
'<td class="num">' + g.
|
|
899
|
+
'<td class="num">' + collapseRate(g.inRates) + '</td>' +
|
|
900
|
+
'<td class="num">' + collapseRate(g.outRates) + '</td>' +
|
|
801
901
|
'<td class="num">' + sanitize(usd) + '</td>' +
|
|
802
902
|
'<td class="num">' + sanitize(when) + '</td>' +
|
|
803
903
|
'</tr>';
|
package/package.json
CHANGED
package/src/compact.ts
CHANGED
|
@@ -23,7 +23,11 @@ function truncate(s: string, max: number): string {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function firstText(m: EngineMessage): string | undefined {
|
|
26
|
-
|
|
26
|
+
// PREVENT crash: pi can hand us a message with text: undefined (pure
|
|
27
|
+
// tool-call/tool-result). Guard the trim so the legacy summarizeMessages
|
|
28
|
+
// path can't throw the same undefined-text crash the extractive path did.
|
|
29
|
+
const raw = m.text ?? "";
|
|
30
|
+
const t = raw.trim();
|
|
27
31
|
return t.length > 0 ? t : undefined;
|
|
28
32
|
}
|
|
29
33
|
|
package/src/supersede.ts
CHANGED
|
@@ -12,12 +12,15 @@ import { extractFileCandidates } from "./compact.js";
|
|
|
12
12
|
|
|
13
13
|
/** Classify a message's relationship to a file path. */
|
|
14
14
|
function fileOps(msg: EngineMessage): { path: string; op: "read" | "write" }[] {
|
|
15
|
-
// msg.text may be undefined for pure tool-call/result
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
15
|
+
// PREVENT crash: msg.text may be undefined for pure tool-call/result
|
|
16
|
+
// messages. extractFileCandidates guards the split, but if it returned a
|
|
17
|
+
// hit we'd still call .toLowerCase() on the raw (possibly-undefined) text.
|
|
18
|
+
// Coerce once so both extractFileCandidates and the write-detection regex
|
|
19
|
+
// are safe, and the early return still skips empty messages.
|
|
20
|
+
const text = msg.text ?? "";
|
|
21
|
+
const paths = extractFileCandidates(text);
|
|
19
22
|
if (paths.length === 0) return [];
|
|
20
|
-
const low =
|
|
23
|
+
const low = text.toLowerCase();
|
|
21
24
|
const isWrite = /\b(write|edit|create|save|append|overwrite|update|patch|modify)\b/.test(low);
|
|
22
25
|
return paths.map((p) => ({ path: p, op: isWrite ? "write" : "read" }));
|
|
23
26
|
}
|