pi-mega-compact 0.4.21 → 0.4.23

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.
Files changed (43) hide show
  1. package/dist/extensions/dashboard-server.js +3 -3
  2. package/dist/extensions/mega-compact-driver.js +79 -0
  3. package/dist/extensions/mega-compact.test.js +54 -18
  4. package/dist/extensions/mega-config.js +10 -0
  5. package/dist/extensions/mega-events.js +45 -23
  6. package/dist/extensions/mega-pipeline.js +77 -3
  7. package/dist/src/config/dedup.js +4 -1
  8. package/dist/src/config.js +21 -0
  9. package/dist/src/dedup/raptor/index.js +28 -6
  10. package/dist/src/dedup/raptor/promote.test.js +69 -0
  11. package/dist/src/engine.js +1 -0
  12. package/dist/src/recall.js +30 -4
  13. package/dist/src/recall.test.js +28 -0
  14. package/dist/src/store/backfill.js +5 -6
  15. package/dist/src/store/compression.js +47 -7
  16. package/dist/src/store/compression.test.js +48 -0
  17. package/dist/src/store/sqlite.js +64 -41
  18. package/dist/src/store.test.js +19 -0
  19. package/dist/src/vectorStore.js +56 -1
  20. package/extensions/DASHBOARD.md +3 -3
  21. package/extensions/dashboard-server.ts +4 -4
  22. package/extensions/mega-compact-driver.ts +105 -0
  23. package/extensions/mega-compact.test.ts +65 -18
  24. package/extensions/mega-config.ts +25 -0
  25. package/extensions/mega-events.ts +43 -24
  26. package/extensions/mega-pipeline.ts +83 -4
  27. package/package.json +6 -7
  28. package/src/config/dedup.ts +4 -1
  29. package/src/config.ts +26 -0
  30. package/src/dedup/raptor/index.ts +42 -7
  31. package/src/dedup/raptor/promote.test.ts +82 -0
  32. package/src/engine.ts +5 -0
  33. package/src/recall.test.ts +44 -0
  34. package/src/recall.ts +43 -4
  35. package/src/store/backfill.ts +10 -11
  36. package/src/store/compression.test.ts +58 -0
  37. package/src/store/compression.ts +48 -7
  38. package/src/store/sqlite.ts +72 -49
  39. package/src/store.test.ts +22 -0
  40. package/src/vectorStore.ts +63 -1
  41. package/dist/extensions/openclaw-mega-compact.js +0 -291
  42. package/dist/src/minilm.js +0 -92
  43. package/dist/src/wordpiece.js +0 -129
@@ -14,6 +14,8 @@
14
14
  * extension decides where it lands.
15
15
  */
16
16
  import { recall as searchRecall } from "./engine.js";
17
+ import { estimateBlockTokens } from "./tokens.js";
18
+ import { defaultEmbedder, cosineSimilarity } from "./embedder.js";
17
19
  /** Wrap a recall block so the model reads it as restored compacted context. */
