pi-mega-compact 0.4.13 → 0.4.15

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.
@@ -69,6 +69,15 @@ function dashboardHtml(tierName) {
69
69
  .card.safe h2 { color: #3fb950; }
70
70
  .safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
71
71
  .value.ok { color: #3fb950; }
72
+ .label {
73
+ cursor: help;
74
+ border-bottom: 1px dotted #484f58;
75
+ }
76
+ .card.legend { grid-column: 1 / -1; }
77
+ .legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
78
+ .legend-list li { margin-bottom: 8px; line-height: 1.5; }
79
+ .legend-list b { color: #f0f6fc; }
80
+ .legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
72
81
  .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
73
82
  .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
74
83
  .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
@@ -126,15 +135,15 @@ function dashboardHtml(tierName) {
126
135
  <div class="card">
127
136
  <h2>Vector Store</h2>
128
137
  <div class="stat-grid">
129
- <span class="label">Checkpoints</span><span class="value" id="st-count">0</span>
130
- <span class="label">Tokens Stored</span><span class="value" id="st-tokens">0</span>
131
- <span class="label">Original Tokens</span><span class="value" id="st-orig">0</span>
132
- <span class="label">Tokens Saved</span><span class="value" id="st-saved">0</span>
133
- <span class="label">Injected</span><span class="value" id="st-injected">0</span>
134
- <span class="label">Dedup Rate</span><span class="value" id="st-dedup">0%</span>
135
- <span class="label">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
136
- <span class="label">Collapsed</span><span class="value" id="st-collapsed">0</span>
137
- <span class="label">Last ID</span><span class="value" id="st-lastid">—</span>
138
+ <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>
139
+ <span class="label" title="How much conversation we are currently holding as compact summaries (the 'memory' this extension keeps). Smaller is better.">Tokens Stored</span><span class="value" id="st-tokens">0</span>
140
+ <span class="label" title="Total size of the original conversation text before it was compacted.">Original Tokens</span><span class="value" id="st-orig">0</span>
141
+ <span class="label" title="How much conversation space we have freed up for you (original size minus the compact summary we kept).">Tokens Saved</span><span class="value" id="st-saved">0</span>
142
+ <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>
143
+ <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>
144
+ <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>
145
+ <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>
146
+ <span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
138
147
  </div>
139
148
  </div>
140
149
  <div class="card">
@@ -177,6 +186,18 @@ function dashboardHtml(tierName) {
177
186
  <span class="label">Status</span><span class="value" id="cr-status">idle</span>
178
187
  </div>
179
188
  </div>
189
+ <div class="card legend">
190
+ <h2>What these numbers mean</h2>
191
+ <ul class="legend-list">
192
+ <li><b>Tokens saved</b> — conversation space this extension has freed up for you (it compacted old text into short summaries).</li>
193
+ <li><b>Tokens stored</b> — how much "memory" (compact summaries) the extension is currently holding for this repo.</li>
194
+ <li><b>Injected</b> — times old context was automatically pasted back in because it was relevant to your current task.</li>
195
+ <li><b>Recall relevance</b> — of those, how often the recalled context was actually on-topic.</li>
196
+ <li><b>Storage dedup</b> — how often new content matched something already saved, so a duplicate copy was skipped (saves space).</li>
197
+ <li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
198
+ </ul>
199
+ <p class="legend-note">Hover any label above for a quick explanation.</p>
200
+ </div>
180
201
  </div>
181
202
 
182
203
  <div class="events">
@@ -34,7 +34,9 @@ import { recallAndInline } from "../src/recall.js";
34
34
  import { autoCompactCheck } from "../src/compact.js";
35
35
  import { estimateSessionTokens } from "../src/tokens.js";
36
36
  import { normalizeSessionId } from "../src/store.js";
37
- import { touchSession, logDaily } from "../src/store/sqlite.js";
37
+ import { touchSession, logDaily, listCheckpoints } from "../src/store/sqlite.js";
38
+ import { decompressSmart } from "../src/store/compression.js";
39
+ import { loadMetrics, fpRate, p95 } from "../src/monitoring.js";
38
40
  import { Logger } from "../src/log.js";
39
41
  import { writeFileSync, appendFileSync, readFileSync } from "node:fs";
40
42
  import { existsSync, mkdirSync, unlinkSync } from "node:fs";
@@ -258,8 +260,8 @@ export default function (pi) {
258
260
  // Phase 3 — pulsing status glyph while a compaction is in flight.
259
261
  const pulse = pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
260
262
  const lines = [
261
- ` ${C.amber}⚡ ${config.tier}${C.reset} │ ${tokStr}/${maxStr} tokens (${C.bold}${pctStr}${C.reset}) │ ${st.checkpointCount} chkpt${agentStr}${turnStr}`,
262
- ` ${triggerLabel} │ ${C.magenta}dedup: ${dedupStr}${C.reset} │ ${C.gray}used:${C.reset} ${usedStr} │ ${C.gray}saved:${C.reset} ${savedStr}`,
263
+ ` ${C.amber}⚡ ${config.tier}${C.reset} │ ${tokStr}/${maxStr} tokens (${C.bold}${pctStr}${C.reset}) │ ${st.checkpointCount} saved${agentStr}${turnStr}`,
264
+ ` ${triggerLabel} │ ${C.magenta}repeat-skipped: ${dedupStr}${C.reset} │ ${C.gray}memory held:${C.reset} ${usedStr} │ ${C.gray}space freed:${C.reset} ${savedStr}`,
263
265
  ];
264
266
  // Phase 3 — compact progress bar: session tokens saved toward the rolling goal.
265
267
  if (rt.tokensSaved > 0) {
@@ -287,11 +289,16 @@ export default function (pi) {
287
289
  // Phase 3 — recall/activity ticker (most-recent first), fresh only.
288
290
  if (fresh) {
289
291
  for (let i = ticker.length - 1; i >= 0; i--) {
290
- if (lines.length >= 10)
291
- break; // MAX_WIDGET_LINES guard
292
+ if (lines.length >= 9)
293
+ break; // leave room for the hint line (MAX 10)
292
294
  lines.push(` ${i === ticker.length - 1 ? "" : C.dim}${ticker[i].text}${C.reset}`);
293
295
  }
294
296
  }
297
+ // Plain-language hint so first-time users understand the widget. Always
298
+ // last, dimmed. "/mega-help explains these terms."
299
+ if (lines.length < 10) {
300
+ lines.push(` ${C.dim}auto-compresses old context to free space · nothing deleted · /mega-help${C.reset}`);
301
+ }
295
302
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
296
303
  }
297
304
  }
@@ -732,9 +739,25 @@ export default function (pi) {
732
739
  const tokens = usage?.tokens != null ? `${usage.tokens} tok` : "n/a";
733
740
  const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
734
741
  const st = store.stats(sid);
742
+ const repo = store.repoStats();
735
743
  const di = store.dataInvariant();
736
744
  const fmtB = (b) => b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MiB` :
737
745
  b >= 1024 ? `${(b / 1024).toFixed(1)} KiB` : `${b} B`;
746
+ // Tangible cost: turn "tokens saved" into a dollar figure + context-days
747
+ // extended, so the counter is concrete rather than opaque. ~$3 / 1M tok
748
+ // (rough blended rate); contextWindow ÷ savedRate = days of context bought.
749
+ const usd = (repo.tokensSaved / 1_000_000 * 3).toFixed(2);
750
+ const ctxWindow = usage?.contextWindow ?? 0;
751
+ const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
752
+ ? (repo.tokensSaved / ctxWindow).toFixed(1)
753
+ : "0";
754
+ const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
755
+ // Recall-quality badge (Phase 4): trust score from monitoring metrics.
756
+ const m = loadMetrics(currentStateDir);
757
+ const fp = fpRate(m, "L2");
758
+ const p95L2 = p95(m.latency.L2 ?? []);
759
+ const relPct = (st.dedupHitRate * 100).toFixed(0);
760
+ const qualityStr = `recall ${relPct}% relevant · FP ${(fp * 100).toFixed(1)}% · L2 p95 ${p95L2.toFixed(0)}ms`;
738
761
  ctx.ui.notify(`[mega-compact] pct=${pct} tokens=${tokens} tier=${config.tier} fastGate=${config.fastGatePct}% ` +
739
762
  `threshold=${config.thresholdTokens} auto=${config.auto} autoInline=${config.autoInline}\n` +
740
763
  `[mega-compact] store: ${st.checkpointCount} chkpt · ` +
@@ -746,9 +769,100 @@ export default function (pi) {
746
769
  `(${fmtB(di.compressedOriginalBytes)} compressed-original) · ` +
747
770
  `${di.duplicatesCollapsed} dedup-duplicates collapsed · ` +
748
771
  `${C.green}0 bytes permanently deleted${C.reset}\n` +
772
+ `[mega-compact] 💰 ${costStr}\n` +
773
+ `[mega-compact] 🎯 ${qualityStr}\n` +
749
774
  `[mega-compact] stateDir=${currentStateDir}`);
750
775
  },
751
776
  });
777
+ // ---- Phase 4: cheap standout commands (data is already persisted) -------
778
+ /** Resolve a checkpoint by id (or "recent"/"last") from this session's store. */
779
+ function findCheckpoint(sid, ref) {
780
+ const all = listCheckpoints(sid, currentStateDir);
781
+ if (all.length === 0)
782
+ return undefined;
783
+ if (!ref || ref === "recent" || ref === "last")
784
+ return all[all.length - 1];
785
+ return all.find((c) => c.checkpointId === ref) ?? all.find((c) => c.checkpointId.endsWith(ref));
786
+ }
787
+ pi.registerCommand("mega-restore", {
788
+ description: "Re-inject a checkpoint's verbatim original region into context. Usage: /mega-restore <chkpt|recent>",
789
+ handler: async (args, ctx) => {
790
+ bindRepo(ctx.cwd);
791
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
792
+ const cp = findCheckpoint(sid, args.trim());
793
+ if (!cp) {
794
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""} in this session. Try /mega-history.`);
795
+ return;
796
+ }
797
+ if (!cp.compressedOriginal) {
798
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} has no recoverable original (pre-blob or direct add). Cannot restore verbatim.`);
799
+ return;
800
+ }
801
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
802
+ // Re-inject verbatim via before_agent_start (PREVENT-PI-003) — never
803
+ // touches live messages, only prepends the restored region to systemPrompt.
804
+ pendingRecallBlock = `The following compacted context was RESTORED from checkpoint ${cp.checkpointId} (verbatim original region):\n\n${original}`;
805
+ const files = cp.filesModified?.length ? cp.filesModified.join(", ") : "(no files captured)";
806
+ ctx.ui.notify(`[mega-compact] ♻ restored ${cp.checkpointId} — ${original.length} chars re-injected on next turn.\n` +
807
+ `[mega-compact] files: ${files}`);
808
+ dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
809
+ },
810
+ });
811
+ pi.registerCommand("mega-history", {
812
+ description: "List this session's checkpoints (id, date, files, tokens). Usage: /mega-history",
813
+ handler: async (_args, ctx) => {
814
+ bindRepo(ctx.cwd);
815
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
816
+ const all = listCheckpoints(sid, currentStateDir);
817
+ if (all.length === 0) {
818
+ ctx.ui.notify("[mega-compact] no checkpoints in this session yet.");
819
+ return;
820
+ }
821
+ const rows = all.map((c) => {
822
+ const when = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "—";
823
+ const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop()).join(", ") : "—";
824
+ const orig = c.originalTokenEstimate ?? 0;
825
+ const stored = c.tokenEstimate ?? 0;
826
+ const saved = Math.max(0, orig - stored);
827
+ return ` ${c.checkpointId} ${when} ${C.cyan}${saved}t saved${C.reset} ${files}`;
828
+ });
829
+ ctx.ui.notify(`[mega-compact] ${all.length} checkpoint(s) in this session:\n` + rows.join("\n") +
830
+ `\n[mega-compact] /mega-view <chkpt> to see the original region · /mega-restore <chkpt> to re-inject it`);
831
+ },
832
+ });
833
+ pi.registerCommand("mega-view", {
834
+ description: "Show a checkpoint's verbatim original region. Usage: /mega-view <chkpt|recent>",
835
+ handler: async (args, ctx) => {
836
+ bindRepo(ctx.cwd);
837
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
838
+ const cp = findCheckpoint(sid, args.trim());
839
+ if (!cp) {
840
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""}. Try /mega-history.`);
841
+ return;
842
+ }
843
+ if (!cp.compressedOriginal) {
844
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} summary:\n${cp.summary.slice(0, 500)}${cp.summary.length > 500 ? "…" : ""}\n(no verbatim original stored)`);
845
+ return;
846
+ }
847
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
848
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
849
+ `${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`);
850
+ },
851
+ });
852
+ pi.registerCommand("mega-help", {
853
+ description: "Plain-language glossary of what mega-compact's stats mean.",
854
+ handler: async (_args, ctx) => {
855
+ ctx.ui.notify(`[mega-compact] glossary — what the numbers mean:\n` +
856
+ `• token — a chunk of text (~4 chars). Context window = how much text fits in memory at once.\n` +
857
+ `• space freed — how much conversation we've compressed away to make room (the win).\n` +
858
+ `• memory held — how much compact summary we're currently keeping as your 'notes'.\n` +
859
+ `• saved checkpoint — a compact summary of an old conversation chunk we stored.\n` +
860
+ `• repeat-skipped — how often new text matched something we already had, so we didn't store a duplicate.\n` +
861
+ `• injected — times we pasted an old saved note back into the chat because it was relevant.\n` +
862
+ `• recall relevance — of those, how often the note was actually on-topic.\n` +
863
+ `• data safety — every compressed region is kept verbatim; nothing is permanently deleted. /mega-restore brings any of it back.`);
864
+ },
865
+ });
752
866
  pi.registerCommand("mega-tier", {
753
867
  description: "Show or change the compaction tier at runtime. Usage: /mega-tier [low|medium|high|ultra|mega]",
754
868
  handler: async (args, ctx) => {
@@ -0,0 +1,54 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { VectorStore } from "../vectorStore.js";
7
+ import { listCheckpoints, dataInvariantStats } from "./sqlite.js";
8
+ import { decompressSmart, compressSmart } from "./compression.js";
9
+ const baseTmp = mkdtempSync(join(tmpdir(), "mc-p04-"));
10
+ let counter = 0;
11
+ function store(opts = {}) {
12
+ const dir = join(baseTmp, `run-${counter++}`);
13
+ return { s: new VectorStore({ dedupSim: opts.dedupSim ?? 0.9, stateDir: dir }), dir };
14
+ }
15
+ test("Phase 4: added checkpoint is listed and its compressed-original round-trips", () => {
16
+ const { s, dir } = store();
17
+ const original = "the original region text that gets compacted and must be restorable verbatim";
18
+ const r = s.add({ sessionId: "sess_a", summary: "s", regionText: original, tokenEstimate: 5, originalTokenEstimate: 60, timestamp: 1 });
19
+ const all = listCheckpoints("sess_a", dir);
20
+ assert.equal(all.length, 1, "one checkpoint listed");
21
+ const cp = all[0];
22
+ assert.ok(cp.compressedOriginal, "compressed-original blob present");
23
+ // The DR/restore path: decompressSmart must return the exact original.
24
+ const restored = decompressSmart(cp.compressedOriginal).toString("utf-8");
25
+ assert.equal(restored, original, "restored verbatim === original");
26
+ assert.equal(cp.checkpointId, r.checkpoint.checkpointId);
27
+ });
28
+ test("Phase 4: findCheckpoint-by-id resolves a listed checkpoint", () => {
29
+ const { s, dir } = store();
30
+ s.add({ sessionId: "sess_b", summary: "s1", regionText: "region one text content here", tokenEstimate: 4, originalTokenEstimate: 40, timestamp: 1 });
31
+ const r2 = s.add({ sessionId: "sess_b", summary: "s2", regionText: "region two text content here different", tokenEstimate: 4, originalTokenEstimate: 45, timestamp: 2 });
32
+ const all = listCheckpoints("sess_b", dir);
33
+ assert.equal(all.length, 2);
34
+ const wanted = all.find((c) => c.checkpointId === r2.checkpoint.checkpointId);
35
+ assert.ok(wanted, "checkpoint resolved by id");
36
+ assert.ok(wanted.compressedOriginal, "has restorable original");
37
+ });
38
+ test("Phase 4: dataInvariantStats sanity for restore trust (0 deleted)", () => {
39
+ const { s, dir } = store();
40
+ s.add({ sessionId: "sess_c", summary: "s", regionText: "retained region body text", tokenEstimate: 5, originalTokenEstimate: 50, timestamp: 1 });
41
+ const di = dataInvariantStats(dir);
42
+ assert.equal(di.regionsRetained, 1);
43
+ assert.ok(di.compressedOriginalBytes > 0);
44
+ assert.equal(di.bytesPermanentlyDeleted, 0);
45
+ });
46
+ test("Phase 4: compressSmart/decompressSmart round-trips arbitrary text", () => {
47
+ const text = "x".repeat(2000);
48
+ const back = decompressSmart(compressSmart(Buffer.from(text, "utf-8"))).toString("utf-8");
49
+ assert.equal(back, text);
50
+ });
51
+ process.on("exit", () => { try {
52
+ rmSync(baseTmp, { recursive: true, force: true });
53
+ }
54
+ catch { /* ignore */ } });
@@ -133,6 +133,15 @@ function dashboardHtml(tierName: string): string {
133
133
  .card.safe h2 { color: #3fb950; }
134
134
  .safe-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; line-height: 1.5; }
135
135
  .value.ok { color: #3fb950; }
136
+ .label {
137
+ cursor: help;
138
+ border-bottom: 1px dotted #484f58;
139
+ }
140
+ .card.legend { grid-column: 1 / -1; }
141
+ .legend-list { margin: 0; padding-left: 18px; color: #c9d1d9; }
142
+ .legend-list li { margin-bottom: 8px; line-height: 1.5; }
143
+ .legend-list b { color: #f0f6fc; }
144
+ .legend-note { font-size: 12px; color: #8b949e; margin: 12px 0 0; font-style: italic; }
136
145
  .card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; font-weight: 600; }
137
146
  .meter-track { background: #21262d; border-radius: 4px; height: 20px; overflow: hidden; margin: 8px 0; }
138
147
  .meter-fill { height: 100%; border-radius: 4px; transition: width .6s ease; min-width: 2px; }
@@ -190,15 +199,15 @@ function dashboardHtml(tierName: string): string {
190
199
  <div class="card">
191
200
  <h2>Vector Store</h2>
192
201
  <div class="stat-grid">
193
- <span class="label">Checkpoints</span><span class="value" id="st-count">0</span>
194
- <span class="label">Tokens Stored</span><span class="value" id="st-tokens">0</span>
195
- <span class="label">Original Tokens</span><span class="value" id="st-orig">0</span>
196
- <span class="label">Tokens Saved</span><span class="value" id="st-saved">0</span>
197
- <span class="label">Injected</span><span class="value" id="st-injected">0</span>
198
- <span class="label">Dedup Rate</span><span class="value" id="st-dedup">0%</span>
199
- <span class="label">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
200
- <span class="label">Collapsed</span><span class="value" id="st-collapsed">0</span>
201
- <span class="label">Last ID</span><span class="value" id="st-lastid">—</span>
202
+ <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>
203
+ <span class="label" title="How much conversation we are currently holding as compact summaries (the 'memory' this extension keeps). Smaller is better.">Tokens Stored</span><span class="value" id="st-tokens">0</span>
204
+ <span class="label" title="Total size of the original conversation text before it was compacted.">Original Tokens</span><span class="value" id="st-orig">0</span>
205
+ <span class="label" title="How much conversation space we have freed up for you (original size minus the compact summary we kept).">Tokens Saved</span><span class="value" id="st-saved">0</span>
206
+ <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>
207
+ <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>
208
+ <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>
209
+ <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>
210
+ <span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
202
211
  </div>
203
212
  </div>
204
213
  <div class="card">
@@ -241,6 +250,18 @@ function dashboardHtml(tierName: string): string {
241
250
  <span class="label">Status</span><span class="value" id="cr-status">idle</span>
242
251
  </div>
243
252
  </div>
253
+ <div class="card legend">
254
+ <h2>What these numbers mean</h2>
255
+ <ul class="legend-list">
256
+ <li><b>Tokens saved</b> — conversation space this extension has freed up for you (it compacted old text into short summaries).</li>
257
+ <li><b>Tokens stored</b> — how much "memory" (compact summaries) the extension is currently holding for this repo.</li>
258
+ <li><b>Injected</b> — times old context was automatically pasted back in because it was relevant to your current task.</li>
259
+ <li><b>Recall relevance</b> — of those, how often the recalled context was actually on-topic.</li>
260
+ <li><b>Storage dedup</b> — how often new content matched something already saved, so a duplicate copy was skipped (saves space).</li>
261
+ <li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
262
+ </ul>
263
+ <p class="legend-note">Hover any label above for a quick explanation.</p>
264
+ </div>
244
265
  </div>
245
266
 
246
267
  <div class="events">
@@ -37,7 +37,9 @@ import { recallAndInline } from "../src/recall.js";
37
37
  import { autoCompactCheck } from "../src/compact.js";
38
38
  import { estimateSessionTokens } from "../src/tokens.js";
39
39
  import { normalizeSessionId } from "../src/store.js";
40
- import { touchSession, logDaily } from "../src/store/sqlite.js";
40
+ import { touchSession, logDaily, listCheckpoints } from "../src/store/sqlite.js";
41
+ import { decompressSmart } from "../src/store/compression.js";
42
+ import { loadMetrics, fpRate, p95 } from "../src/monitoring.js";
41
43
  import { Logger } from "../src/log.js";
42
44
  import type { EngineMessage } from "../src/types.js";
43
45
  import { writeFileSync, appendFileSync, readFileSync } from "node:fs";
@@ -361,8 +363,8 @@ export default function (pi: ExtensionAPI) {
361
363
  // Phase 3 — pulsing status glyph while a compaction is in flight.
362
364
  const pulse = pulsing ? `${C.cyan}${PULSE[Math.floor(Date.now() / 250) % PULSE.length]}${C.reset} ` : "";
363
365
  const lines = [
364
- ` ${C.amber}⚡ ${config.tier}${C.reset} │ ${tokStr}/${maxStr} tokens (${C.bold}${pctStr}${C.reset}) │ ${st.checkpointCount} chkpt${agentStr}${turnStr}`,
365
- ` ${triggerLabel} │ ${C.magenta}dedup: ${dedupStr}${C.reset} │ ${C.gray}used:${C.reset} ${usedStr} │ ${C.gray}saved:${C.reset} ${savedStr}`,
366
+ ` ${C.amber}⚡ ${config.tier}${C.reset} │ ${tokStr}/${maxStr} tokens (${C.bold}${pctStr}${C.reset}) │ ${st.checkpointCount} saved${agentStr}${turnStr}`,
367
+ ` ${triggerLabel} │ ${C.magenta}repeat-skipped: ${dedupStr}${C.reset} │ ${C.gray}memory held:${C.reset} ${usedStr} │ ${C.gray}space freed:${C.reset} ${savedStr}`,
366
368
  ];
367
369
  // Phase 3 — compact progress bar: session tokens saved toward the rolling goal.
368
370
  if (rt.tokensSaved > 0) {
@@ -387,10 +389,15 @@ export default function (pi: ExtensionAPI) {
387
389
  // Phase 3 — recall/activity ticker (most-recent first), fresh only.
388
390
  if (fresh) {
389
391
  for (let i = ticker.length - 1; i >= 0; i--) {
390
- if (lines.length >= 10) break; // MAX_WIDGET_LINES guard
392
+ if (lines.length >= 9) break; // leave room for the hint line (MAX 10)
391
393
  lines.push(` ${i === ticker.length - 1 ? "" : C.dim}${ticker[i].text}${C.reset}`);
392
394
  }
393
395
  }
396
+ // Plain-language hint so first-time users understand the widget. Always
397
+ // last, dimmed. "/mega-help explains these terms."
398
+ if (lines.length < 10) {
399
+ lines.push(` ${C.dim}auto-compresses old context to free space · nothing deleted · /mega-help${C.reset}`);
400
+ }
394
401
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "aboveEditor" });
395
402
  }
396
403
  }
@@ -869,10 +876,26 @@ export default function (pi: ExtensionAPI) {
869
876
  const tokens = usage?.tokens != null ? `${usage.tokens} tok` : "n/a";
870
877
  const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
871
878
  const st = store.stats(sid);
879
+ const repo = store.repoStats();
872
880
  const di = store.dataInvariant();
873
881
  const fmtB = (b: number) =>
874
882
  b >= 1_048_576 ? `${(b / 1_048_576).toFixed(1)} MiB` :
875
883
  b >= 1024 ? `${(b / 1024).toFixed(1)} KiB` : `${b} B`;
884
+ // Tangible cost: turn "tokens saved" into a dollar figure + context-days
885
+ // extended, so the counter is concrete rather than opaque. ~$3 / 1M tok
886
+ // (rough blended rate); contextWindow ÷ savedRate = days of context bought.
887
+ const usd = (repo.tokensSaved / 1_000_000 * 3).toFixed(2);
888
+ const ctxWindow = usage?.contextWindow ?? 0;
889
+ const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
890
+ ? (repo.tokensSaved / ctxWindow).toFixed(1)
891
+ : "0";
892
+ const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
893
+ // Recall-quality badge (Phase 4): trust score from monitoring metrics.
894
+ const m = loadMetrics(currentStateDir);
895
+ const fp = fpRate(m, "L2");
896
+ const p95L2 = p95(m.latency.L2 ?? []);
897
+ const relPct = (st.dedupHitRate * 100).toFixed(0);
898
+ const qualityStr = `recall ${relPct}% relevant · FP ${(fp * 100).toFixed(1)}% · L2 p95 ${p95L2.toFixed(0)}ms`;
876
899
  ctx.ui.notify(
877
900
  `[mega-compact] pct=${pct} tokens=${tokens} tier=${config.tier} fastGate=${config.fastGatePct}% ` +
878
901
  `threshold=${config.thresholdTokens} auto=${config.auto} autoInline=${config.autoInline}\n` +
@@ -885,11 +908,114 @@ export default function (pi: ExtensionAPI) {
885
908
  `(${fmtB(di.compressedOriginalBytes)} compressed-original) · ` +
886
909
  `${di.duplicatesCollapsed} dedup-duplicates collapsed · ` +
887
910
  `${C.green}0 bytes permanently deleted${C.reset}\n` +
911
+ `[mega-compact] 💰 ${costStr}\n` +
912
+ `[mega-compact] 🎯 ${qualityStr}\n` +
888
913
  `[mega-compact] stateDir=${currentStateDir}`,
889
914
  );
890
915
  },
891
916
  });
892
917
 
918
+ // ---- Phase 4: cheap standout commands (data is already persisted) -------
919
+
920
+ /** Resolve a checkpoint by id (or "recent"/"last") from this session's store. */
921
+ function findCheckpoint(sid: string, ref: string) {
922
+ const all = listCheckpoints(sid, currentStateDir);
923
+ if (all.length === 0) return undefined;
924
+ if (!ref || ref === "recent" || ref === "last") return all[all.length - 1];
925
+ return all.find((c) => c.checkpointId === ref) ?? all.find((c) => c.checkpointId.endsWith(ref));
926
+ }
927
+
928
+ pi.registerCommand("mega-restore", {
929
+ description: "Re-inject a checkpoint's verbatim original region into context. Usage: /mega-restore <chkpt|recent>",
930
+ handler: async (args: string, ctx: ExtensionContext) => {
931
+ bindRepo(ctx.cwd);
932
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
933
+ const cp = findCheckpoint(sid, args.trim());
934
+ if (!cp) {
935
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""} in this session. Try /mega-history.`);
936
+ return;
937
+ }
938
+ if (!cp.compressedOriginal) {
939
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} has no recoverable original (pre-blob or direct add). Cannot restore verbatim.`);
940
+ return;
941
+ }
942
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
943
+ // Re-inject verbatim via before_agent_start (PREVENT-PI-003) — never
944
+ // touches live messages, only prepends the restored region to systemPrompt.
945
+ pendingRecallBlock = `The following compacted context was RESTORED from checkpoint ${cp.checkpointId} (verbatim original region):\n\n${original}`;
946
+ const files = cp.filesModified?.length ? cp.filesModified.join(", ") : "(no files captured)";
947
+ ctx.ui.notify(
948
+ `[mega-compact] ♻ restored ${cp.checkpointId} — ${original.length} chars re-injected on next turn.\n` +
949
+ `[mega-compact] files: ${files}`,
950
+ );
951
+ dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
952
+ },
953
+ });
954
+
955
+ pi.registerCommand("mega-history", {
956
+ description: "List this session's checkpoints (id, date, files, tokens). Usage: /mega-history",
957
+ handler: async (_args: string, ctx: ExtensionContext) => {
958
+ bindRepo(ctx.cwd);
959
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
960
+ const all = listCheckpoints(sid, currentStateDir);
961
+ if (all.length === 0) {
962
+ ctx.ui.notify("[mega-compact] no checkpoints in this session yet.");
963
+ return;
964
+ }
965
+ const rows = all.map((c) => {
966
+ const when = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "—";
967
+ const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop()).join(", ") : "—";
968
+ const orig = c.originalTokenEstimate ?? 0;
969
+ const stored = c.tokenEstimate ?? 0;
970
+ const saved = Math.max(0, orig - stored);
971
+ return ` ${c.checkpointId} ${when} ${C.cyan}${saved}t saved${C.reset} ${files}`;
972
+ });
973
+ ctx.ui.notify(
974
+ `[mega-compact] ${all.length} checkpoint(s) in this session:\n` + rows.join("\n") +
975
+ `\n[mega-compact] /mega-view <chkpt> to see the original region · /mega-restore <chkpt> to re-inject it`,
976
+ );
977
+ },
978
+ });
979
+
980
+ pi.registerCommand("mega-view", {
981
+ description: "Show a checkpoint's verbatim original region. Usage: /mega-view <chkpt|recent>",
982
+ handler: async (args: string, ctx: ExtensionContext) => {
983
+ bindRepo(ctx.cwd);
984
+ const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
985
+ const cp = findCheckpoint(sid, args.trim());
986
+ if (!cp) {
987
+ ctx.ui.notify(`[mega-compact] no checkpoint found${args.trim() ? ` for "${args.trim()}"` : ""}. Try /mega-history.`);
988
+ return;
989
+ }
990
+ if (!cp.compressedOriginal) {
991
+ ctx.ui.notify(`[mega-compact] ${cp.checkpointId} summary:\n${cp.summary.slice(0, 500)}${cp.summary.length > 500 ? "…" : ""}\n(no verbatim original stored)`);
992
+ return;
993
+ }
994
+ const original = decompressSmart(cp.compressedOriginal).toString("utf-8");
995
+ ctx.ui.notify(
996
+ `[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
997
+ `${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`,
998
+ );
999
+ },
1000
+ });
1001
+
1002
+ pi.registerCommand("mega-help", {
1003
+ description: "Plain-language glossary of what mega-compact's stats mean.",
1004
+ handler: async (_args: string, ctx: ExtensionContext) => {
1005
+ ctx.ui.notify(
1006
+ `[mega-compact] glossary — what the numbers mean:\n` +
1007
+ `• token — a chunk of text (~4 chars). Context window = how much text fits in memory at once.\n` +
1008
+ `• space freed — how much conversation we've compressed away to make room (the win).\n` +
1009
+ `• memory held — how much compact summary we're currently keeping as your 'notes'.\n` +
1010
+ `• saved checkpoint — a compact summary of an old conversation chunk we stored.\n` +
1011
+ `• repeat-skipped — how often new text matched something we already had, so we didn't store a duplicate.\n` +
1012
+ `• injected — times we pasted an old saved note back into the chat because it was relevant.\n` +
1013
+ `• recall relevance — of those, how often the note was actually on-topic.\n` +
1014
+ `• data safety — every compressed region is kept verbatim; nothing is permanently deleted. /mega-restore brings any of it back.`,
1015
+ );
1016
+ },
1017
+ });
1018
+
893
1019
  pi.registerCommand("mega-tier", {
894
1020
  description: "Show or change the compaction tier at runtime. Usage: /mega-tier [low|medium|high|ultra|mega]",
895
1021
  handler: async (args: string, ctx: ExtensionContext) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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",
@@ -0,0 +1,58 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { VectorStore } from "../vectorStore.js";
7
+ import { listCheckpoints, dataInvariantStats } from "./sqlite.js";
8
+ import { decompressSmart, compressSmart } from "./compression.js";
9
+
10
+ const baseTmp = mkdtempSync(join(tmpdir(), "mc-p04-"));
11
+
12
+ let counter = 0;
13
+ function store(opts: { dedupSim?: number } = {}) {
14
+ const dir = join(baseTmp, `run-${counter++}`);
15
+ return { s: new VectorStore({ dedupSim: opts.dedupSim ?? 0.9, stateDir: dir }), dir };
16
+ }
17
+
18
+ test("Phase 4: added checkpoint is listed and its compressed-original round-trips", () => {
19
+ const { s, dir } = store();
20
+ const original = "the original region text that gets compacted and must be restorable verbatim";
21
+ const r = s.add({ sessionId: "sess_a", summary: "s", regionText: original, tokenEstimate: 5, originalTokenEstimate: 60, timestamp: 1 });
22
+ const all = listCheckpoints("sess_a", dir);
23
+ assert.equal(all.length, 1, "one checkpoint listed");
24
+ const cp = all[0];
25
+ assert.ok(cp.compressedOriginal, "compressed-original blob present");
26
+ // The DR/restore path: decompressSmart must return the exact original.
27
+ const restored = decompressSmart(cp.compressedOriginal!).toString("utf-8");
28
+ assert.equal(restored, original, "restored verbatim === original");
29
+ assert.equal(cp.checkpointId, r.checkpoint.checkpointId);
30
+ });
31
+
32
+ test("Phase 4: findCheckpoint-by-id resolves a listed checkpoint", () => {
33
+ const { s, dir } = store();
34
+ s.add({ sessionId: "sess_b", summary: "s1", regionText: "region one text content here", tokenEstimate: 4, originalTokenEstimate: 40, timestamp: 1 });
35
+ const r2 = s.add({ sessionId: "sess_b", summary: "s2", regionText: "region two text content here different", tokenEstimate: 4, originalTokenEstimate: 45, timestamp: 2 });
36
+ const all = listCheckpoints("sess_b", dir);
37
+ assert.equal(all.length, 2);
38
+ const wanted = all.find((c) => c.checkpointId === r2.checkpoint.checkpointId)!;
39
+ assert.ok(wanted, "checkpoint resolved by id");
40
+ assert.ok(wanted.compressedOriginal, "has restorable original");
41
+ });
42
+
43
+ test("Phase 4: dataInvariantStats sanity for restore trust (0 deleted)", () => {
44
+ const { s, dir } = store();
45
+ s.add({ sessionId: "sess_c", summary: "s", regionText: "retained region body text", tokenEstimate: 5, originalTokenEstimate: 50, timestamp: 1 });
46
+ const di = dataInvariantStats(dir);
47
+ assert.equal(di.regionsRetained, 1);
48
+ assert.ok(di.compressedOriginalBytes > 0);
49
+ assert.equal(di.bytesPermanentlyDeleted, 0);
50
+ });
51
+
52
+ test("Phase 4: compressSmart/decompressSmart round-trips arbitrary text", () => {
53
+ const text = "x".repeat(2000);
54
+ const back = decompressSmart(compressSmart(Buffer.from(text, "utf-8"))).toString("utf-8");
55
+ assert.equal(back, text);
56
+ });
57
+
58
+ process.on("exit", () => { try { rmSync(baseTmp, { recursive: true, force: true }); } catch { /* ignore */ } });