pi-mega-compact 0.6.3 → 0.6.5
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/conflict-scan.js +133 -60
- package/dist/extensions/dashboard-server.js +161 -35
- package/dist/extensions/dashboard-server.test.js +2 -2
- package/dist/extensions/mega-runtime.js +51 -23
- package/dist/src/compact.js +7 -1
- package/dist/src/supersede.js +3 -0
- package/extensions/conflict-scan.test.ts +129 -0
- package/extensions/conflict-scan.ts +243 -158
- package/extensions/dashboard-server.test.ts +2 -2
- package/extensions/dashboard-server.ts +166 -36
- package/extensions/mega-dashboard.ts +16 -0
- package/extensions/mega-runtime.ts +52 -23
- package/package.json +1 -1
- package/src/compact.ts +7 -2
- package/src/supersede.ts +3 -0
|
@@ -59,6 +59,7 @@ function getIndexDir(): string {
|
|
|
59
59
|
interface IndexRepo {
|
|
60
60
|
repoRoot: string;
|
|
61
61
|
displayName: string;
|
|
62
|
+
stateDir: string;
|
|
62
63
|
checkpointCount: number;
|
|
63
64
|
tokensSaved: number;
|
|
64
65
|
compressedOriginalBytes: number;
|
|
@@ -71,8 +72,17 @@ interface IndexRepo {
|
|
|
71
72
|
lastSeen: number;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
interface IndexSummary {
|
|
76
|
+
totalRepos: number;
|
|
77
|
+
totalCheckpoints: number;
|
|
78
|
+
totalTokensSaved: number;
|
|
79
|
+
totalCompressedOriginalBytes: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type IndexIndex = { updatedAt: string; summary: IndexSummary | null; repos: IndexRepo[] };
|
|
83
|
+
|
|
74
84
|
/** Read the machine-wide repo registry from SQLite (read-only, single shot). */
|
|
75
|
-
function readIndex():
|
|
85
|
+
function readIndex(): IndexIndex | null {
|
|
76
86
|
const indexPath = join(getIndexDir(), "index.sqlite");
|
|
77
87
|
if (!existsSync(indexPath)) return null;
|
|
78
88
|
let db: DatabaseSync | undefined;
|
|
@@ -86,6 +96,7 @@ function readIndex(): { updatedAt: string; summary: unknown; repos: unknown[] }
|
|
|
86
96
|
const mapped: IndexRepo[] = rows.map((r) => ({
|
|
87
97
|
repoRoot: String(r.repo_root ?? ""),
|
|
88
98
|
displayName: String(r.display_name ?? ""),
|
|
99
|
+
stateDir: String(r.state_dir ?? ""),
|
|
89
100
|
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
90
101
|
tokensSaved: Number(r.tokens_saved ?? 0),
|
|
91
102
|
compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
|
|
@@ -197,6 +208,10 @@ interface Snapshot {
|
|
|
197
208
|
duplicatesCollapsed: number;
|
|
198
209
|
bytesPermanentlyDeleted: number;
|
|
199
210
|
};
|
|
211
|
+
compression: {
|
|
212
|
+
session: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
213
|
+
repo: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
214
|
+
};
|
|
200
215
|
model?: {
|
|
201
216
|
name: string;
|
|
202
217
|
provider: string;
|
|
@@ -374,27 +389,31 @@ function dashboardHtml(tierName: string): string {
|
|
|
374
389
|
<h2>Vector Store</h2>
|
|
375
390
|
<div class="stat-grid">
|
|
376
391
|
<span class="label" title="A saved summary of a chunk of your conversation that was compacted to free up space.">Checkpoints</span><span class="value" id="st-count">0</span>
|
|
377
|
-
<span class="label" title="
|
|
378
|
-
<span class="label" title="
|
|
379
|
-
<span class="label" title="
|
|
392
|
+
<span class="label" title="Total size of the original conversation text dropped into compaction this session, including redundant regions skipped by dedup. This is the 'in'.">Original (dropped)</span><span class="value" id="st-in">0</span>
|
|
393
|
+
<span class="label" title="Compact summaries we are currently holding as 'memory' for this session (the 'out'). Smaller is better.">Kept (summaries)</span><span class="value" id="st-kept">0</span>
|
|
394
|
+
<span class="label" title="Conversation space freed = dropped − kept (the 'saved').">Freed (dropped − kept)</span><span class="value" id="st-freed">0</span>
|
|
380
395
|
<span class="label" title="How many times old context was automatically brought back into the conversation because it was relevant to what you were doing.">Injected</span><span class="value" id="st-injected">0</span>
|
|
381
396
|
<span class="label" title="Of the times we recalled old context, how often it was actually on-topic.">Recall Relevance</span><span class="value" id="st-dedup">0%</span>
|
|
382
397
|
<span class="label" title="How often new content matched something we already had, so we skipped storing a duplicate copy. Higher = less wasted space.">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
|
|
383
398
|
<span class="label" title="How many duplicate chunks we collapsed into one instead of storing separately.">Collapsed</span><span class="value" id="st-collapsed">0</span>
|
|
384
399
|
<span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
|
|
385
400
|
</div>
|
|
401
|
+
<div class="meter-track" style="margin-top:10px"><div class="meter-fill" id="st-compress-bar" style="width:0%"></div></div>
|
|
402
|
+
<div class="meter-sub" id="st-compress-sub">waiting for compaction…</div>
|
|
386
403
|
</div>
|
|
387
404
|
<div class="card">
|
|
388
405
|
<h2>Repo (all sessions)</h2>
|
|
389
406
|
<div class="stat-grid">
|
|
390
407
|
<span class="label">Checkpoints</span><span class="value" id="rp-count">0</span>
|
|
391
|
-
<span class="label">
|
|
392
|
-
<span class="label">
|
|
393
|
-
<span class="label">
|
|
408
|
+
<span class="label">Original (dropped)</span><span class="value" id="rp-in">0</span>
|
|
409
|
+
<span class="label">Kept (summaries)</span><span class="value" id="rp-kept">0</span>
|
|
410
|
+
<span class="label">Freed (dropped − kept)</span><span class="value" id="rp-freed">0</span>
|
|
394
411
|
<span class="label">Sessions</span><span class="value" id="rp-sessions">0</span>
|
|
395
412
|
<span class="label">Collapsed</span><span class="value" id="rp-collapsed">0</span>
|
|
396
413
|
<span class="label">Storage Dedup</span><span class="value" id="rp-sdedup">0%</span>
|
|
397
414
|
</div>
|
|
415
|
+
<div class="meter-track" style="margin-top:10px"><div class="meter-fill" id="rp-compress-bar" style="width:0%"></div></div>
|
|
416
|
+
<div class="meter-sub" id="rp-compress-sub">waiting for compaction…</div>
|
|
398
417
|
</div>
|
|
399
418
|
<div class="card safe">
|
|
400
419
|
<h2>🛡 Data Safety</h2>
|
|
@@ -440,11 +459,11 @@ function dashboardHtml(tierName: string): string {
|
|
|
440
459
|
<div class="card legend">
|
|
441
460
|
<h2>What these numbers mean</h2>
|
|
442
461
|
<ul class="legend-list">
|
|
443
|
-
<li><b>
|
|
444
|
-
<li><b>
|
|
445
|
-
<li><b>
|
|
446
|
-
<li><b>
|
|
447
|
-
<li><b>Storage dedup
|
|
462
|
+
<li><b>Original (dropped)</b> — everything compacted away (including duplicates caught by dedup). The "in."</li>
|
|
463
|
+
<li><b>Kept (summaries)</b> — compact summaries still held as "memory" (the "out").</li>
|
|
464
|
+
<li><b>Freed</b> = dropped − kept — tokens saved so far (higher = better).</li>
|
|
465
|
+
<li><b>Compression %</b> — Freed ÷ Dropped — the headline efficiency number. Higher = more space reclaimed.</li>
|
|
466
|
+
<li><b>Storage dedup %</b> — how often new content matched something already saved, so no duplicate copy was written.</li>
|
|
448
467
|
<li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
|
|
449
468
|
</ul>
|
|
450
469
|
<p class="legend-note">Hover any label above for a quick explanation.</p>
|
|
@@ -515,6 +534,23 @@ function dashboardHtml(tierName: string): string {
|
|
|
515
534
|
<div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
|
|
516
535
|
<div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
|
|
517
536
|
</div>
|
|
537
|
+
|
|
538
|
+
<h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
|
|
539
|
+
<p class="legend-note" style="margin-bottom:10px">How much context & cost mega-compact has reclaimed, grouped by the model you were running. Compression ratio reflects workload/content, not model quality.</p>
|
|
540
|
+
<table class="repos">
|
|
541
|
+
<thead>
|
|
542
|
+
<tr>
|
|
543
|
+
<th>Model</th><th>Provider</th>
|
|
544
|
+
<th style="text-align:right">Repos</th>
|
|
545
|
+
<th style="text-align:right">Checkpoints</th>
|
|
546
|
+
<th style="text-align:right">Tokens Saved</th>
|
|
547
|
+
<th style="text-align:right">$ Saved</th>
|
|
548
|
+
<th style="text-align:right">Last Used</th>
|
|
549
|
+
</tr>
|
|
550
|
+
</thead>
|
|
551
|
+
<tbody id="bm-rows"><tr><td colspan="7" class="repo-none">loading…</td></tr></tbody>
|
|
552
|
+
</table>
|
|
553
|
+
|
|
518
554
|
<div class="updated" id="sm-updated"></div>
|
|
519
555
|
</div>
|
|
520
556
|
|
|
@@ -548,10 +584,20 @@ function dashboardHtml(tierName: string): string {
|
|
|
548
584
|
d.trigger.armed ? 'past fast gate — monitoring token count' : 'idle — below fast gate';
|
|
549
585
|
document.getElementById('tr-state').textContent = state;
|
|
550
586
|
|
|
587
|
+
// ---- Vector Store — reconciled token accounting (same formula as widget) -
|
|
551
588
|
document.getElementById('st-count').textContent = d.store.checkpointCount;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
589
|
+
// Compression block from the snapshot (Freed = In − Out, single formula).
|
|
590
|
+
var c = d.compression || {};
|
|
591
|
+
var sess = c.session || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
|
|
592
|
+
var cRepo = c.repo || { tokensIn:0, tokensOut:0, tokensFreed:0, compressionPct:0, dedupPct:0 };
|
|
593
|
+
document.getElementById('st-in').textContent = sess.tokensIn.toLocaleString();
|
|
594
|
+
document.getElementById('st-kept').textContent = sess.tokensOut.toLocaleString();
|
|
595
|
+
document.getElementById('st-freed').textContent = sess.tokensFreed.toLocaleString();
|
|
596
|
+
var sp = sess.compressionPct || 0;
|
|
597
|
+
document.getElementById('st-compress-bar').style.width = Math.max(sp * 100, 0.5) + '%';
|
|
598
|
+
document.getElementById('st-compress-bar').className = 'meter-fill ' + (sp >= 0.9 ? 'meter-green' : sp >= 0.6 ? 'meter-yellow' : 'meter-red');
|
|
599
|
+
document.getElementById('st-compress-sub').textContent = (sp * 100 >= 10 ? Math.round(sp * 100) : (sp * 100).toFixed(1)) + '% tokens saved · dedup: ' + (sess.dedupPct * 100 >= 10 ? Math.round(sess.dedupPct * 100) : (sess.dedupPct * 100).toFixed(1)) + '%';
|
|
600
|
+
// ------
|
|
555
601
|
document.getElementById('st-injected').textContent = d.store.injectedCount;
|
|
556
602
|
document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
|
|
557
603
|
var sdr = d.store.storageDedupRate || 0;
|
|
@@ -559,16 +605,19 @@ function dashboardHtml(tierName: string): string {
|
|
|
559
605
|
document.getElementById('st-collapsed').textContent = d.store.dedupCollapsed || 0;
|
|
560
606
|
document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
|
|
561
607
|
|
|
562
|
-
// Repo
|
|
563
|
-
|
|
564
|
-
document.getElementById('rp-
|
|
565
|
-
document.getElementById('rp-
|
|
566
|
-
document.getElementById('rp-
|
|
567
|
-
document.getElementById('rp-
|
|
568
|
-
document.getElementById('rp-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
608
|
+
// ---- Repo (all sessions) — same compression fields, repo scope ----------
|
|
609
|
+
document.getElementById('rp-count').textContent = (d.repo && d.repo.checkpointCount || 0).toLocaleString();
|
|
610
|
+
document.getElementById('rp-in').textContent = cRepo.tokensIn.toLocaleString();
|
|
611
|
+
document.getElementById('rp-kept').textContent = cRepo.tokensOut.toLocaleString();
|
|
612
|
+
document.getElementById('rp-freed').textContent = cRepo.tokensFreed.toLocaleString();
|
|
613
|
+
document.getElementById('rp-sessions').textContent = (d.repo && d.repo.sessionCount || 0).toLocaleString();
|
|
614
|
+
document.getElementById('rp-collapsed').textContent = (d.repo && d.repo.dedupCollapsed || 0).toLocaleString();
|
|
615
|
+
var rdr = d.repo && d.repo.storageDedupRate || 0;
|
|
616
|
+
document.getElementById('rp-sdedup').textContent = (rdr * 100 >= 10 ? Math.round(rdr * 100) : (rdr * 100).toFixed(1)) + '%';
|
|
617
|
+
var rp = cRepo.compressionPct || 0;
|
|
618
|
+
document.getElementById('rp-compress-bar').style.width = Math.max(rp * 100, 0.5) + '%';
|
|
619
|
+
document.getElementById('rp-compress-bar').className = 'meter-fill ' + (rp >= 0.9 ? 'meter-green' : rp >= 0.6 ? 'meter-yellow' : 'meter-red');
|
|
620
|
+
document.getElementById('rp-compress-sub').textContent = (rp * 100 >= 10 ? Math.round(rp * 100) : (rp * 100).toFixed(1)) + '% tokens saved · dedup: ' + (cRepo.dedupPct * 100 >= 10 ? Math.round(cRepo.dedupPct * 100) : (cRepo.dedupPct * 100).toFixed(1)) + '%';
|
|
572
621
|
|
|
573
622
|
// Data-safety invariant (Phase 0 — trust foundation).
|
|
574
623
|
var ig = d.integrity || { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 };
|
|
@@ -608,10 +657,11 @@ function dashboardHtml(tierName: string): string {
|
|
|
608
657
|
document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
|
|
609
658
|
document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
|
|
610
659
|
document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
|
|
611
|
-
|
|
612
|
-
|
|
660
|
+
var repoSaved = cRepo.tokensFreed || 0;
|
|
661
|
+
if (model && model.inputRate && repoSaved > 0) {
|
|
662
|
+
var usd = (repoSaved * model.inputRate);
|
|
613
663
|
var win = d.context.contextWindow || 0;
|
|
614
|
-
var windows = win > 0 ? (
|
|
664
|
+
var windows = win > 0 ? (repoSaved / win).toFixed(1) : '0';
|
|
615
665
|
document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
|
|
616
666
|
document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
|
|
617
667
|
} else {
|
|
@@ -709,6 +759,49 @@ function dashboardHtml(tierName: string): string {
|
|
|
709
759
|
document.getElementById('cur-updated').textContent = stamp;
|
|
710
760
|
document.getElementById('all-updated').textContent = stamp;
|
|
711
761
|
document.getElementById('sm-updated').textContent = stamp;
|
|
762
|
+
renderByModel(repos);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Savings-by-model aggregation for the Summary tab — groups the machine-
|
|
766
|
+
// wide repo registry by (modelName || '(unknown)') so the user can see how
|
|
767
|
+
// much context + cost mega-compact has reclaimed, broken down by which model
|
|
768
|
+
// they were running. $ Saved = Σ(tokensSaved × inputRate) per model. Sorted
|
|
769
|
+
// by tokens saved descending so the biggest-reclaim model wins the top row.
|
|
770
|
+
function renderByModel(repos) {
|
|
771
|
+
var rows = document.getElementById('bm-rows');
|
|
772
|
+
if (!rows) return;
|
|
773
|
+
if (!repos || !repos.length) {
|
|
774
|
+
rows.innerHTML = '<tr><td colspan="7" class="repo-none">No repositories registered yet.</td></tr>';
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
var groups = {};
|
|
778
|
+
for (var i = 0; i < repos.length; i++) {
|
|
779
|
+
var r = repos[i];
|
|
780
|
+
var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
|
|
781
|
+
if (!groups[key]) groups[key] = { model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0, tokensSaved: 0, usd: 0, lastAt: 0, rates: [] };
|
|
782
|
+
var g = groups[key];
|
|
783
|
+
g.repos++;
|
|
784
|
+
g.checkpoints += (r.checkpointCount || 0);
|
|
785
|
+
g.tokensSaved += (r.tokensSaved || 0);
|
|
786
|
+
if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.rates.push(r.inputRate); }
|
|
787
|
+
if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
|
|
788
|
+
}
|
|
789
|
+
var arr = [];
|
|
790
|
+
for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
|
|
791
|
+
arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
|
|
792
|
+
rows.innerHTML = arr.map(function(g) {
|
|
793
|
+
var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
|
|
794
|
+
var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
|
|
795
|
+
return '<tr>' +
|
|
796
|
+
'<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
|
|
797
|
+
'<td>' + sanitize(g.provider) + '</td>' +
|
|
798
|
+
'<td class="num">' + g.repos.toLocaleString() + '</td>' +
|
|
799
|
+
'<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
|
|
800
|
+
'<td class="num">' + g.tokensSaved.toLocaleString() + '</td>' +
|
|
801
|
+
'<td class="num">' + sanitize(usd) + '</td>' +
|
|
802
|
+
'<td class="num">' + sanitize(when) + '</td>' +
|
|
803
|
+
'</tr>';
|
|
804
|
+
}).join('');
|
|
712
805
|
}
|
|
713
806
|
|
|
714
807
|
// Per-repo detail modal ---------------------------------------------------
|
|
@@ -838,7 +931,40 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
838
931
|
|
|
839
932
|
let eventOffset = 0;
|
|
840
933
|
|
|
934
|
+
// Overlay the live current-repo snapshot (snapshot.json, rewritten every
|
|
935
|
+
// context event) onto its registry row so the All-repos / Summary views stay
|
|
936
|
+
// in sync with the live menu bar + Current-repo card in real time. The
|
|
937
|
+
// registry (index.sqlite) is only written on repo-switch (bindRepo), so
|
|
938
|
+
// without this the current repo's row freezes between switches. Read-only —
|
|
939
|
+
// no extra writes to index.sqlite. Matched by stateDir, which equals the
|
|
940
|
+
// value this server was launched with (runtime.currentStateDir).
|
|
941
|
+
function overlayCurrentRepo(idx: IndexIndex | null): void {
|
|
942
|
+
if (!idx || !idx.repos.length) return;
|
|
943
|
+
let snap: Snapshot | null = null;
|
|
944
|
+
try { snap = readSnapshot(snapshotPath); } catch { return; }
|
|
945
|
+
if (!snap || !snap.repo) return;
|
|
946
|
+
const cur = idx.repos.find((r) => r.stateDir === stateDir);
|
|
947
|
+
if (!cur) return;
|
|
948
|
+
const prevSaved = cur.tokensSaved;
|
|
949
|
+
const prevCp = cur.checkpointCount;
|
|
950
|
+
const prevBytes = cur.compressedOriginalBytes;
|
|
951
|
+
const comp = snap.compression?.repo;
|
|
952
|
+
const liveSaved = comp ? comp.tokensFreed : (snap.repo.tokensSaved ?? prevSaved);
|
|
953
|
+
const liveCp = snap.repo.checkpointCount ?? prevCp;
|
|
954
|
+
const liveBytes = snap.integrity?.compressedOriginalBytes ?? prevBytes;
|
|
955
|
+
cur.tokensSaved = liveSaved;
|
|
956
|
+
cur.checkpointCount = liveCp;
|
|
957
|
+
cur.compressedOriginalBytes = liveBytes;
|
|
958
|
+
if (idx.summary) {
|
|
959
|
+
idx.summary.totalTokensSaved += liveSaved - prevSaved;
|
|
960
|
+
idx.summary.totalCheckpoints += liveCp - prevCp;
|
|
961
|
+
idx.summary.totalCompressedOriginalBytes += liveBytes - prevBytes;
|
|
962
|
+
}
|
|
963
|
+
idx.updatedAt = snap.updatedAt ?? idx.updatedAt;
|
|
964
|
+
}
|
|
965
|
+
|
|
841
966
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
967
|
+
// guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard localhost server (loopback-only) — CORS open for local browser access
|
|
842
968
|
// CORS for local access
|
|
843
969
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
844
970
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
@@ -876,8 +1002,10 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
876
1002
|
// directly from SQLite (index.sqlite). Lets one dashboard show every repo's
|
|
877
1003
|
// checkpoints, tokens saved, and active model. Read-only.
|
|
878
1004
|
if (req.url === "/api/index") {
|
|
1005
|
+
const idx = readIndex();
|
|
1006
|
+
if (idx) overlayCurrentRepo(idx);
|
|
879
1007
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
880
|
-
res.end(JSON.stringify(
|
|
1008
|
+
res.end(JSON.stringify(idx ?? { updatedAt: null, summary: null, repos: [] }));
|
|
881
1009
|
return;
|
|
882
1010
|
}
|
|
883
1011
|
|
|
@@ -887,8 +1015,9 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
887
1015
|
if (req.url?.startsWith("/api/repos")) {
|
|
888
1016
|
const url = new URL(req.url, "http://x");
|
|
889
1017
|
const activeParam = url.searchParams.get("active");
|
|
890
|
-
const idx = readIndex()
|
|
891
|
-
|
|
1018
|
+
const idx = readIndex();
|
|
1019
|
+
if (idx) overlayCurrentRepo(idx);
|
|
1020
|
+
let repos = idx?.repos ?? [];
|
|
892
1021
|
if (activeParam) {
|
|
893
1022
|
const m = /^(\d+)h$/.exec(activeParam);
|
|
894
1023
|
if (m) {
|
|
@@ -897,7 +1026,7 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
897
1026
|
}
|
|
898
1027
|
}
|
|
899
1028
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
900
|
-
res.end(JSON.stringify({ updatedAt: idx
|
|
1029
|
+
res.end(JSON.stringify({ updatedAt: idx?.updatedAt ?? null, repos, count: repos.length }));
|
|
901
1030
|
return;
|
|
902
1031
|
}
|
|
903
1032
|
|
|
@@ -905,14 +1034,15 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
905
1034
|
// small for embed scenarios). activeRepos mirrors the /api/repos?active=24h
|
|
906
1035
|
// count so the dashboard can render the active badge alongside totals.
|
|
907
1036
|
if (req.url?.startsWith("/api/summary")) {
|
|
908
|
-
const idx = readIndex()
|
|
909
|
-
|
|
1037
|
+
const idx = readIndex();
|
|
1038
|
+
if (idx) overlayCurrentRepo(idx);
|
|
1039
|
+
const repos = idx?.repos ?? [];
|
|
910
1040
|
const cutoffSec = Math.floor(Date.now() / 1000) - 24 * 3600;
|
|
911
1041
|
const activeRepos = repos.filter((r) => (r.lastSeen ?? 0) >= cutoffSec).length;
|
|
912
1042
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
913
1043
|
res.end(JSON.stringify({
|
|
914
|
-
updatedAt: idx
|
|
915
|
-
summary: idx
|
|
1044
|
+
updatedAt: idx?.updatedAt ?? null,
|
|
1045
|
+
summary: idx?.summary ?? null,
|
|
916
1046
|
activeRepos,
|
|
917
1047
|
totalRepos: repos.length,
|
|
918
1048
|
}));
|
|
@@ -80,6 +80,22 @@ export interface DashboardSnapshot {
|
|
|
80
80
|
dedupCollapsed: number; // cumulative deduped collapses (store-wide)
|
|
81
81
|
storageDedupRate: number; // deduped / attempts, 0..1
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Reconciled token accounting — ONE canonical formula for both session + repo
|
|
85
|
+
* so the dashboard and widget tell the same story:
|
|
86
|
+
* tokensIn = original conversation dropped into compaction (incl. the
|
|
87
|
+
* redundant regions skipped by dedup) — the "in".
|
|
88
|
+
* tokensOut = compact summaries currently held (stored) — the "out".
|
|
89
|
+
* tokensFreed= tokensIn − tokensOut (the "saved").
|
|
90
|
+
* compressionPct = tokensFreed / tokensIn (0..1) — the headline "% saved".
|
|
91
|
+
* dedupPct = storageDedupRate (0..1) — share of adds that collapsed.
|
|
92
|
+
* session.Freed = rt.tokensSaved (honest net freed this session, incl. deduped-away);
|
|
93
|
+
* repo.Freed = repo.tokensSaved meta (honest net freed repo-wide).
|
|
94
|
+
*/
|
|
95
|
+
compression: {
|
|
96
|
+
session: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
97
|
+
repo: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
|
|
98
|
+
};
|
|
83
99
|
/** Phase 0 data-safety invariant (trust foundation). */
|
|
84
100
|
integrity: {
|
|
85
101
|
regionsRetained: number; // checkpoints with a recoverable compressed-original
|
|
@@ -282,6 +282,25 @@ export class MegaRuntime {
|
|
|
282
282
|
trigger: { armed, ready, currentTokens: this.lastCtxTokens, thresholdTokens: this.config.thresholdTokens, fastGatePct: this.config.fastGatePct },
|
|
283
283
|
crew: { activeAgents: this.activeAgents, currentTurn: this.currentTurn },
|
|
284
284
|
store: { checkpointCount: st.checkpointCount, totalTokenEstimate: st.totalTokenEstimate, originalTokens: st.originalTokens, tokensSaved: this.rt.tokensSaved, injectedCount: st.injectedCount, dedupHitRate: st.dedupHitRate, storageDedupRate: st.storageDedupRate, dedupAttempts: st.dedupAttempts, dedupCollapsed: st.dedupCollapsed },
|
|
285
|
+
// Reconciled token accounting (single canonical formula, session + repo).
|
|
286
|
+
// Freed = In − Out; In = Freed + Out. session.Freed = rt.tokensSaved (incl.
|
|
287
|
+
// deduped-away originals); repo.Freed = repo.tokensSaved meta counter.
|
|
288
|
+
compression: {
|
|
289
|
+
session: {
|
|
290
|
+
tokensIn: this.rt.tokensSaved + st.totalTokenEstimate,
|
|
291
|
+
tokensOut: st.totalTokenEstimate,
|
|
292
|
+
tokensFreed: this.rt.tokensSaved,
|
|
293
|
+
compressionPct: (this.rt.tokensSaved + st.totalTokenEstimate) > 0 ? this.rt.tokensSaved / (this.rt.tokensSaved + st.totalTokenEstimate) : 0,
|
|
294
|
+
dedupPct: st.storageDedupRate,
|
|
295
|
+
},
|
|
296
|
+
repo: {
|
|
297
|
+
tokensIn: repo.tokensSaved + repo.totalTokenEstimate,
|
|
298
|
+
tokensOut: repo.totalTokenEstimate,
|
|
299
|
+
tokensFreed: repo.tokensSaved,
|
|
300
|
+
compressionPct: (repo.tokensSaved + repo.totalTokenEstimate) > 0 ? repo.tokensSaved / (repo.tokensSaved + repo.totalTokenEstimate) : 0,
|
|
301
|
+
dedupPct: repo.storageDedupRate,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
285
304
|
repo: {
|
|
286
305
|
checkpointCount: repo.checkpointCount,
|
|
287
306
|
totalTokenEstimate: repo.totalTokenEstimate,
|
|
@@ -319,33 +338,42 @@ export class MegaRuntime {
|
|
|
319
338
|
const dedupStr = storageRate * 100 >= 10
|
|
320
339
|
? `${Math.round(storageRate * 100)}%`
|
|
321
340
|
: `${(storageRate * 100).toFixed(1)}%`;
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
341
|
+
// Reconciled token accounting — ONE canonical formula for session + repo,
|
|
342
|
+
// matching the dashboard so the two never disagree. unit format: M at/above
|
|
343
|
+
// 1e6, k at/above 1e3, raw below — so 5,472,700 → "5.5M", 24,100 → "24.1k",
|
|
344
|
+
// 142 → "142". Dropped (in) = Freed + Kept; Freed = rt.tokensSaved (session)
|
|
345
|
+
// / repo.tokensSaved meta (repo); Kept = totalTokenEstimate (stored).
|
|
346
|
+
const fmt = (x: number) =>
|
|
347
|
+
x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M`
|
|
348
|
+
: x >= 1000 ? `${(x / 1000).toFixed(1)}k`
|
|
349
|
+
: `${Math.round(x)}`;
|
|
331
350
|
const agentStr = this.activeAgents > 0 ? ` │ 🤖 ${this.activeAgents} agent${this.activeAgents === 1 ? "" : "s"}` : "";
|
|
332
351
|
const turnStr = this.currentTurn > 0 ? ` │ turn ${this.currentTurn}` : "";
|
|
333
352
|
// Phase 3 — pulsing status glyph while a compaction is in flight.
|
|
334
353
|
const pulse = this.pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
|
|
354
|
+
// --- reconciled in/out view (session + repo) ---------------------------
|
|
355
|
+
const sessIn = this.rt.tokensSaved + st.totalTokenEstimate;
|
|
356
|
+
const sessKept = st.totalTokenEstimate;
|
|
357
|
+
const sessFreed = this.rt.tokensSaved;
|
|
358
|
+
const sessPct = sessIn > 0 ? sessFreed / sessIn : 0;
|
|
359
|
+
const repoIn = repo.tokensSaved + repo.totalTokenEstimate;
|
|
360
|
+
const repoKept = repo.totalTokenEstimate;
|
|
361
|
+
const repoFreed = repo.tokensSaved;
|
|
362
|
+
const repoPct = repoIn > 0 ? repoFreed / repoIn : 0;
|
|
335
363
|
const lines = [
|
|
336
364
|
` ${C.amber}⚡ ${tierLabel}${C.reset} v${C.bold}${ownVersion()}${C.reset} │ ${tokStr}/${maxStr} tokens (${C.bold}${pctStr}${C.reset}) │ ${st.checkpointCount} saved${agentStr}${turnStr}`,
|
|
337
|
-
` ${triggerLabel} │ ${C.magenta}repeat-skipped: ${dedupStr}${C.reset}
|
|
365
|
+
` ${triggerLabel} │ ${C.magenta}repeat-skipped: ${dedupStr}${C.reset}`,
|
|
366
|
+
` ${C.gray}dropped ${fmt(sessIn)} → kept ${fmt(sessKept)} sess / ${fmt(repoKept)} repo · freed ${fmt(sessFreed)} sess / ${fmt(repoFreed)} repo${C.reset}`,
|
|
338
367
|
];
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
const filled = Math.
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
lines.push(` ${C.green}saved ${fmt(this.rt.tokensSaved)} ${bar}${C.reset} ${pct}% of ${fmt(goal)} ${C.gray}│${C.reset} ${C.blue}${fmt(this.rt.tokensSaved)}${C.reset}/${C.blue}${fmt(totalHeld)}${C.reset} tok held`);
|
|
368
|
+
// Compression meter — the single headline "% tokens saved" (Freed / In),
|
|
369
|
+
// same formula as the dashboard. Higher = better, so it reads green.
|
|
370
|
+
{
|
|
371
|
+
const w = 10;
|
|
372
|
+
const filled = Math.max(0, Math.min(w, Math.round(sessPct * w)));
|
|
373
|
+
const cbar = C.green + "▓".repeat(filled) + C.dim + "░".repeat(w - filled) + C.reset;
|
|
374
|
+
const sTxt = (sessPct * 100).toFixed(sessPct * 100 >= 10 ? 0 : 1);
|
|
375
|
+
const rTxt = (repoPct * 100).toFixed(repoPct * 100 >= 10 ? 0 : 1);
|
|
376
|
+
lines.push(` ${cbar} ${sTxt}% tokens saved (sess) · ${rTxt}% repo${C.reset}`);
|
|
349
377
|
}
|
|
350
378
|
// Live "now processing" line + why + recent deduped/compacted events,
|
|
351
379
|
// collapsed to ONE rotating line (fresh only). The ticker ring buffer
|
|
@@ -366,10 +394,11 @@ export class MegaRuntime {
|
|
|
366
394
|
} else if (this.pulsing) {
|
|
367
395
|
lines.push(` ${pulse}${C.teal}compacting…${C.reset}`);
|
|
368
396
|
}
|
|
369
|
-
//
|
|
370
|
-
//
|
|
397
|
+
// Token accounting summary, always last + dimmed. Shows the total
|
|
398
|
+
// tokens dropped (in) + kept (out) for this session, then the freed
|
|
399
|
+
// (saved) tokens for both this session and all-time across the repo.
|
|
371
400
|
if (lines.length < 10) {
|
|
372
|
-
lines.push(` ${C.dim}
|
|
401
|
+
lines.push(` ${C.dim}session ↑${fmt(sessIn)} in ↓${fmt(sessKept)} out · saved ${fmt(sessFreed)} session / ${fmt(repoFreed)} all-time${C.reset}`);
|
|
373
402
|
}
|
|
374
403
|
ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
|
|
375
404
|
}
|
package/package.json
CHANGED
package/src/compact.ts
CHANGED
|
@@ -36,8 +36,13 @@ export function isChatty(text: string): boolean {
|
|
|
36
36
|
return text.length < 40 && !/(\/|\.|\{|import |def |function )/.test(text);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** Extract plausible file paths (contain '/' + an interesting extension).
|
|
40
|
-
|
|
39
|
+
/** Extract plausible file paths (contain '/' + an interesting extension).
|
|
40
|
+
* Defensive against a missing/empty payload: pi's adapter can hand the engine
|
|
41
|
+
* a message whose `text`/`input`/`output` is undefined (e.g. a pure tool-call
|
|
42
|
+
* or tool-result message), and `.split` on undefined throws and takes down the
|
|
43
|
+
* whole compaction. Guard once at the source so every caller is safe. */
|
|
44
|
+
export function extractFileCandidates(content: string | undefined | null): string[] {
|
|
45
|
+
if (!content) return [];
|
|
41
46
|
const out: string[] = [];
|
|
42
47
|
for (const raw of content.split(/\s+/)) {
|
|
43
48
|
// Trim surrounding punctuation only — do NOT strip internal dots, or we
|
package/src/supersede.ts
CHANGED
|
@@ -12,6 +12,9 @@ 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 messages; the guard
|
|
16
|
+
// lives in extractFileCandidates, but the early return short-circuits the
|
|
17
|
+
// write-detection regex too so we never classify an empty message.
|
|
15
18
|
const paths = extractFileCandidates(msg.text);
|
|
16
19
|
if (paths.length === 0) return [];
|
|
17
20
|
const low = msg.text.toLowerCase();
|