pi-mega-compact 0.6.7 → 0.7.0

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/README.md CHANGED
@@ -6,7 +6,7 @@ sessions into a **local SQLite store** and offers **deduped inline recall** —
6
6
  running **locally inside the extension**, with **no remote MCP server** and
7
7
  **zero network calls at runtime** (PREVENT-PI-004).
8
8
 
9
- > **Current version:** `v0.6.2` — storage backend is **`node:sqlite`**
9
+ > **Current version:** `v0.6.9` — storage backend is **`node:sqlite`**
10
10
  > (`DatabaseSync`, a Node ≥22.13 built-in), replacing the old `better-sqlite3`
11
11
  > native addon and the per-session gzipped JSON checkpoint files. **Zero native
12
12
  > build step, fully local, zero network at runtime.** Legacy
@@ -23,10 +23,12 @@ running **locally inside the extension**, with **no remote MCP server** and
23
23
  pi's context window is finite. When a session gets long — especially a team run
24
24
  with sub-agents — pi-mega-compact keeps it going without overflowing:
25
25
 
26
- 1. **Watches one signal.** A single live `pressure = currentTokens / thresholdTokens`
26
+ 1. **Watches one signal.** A single live `pressure = currentTokens / effectiveThreshold`
27
27
  drives everything — the tier label, how aggressively the live trim drops
28
- context, and how often durable memory is reviewed. As context fills, the whole
29
- system reacts together; as it's relieved, it backs off.
28
+ context, and how often durable memory is reviewed. `effectiveThreshold` is
29
+ `tierPct × contextWindow` (a **% of the model's context window**), so the
30
+ trim fires below pi's native ~80% auto-compaction for any model size. As
31
+ context fills, the whole system reacts together; as it's relieved, it backs off.
30
32
  2. **Compacts in two layers.** On every LLM call it returns a **live, compacted
31
33
  view** (the model sees a summary + recent anchor, non-destructively). And it
32
34
  persists a durable **checkpoint** — and, at each agent settle during a team
@@ -233,7 +235,7 @@ The commands (slash commands inside pi):
233
235
  | `/mega-help` | Explain the toolbar widget terms (live tier, gate, dedup, tokens saved). |
234
236
  | `/mega-compat-check` | Detect extension conflicts (duplicate commands / overlapping handlers) across installed pi extensions. |
235
237
 
236
- The **tier** you see in the toolbar and dashboard is a *live pressure band* (`low` → `medium` → `high` → `ultra` → `mega`) that climbs automatically as your context window fills and falls back as it's relieved — it is driven by `currentTokens / thresholdTokens`, not a manual setting. The base compaction *threshold* (token budget) is still chosen by the `MEGACOMPACT_TIER` env var at startup (`low`/`medium`/`high`/`ultra`/`mega`, default `low`); `/mega-tier` was removed in v0.6.0. Higher pressure also deepens the live trim and reviews durable memory more often — the whole system reacts as one.
238
+ The **tier** you see in the toolbar and dashboard is a *live pressure band* (`low` → `medium` → `high` → `ultra` → `mega`) that climbs automatically as your context window fills and falls back as it's relieved — it is driven by `currentTokens / effectiveThreshold`, not a manual setting. The base compaction *threshold* is set by `MEGACOMPACT_TIER` at startup as a **% of the model context window** (`low` 50% · `medium` 60% · `high` 70% · `ultra` 70% · `mega` 75%; default `low`) — the fire point is `tierPct × contextWindow`, so it always lands below pi's native ~80% auto-compaction (any model size). The old static token amounts (50k/100k/200k/1M/10M) are now only the boot fallback used before the first context event reports a window. `/mega-tier` was removed in v0.6.0. Higher pressure also deepens the live trim and reviews durable memory more often — the whole system reacts as one.
237
239
  | `/mega-dashboard` | Start the **localhost-only** live dashboard and open it in a browser (token gauge, store stats, live event stream, per-repo + cross-repo drift). |
238
240
  | `/mega-dashboard-status` | Report dashboard server status. |
239
241
  | `/mega-dashboard-stop` | Stop the dashboard server. |
@@ -266,9 +268,9 @@ before starting pi.
266
268
 
267
269
  | Variable | Default | Meaning |
268
270
  |---|---|---|
269
- | `MEGACOMPACT_FAST_GATE_PCT` | `70` | Context-usage % that arms the auto-trigger. |
270
- | `MEGACOMPACT_TIER` | `low` | Named trigger preset — sets the token threshold. `low`(50k) `medium`(100k) `high`(200k) `ultra`(1M) `mega`(10M). |
271
- | `MEGACOMPACT_THRESHOLD_TOKENS` | _(tier default)_ | Explicit token budget confirming compaction. Overrides `MEGACOMPACT_TIER` when set. |
271
+ | `MEGACOMPACT_FAST_GATE_PCT` | `70` | Context-usage % that arms the auto-trigger. Defaults to the tier's % of window (`tierPct*100`): low 50 · med 60 · high 70 · ultra 70 · mega 75. Override raises the arming floor. |
272
+ | `MEGACOMPACT_TIER` | `low` | Named trigger preset — sets the compaction threshold as a **% of the model context window**: `low`(50%) `medium`(60%) `high`(70%) `ultra`(70%) `mega`(75%). Fire point = `tierPct × contextWindow`, so it always fires below pi's native ~80% auto-compaction (any model size). The old static token amounts (50k/100k/200k/1M/10M) are now only the **boot fallback** used before the first context event reports a window. Default `low`. |
273
+ | `MEGACOMPACT_THRESHOLD_TOKENS` | _(tier default)_ | Explicit **absolute** token budget (the `custom` tier). Overrides `MEGACOMPACT_TIER` when set and is **never percent-scaled** — use this to pin an exact token fire point regardless of model window. |
272
274
  | `MEGACOMPACT_ANCHOR_USER_MESSAGES` | `3` | Never drop the most recent N user messages (anchor floor). |
273
275
  | `MEGACOMPACT_PRESERVE_RECENT` | `4` | Preserve the most recent N messages verbatim. |
274
276
  | `MEGACOMPACT_AUTO` | `true` | Enable the auto-trigger. |
@@ -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
@@ -139,14 +184,15 @@ function readSnapshot(snapshotPath) {
139
184
  tier: "unknown",
140
185
  presetTier: "unknown",
141
186
  pressure: 0,
142
- config: { fastGatePct: 80, thresholdTokens: 100_000, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
187
+ config: { fastGatePct: 80, thresholdTokens: 100_000, tierPct: null, effectiveThresholdPct: null, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
143
188
  session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
144
189
  context: { tokens: null, percent: null, contextWindow: 0 },
145
- trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80 },
190
+ trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80, tierPct: null, effectiveThresholdPct: null },
146
191
  store: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
147
192
  crew: { activeAgents: 0, currentTurn: 0 },
148
193
  repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
149
194
  integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
195
+ compression: { session: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 }, repo: { tokensIn: 0, tokensOut: 0, tokensFreed: 0, compressionPct: 0, dedupPct: 0 } },
150
196
  model: undefined,
151
197
  };
