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.
- package/dist/extensions/dashboard-server.js +3 -3
- package/dist/extensions/mega-compact-driver.js +79 -0
- package/dist/extensions/mega-compact.test.js +54 -18
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-events.js +45 -23
- package/dist/extensions/mega-pipeline.js +77 -3
- package/dist/src/config/dedup.js +4 -1
- package/dist/src/config.js +21 -0
- package/dist/src/dedup/raptor/index.js +28 -6
- package/dist/src/dedup/raptor/promote.test.js +69 -0
- package/dist/src/engine.js +1 -0
- package/dist/src/recall.js +30 -4
- package/dist/src/recall.test.js +28 -0
- package/dist/src/store/backfill.js +5 -6
- package/dist/src/store/compression.js +47 -7
- package/dist/src/store/compression.test.js +48 -0
- package/dist/src/store/sqlite.js +64 -41
- package/dist/src/store.test.js +19 -0
- package/dist/src/vectorStore.js +56 -1
- package/extensions/DASHBOARD.md +3 -3
- package/extensions/dashboard-server.ts +4 -4
- package/extensions/mega-compact-driver.ts +105 -0
- package/extensions/mega-compact.test.ts +65 -18
- package/extensions/mega-config.ts +25 -0
- package/extensions/mega-events.ts +43 -24
- package/extensions/mega-pipeline.ts +83 -4
- package/package.json +6 -7
- package/src/config/dedup.ts +4 -1
- package/src/config.ts +26 -0
- package/src/dedup/raptor/index.ts +42 -7
- package/src/dedup/raptor/promote.test.ts +82 -0
- package/src/engine.ts +5 -0
- package/src/recall.test.ts +44 -0
- package/src/recall.ts +43 -4
- package/src/store/backfill.ts +10 -11
- package/src/store/compression.test.ts +58 -0
- package/src/store/compression.ts +48 -7
- package/src/store/sqlite.ts +72 -49
- package/src/store.test.ts +22 -0
- package/src/vectorStore.ts +63 -1
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
package/dist/src/recall.js
CHANGED
|
@@ -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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
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 =
|
|
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,
|
package/dist/src/recall.test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
+
});
|
package/dist/src/store/sqlite.js
CHANGED
|
@@ -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 (
|
|
6
|
-
* honors PREVENT-PI-004).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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] =
|
|
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
|
|
56
|
-
db.
|
|
57
|
-
db.
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
122
|
+
indexCache = iddb;
|
|
118
123
|
indexCacheDir = indexDir;
|
|
119
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/src/store.test.js
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
package/dist/src/vectorStore.js
CHANGED
|
@@ -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
|
package/extensions/DASHBOARD.md
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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 (`
|
|
77
|
+
- Serves static HTML; reads the multi-repo index from SQLite (`node:sqlite`)
|
|
78
78
|
|
|
79
79
|
## Browser UI
|
|
80
80
|
|