18
20
  export function formatRecallBlock(hits) {
19
21
  if (hits.length === 0)
@@ -38,18 +40,42 @@ export function formatRecallBlock(hits) {
38
40
  export function recallAndInline(opts, store) {
39
41
  const limit = opts.limit ?? 3;
40
42
  const skip = opts.skipInjected ?? true;
43
+ const maxTokens = opts.recallMaxTokens ?? 0; // 0 = unbounded (legacy behavior)
44
+ const doWindowDedupe = opts.windowDedupe ?? false;
45
+ const dedupSim = opts.dedupSim ?? 0.9;
41
46
  const { hits } = searchRecall({ sessionId: opts.sessionId, query: opts.query, limit, skipInjected: false }, store);
42
- // Shared dedup: drop checkpoints already injected this session, then mark the
43
- // survivors so repeated triggers are free. (Cosine near-dup collapse already
44
- // happened inside store.search.)
47
+ // Precompute live-window embeddings once for inline dedupe (Fix C). Trigram
48
+ // embedder is local + cheap; never a network call (PREVENT-PI-004).
49
+ let liveEmbeddings = [];
50
+ if (doWindowDedupe && opts.liveWindow && opts.liveWindow.length > 0) {
51
+ const embedder = defaultEmbedder();
52
+ liveEmbeddings = opts.liveWindow.map((m) => embedder.embed(m));
53
+ }
54
+ // Shared dedup + bounded/inline block assembly. We build the block
55
+ // incrementally so the token cap can stop mid-stream (Fix C).
45
56
  const toInject = [];
57
+ const parts = [];
58
+ let blockTokens = 0;
46
59
  for (const h of hits) {
47
60
  if (skip && store.wasInjected(opts.sessionId, h.checkpoint.checkpointId))
48
61
  continue;
62
+ // Inline dedupe: skip a hit already resident in the live window (Fix C).
63
+ if (doWindowDedupe && liveEmbeddings.length > 0) {
64
+ const hitVec = defaultEmbedder().embed(h.checkpoint.summary);
65
+ if (liveEmbeddings.some((v) => cosineSimilarity(v, hitVec) >= dedupSim))
66
+ continue;
67
+ }
68
+ const part = formatRecallBlock([h]);
69
+ const partTokens = estimateBlockTokens(part);
70
+ // Token cap: never push a chunk that would overrun the ceiling.
71
+ if (maxTokens > 0 && blockTokens + partTokens > maxTokens)
72
+ break;
73
+ parts.push(part);
49
74
  toInject.push(h);
75
+ blockTokens += partTokens;
50
76
  store.markInjected(opts.sessionId, h.checkpoint.checkpointId);
51
77
  }
52
- const block = formatRecallBlock(toInject);
78
+ const block = parts.join("\n");
53
79
  const report = toInject.map((h) => ` • ${h.checkpoint.checkpointId} (${h.checkpoint.summary.slice(0, 60).replace(/\n/g, " ")}…)`);
54
80
  return {
55
81
  toInject,
@@ -45,6 +45,34 @@ test("recallAndInline empty when store has nothing for query", () => {
45
45
  assert.equal(r.empty, true);
46
46
  assert.equal(r.block, "");
47
47
  });
48
+ test("Fix C: recallMaxTokens caps the injected block", () => {
49
+ const s = store();
50
+ // Three distinct checkpoints so we can observe the cap bite mid-stream.
51
+ compactSession({ sessionId: SESS, messages: [msg("user", "alpha module wiring and bootstrap sequence"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
52
+ compactSession({ sessionId: SESS, messages: [msg("user", "beta module config and env resolution"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
53
+ compactSession({ sessionId: SESS, messages: [msg("user", "gamma module shutdown and cleanup hooks"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 3 }, s);
54
+ // A ceiling of 100 tokens fits the first checkpoint (~82) but stops before the
55
+ // second (~163 cumulative) — proving the cap bites mid-stream.
56
+ const r = recallAndInline({ sessionId: SESS, query: "module wiring config shutdown", limit: 5, source: "command", recallMaxTokens: 100, skipInjected: false }, s);
57
+ assert.ok(r.toInject.length >= 1, "at least one injected under the cap");
58
+ assert.ok(r.toInject.length < 3, "cap prevented all three from injecting");
59
+ assert.ok(r.block.length > 0, "block non-empty");
60
+ });
61
+ test("Fix C: inline dedupe drops a hit already resident in the live window", () => {
62
+ const s = store();
63
+ const resident = "alpha module wiring and bootstrap sequence";
64
+ compactSession({ sessionId: SESS, messages: [msg("user", resident), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
65
+ compactSession({ sessionId: SESS, messages: [msg("user", "omega module telemetry and tracing spans"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
66
+ // Baseline: with dedupe OFF, both checkpoints are candidates.
67
+ const rNoDedup = recallAndInline({ sessionId: SESS, query: "module wiring telemetry", limit: 5, source: "command", skipInjected: false }, s);
68
+ // The live window contains the exact summary of the first checkpoint — as it
69
+ // would be if a prior recall already injected it. Inline dedupe must drop it
70
+ // (strictly fewer injected than the no-dedupe baseline).
71
+ const residentSummary = rNoDedup.toInject[0].checkpoint.summary;
72
+ const rDedup = recallAndInline({ sessionId: SESS, query: "module wiring telemetry", limit: 5, source: "command", skipInjected: false, windowDedupe: true, liveWindow: [residentSummary], dedupSim: 0.9 }, s);
73
+ assert.ok(rDedup.toInject.length <= rNoDedup.toInject.length, "dedupe never adds hits");
74
+ assert.ok(rDedup.toInject.length < rNoDedup.toInject.length, "inline dedupe dropped a resident hit");
75
+ });
48
76
  test("cleanup", () => {
49
77
  rmSync(baseTmp, { recursive: true, force: true });
50
78
  });
@@ -19,7 +19,7 @@ import { openStore } from "./sqlite.js";
19
19
  import { computeContentDigest } from "../dedup/digest.js";
20
20
  import { minhashSignature, SIGNATURE_VERSION, NUM_HASHES } from "../dedup/l1-minhash.js";
21
21
  import { lshBands } from "../dedup/l1-lsh.js";
22
- import { upsertMinhashSignature, insertLshBuckets, listCheckpoints, saveRaptorTree } from "./sqlite.js";
22
+ import { upsertMinhashSignature, insertLshBuckets, listCheckpoints, saveRaptorTree, withTx } from "./sqlite.js";
23
23
  import { buildRaptorTree } from "../dedup/raptor/tree.js";
24
24
  import { defaultEmbedder } from "../embedder.js";
25
25
  import { getStateDir } from "../store.js";
@@ -64,7 +64,7 @@ export function backfillContentHashes(stateDir = getStateDir()) {
64
64
  let processed = 0;
65
65
  let lastSid = start.lastSid;
66
66
  let lastId = start.lastId;
67
- const tx = db.transaction((rows) => {
67
+ function applyRows(rows) {
68
68
  const lookup = db.prepare("SELECT id FROM context_chunks WHERE session_id = ? AND content_hash = ? AND content_hash2 = ? AND id != ? LIMIT 1");
69
69
  const update = db.prepare(`UPDATE context_chunks
70
70
  SET content_hash=?, content_hash2=?, content_hash_version=?, normalized_text=?,
@@ -87,9 +87,9 @@ export function backfillContentHashes(stateDir = getStateDir()) {
87
87
  lastId = row.id;
88
88
  processed++;
89
89
  }
90
- });
90
+ }
91
91
  if (pending.length > 0) {
92
- tx(pending);
92
+ withTx(db, () => applyRows(pending));
93
93
  db.prepare("INSERT INTO backfill_progress(name, last_session_id, last_id, updated, duplicates_resolved) VALUES('content_hashes',?,?,?,?) ON CONFLICT(name) DO UPDATE SET last_session_id=excluded.last_session_id, last_id=excluded.last_id, updated=excluded.updated, duplicates_resolved=excluded.duplicates_resolved").run(lastSid, lastId, updated, duplicatesResolved);
94
94
  }
95
95
  if (THROTTLE_MS > 0) {
@@ -135,7 +135,7 @@ export function backfillPhase(phase, sessionId, stateDir, opts = {}) {
135
135
  let cursor = lastId ?? undefined;
136
136
  for (let i = Math.max(0, startIndex); i < all.length; i += batchSize) {
137
137
  const batch = all.slice(i, i + batchSize);
138
- const tx = db.transaction(() => {
138
+ withTx(db, () => {
139
139
  for (const cp of batch) {
140
140
  const sig = minhashSignature(cp.normalizedText ?? cp.summary ?? "");
141
141
  if (sig.length === NUM_HASHES) {
@@ -148,7 +148,6 @@ export function backfillPhase(phase, sessionId, stateDir, opts = {}) {
148
148
  processed++;
149
149
  }
150
150
  });
151
- tx();
152
151
  savePhaseCursor(db, phase, cursor ?? null, processed);
153
152
  batches++;
154
153
  if (THROTTLE_MS > 0) {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 1. `compressSmart` / `decompressSmart` — SYNCHRONOUS, zlib-based. Used by the
7
7
  * VectorStore write path (which must stay synchronous — see Sprint 8 plan:
8
- * better-sqlite3 replaced PGlite precisely to avoid an async cascade).
8
+ * node:sqlite replaced PGlite precisely to avoid an async cascade).
9
9
  *
10
10
  * 2. `compressZstd` / `decompressZstd` — ASYNCHRONOUS, via @mongodb-js/zstd.
11
11
  * Optional, used for DR-export / large-blob paths where an await is fine.
@@ -25,7 +25,13 @@
25
25
  * decompressSmart detects the magic first, so all three eras roundtrip together.
26
26
  */
27
27
  import { gzipSync, gunzipSync, brotliCompressSync, brotliDecompressSync, constants as zlibConstants, } from "node:zlib";
28
- import zstd from "@mongodb-js/zstd";
28
+ // zstd is loaded lazily (see compressZstdWithLevel / decompressZstd). It is an
29
+ // OPTIONAL async DR-export dependency: its native addon (`zstd.node`) is not in
30
+ // the npm tarball and may be absent on a clean/allowScripts-blocked install, so
31
+ // a static import here would crash the whole extension at load time. Lazy
32
+ // import keeps the extension loadable even when the binary is missing; the DR
33
+ // path throws a clear error only if it is actually used. (Fix A.)
34
+ // import zstd from "@mongodb-js/zstd";
29
35
  // --- Versioned format markers ----------------------------------------------
30
36
  const MAGIC_HI = 0xec;
31
37
  const MAGIC_LO = 0x01; // format version 1
@@ -45,6 +51,12 @@ const SIZE_MEDIUM = 32768;
45
51
  function header(ver, tag) {
46
52
  return Buffer.from([MAGIC_HI, MAGIC_LO, ver, tag]);
47
53
  }
54
+ /** Clamp a value to the [0, 1] range (pressure bands). */
55
+ function clamp01(n) {
56
+ if (Number.isNaN(n))
57
+ return 0;
58
+ return n < 0 ? 0 : n > 1 ? 1 : n;
59
+ }
48
60
  /**
49
61
  * Compress synchronously using the best zlib tier for the payload size.
50
62
  *
@@ -54,21 +66,36 @@ function header(ver, tag) {
54
66
  * 4KB–32KB → gzip level 6 (tag 0x02)
55
67
  * > 32 KB → brotli 4 (tag 0x05)
56
68
  *
57
- * Writes the versioned header so readers disambiguate from legacy blobs.
69
+ * `pressure` (0–1, optional) escalates the brotli quality for the large tier
70
+ * when the session is near its context limit — the "variable compression as we
71
+ * approach the limit" design (Fix E). Low/undefined pressure keeps brotli-4;
72
+ * high pressure pushes toward brotli-11. Stays fully synchronous (brotli-11 is
73
+ * sync via brotliCompressSync) so the sync `add()` contract is preserved; zstd
74
+ * is reserved for the async DR-export path only. Same versioned header/tags for
75
+ * every pressure, so decompressSmart is unaffected.
58
76
  */
59
- export function compressSmart(data) {
77
+ export function compressSmart(data, pressure = 0) {
78
+ const p = clamp01(pressure);
60
79
  const len = data.length;
61
80
  if (len < SIZE_TINY) {
62
81
  return Buffer.concat([header(1, TAG_RAW), data]);
63
82
  }
64
83
  if (len < SIZE_SMALL) {
65
- return Buffer.concat([header(1, TAG_GZIP_1), gzipSync(data, { level: 1 })]);
84
+ // Small tier: escalate gzip level 1 → 9 with context pressure (Fix E) so
85
+ // the "variable compression as we approach the limit" dial bites for
86
+ // short sessions too, not just the >32KB brotli tier.
87
+ const level = Math.max(1, Math.min(9, Math.round(1 + 8 * p)));
88
+ return Buffer.concat([header(1, TAG_GZIP_1), gzipSync(data, { level })]);
66
89
  }
67
90
  if (len < SIZE_MEDIUM) {
68
- return Buffer.concat([header(1, TAG_GZIP_6), gzipSync(data, { level: 6 })]);
91
+ // Medium tier: escalate gzip level 6 → 9 with context pressure (Fix E).
92
+ const level = Math.max(6, Math.min(9, Math.round(6 + 3 * p)));
93
+ return Buffer.concat([header(1, TAG_GZIP_6), gzipSync(data, { level })]);
69
94
  }
95
+ // Large tier: escalate brotli quality 4 → 11 with context pressure (Fix E).
96
+ const quality = Math.max(4, Math.min(11, Math.round(4 + 7 * p)));
70
97
  const compressed = brotliCompressSync(data, {
71
- params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 },
98
+ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: quality },
72
99
  });
73
100
  return Buffer.concat([header(1, TAG_BROTLI_4), compressed]);
74
101
  }
@@ -141,6 +168,17 @@ export function decompressSmart(buf) {
141
168
  const ZSTD_MAGIC_HI = 0x5a; // 'Z'
142
169
  const ZSTD_MAGIC_LO = 0x53; // 'S'
143
170
  async function compressZstdWithLevel(data, level) {
171
+ // Lazy import: the native addon may be absent (clean/allowScripts install).
172
+ // Throws a clear, actionable error instead of a load-time crash.
173
+ let zstd;
174
+ try {
175
+ zstd = await import("@mongodb-js/zstd");
176
+ }
177
+ catch {
178
+ throw new Error("zstd is not available — the @mongodb-js/zstd native addon (zstd.node) " +
179
+ "was not built. Run the extension's native install step (or allow npm " +
180
+ "install scripts) to enable DR-export compression.");
181
+ }
144
182
  const compressed = await zstd.compress(data, level);
145
183
  return Buffer.concat([Buffer.from([ZSTD_MAGIC_HI, ZSTD_MAGIC_LO]), compressed]);
146
184
  }
@@ -163,6 +201,8 @@ export async function decompressZstd(buf) {
163
201
  if (!isZstd(buf)) {
164
202
  throw new Error("decompressZstd: buffer is not a zstd blob (missing ZS marker)");
165
203
  }
204
+ // Lazy import (see compressZstdWithLevel for rationale).
205
+ const zstd = await import("@mongodb-js/zstd");
166
206
  return zstd.decompress(buf.subarray(2));
167
207
  }
168
208
  /**
@@ -65,3 +65,51 @@ test("zstd helper roundtrips (async) and is not sync-decoded", async () => {
65
65
  assert.equal(auto.isZstd, true, "flagged as zstd");
66
66
  assert.deepEqual(await decompressZstd(c), data, "zstd roundtrip");
67
67
  });
68
+ test("module loads without a top-level zstd import (Fix A: no load crash)", async () => {
69
+ // The extension must load even when the @mongodb-js/zstd native addon is
70
+ // absent (clean/allowScripts-blocked install). The dynamic import() lives
71
+ // inside the helpers, so importing this module must never throw.
72
+ const mod = await import("./compression.js");
73
+ assert.equal(typeof mod.compressSmart, "function", "compressSmart exported");
74
+ assert.equal(typeof mod.compressZstd, "function", "compressZstd exported");
75
+ // The real invariant: no STATIC `import ... from "@mongodb-js/zstd"` at the
76
+ // top level (that's what crashed the whole extension). zstd must be loaded
77
+ // lazily inside the helpers only. Check the source text.
78
+ const { readFileSync } = await import("node:fs");
79
+ const { join } = await import("node:path");
80
+ // Tests run with cwd at repo root (`node --test`), so resolve the source.
81
+ const src = readFileSync(join(process.cwd(), "src/store/compression.ts"), "utf-8");
82
+ const staticImport = /^import\s+.+\s+from\s+["']@mongodb-js\/zstd["'];?$/m;
83
+ assert.equal(staticImport.test(src), false, "no static top-level import of @mongodb-js/zstd (would crash load if binary absent)");
84
+ assert.ok(src.includes('await import("@mongodb-js/zstd")'), "zstd is loaded lazily via dynamic import() inside the helpers");
85
+ });
86
+ test("compressSmart escalates brotli quality with pressure (Fix E)", () => {
87
+ // Large (>32KB) payloads hit the brotli tier; higher pressure → brotli-11
88
+ // → smaller output than the default brotli-4, and still decodes.
89
+ const words = Array.from({ length: 6000 }, (_, i) => "word" + ((i * 2654435761) % 9973));
90
+ const big = Buffer.from(words.join(" "));
91
+ const low = compressSmart(big, 0);
92
+ const high = compressSmart(big, 1);
93
+ assert.equal(isVersioned(low), true, "versioned header preserved at p=0");
94
+ assert.equal(isVersioned(high), true, "versioned header preserved at p=1");
95
+ assert.ok(high.length < low.length, "high pressure compresses smaller");
96
+ assert.deepEqual(decompressSmart(low), big, "p=0 roundtrip");
97
+ assert.deepEqual(decompressSmart(high), big, "p=1 roundtrip");
98
+ // Small payloads ignore pressure (gzip tier) but still roundtrip.
99
+ const small = buf("hello world ", 300);
100
+ assert.deepEqual(decompressSmart(compressSmart(small, 1)), small, "small ignores pressure");
101
+ // pressure out of range is clamped (no throw, still versioned + decodable).
102
+ assert.deepEqual(decompressSmart(compressSmart(big, 5)), big, "over-pressure clamped");
103
+ assert.deepEqual(decompressSmart(compressSmart(big, -1)), big, "under-pressure clamped");
104
+ });
105
+ test("pressureFromPct + preserveRecentForPressure scale with context (Fix E)", async () => {
106
+ const { pressureFromPct, preserveRecentForPressure } = await import("../config.js");
107
+ assert.equal(pressureFromPct(50), 0.5, "pct→pressure");
108
+ assert.equal(pressureFromPct(null), 0, "null pct → 0");
109
+ assert.equal(pressureFromPct(150), 1, "pct clamped");
110
+ // low pressure keeps preserveRecent; high pressure compacts deeper (min floor).
111
+ assert.equal(preserveRecentForPressure(0, 4, 2), 4, "p=0 → preserveRecent");
112
+ assert.equal(preserveRecentForPressure(1, 4, 2), 2, "p=1 → preserveRecentMin");
113
+ assert.equal(preserveRecentForPressure(0.5, 4, 2), 3, "p=0.5 → interpolates");
114
+ assert.ok(preserveRecentForPressure(1, 4, 2) >= 2, "never below floor");
115
+ });
@@ -2,11 +2,13 @@
2
2
  * sqlite.ts — Sprint 8 storage backbone (the "one store").
3
3
  *
4
4
  * Replaces the per-session gzipped-JSON checkpoint files with a single local
5
- * SQLite database (better-sqlite3, in-process, FS-backed, ZERO network calls —
6
- * honors PREVENT-PI-004). Chosen over PGlite because PGlite is async-only in
7
- * every published version, and VectorStore (engine.ts / recall.ts / the
8
- * extension) is fully synchronous adopting PGlite would have cascaded async
9
- * through the whole call chain. SQLite keeps every VectorStore signature sync.
5
+ * SQLite database (node:sqlite — the Node built-in, in-process, FS-backed,
6
+ * ZERO network calls — honors PREVENT-PI-004). No native build and no install
7
+ * scripts, so it survives pi's npm blocked-install-scripts gate (better-sqlite3's
8
+ * native binary could not be built under pi, which crashed every `pi update
9
+ * --extensions`). node:sqlite is synchronous, so every VectorStore signature
10
+ * stays sync. PGlite + pgvector (async) is layered on in vectorIndex.ts for
11
+ * real HNSW indexing (Slice 2 of the dual-backend plan).
10
12
  *
11
13
  * FTS5 `trigram` tokenizer is created for the Sprint 9+ dedup tiers (MinHash/LSH
12
14
  * / pg_trgm-equivalent verification). The default cosine path stays a linear
@@ -14,7 +16,7 @@
14
16
  *
15
17
  * All queries are parameterized (PREVENT-002) — never string-concatenated.
16
18
  */
17
- import Database from "better-sqlite3";
19
+ import { DatabaseSync } from "node:sqlite";
18
20
  import { existsSync, mkdirSync } from "node:fs";
19
21
  import { homedir, tmpdir } from "node:os";
20
22
  import { join } from "node:path";
@@ -28,14 +30,17 @@ function encodeEmbedding(v) {
28
30
  buf.writeFloatLE(v[i] ?? 0, i * 4);
29
31
  return buf;
30
32
  }
31
- /** Decode a Float32 BLOB back to a number[]. */
33
+ /** Decode a Float32 BLOB back to a number[]. node:sqlite returns BLOBs as
34
+ * Uint8Array, so decode via DataView (Buffer is a Uint8Array subclass — both
35
+ * work). */
32
36
  function decodeEmbedding(buf) {
33
37
  if (!buf || buf.length === 0)
34
38
  return [];
39
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
35
40
  const n = buf.length / 4;
36
41
  const out = new Array(n);
37
42
  for (let i = 0; i < n; i++)
38
- out[i] = buf.readFloatLE(i * 4);
43
+ out[i] = dv.getFloat32(i * 4, true);
39
44
  return out;
40
45
  }
41
46
  function jsonText(v) {
@@ -52,9 +57,9 @@ export function openStore(stateDir = getStateDir()) {
52
57
  return existing;
53
58
  if (!existsSync(stateDir))
54
59
  mkdirSync(stateDir, { recursive: true });
55
- const db = new Database(join(stateDir, "sqlite.db"));
56
- db.pragma("journal_mode = WAL");
57
- db.pragma("foreign_keys = ON");
60
+ const db = new DatabaseSync(join(stateDir, "sqlite.db"));
61
+ db.exec("PRAGMA journal_mode = WAL");
62
+ db.exec("PRAGMA foreign_keys = ON");
58
63
  initSchema(db);
59
64
  cache.set(stateDir, db);
60
65
  return db;
@@ -91,10 +96,10 @@ export function openIndexStore(indexDir = getIndexDir()) {
91
96
  return indexCache;
92
97
  if (!existsSync(indexDir))
93
98
  mkdirSync(indexDir, { recursive: true });
94
- const db = new Database(join(indexDir, "index.sqlite"));
95
- db.pragma("journal_mode = WAL");
96
- db.pragma("busy_timeout = 3000"); // tolerate brief cross-process write contention
97
- db.exec(`
99
+ const iddb = new DatabaseSync(join(indexDir, "index.sqlite"));
100
+ iddb.exec("PRAGMA journal_mode = WAL");
101
+ iddb.exec("PRAGMA busy_timeout = 3000"); // tolerate brief cross-process write contention
102
+ iddb.exec(`
98
103
  CREATE TABLE IF NOT EXISTS repo_registry (
99
104
  repo_root TEXT PRIMARY KEY,
100
105
  display_name TEXT,
@@ -114,9 +119,9 @@ export function openIndexStore(indexDir = getIndexDir()) {
114
119
  );
115
120
  CREATE INDEX IF NOT EXISTS idx_registry_last_seen ON repo_registry(last_seen DESC);
116
121
  `);
117
- indexCache = db;
122
+ indexCache = iddb;
118
123
  indexCacheDir = indexDir;
119
- return db;
124
+ return iddb;
120
125
  }
121
126
  /**
122
127
  * Upsert a repo's aggregate stats into the global index. Called on repo-switch
@@ -535,6 +540,24 @@ function mapMemoryRow(row) {
535
540
  lastRecalledAt: row.last_recalled_at ?? null,
536
541
  };
537
542
  }
543
+ /**
544
+ * Run `fn` atomically. Uses SAVEPOINT so it nests safely under an outer
545
+ * transaction (unlike `BEGIN`, which SQLite rejects when one is already open).
546
+ * Mirrors better-sqlite3's `db.transaction(fn)` semantics — callers that wrap a
547
+ * batch in withTx (e.g. backfill) can still call helpers that also use withTx.
548
+ */
549
+ export function withTx(db, fn) {
550
+ db.exec("SAVEPOINT mc_tx");
551
+ try {
552
+ fn();
553
+ db.exec("RELEASE mc_tx");
554
+ }
555
+ catch (e) {
556
+ db.exec("ROLLBACK TO mc_tx");
557
+ db.exec("RELEASE mc_tx");
558
+ throw e;
559
+ }
560
+ }
538
561
  /** Map a DB row to the public StoredCheckpoint shape. */
539
562
  function rowToCheckpoint(row) {
540
563
  return {
@@ -553,7 +576,9 @@ function rowToCheckpoint(row) {
553
576
  contentHash2: row.content_hash2 ?? undefined,
554
577
  contentHashVersion: row.content_hash_version ?? undefined,
555
578
  normalizedText: row.normalized_text ?? undefined,
556
- compressedOriginal: row.compressed_original ?? undefined,
579
+ // node:sqlite returns BLOBs as Uint8Array; normalize to Buffer so callers
580
+ // (e.g. decompressSmart → Buffer.toString) behave as under better-sqlite3.
581
+ compressedOriginal: row.compressed_original ? Buffer.from(row.compressed_original) : undefined,
557
582
  embedding: decodeEmbedding(row.embedding_blob),
558
583
  timestamp: Number(row.timestamp ?? 0),
559
584
  dedupStatus: row.dedup_status ?? undefined,
@@ -563,7 +588,7 @@ function rowToCheckpoint(row) {
563
588
  export function upsertCheckpoint(cp, stateDir = getStateDir()) {
564
589
  const db = openStore(stateDir);
565
590
  const sid = normalizeSessionId(cp.sessionId);
566
- const tx = db.transaction(() => {
591
+ withTx(db, () => {
567
592
  db.prepare(`INSERT INTO context_chunks
568
593
  (id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
569
594
  normalized_text, summary, topic_summary, summary_hash,
@@ -586,25 +611,25 @@ export function upsertCheckpoint(cp, stateDir = getStateDir()) {
586
611
  timestamp=excluded.timestamp,
587
612
  dedup_status=excluded.dedup_status,
588
613
  compressed_original=excluded.compressed_original`).run({
589
- id: cp.checkpointId,
590
- sid,
591
- region_hash: cp.regionHash ?? null,
592
- content_hash: cp.contentHash ?? null,
593
- content_hash2: cp.contentHash2 ?? null,
594
- content_hash_version: cp.contentHashVersion ?? null,
595
- normalized_text: cp.normalizedText ?? null,
596
- summary: cp.summary ?? "",
597
- topic_summary: cp.topicSummary ?? null,
598
- summary_hash: cp.summaryHash ?? null,
599
- key_decisions: jsonText(cp.keyDecisions),
600
- next_steps: jsonText(cp.nextSteps),
601
- files_modified: jsonText(cp.filesModified),
602
- embedding_blob: encodeEmbedding(cp.embedding ?? []),
603
- token_estimate: cp.tokenEstimate ?? 0,
604
- original_token_estimate: cp.originalTokenEstimate ?? null,
605
- timestamp: cp.timestamp ?? 0,
606
- dedup_status: "active",
607
- compressed_original: cp.compressedOriginal ?? null,
614
+ "@id": cp.checkpointId,
615
+ "@sid": sid,
616
+ "@region_hash": cp.regionHash ?? null,
617
+ "@content_hash": cp.contentHash ?? null,
618
+ "@content_hash2": cp.contentHash2 ?? null,
619
+ "@content_hash_version": cp.contentHashVersion ?? null,
620
+ "@normalized_text": cp.normalizedText ?? null,
621
+ "@summary": cp.summary ?? "",
622
+ "@topic_summary": cp.topicSummary ?? null,
623
+ "@summary_hash": cp.summaryHash ?? null,
624
+ "@key_decisions": jsonText(cp.keyDecisions),
625
+ "@next_steps": jsonText(cp.nextSteps),
626
+ "@files_modified": jsonText(cp.filesModified),
627
+ "@embedding_blob": encodeEmbedding(cp.embedding ?? []),
628
+ "@token_estimate": cp.tokenEstimate ?? 0,
629
+ "@original_token_estimate": cp.originalTokenEstimate ?? null,
630
+ "@timestamp": cp.timestamp ?? 0,
631
+ "@dedup_status": "active",
632
+ "@compressed_original": cp.compressedOriginal ?? null,
608
633
  });
609
634
  // FTS5 virtual tables don't support UPSERT — delete any prior row, reinsert.
610
635
  // Store normalized_text (the L1 verify key); fall back to summary for rows
@@ -612,7 +637,6 @@ export function upsertCheckpoint(cp, stateDir = getStateDir()) {
612
637
  db.prepare("DELETE FROM context_chunks_trgm WHERE id = ?").run(cp.checkpointId);
613
638
  db.prepare("INSERT INTO context_chunks_trgm(id, normalized_text) VALUES(?, ?)").run(cp.checkpointId, cp.normalizedText ?? cp.summary ?? "");
614
639
  });
615
- tx();
616
640
  }
617
641
  // --- Sprint 11: MinHash signatures + LSH buckets --------------------------
618
642
  /** Persist a checkpoint's MinHash signature (idempotent by chunk_id + version). */
@@ -630,12 +654,11 @@ export function insertLshBuckets(chunkId, sessionId, signatureVersion, bucketKey
630
654
  const sid = normalizeSessionId(sessionId);
631
655
  const del = db.prepare("DELETE FROM dedup_lsh_buckets WHERE chunk_id = ?");
632
656
  const ins = db.prepare("INSERT OR IGNORE INTO dedup_lsh_buckets(bucket_key, chunk_id, session_id, signature_version) VALUES(?, ?, ?, ?)");
633
- const tx = db.transaction(() => {
657
+ withTx(db, () => {
634
658
  del.run(chunkId);
635
659
  for (const key of bucketKeys)
636
660
  ins.run(key, chunkId, sid, signatureVersion);
637
661
  });
638
- tx();
639
662
  }
640
663
  /**
641
664
  * Candidate chunk_ids sharing any LSH bucket with `bucketKeys`, scoped to the
@@ -109,6 +109,25 @@ test("compression tier: GZIP-1 and GZIP-6 tiers produce valid, smaller output",
109
109
  assert.ok(big.length < Buffer.from("compress me ".repeat(1800)).length, "GZIP-6 output smaller than input");
110
110
  assert.deepEqual(decompressSmart(big), Buffer.from("compress me ".repeat(1800)));
111
111
  });
112
+ test("Fix E: pressure escalates gzip tier strength (sync, no zstd)", () => {
113
+ const small = Buffer.from("compress me ".repeat(200)); // 512B–4KB band
114
+ const medium = Buffer.from("compress me ".repeat(1800)); // 4KB–32KB band
115
+ // Low pressure → cheap levels (gzip-1 / gzip-6).
116
+ const lowSmall = compressSmart(small, 0);
117
+ const lowMedium = compressSmart(medium, 0);
118
+ assert.equal(lowSmall[3], 0x01, "small tier tag");
119
+ assert.equal(lowMedium[3], 0x02, "medium tier tag");
120
+ assert.ok(decompressSmart(lowSmall).equals(small), "low-pressure small roundtrips");
121
+ assert.ok(decompressSmart(lowMedium).equals(medium), "low-pressure medium roundtrips");
122
+ // High pressure → stronger levels (gzip-9 / gzip-9); tag unchanged, output
123
+ // must still decode to the exact original (versioned header preserved).
124
+ const highSmall = compressSmart(small, 1);
125
+ const highMedium = compressSmart(medium, 1);
126
+ assert.equal(highSmall[3], 0x01, "tag unchanged under pressure");
127
+ assert.equal(highMedium[3], 0x02, "tag unchanged under pressure");
128
+ assert.ok(decompressSmart(highSmall).equals(small), "high-pressure small roundtrips");
129
+ assert.ok(decompressSmart(highMedium).equals(medium), "high-pressure medium roundtrips");
130
+ });
112
131
  // ---------------------------------------------------------------------------
113
132
  // normalizeSessionId
114
133
  // ---------------------------------------------------------------------------
@@ -20,6 +20,8 @@ import { mmrRerank } from "./dedup/mmr.js";
20
20
  import { topK } from "./dedup/topk.js";
21
21
  import { openBloom, saveBloom } from "./store/bloom.js";
22
22
  import { listCheckpoints, nextCheckpointId, upsertCheckpoint, loadSessionState, saveSessionState, upsertMinhashSignature, insertLshBuckets, lshCandidateChunks, setDedupStatus, addTokensSaved, getDedupStats, bumpDedupStats, repoStats as repoStatsFromStore, dataInvariantStats, } from "./store/sqlite.js";
23
+ import { rehydrateRaptorTree } from "./dedup/raptor/index.js";
24
+ import { stagedExpansion } from "./dedup/raptor/retrieval.js";
23
25
  import { migrateJsonToSqlite } from "./store/migrate.js";
24
26
  /** Default L2 semantic-dedup enable flag (trigram embedder is local, zero-network). */
25
27
  export const L2_ENABLED = true;
@@ -242,7 +244,7 @@ export class VectorStore {
242
244
  contentHash2: digest.contentHash2,
243
245
  contentHashVersion: digest.contentHashVersion,
244
246
  normalizedText: digest.normalizedText,
245
- compressedOriginal: compressSmart(Buffer.from(input.regionText, "utf-8")),
247
+ compressedOriginal: compressSmart(Buffer.from(input.regionText, "utf-8"), input.compressionPressure),
246
248
  embedding,
247
249
  timestamp: input.timestamp,
248
250
  };
@@ -339,6 +341,27 @@ export class VectorStore {
339
341
  // (Sprint 14 flag), returning the plain relevance-ranked window instead.
340
342
  if (!this.cfg.L2_ENABLED)
341
343
  return window.slice(0, k);
344
+ // Fix D: when RAPTOR is promoted, ALSO recall high-level tree summaries and
345
+ // merge them with the flat hits via MMR so RAPTOR + flat don't double-cover.
346
+ // RAPTOR returns fewer, broader hits (O(log n) high-level nodes) than the
347
+ // O(n) flat leaves, tightening the block at read time.
348
+ if (this.cfg.RAPTOR_ENABLED) {
349
+ const raptorHits = this.raptorSearchHits(sid, query, k);
350
+ if (raptorHits.length > 0) {
351
+ const merged = [...window];
352
+ for (const rh of raptorHits) {
353
+ if (!merged.some((m) => m.checkpoint.checkpointId === rh.checkpoint.checkpointId)) {
354
+ merged.push(rh);
355
+ }
356
+ }
357
+ const mmrItems = merged.map((h) => ({
358
+ item: h,
359
+ vector: h.checkpoint.embedding,
360
+ relevance: h.score,
361
+ }));
362
+ return mmrRerank(mmrItems, k, this.cfg.MMR_LAMBDA);
363
+ }
364
+ }
342
365
  const mmrItems = window.map((h) => ({
343
366
  item: h,
344
367
  vector: h.checkpoint.embedding,
@@ -347,6 +370,38 @@ export class VectorStore {
347
370
  const ranked = mmrRerank(mmrItems, k, this.cfg.MMR_LAMBDA);
348
371
  return ranked;
349
372
  }
373
+ /**
374
+ * Serve the RAPTOR tree for a query (Fix D): rehydrate the persisted tree and
375
+ * return its staged-expansion leaf hits as SearchHits. Returns [] when no tree
376
+ * exists (small sessions — flat search remains the path). Best-effort/non-fatal.
377
+ */
378
+ raptorSearchHits(sid, query, k) {
379
+ try {
380
+ const tree = rehydrateRaptorTree(sid, this.stateDir);
381
+ if (!tree || !tree.rootId)
382
+ return [];
383
+ const leafIds = stagedExpansion(query, tree, {
384
+ embedder: this.embedder,
385
+ k,
386
+ topM: this.cfg.RAPTOR_CLUSTERS_PER_LEVEL,
387
+ mmrLambda: this.cfg.MMR_LAMBDA,
388
+ });
389
+ if (leafIds.length === 0)
390
+ return [];
391
+ const all = listCheckpoints(sid, this.stateDir).filter((cp) => cp.dedupStatus !== "removed");
392
+ const qv = this.embedder.embed(query);
393
+ const hits = [];
394
+ for (const id of leafIds) {
395
+ const cp = all.find((c) => c.checkpointId === id);
396
+ if (cp)
397
+ hits.push({ checkpoint: cp, score: cosineSimilarity(qv, cp.embedding) });
398
+ }
399
+ return hits;
400
+ }
401
+ catch {
402
+ return [];
403
+ }
404
+ }
350
405
  /**
351
406
  * SemDeDup offline cleanup (Sprint 12, QA #17): within a session, mark the
352
407
  * lower-quality row of any pair scoring cosine > `threshold` as
@@ -2,8 +2,8 @@
2
2
 
3
3
  A lightweight local web dashboard for monitoring mega-compact's live state — compactions, context usage, checkpoints, and recall hits.
4
4
 
5
- Uses Node built-in modules (`http`, `fs`, `path`) plus `better-sqlite3` (the
6
- project's one-store DB backend) to read the machine-wide multi-repo index.
5
+ Uses Node built-in modules (`http`, `fs`, `path`, `node:sqlite` the project's
6
+ one-store DB backend) to read the machine-wide multi-repo index.
7
7
 
8
8
  ## Quick Start
9
9
 
@@ -74,7 +74,7 @@ The server runs as a detached child process, independent of the pi session. It:
74
74
  - Auto-discovers the state directory from the `port.pid` file
75
75
  - Cleans up stale `port.pid` files from dead processes
76
76
  - Supports `SIGTERM`/`SIGINT` for graceful shutdown
77
- - Serves static HTML; reads the multi-repo index from SQLite (`better-sqlite3`)
77
+ - Serves static HTML; reads the multi-repo index from SQLite (`node:sqlite`)
78
78
 
79
79
  ## Browser UI
80
80