pi-mega-compact 0.6.4 → 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.
@@ -436,6 +436,23 @@ function dashboardHtml(tierName) {
436
436
  <div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
437
437
  <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
438
438
  </div>
439
+
440
+ <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">Savings by Model</h2>
441
+ <p class="legend-note" style="margin-bottom:10px">How much context &amp; cost mega-compact has reclaimed, grouped by the model you were running. Compression ratio reflects workload/content, not model quality.</p>
442
+ <table class="repos">
443
+ <thead>
444
+ <tr>
445
+ <th>Model</th><th>Provider</th>
446
+ <th style="text-align:right">Repos</th>
447
+ <th style="text-align:right">Checkpoints</th>
448
+ <th style="text-align:right">Tokens Saved</th>
449
+ <th style="text-align:right">$ Saved</th>
450
+ <th style="text-align:right">Last Used</th>
451
+ </tr>
452
+ </thead>
453
+ <tbody id="bm-rows"><tr><td colspan="7" class="repo-none">loading…</td></tr></tbody>
454
+ </table>
455
+
439
456
  <div class="updated" id="sm-updated"></div>
440
457
  </div>
441
458
 
@@ -644,6 +661,49 @@ function dashboardHtml(tierName) {
644
661
  document.getElementById('cur-updated').textContent = stamp;
645
662
  document.getElementById('all-updated').textContent = stamp;
646
663
  document.getElementById('sm-updated').textContent = stamp;
664
+ renderByModel(repos);
665
+ }
666
+
667
+ // Savings-by-model aggregation for the Summary tab — groups the machine-
668
+ // wide repo registry by (modelName || '(unknown)') so the user can see how
669
+ // much context + cost mega-compact has reclaimed, broken down by which model
670
+ // they were running. $ Saved = Σ(tokensSaved × inputRate) per model. Sorted
671
+ // by tokens saved descending so the biggest-reclaim model wins the top row.
672
+ function renderByModel(repos) {
673
+ var rows = document.getElementById('bm-rows');
674
+ if (!rows) return;
675
+ if (!repos || !repos.length) {
676
+ rows.innerHTML = '<tr><td colspan="7" class="repo-none">No repositories registered yet.</td></tr>';
677
+ return;
678
+ }
679
+ var groups = {};
680
+ for (var i = 0; i < repos.length; i++) {
681
+ var r = repos[i];
682
+ var key = (r.modelName && String(r.modelName).trim()) || '(unknown)';
683
+ if (!groups[key]) groups[key] = { model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0, tokensSaved: 0, usd: 0, lastAt: 0, rates: [] };
684
+ var g = groups[key];
685
+ g.repos++;
686
+ g.checkpoints += (r.checkpointCount || 0);
687
+ g.tokensSaved += (r.tokensSaved || 0);
688
+ if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.rates.push(r.inputRate); }
689
+ if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
690
+ }
691
+ var arr = [];
692
+ for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
693
+ arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
694
+ rows.innerHTML = arr.map(function(g) {
695
+ var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
696
+ var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
697
+ return '<tr>' +
698
+ '<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
699
+ '<td>' + sanitize(g.provider) + '</td>' +
700
+ '<td class="num">' + g.repos.toLocaleString() + '</td>' +
701
+ '<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
702
+ '<td class="num">' + g.tokensSaved.toLocaleString() + '</td>' +
703
+ '<td class="num">' + sanitize(usd) + '</td>' +
704
+ '<td class="num">' + sanitize(when) + '</td>' +
705
+ '</tr>';
706
+ }).join('');
647
707
  }
648
708
 
649
709
  // Per-repo detail modal ---------------------------------------------------
@@ -367,10 +367,11 @@ export class MegaRuntime {
367
367
  else if (this.pulsing) {
368
368
  lines.push(` ${pulse}${C.teal}compacting…${C.reset}`);
369
369
  }
370
- // Plain-language hint so first-time users understand the widget. Always
371
- // last, dimmed. "/mega-help explains these terms."
370
+ // Token accounting summary, always last + dimmed. Shows the total
371
+ // tokens dropped (in) + kept (out) for this session, then the freed
372
+ // (saved) tokens for both this session and all-time across the repo.
372
373
  if (lines.length < 10) {
373
- lines.push(` ${C.dim}auto-compresses old context to free space · nothing deleted · /mega-help${C.reset}`);
374
+ lines.push(` ${C.dim}session ↑${fmt(sessIn)} in ↓${fmt(sessKept)} out · saved ${fmt(sessFreed)} session / ${fmt(repoFreed)} all-time${C.reset}`);
374
375
  }
375
376
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
376
377
  }
@@ -27,8 +27,14 @@ export function isChatty(text) {
27
27
  }
28
28
  return text.length < 40 && !/(\/|\.|\{|import |def |function )/.test(text);
29
29
  }
30
- /** Extract plausible file paths (contain '/' + an interesting extension). */
30
+ /** Extract plausible file paths (contain '/' + an interesting extension).
31
+ * Defensive against a missing/empty payload: pi's adapter can hand the engine
32
+ * a message whose `text`/`input`/`output` is undefined (e.g. a pure tool-call
33
+ * or tool-result message), and `.split` on undefined throws and takes down the
34
+ * whole compaction. Guard once at the source so every caller is safe. */
31
35
  export function extractFileCandidates(content) {
36
+ if (!content)
37
+ return [];
32
38
  const out = [];
33
39
  for (const raw of content.split(/\s+/)) {
34
40
  // Trim surrounding punctuation only — do NOT strip internal dots, or we
@@ -9,6 +9,9 @@
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 messages; the guard
13
+ // lives in extractFileCandidates, but the early return short-circuits the
14
+ // write-detection regex too so we never classify an empty message.
12
15
  const paths = extractFileCandidates(msg.text);
13
16
  if (paths.length === 0)
14
17
  return [];
@@ -534,6 +534,23 @@ function dashboardHtml(tierName: string): string {
534
534
  <div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
535
535
  <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
536
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 &amp; 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
+
537
554
  <div class="updated" id="sm-updated"></div>
538
555
  </div>
539
556
 
@@ -742,6 +759,49 @@ function dashboardHtml(tierName: string): string {
742
759
  document.getElementById('cur-updated').textContent = stamp;
743
760
  document.getElementById('all-updated').textContent = stamp;
744
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('');
745
805
  }
746
806
 
747
807
  // Per-repo detail modal ---------------------------------------------------
@@ -394,10 +394,11 @@ export class MegaRuntime {
394
394
  } else if (this.pulsing) {
395
395
  lines.push(` ${pulse}${C.teal}compacting…${C.reset}`);
396
396
  }
397
- // Plain-language hint so first-time users understand the widget. Always
398
- // last, dimmed. "/mega-help explains these terms."
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.
399
400
  if (lines.length < 10) {
400
- lines.push(` ${C.dim}auto-compresses old context to free space · nothing deleted · /mega-help${C.reset}`);
401
+ lines.push(` ${C.dim}session ↑${fmt(sessIn)} in ↓${fmt(sessKept)} out · saved ${fmt(sessFreed)} session / ${fmt(repoFreed)} all-time${C.reset}`);
401
402
  }
402
403
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
403
404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-2-Clause",
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
- export function extractFileCandidates(content: string): string[] {
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();