pi-mega-compact 0.4.20 → 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/conflict-scan.js +201 -0
- package/dist/extensions/dashboard-server.js +3 -3
- package/dist/extensions/mega-compact-driver.js +79 -0
- package/dist/extensions/mega-compact.js +2 -0
- package/dist/extensions/mega-compact.test.js +54 -18
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-conflict-cmds.js +121 -0
- package/dist/extensions/mega-events.js +45 -23
- package/dist/extensions/mega-pipeline.js +80 -8
- package/dist/extensions/mega-runtime.js +14 -20
- 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 +123 -41
- package/dist/src/store.test.js +19 -0
- package/dist/src/vectorStore.js +56 -1
- package/extensions/DASHBOARD.md +3 -3
- package/extensions/conflict-scan.ts +209 -0
- 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-compact.ts +2 -0
- package/extensions/mega-config.ts +25 -0
- package/extensions/mega-conflict-cmds.ts +129 -0
- package/extensions/mega-events.ts +43 -24
- package/extensions/mega-pipeline.ts +86 -9
- package/extensions/mega-runtime.ts +14 -18
- 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 +156 -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/src/store/sqlite.ts
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
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
* All queries are parameterized (PREVENT-002) — never string-concatenated.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import
|
|
20
|
+
import { DatabaseSync } from "node:sqlite";
|
|
19
21
|
import { existsSync, mkdirSync } from "node:fs";
|
|
20
22
|
import { homedir, tmpdir } from "node:os";
|
|
21
23
|
import { join } from "node:path";
|
|
@@ -31,12 +33,15 @@ function encodeEmbedding(v: number[]): Buffer {
|
|
|
31
33
|
for (let i = 0; i < v.length; i++) buf.writeFloatLE(v[i] ?? 0, i * 4);
|
|
32
34
|
return buf;
|
|
33
35
|
}
|
|
34
|
-
/** Decode a Float32 BLOB back to a number[].
|
|
35
|
-
|
|
36
|
+
/** Decode a Float32 BLOB back to a number[]. node:sqlite returns BLOBs as
|
|
37
|
+
* Uint8Array, so decode via DataView (Buffer is a Uint8Array subclass — both
|
|
38
|
+
* work). */
|
|
39
|
+
function decodeEmbedding(buf: Uint8Array | null | undefined): number[] {
|
|
36
40
|
if (!buf || buf.length === 0) return [];
|
|
41
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
37
42
|
const n = buf.length / 4;
|
|
38
43
|
const out = new Array<number>(n);
|
|
39
|
-
for (let i = 0; i < n; i++) out[i] =
|
|
44
|
+
for (let i = 0; i < n; i++) out[i] = dv.getFloat32(i * 4, true);
|
|
40
45
|
return out;
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -47,17 +52,17 @@ function jsonText(v: unknown): string {
|
|
|
47
52
|
// In-process cache so the same stateDir reuses one connection (and so a fresh
|
|
48
53
|
// VectorStore over the same dir shares the open DB). Cross-process durability
|
|
49
54
|
// comes from reopening the same file path — proven by the integration test.
|
|
50
|
-
const cache = new Map<string,
|
|
55
|
+
const cache = new Map<string, DatabaseSync>();
|
|
51
56
|
|
|
52
57
|
/** Open (or reuse) the SQLite store for a state dir. */
|
|
53
|
-
export function openStore(stateDir: string = getStateDir()):
|
|
58
|
+
export function openStore(stateDir: string = getStateDir()): DatabaseSync {
|
|
54
59
|
const existing = cache.get(stateDir);
|
|
55
60
|
if (existing) return existing;
|
|
56
61
|
|
|
57
62
|
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
|
|
58
|
-
const db = new
|
|
59
|
-
db.
|
|
60
|
-
db.
|
|
63
|
+
const db = new DatabaseSync(join(stateDir, "sqlite.db"));
|
|
64
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
65
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
61
66
|
initSchema(db);
|
|
62
67
|
cache.set(stateDir, db);
|
|
63
68
|
return db;
|
|
@@ -87,17 +92,17 @@ export function getIndexDir(): string {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
let indexCache:
|
|
95
|
+
let indexCache: DatabaseSync | undefined;
|
|
91
96
|
let indexCacheDir: string | undefined;
|
|
92
97
|
|
|
93
98
|
/** Open (or reuse) the machine-wide index DB. WAL for concurrent writers. */
|
|
94
|
-
export function openIndexStore(indexDir: string = getIndexDir()):
|
|
99
|
+
export function openIndexStore(indexDir: string = getIndexDir()): DatabaseSync {
|
|
95
100
|
if (indexCache && indexCacheDir === indexDir) return indexCache;
|
|
96
101
|
if (!existsSync(indexDir)) mkdirSync(indexDir, { recursive: true });
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
const iddb = new DatabaseSync(join(indexDir, "index.sqlite"));
|
|
103
|
+
iddb.exec("PRAGMA journal_mode = WAL");
|
|
104
|
+
iddb.exec("PRAGMA busy_timeout = 3000"); // tolerate brief cross-process write contention
|
|
105
|
+
iddb.exec(`
|
|
101
106
|
CREATE TABLE IF NOT EXISTS repo_registry (
|
|
102
107
|
repo_root TEXT PRIMARY KEY,
|
|
103
108
|
display_name TEXT,
|
|
@@ -117,9 +122,9 @@ export function openIndexStore(indexDir: string = getIndexDir()): Database.Datab
|
|
|
117
122
|
);
|
|
118
123
|
CREATE INDEX IF NOT EXISTS idx_registry_last_seen ON repo_registry(last_seen DESC);
|
|
119
124
|
`);
|
|
120
|
-
indexCache =
|
|
125
|
+
indexCache = iddb;
|
|
121
126
|
indexCacheDir = indexDir;
|
|
122
|
-
return
|
|
127
|
+
return iddb;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
/** One row of the global repo registry (multi-repo dashboard source). */
|
|
@@ -276,7 +281,7 @@ export function closeIndexStore(): void {
|
|
|
276
281
|
}
|
|
277
282
|
}
|
|
278
283
|
|
|
279
|
-
function initSchema(db:
|
|
284
|
+
function initSchema(db: DatabaseSync): void {
|
|
280
285
|
db.exec(`
|
|
281
286
|
CREATE TABLE IF NOT EXISTS context_chunks (
|
|
282
287
|
id TEXT NOT NULL,
|
|
@@ -415,6 +420,20 @@ function initSchema(db: Database.Database): void {
|
|
|
415
420
|
ts INTEGER
|
|
416
421
|
);
|
|
417
422
|
|
|
423
|
+
-- Durable "save to memory" store (taken over from memory extensions).
|
|
424
|
+
-- One row per saved memory; scoped by repo so memory travels with the
|
|
425
|
+
-- clone. All params are parameterized (PREVENT-002).
|
|
426
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
427
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
428
|
+
repo TEXT,
|
|
429
|
+
kind TEXT DEFAULT 'note', -- note | fact | decision | preference
|
|
430
|
+
content TEXT NOT NULL,
|
|
431
|
+
tags TEXT, -- JSON array of strings
|
|
432
|
+
created_at INTEGER,
|
|
433
|
+
last_recalled_at INTEGER
|
|
434
|
+
);
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo);
|
|
436
|
+
|
|
418
437
|
-- FTS5 trigram virtual table (Sprint 9+ pg_trgm-equivalent verification).
|
|
419
438
|
CREATE VIRTUAL TABLE IF NOT EXISTS context_chunks_trgm USING fts5(
|
|
420
439
|
id UNINDEXED,
|
|
@@ -443,7 +462,7 @@ function initSchema(db: Database.Database): void {
|
|
|
443
462
|
* input), so the unavoidable identifier interpolation here does not violate
|
|
444
463
|
* PREVENT-002 (no external data reaches this SQL).
|
|
445
464
|
*/
|
|
446
|
-
function ensureColumn(db:
|
|
465
|
+
function ensureColumn(db: DatabaseSync, table: string, column: string, decl: string): void {
|
|
447
466
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
448
467
|
if (cols.some((c) => c.name === column)) return;
|
|
449
468
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
@@ -582,6 +601,94 @@ export function addLesson(
|
|
|
582
601
|
).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
|
|
583
602
|
}
|
|
584
603
|
|
|
604
|
+
// --- Durable memory (save-to-memory takeover) ---------------------------------
|
|
605
|
+
// One SQLite store for user-saved memories, scoped by repo. Mirrors the
|
|
606
|
+
// lessons/sessions pattern: all state lives in SQLite from day one.
|
|
607
|
+
|
|
608
|
+
export interface MemoryRecord {
|
|
609
|
+
id: number;
|
|
610
|
+
repo: string | null;
|
|
611
|
+
kind: string;
|
|
612
|
+
content: string;
|
|
613
|
+
tags: string[];
|
|
614
|
+
createdAt: number;
|
|
615
|
+
lastRecalledAt: number | null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Save a memory to the current repo's store. Returns the new row id. */
|
|
619
|
+
export function addMemory(
|
|
620
|
+
memory: { kind?: string; content: string; tags?: string[] },
|
|
621
|
+
repo: string | null,
|
|
622
|
+
stateDir: string = getStateDir(),
|
|
623
|
+
): number {
|
|
624
|
+
const db = openStore(stateDir);
|
|
625
|
+
const now = Math.floor(Date.now() / 1000);
|
|
626
|
+
const res = db
|
|
627
|
+
.prepare(
|
|
628
|
+
`INSERT INTO memories(repo, kind, content, tags, created_at, last_recalled_at)
|
|
629
|
+
VALUES(?, ?, ?, ?, ?, NULL)`,
|
|
630
|
+
)
|
|
631
|
+
.run(repo ?? null, memory.kind ?? "note", memory.content, JSON.stringify(memory.tags ?? []), now);
|
|
632
|
+
return Number(res.lastInsertRowid);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** List recent memories for a repo (or all repos when repo is null). */
|
|
636
|
+
export function listMemories(repo: string | null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
|
|
637
|
+
const db = openStore(stateDir);
|
|
638
|
+
const rows = repo
|
|
639
|
+
? db.prepare("SELECT * FROM memories WHERE repo = ? ORDER BY created_at DESC LIMIT ?").all(repo, limit)
|
|
640
|
+
: db.prepare("SELECT * FROM memories ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
641
|
+
return (rows as any[]).map(mapMemoryRow);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Substring search across content + tags. */
|
|
645
|
+
export function searchMemories(query: string, repo: string | null = null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
|
|
646
|
+
const db = openStore(stateDir);
|
|
647
|
+
const like = `%${query}%`;
|
|
648
|
+
const rows = repo
|
|
649
|
+
? db.prepare("SELECT * FROM memories WHERE repo = ? AND (content LIKE ? OR tags LIKE ?) ORDER BY created_at DESC LIMIT ?").all(repo, like, like, limit)
|
|
650
|
+
: db.prepare("SELECT * FROM memories WHERE content LIKE ? OR tags LIKE ? ORDER BY created_at DESC LIMIT ?").all(like, like, limit);
|
|
651
|
+
return (rows as any[]).map(mapMemoryRow);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Mark a memory as recalled (updates last_recalled_at). Returns true if found. */
|
|
655
|
+
export function recallMemory(id: number, stateDir: string = getStateDir()): boolean {
|
|
656
|
+
const db = openStore(stateDir);
|
|
657
|
+
const now = Math.floor(Date.now() / 1000);
|
|
658
|
+
const res = db.prepare("UPDATE memories SET last_recalled_at = ? WHERE id = ?").run(now, id);
|
|
659
|
+
return res.changes > 0;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function mapMemoryRow(row: any): MemoryRecord {
|
|
663
|
+
return {
|
|
664
|
+
id: row.id,
|
|
665
|
+
repo: row.repo ?? null,
|
|
666
|
+
kind: row.kind ?? "note",
|
|
667
|
+
content: row.content ?? "",
|
|
668
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
669
|
+
createdAt: row.created_at ?? 0,
|
|
670
|
+
lastRecalledAt: row.last_recalled_at ?? null,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Run `fn` atomically. Uses SAVEPOINT so it nests safely under an outer
|
|
676
|
+
* transaction (unlike `BEGIN`, which SQLite rejects when one is already open).
|
|
677
|
+
* Mirrors better-sqlite3's `db.transaction(fn)` semantics — callers that wrap a
|
|
678
|
+
* batch in withTx (e.g. backfill) can still call helpers that also use withTx.
|
|
679
|
+
*/
|
|
680
|
+
export function withTx(db: DatabaseSync, fn: () => void): void {
|
|
681
|
+
db.exec("SAVEPOINT mc_tx");
|
|
682
|
+
try {
|
|
683
|
+
fn();
|
|
684
|
+
db.exec("RELEASE mc_tx");
|
|
685
|
+
} catch (e) {
|
|
686
|
+
db.exec("ROLLBACK TO mc_tx");
|
|
687
|
+
db.exec("RELEASE mc_tx");
|
|
688
|
+
throw e;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
585
692
|
/** Map a DB row to the public StoredCheckpoint shape. */
|
|
586
693
|
function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
587
694
|
return {
|
|
@@ -600,7 +707,9 @@ function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
|
600
707
|
contentHash2: row.content_hash2 ?? undefined,
|
|
601
708
|
contentHashVersion: row.content_hash_version ?? undefined,
|
|
602
709
|
normalizedText: row.normalized_text ?? undefined,
|
|
603
|
-
|
|
710
|
+
// node:sqlite returns BLOBs as Uint8Array; normalize to Buffer so callers
|
|
711
|
+
// (e.g. decompressSmart → Buffer.toString) behave as under better-sqlite3.
|
|
712
|
+
compressedOriginal: row.compressed_original ? Buffer.from(row.compressed_original) : undefined,
|
|
604
713
|
embedding: decodeEmbedding(row.embedding_blob),
|
|
605
714
|
timestamp: Number(row.timestamp ?? 0),
|
|
606
715
|
dedupStatus: row.dedup_status ?? undefined,
|
|
@@ -611,7 +720,7 @@ function rowToCheckpoint(row: any): StoredCheckpoint {
|
|
|
611
720
|
export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getStateDir()): void {
|
|
612
721
|
const db = openStore(stateDir);
|
|
613
722
|
const sid = normalizeSessionId(cp.sessionId);
|
|
614
|
-
|
|
723
|
+
withTx(db, () => {
|
|
615
724
|
db.prepare(
|
|
616
725
|
`INSERT INTO context_chunks
|
|
617
726
|
(id, session_id, region_hash, content_hash, content_hash2, content_hash_version,
|
|
@@ -636,25 +745,25 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
636
745
|
dedup_status=excluded.dedup_status,
|
|
637
746
|
compressed_original=excluded.compressed_original`,
|
|
638
747
|
).run({
|
|
639
|
-
id: cp.checkpointId,
|
|
640
|
-
sid,
|
|
641
|
-
region_hash: cp.regionHash ?? null,
|
|
642
|
-
content_hash: cp.contentHash ?? null,
|
|
643
|
-
content_hash2: cp.contentHash2 ?? null,
|
|
644
|
-
content_hash_version: cp.contentHashVersion ?? null,
|
|
645
|
-
normalized_text: cp.normalizedText ?? null,
|
|
646
|
-
summary: cp.summary ?? "",
|
|
647
|
-
topic_summary: cp.topicSummary ?? null,
|
|
648
|
-
summary_hash: cp.summaryHash ?? null,
|
|
649
|
-
key_decisions: jsonText(cp.keyDecisions),
|
|
650
|
-
next_steps: jsonText(cp.nextSteps),
|
|
651
|
-
files_modified: jsonText(cp.filesModified),
|
|
652
|
-
embedding_blob: encodeEmbedding(cp.embedding ?? []),
|
|
653
|
-
token_estimate: cp.tokenEstimate ?? 0,
|
|
654
|
-
original_token_estimate: cp.originalTokenEstimate ?? null,
|
|
655
|
-
timestamp: cp.timestamp ?? 0,
|
|
656
|
-
dedup_status: "active",
|
|
657
|
-
compressed_original: cp.compressedOriginal ?? null,
|
|
748
|
+
"@id": cp.checkpointId,
|
|
749
|
+
"@sid": sid,
|
|
750
|
+
"@region_hash": cp.regionHash ?? null,
|
|
751
|
+
"@content_hash": cp.contentHash ?? null,
|
|
752
|
+
"@content_hash2": cp.contentHash2 ?? null,
|
|
753
|
+
"@content_hash_version": cp.contentHashVersion ?? null,
|
|
754
|
+
"@normalized_text": cp.normalizedText ?? null,
|
|
755
|
+
"@summary": cp.summary ?? "",
|
|
756
|
+
"@topic_summary": cp.topicSummary ?? null,
|
|
757
|
+
"@summary_hash": cp.summaryHash ?? null,
|
|
758
|
+
"@key_decisions": jsonText(cp.keyDecisions),
|
|
759
|
+
"@next_steps": jsonText(cp.nextSteps),
|
|
760
|
+
"@files_modified": jsonText(cp.filesModified),
|
|
761
|
+
"@embedding_blob": encodeEmbedding(cp.embedding ?? []),
|
|
762
|
+
"@token_estimate": cp.tokenEstimate ?? 0,
|
|
763
|
+
"@original_token_estimate": cp.originalTokenEstimate ?? null,
|
|
764
|
+
"@timestamp": cp.timestamp ?? 0,
|
|
765
|
+
"@dedup_status": "active",
|
|
766
|
+
"@compressed_original": cp.compressedOriginal ?? null,
|
|
658
767
|
});
|
|
659
768
|
|
|
660
769
|
// FTS5 virtual tables don't support UPSERT — delete any prior row, reinsert.
|
|
@@ -665,7 +774,6 @@ export function upsertCheckpoint(cp: StoredCheckpoint, stateDir: string = getSta
|
|
|
665
774
|
"INSERT INTO context_chunks_trgm(id, normalized_text) VALUES(?, ?)",
|
|
666
775
|
).run(cp.checkpointId, cp.normalizedText ?? cp.summary ?? "");
|
|
667
776
|
});
|
|
668
|
-
tx();
|
|
669
777
|
}
|
|
670
778
|
|
|
671
779
|
// --- Sprint 11: MinHash signatures + LSH buckets --------------------------
|
|
@@ -702,11 +810,10 @@ export function insertLshBuckets(
|
|
|
702
810
|
const ins = db.prepare(
|
|
703
811
|
"INSERT OR IGNORE INTO dedup_lsh_buckets(bucket_key, chunk_id, session_id, signature_version) VALUES(?, ?, ?, ?)",
|
|
704
812
|
);
|
|
705
|
-
|
|
813
|
+
withTx(db, () => {
|
|
706
814
|
del.run(chunkId);
|
|
707
815
|
for (const key of bucketKeys) ins.run(key, chunkId, sid, signatureVersion);
|
|
708
816
|
});
|
|
709
|
-
tx();
|
|
710
817
|
}
|
|
711
818
|
|
|
712
819
|
/**
|
|
@@ -780,7 +887,7 @@ export function setDedupStatus(
|
|
|
780
887
|
|
|
781
888
|
// --- Session state (injection tracking) ------------------------------------
|
|
782
889
|
|
|
783
|
-
function loadSessionStateRow(sid: string, db:
|
|
890
|
+
function loadSessionStateRow(sid: string, db: DatabaseSync): SessionState {
|
|
784
891
|
const row = db.prepare("SELECT * FROM session_state WHERE session_id = ?").get(sid) as any;
|
|
785
892
|
if (!row) {
|
|
786
893
|
return { injectedCheckpointIds: [], storedRegionHashes: [] };
|
package/src/store.test.ts
CHANGED
|
@@ -130,6 +130,28 @@ test("compression tier: GZIP-1 and GZIP-6 tiers produce valid, smaller output",
|
|
|
130
130
|
assert.deepEqual(decompressSmart(big), Buffer.from("compress me ".repeat(1800)));
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test("Fix E: pressure escalates gzip tier strength (sync, no zstd)", () => {
|
|
134
|
+
const small = Buffer.from("compress me ".repeat(200)); // 512B–4KB band
|
|
135
|
+
const medium = Buffer.from("compress me ".repeat(1800)); // 4KB–32KB band
|
|
136
|
+
|
|
137
|
+
// Low pressure → cheap levels (gzip-1 / gzip-6).
|
|
138
|
+
const lowSmall = compressSmart(small, 0);
|
|
139
|
+
const lowMedium = compressSmart(medium, 0);
|
|
140
|
+
assert.equal(lowSmall[3], 0x01, "small tier tag");
|
|
141
|
+
assert.equal(lowMedium[3], 0x02, "medium tier tag");
|
|
142
|
+
assert.ok(decompressSmart(lowSmall).equals(small), "low-pressure small roundtrips");
|
|
143
|
+
assert.ok(decompressSmart(lowMedium).equals(medium), "low-pressure medium roundtrips");
|
|
144
|
+
|
|
145
|
+
// High pressure → stronger levels (gzip-9 / gzip-9); tag unchanged, output
|
|
146
|
+
// must still decode to the exact original (versioned header preserved).
|
|
147
|
+
const highSmall = compressSmart(small, 1);
|
|
148
|
+
const highMedium = compressSmart(medium, 1);
|
|
149
|
+
assert.equal(highSmall[3], 0x01, "tag unchanged under pressure");
|
|
150
|
+
assert.equal(highMedium[3], 0x02, "tag unchanged under pressure");
|
|
151
|
+
assert.ok(decompressSmart(highSmall).equals(small), "high-pressure small roundtrips");
|
|
152
|
+
assert.ok(decompressSmart(highMedium).equals(medium), "high-pressure medium roundtrips");
|
|
153
|
+
});
|
|
154
|
+
|
|
133
155
|
// ---------------------------------------------------------------------------
|
|
134
156
|
// normalizeSessionId
|
|
135
157
|
// ---------------------------------------------------------------------------
|
package/src/vectorStore.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
repoStats as repoStatsFromStore,
|
|
39
39
|
dataInvariantStats,
|
|
40
40
|
} from "./store/sqlite.js";
|
|
41
|
+
import { rehydrateRaptorTree } from "./dedup/raptor/index.js";
|
|
42
|
+
import { stagedExpansion } from "./dedup/raptor/retrieval.js";
|
|
41
43
|
import { migrateJsonToSqlite } from "./store/migrate.js";
|
|
42
44
|
|
|
43
45
|
export interface SearchHit {
|
|
@@ -66,6 +68,9 @@ export interface AddInput {
|
|
|
66
68
|
* Lets the UI render live per-tier progress during compaction. Never awaited;
|
|
67
69
|
* must be cheap. Optional for back-compat. */
|
|
68
70
|
onTier?: (ev: { tier: "L0" | "L1" | "L2" | "new"; status: "scanning" | "deduped" | "passed" | "stored"; detail?: string }) => void;
|
|
71
|
+
/** Context-window pressure (0–1) — escalates the stored checkpoint's sync
|
|
72
|
+
* compression strength (Fix E). Optional; defaults to 0 (brotli-4). */
|
|
73
|
+
compressionPressure?: number;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/** Default L2 semantic-dedup enable flag (trigram embedder is local, zero-network). */
|
|
@@ -318,7 +323,10 @@ export class VectorStore {
|
|
|
318
323
|
contentHash2: digest.contentHash2,
|
|
319
324
|
contentHashVersion: digest.contentHashVersion,
|
|
320
325
|
normalizedText: digest.normalizedText,
|
|
321
|
-
compressedOriginal: compressSmart(
|
|
326
|
+
compressedOriginal: compressSmart(
|
|
327
|
+
Buffer.from(input.regionText, "utf-8"),
|
|
328
|
+
input.compressionPressure,
|
|
329
|
+
),
|
|
322
330
|
embedding,
|
|
323
331
|
timestamp: input.timestamp,
|
|
324
332
|
};
|
|
@@ -432,6 +440,29 @@ export class VectorStore {
|
|
|
432
440
|
// MMR (QA #10) is part of the L2 semantic tier: skip it when L2 is disabled
|
|
433
441
|
// (Sprint 14 flag), returning the plain relevance-ranked window instead.
|
|
434
442
|
if (!this.cfg.L2_ENABLED) return window.slice(0, k);
|
|
443
|
+
|
|
444
|
+
// Fix D: when RAPTOR is promoted, ALSO recall high-level tree summaries and
|
|
445
|
+
// merge them with the flat hits via MMR so RAPTOR + flat don't double-cover.
|
|
446
|
+
// RAPTOR returns fewer, broader hits (O(log n) high-level nodes) than the
|
|
447
|
+
// O(n) flat leaves, tightening the block at read time.
|
|
448
|
+
if (this.cfg.RAPTOR_ENABLED) {
|
|
449
|
+
const raptorHits = this.raptorSearchHits(sid, query, k);
|
|
450
|
+
if (raptorHits.length > 0) {
|
|
451
|
+
const merged: SearchHit[] = [...window];
|
|
452
|
+
for (const rh of raptorHits) {
|
|
453
|
+
if (!merged.some((m) => m.checkpoint.checkpointId === rh.checkpoint.checkpointId)) {
|
|
454
|
+
merged.push(rh);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const mmrItems: MmrItem<SearchHit>[] = merged.map((h) => ({
|
|
458
|
+
item: h,
|
|
459
|
+
vector: h.checkpoint.embedding,
|
|
460
|
+
relevance: h.score,
|
|
461
|
+
}));
|
|
462
|
+
return mmrRerank(mmrItems, k, this.cfg.MMR_LAMBDA);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
435
466
|
const mmrItems: MmrItem<SearchHit>[] = window.map((h) => ({
|
|
436
467
|
item: h,
|
|
437
468
|
vector: h.checkpoint.embedding,
|
|
@@ -441,6 +472,37 @@ export class VectorStore {
|
|
|
441
472
|
return ranked;
|
|
442
473
|
}
|
|
443
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Serve the RAPTOR tree for a query (Fix D): rehydrate the persisted tree and
|
|
477
|
+
* return its staged-expansion leaf hits as SearchHits. Returns [] when no tree
|
|
478
|
+
* exists (small sessions — flat search remains the path). Best-effort/non-fatal.
|
|
479
|
+
*/
|
|
480
|
+
private raptorSearchHits(sid: string, query: string, k: number): SearchHit[] {
|
|
481
|
+
try {
|
|
482
|
+
const tree = rehydrateRaptorTree(sid, this.stateDir);
|
|
483
|
+
if (!tree || !tree.rootId) return [];
|
|
484
|
+
const leafIds = stagedExpansion(query, tree, {
|
|
485
|
+
embedder: this.embedder,
|
|
486
|
+
k,
|
|
487
|
+
topM: this.cfg.RAPTOR_CLUSTERS_PER_LEVEL,
|
|
488
|
+
mmrLambda: this.cfg.MMR_LAMBDA,
|
|
489
|
+
});
|
|
490
|
+
if (leafIds.length === 0) return [];
|
|
491
|
+
const all = listCheckpoints(sid, this.stateDir).filter(
|
|
492
|
+
(cp) => cp.dedupStatus !== "removed",
|
|
493
|
+
);
|
|
494
|
+
const qv = this.embedder.embed(query);
|
|
495
|
+
const hits: SearchHit[] = [];
|
|
496
|
+
for (const id of leafIds) {
|
|
497
|
+
const cp = all.find((c) => c.checkpointId === id);
|
|
498
|
+
if (cp) hits.push({ checkpoint: cp, score: cosineSimilarity(qv, cp.embedding) });
|
|
499
|
+
}
|
|
500
|
+
return hits;
|
|
501
|
+
} catch {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
444
506
|
/**
|
|
445
507
|
* SemDeDup offline cleanup (Sprint 12, QA #17): within a session, mark the
|
|
446
508
|
* lower-quality row of any pair scoring cosine > `threshold` as
|