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.
- package/dist/extensions/dashboard-server.js +30 -9
- package/dist/extensions/mega-compact.js +119 -5
- package/dist/src/store/phase04.test.js +54 -0
- package/extensions/dashboard-server.ts +30 -9
- package/extensions/mega-compact.ts +130 -4
- package/package.json +1 -1
- package/src/store/phase04.test.ts +58 -0
|
@@ -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">
|
|
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}
|
|
262
|
-
` ${triggerLabel} │ ${C.magenta}
|
|
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 >=
|
|
291
|
-
break; //
|
|
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">
|
|
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}
|
|
365
|
-
` ${triggerLabel} │ ${C.magenta}
|
|
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 >=
|
|
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.
|
|
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 */ } });
|