152
198
  }
@@ -332,9 +378,9 @@ function dashboardHtml(tierName) {
332
378
  <div class="conf-grid">
333
379
  <span class="label" title="Live pressure band — climbs low→mega as context fills the window.">Tier (live)</span><span class="value" id="cf-tier">${tierName}</span>
334
380
  <span class="label" title="The env-resolved base compaction preset (low/medium/high/ultra/mega) that set the token threshold.">Preset</span><span class="value" id="cf-preset">—</span>
335
- <span class="label" title="Live pressure = currentTokens / thresholdTokens (0–100%).">Pressure</span><span class="value" id="cf-pressure">—</span>
336
- <span class="label">Threshold</span><span class="value" id="cf-threshold">—</span>
337
- <span class="label">Fast Gate</span><span class="value" id="cf-gate">—</span>
381
+ <span class="label" title="Live pressure = currentTokens / threshold — % of the model context window (threshold fires at the tier's % of window).">Pressure</span><span class="value" id="cf-pressure">—</span>
382
+ <span class="label" title="Compaction threshold = tierPct × model context window — mega-compact trims BELOW pi's native ~80% auto-compact for any model size.">Threshold</span><span class="value" id="cf-threshold">—</span>
383
+ <span class="label" title="Fast-gate arming floor — the live trim arms once context passes this % of the window.">Fast Gate</span><span class="value" id="cf-gate">—</span>
338
384
  <span class="label">Auto</span><span class="value" id="cf-auto">—</span>
339
385
  <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
340
386
  </div>
@@ -443,15 +489,23 @@ function dashboardHtml(tierName) {
443
489
  <thead>
444
490
  <tr>
445
491
  <th>Model</th><th>Provider</th>
446
- <th style="text-align:right">Repos</th>
492
+ <th style="text-align:right" title="Tokens dropped from context by compaction (the input reclaimed)">Tokens In</th>
493
+ <th style="text-align:right" title="Tokens kept as compacted summaries still in context (the output retained)">Tokens Out</th>
494
+ <th style="text-align:right">Freed</th>
495
+ <th style="text-align:right" title="Model context window (max input tokens the model accepts)">Ctx Window</th>
496
+ <th style="text-align:right" title="Model max output tokens per turn">Max Out</th>
497
+ <th style="text-align:right" title="Reasoning-capable model">Reas.</th>
498
+ <th style="text-align:right" title="Distinct sessions with at least one checkpoint">Sessions</th>
447
499
  <th style="text-align:right">Checkpoints</th>
448
- <th style="text-align:right">Tokens Saved</th>
500
+ <th style="text-align:right" title="USD per input token">In $/tok</th>
501
+ <th style="text-align:right" title="USD per output token">Out $/tok</th>
449
502
  <th style="text-align:right">$ Saved</th>
450
503
  <th style="text-align:right">Last Used</th>
451
504
  </tr>
452
505
  </thead>
453
- <tbody id="bm-rows"><tr><td colspan="7" class="repo-none">loading…</td></tr></tbody>
506
+ <tbody id="bm-rows"><tr><td colspan="14" class="repo-none">loading…</td></tr></tbody>
454
507
  </table>
508
+ <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
509
 
456
510
  <div class="updated" id="sm-updated"></div>
457
511
  </div>
@@ -547,7 +601,17 @@ function dashboardHtml(tierName) {
547
601
  document.getElementById('cf-tier').textContent = d.tier + ' (live)';
548
602
  document.getElementById('cf-preset').textContent = d.presetTier;
549
603
  document.getElementById('cf-pressure').textContent = Math.round((d.pressure || 0) * 100) + '%';
550
- document.getElementById('cf-threshold').textContent = d.config.thresholdTokens.toLocaleString();
604
+ // (b) Threshold: show the effective token threshold AND the % of the model
605
+ // context window it represents (percentage-based tiers). d.config.tierPct
606
+ // is present on the live snapshot written by the runtime (Phase-1/2a).
607
+ var cfgPct = d.config.tierPct;
608
+ var cw = d.context.contextWindow || 0;
609
+ var thresholdTxt = d.config.thresholdTokens.toLocaleString();
610
+ if (cfgPct != null && cw > 0) {
611
+ thresholdTxt += ' (' + Math.round(cfgPct * 100) + '% of ' + cw.toLocaleString() + ')';
612
+ }
613
+ document.getElementById('cf-threshold').textContent = thresholdTxt;
614
+ // (c) Fast Gate: arming floor — live trim arms once context passes this %.
551
615
  document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
552
616
  document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
553
617
  document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
@@ -673,33 +737,67 @@ function dashboardHtml(tierName) {
673
737
  var rows = document.getElementById('bm-rows');
674
738
  if (!rows) return;
675
739
  if (!repos || !repos.length) {
676
- rows.innerHTML = '<tr><td colspan="7" class="repo-none">No repositories registered yet.</td></tr>';
740
+ rows.innerHTML = '<tr><td colspan="14" class="repo-none">No repositories registered yet.</td></tr>';
677
741
  return;
678
742
  }
679
743
  var groups = {};
680
744
  for (var i = 0; i < repos.length; i++) {
681
745
  var r = repos[i];
682
746
  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: [] };
747
+ if (!groups[key]) groups[key] = {
748
+ model: key, provider: r.providerName || r.provider || '—', repos: 0, checkpoints: 0,
749
+ tokensSaved: 0, tokensIn: 0, tokensOut: 0, sessions: 0, usd: 0, lastAt: 0,
750
+ inRates: [], outRates: [], ctxWindows: [], maxTokens: [], reasoning: null,
751
+ };
684
752
  var g = groups[key];
685
753
  g.repos++;
686
754
  g.checkpoints += (r.checkpointCount || 0);
687
755
  g.tokensSaved += (r.tokensSaved || 0);
688
- if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.rates.push(r.inputRate); }
756
+ g.tokensIn += (r.tokensDropped || 0);
757
+ g.tokensOut += (r.tokensKept || 0);
758
+ g.sessions += (r.sessions || 0);
759
+ if (r.inputRate) { g.usd += (r.tokensSaved || 0) * r.inputRate; g.inRates.push(r.inputRate); }
760
+ if (r.outputRate) g.outRates.push(r.outputRate);
761
+ if (r.contextWindow) g.ctxWindows.push(r.contextWindow);
762
+ if (r.maxTokens) g.maxTokens.push(r.maxTokens);
763
+ if (r.reasoning != null) g.reasoning = r.reasoning;
689
764
  if (r.lastCompactedAt && r.lastCompactedAt > g.lastAt) g.lastAt = r.lastCompactedAt;
690
765
  }
691
766
  var arr = [];
692
767
  for (var k in groups) { if (Object.prototype.hasOwnProperty.call(groups, k)) arr.push(groups[k]); }
693
768
  arr.sort(function(a, b) { return b.tokensSaved - a.tokensSaved; });
769
+ // Helper: a set of numeric samples collapses to a single value when all
770
+ // repos in the group agree, otherwise shows the range (min–max) so the
771
+ // user can see mixed-config model groups at a glance.
772
+ function collapseNum(samples) {
773
+ if (!samples || !samples.length) return '—';
774
+ var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
775
+ return lo === hi ? lo.toLocaleString() : lo.toLocaleString() + '–' + hi.toLocaleString();
776
+ }
777
+ function collapseRate(samples) {
778
+ if (!samples || !samples.length) return '—';
779
+ var lo = Math.min.apply(null, samples), hi = Math.max.apply(null, samples);
780
+ var fmt = function(v) { return '$' + v.toFixed(6); };
781
+ return lo === hi ? fmt(lo) : fmt(lo) + '–' + fmt(hi);
782
+ }
694
783
  rows.innerHTML = arr.map(function(g) {
784
+ var freed = (g.tokensIn || 0) - (g.tokensOut || 0);
695
785
  var usd = g.usd > 0 ? '$' + g.usd.toFixed(4) : '—';
696
786
  var when = g.lastAt ? new Date(g.lastAt).toLocaleString() : '—';
787
+ var reas = g.reasoning == null ? '—' : (g.reasoning ? 'yes' : 'no');
697
788
  return '<tr>' +
698
789
  '<td><span class="repo-model">' + sanitize(g.model) + '</span></td>' +
699
790
  '<td>' + sanitize(g.provider) + '</td>' +
700
- '<td class="num">' + g.repos.toLocaleString() + '</td>' +
791
+ '<td class="num">' + (g.tokensIn || 0).toLocaleString() + '</td>' +
792
+ '<td class="num">' + (g.tokensOut || 0).toLocaleString() + '</td>' +
793
+ '<td class="num">' + freed.toLocaleString() + '</td>' +
794
+ '<td class="num">' + collapseNum(g.ctxWindows) + '</td>' +
795
+ '<td class="num">' + collapseNum(g.maxTokens) + '</td>' +
796
+ '<td class="num">' + reas + '</td>' +
797
+ '<td class="num">' + g.sessions.toLocaleString() + '</td>' +
701
798
  '<td class="num">' + g.checkpoints.toLocaleString() + '</td>' +
702
- '<td class="num">' + g.tokensSaved.toLocaleString() + '</td>' +
799
+ '<td class="num">' + collapseRate(g.inRates) + '</td>' +
800
+ '<td class="num">' + collapseRate(g.outRates) + '</td>' +
703
801
  '<td class="num">' + sanitize(usd) + '</td>' +
704
802
  '<td class="num">' + sanitize(when) + '</td>' +
705
803
  '</tr>';
@@ -116,9 +116,20 @@ export function registerCommands(pi, runtime, config) {
116
116
  }
117
117
  catch { /* non-fatal */ }
118
118
  const crossRepoStr = `${crossRepoInjections} cross-repo injections recorded · ${repoCount} repos indexed`;
119
+ // Effective compaction threshold = tierPct × model context window (kept
120
+ // BELOW pi's native ~80% auto-compact for any model size). Falls back to
121
+ // the boot token value when the window is unknown (custom tier / pre-
122
+ // model-select). Display matches the dashboard's percentage-based view.
123
+ const effThreshold = config.tierPct != null && ctxWindow > 0
124
+ ? Math.round(config.tierPct * ctxWindow)
125
+ : config.thresholdTokens;
126
+ const winStr = ctxWindow > 0
127
+ ? (ctxWindow >= 1_000_000 ? `${Math.round(ctxWindow / 1_000_000)}M` : `${Math.round(ctxWindow / 1_000)}k`)
128
+ : "?";
129
+ const tierPctStr = config.tierPct != null ? `${Math.round(config.tierPct * 100)}%` : "n/a";
119
130
  ctx.ui.notify(`[mega-compact] pct=${pct} tokens=${tokens} tier=${runtime.pressureBand} (live) preset=${config.tier} ` +
120
131
  `pressure=${Math.round(runtime.pressure * 100)}% fastGate=${config.fastGatePct}% ` +
121
- `threshold=${config.thresholdTokens} auto=${config.auto} autoInline=${config.autoInline}\n` +
132
+ `threshold=${effThreshold.toLocaleString()} (${tierPctStr} of ${winStr} window) tierPct=${config.tierPct != null ? config.tierPct.toFixed(2) : "n/a"} auto=${config.auto} autoInline=${config.autoInline}\n` +
122
133
  `[mega-compact] store: ${st.checkpointCount} chkpt · ` +
123
134
  `${st.totalTokenEstimate} tok · last=${st.lastCheckpointId ?? "—"} · ` +
124
135
  `injected=${st.injectedCount} · dedup=${(st.dedupHitRate * 100).toFixed(0)}%\n` +