pi-mega-compact 0.8.23 → 0.8.25
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/README.md +26 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
- package/dist/extensions/mega-compact-s38.test.js +263 -14
- package/dist/extensions/mega-compact.js +15 -0
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +211 -26
- package/dist/extensions/mega-events/context-handler.js +45 -7
- package/dist/extensions/mega-events/error-classifier.js +125 -18
- package/dist/extensions/mega-pipeline/compact.js +24 -13
- package/dist/extensions/mega-pipeline/recall.js +31 -2
- package/dist/extensions/mega-runtime/append-event.js +24 -0
- package/dist/extensions/mega-runtime/bind-repo.js +65 -0
- package/dist/extensions/mega-runtime/capture-model.js +87 -0
- package/dist/extensions/mega-runtime/dashboard-snapshot.js +122 -0
- package/dist/extensions/mega-runtime/effects.js +86 -0
- package/dist/extensions/mega-runtime/engine-view.js +11 -0
- package/dist/extensions/mega-runtime/game-state.js +116 -0
- package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
- package/dist/extensions/mega-runtime/perf.js +49 -0
- package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
- package/dist/extensions/mega-runtime/render-widget.js +17 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
- package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +208 -0
- package/dist/extensions/mega-runtime/runtime.js +405 -0
- package/dist/extensions/mega-runtime/snapshot.js +142 -0
- package/dist/extensions/mega-runtime/state.js +5 -1151
- package/dist/extensions/mega-runtime/status.js +11 -0
- package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
- package/dist/extensions/mega-runtime/widget-types.js +8 -0
- package/dist/extensions/mega-runtime/widget.js +15 -204
- package/dist/extensions/openclaw-mega-compact.js +291 -0
- package/dist/src/boundary.js +79 -43
- package/dist/src/boundary.test.js +119 -2
- package/dist/src/canary.js +10 -0
- package/dist/src/config/dedup.js +14 -0
- package/dist/src/config.js +3 -1
- package/dist/src/dedup/raptor/buildHistory.js +164 -0
- package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
- package/dist/src/dedup/raptor/index.js +38 -0
- package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
- package/dist/src/dedup/raptor/multilevel.js +17 -5
- package/dist/src/dedup/raptor/multilevel.test.js +36 -1
- package/dist/src/dedup/raptor/raptor.test.js +43 -0
- package/dist/src/dedup/raptor/retrieval.js +14 -2
- package/dist/src/dedup/raptor/retrieval.test.js +95 -0
- package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
- package/dist/src/dedup/raptor/summarizer.js +1 -0
- package/dist/src/dedup/raptor/tree.js +16 -2
- package/dist/src/engine.js +18 -2
- package/dist/src/httpEmbedder.js +96 -6
- package/dist/src/httpEmbedder.test.js +277 -0
- package/dist/src/mechanical-fix.test.js +65 -0
- package/dist/src/minilm.js +92 -0
- package/dist/src/raptor-inject-summaries.test.js +155 -0
- package/dist/src/recall.js +135 -21
- package/dist/src/recall.test.js +179 -4
- package/dist/src/store/sqlite/dedup-mirror.js +32 -15
- package/dist/src/store/sqlite/maintenance.js +2 -2
- package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
- package/dist/src/store/sqlite/memories.js +5 -5
- package/dist/src/store/sqlite/meta.js +1 -1
- package/dist/src/store/sqlite/raptor.js +56 -17
- package/dist/src/store/sqlite/raptor.test.js +106 -0
- package/dist/src/store/sqlite/schema.js +90 -1
- package/dist/src/store/sqlite/session-state.js +9 -3
- package/dist/src/store/sqlite/stats.js +9 -5
- package/dist/src/store/sqlite/turns.js +181 -0
- package/dist/src/store/sqlite/turns.test.js +183 -0
- package/dist/src/store/sqlite/utils.js +15 -4
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store.js +2 -2
- package/dist/src/vector-search-cache.test.js +157 -0
- package/dist/src/vector-search.js +107 -15
- package/dist/src/vectorStore.js +36 -8
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
- package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
- package/extensions/mega-compact-s38.test.ts +259 -14
- package/extensions/mega-compact.ts +15 -0
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-dashboard.ts +10 -1
- package/extensions/mega-events/agent-handlers.ts +211 -26
- package/extensions/mega-events/context-handler.ts +43 -7
- package/extensions/mega-events/error-classifier.ts +125 -17
- package/extensions/mega-pipeline/compact.ts +28 -16
- package/extensions/mega-pipeline/recall.ts +34 -2
- package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
- package/extensions/mega-runtime/README.md +38 -0
- package/extensions/mega-runtime/append-event.ts +40 -0
- package/extensions/mega-runtime/bind-repo.ts +81 -0
- package/extensions/mega-runtime/capture-model.ts +101 -0
- package/extensions/mega-runtime/dashboard-snapshot.ts +181 -0
- package/extensions/mega-runtime/effects.ts +129 -0
- package/extensions/mega-runtime/engine-view.ts +17 -0
- package/extensions/mega-runtime/game-state.ts +149 -0
- package/extensions/mega-runtime/get-state-dir.ts +19 -0
- package/extensions/mega-runtime/helpers.ts +25 -1
- package/extensions/mega-runtime/perf.ts +60 -0
- package/extensions/mega-runtime/pressure-getters.ts +96 -0
- package/extensions/mega-runtime/render-widget.ts +41 -0
- package/extensions/mega-runtime/runtime-helpers.ts +119 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +293 -0
- package/extensions/mega-runtime/runtime.ts +483 -0
- package/extensions/mega-runtime/snapshot.ts +230 -0
- package/extensions/mega-runtime/state.ts +5 -1268
- package/extensions/mega-runtime/status.ts +26 -0
- package/extensions/mega-runtime/widget-ansi.ts +217 -0
- package/extensions/mega-runtime/widget-types.ts +80 -0
- package/extensions/mega-runtime/widget.ts +34 -285
- package/package.json +1 -1
- package/src/boundary.test.ts +128 -2
- package/src/boundary.ts +75 -39
- package/src/canary.ts +10 -0
- package/src/config/dedup.ts +25 -0
- package/src/config.ts +3 -1
- package/src/dedup/raptor/buildHistory.test.ts +353 -0
- package/src/dedup/raptor/buildHistory.ts +259 -0
- package/src/dedup/raptor/index.ts +38 -0
- package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
- package/src/dedup/raptor/multilevel.test.ts +47 -0
- package/src/dedup/raptor/multilevel.ts +18 -8
- package/src/dedup/raptor/raptor.test.ts +59 -0
- package/src/dedup/raptor/retrieval.test.ts +118 -0
- package/src/dedup/raptor/retrieval.ts +14 -2
- package/src/dedup/raptor/serve-gate.test.ts +348 -0
- package/src/dedup/raptor/summarizer.ts +1 -0
- package/src/dedup/raptor/tree.ts +17 -2
- package/src/engine.ts +32 -3
- package/src/httpEmbedder.test.ts +286 -0
- package/src/httpEmbedder.ts +98 -8
- package/src/mechanical-fix.test.ts +70 -0
- package/src/raptor-inject-summaries.test.ts +212 -0
- package/src/recall.test.ts +220 -4
- package/src/recall.ts +151 -22
- package/src/store/sqlite/dedup-mirror.ts +35 -18
- package/src/store/sqlite/maintenance.ts +2 -2
- package/src/store/sqlite/mechanical-fix.test.ts +162 -0
- package/src/store/sqlite/memories.ts +5 -5
- package/src/store/sqlite/meta.ts +1 -1
- package/src/store/sqlite/raptor.test.ts +139 -0
- package/src/store/sqlite/raptor.ts +135 -81
- package/src/store/sqlite/schema.ts +90 -1
- package/src/store/sqlite/session-state.ts +9 -3
- package/src/store/sqlite/stats.ts +10 -8
- package/src/store/sqlite/turns.test.ts +218 -0
- package/src/store/sqlite/turns.ts +292 -0
- package/src/store/sqlite/utils.ts +14 -4
- package/src/store/sqlite.ts +1 -0
- package/src/store.ts +9 -2
- package/src/vector-search-cache.test.ts +190 -0
- package/src/vector-search.ts +273 -156
- package/src/vectorStore.ts +443 -382
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mechanical-fix.test.ts — focused unit tests for the SQLite-side mechanical-fix
|
|
3
|
+
* batch: safeJson (utils.ts), storeStats numeric MAX + dedup_status filter
|
|
4
|
+
* (stats.ts), addTokensSaved CAST AS REAL (meta.ts), evictMemoryLru named
|
|
5
|
+
* @param bindings (memories.ts). Pi-agnostic; uses isolated state dirs (G7).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, before, after } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { safeJson, closeStore } from "./utils.js";
|
|
13
|
+
import { storeStats } from "./stats.js";
|
|
14
|
+
import { upsertCheckpoint, setDedupStatus } from "./checkpoints.js";
|
|
15
|
+
import { addTokensSaved, getTokensSaved } from "./meta.js";
|
|
16
|
+
import { addMemory, listMemories } from "./memories.js";
|
|
17
|
+
import type { StoredCheckpoint } from "../../store.js";
|
|
18
|
+
|
|
19
|
+
function makeCp(id: string, sessionId: string): StoredCheckpoint {
|
|
20
|
+
return {
|
|
21
|
+
checkpointId: id,
|
|
22
|
+
sessionId,
|
|
23
|
+
summary: `summary-${id}`,
|
|
24
|
+
keyDecisions: [],
|
|
25
|
+
nextSteps: [],
|
|
26
|
+
filesModified: [],
|
|
27
|
+
tokenEstimate: 100,
|
|
28
|
+
regionHash: `hash-${id}`,
|
|
29
|
+
embedding: [],
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// safeJson (utils.ts)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe("mechanical-fix: safeJson (utils.ts)", () => {
|
|
39
|
+
it("returns fallback on null, undefined, empty, and corrupt JSON", () => {
|
|
40
|
+
assert.deepEqual(safeJson<string[]>(null, []), []);
|
|
41
|
+
assert.deepEqual(safeJson<string[]>(undefined, []), []);
|
|
42
|
+
assert.deepEqual(safeJson<string[]>("", []), []);
|
|
43
|
+
assert.deepEqual(safeJson<string[]>("not json", ["fb"]), ["fb"]);
|
|
44
|
+
assert.deepEqual(safeJson<string[]>('["a","b"]', []), ["a", "b"]);
|
|
45
|
+
assert.equal(safeJson<number>(null, 42), 42);
|
|
46
|
+
assert.deepEqual(safeJson<Record<string, number>>('{"k":1}', {}), { k: 1 });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// storeStats (stats.ts) — numeric MAX(id) + dedup_status filter
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
describe("mechanical-fix: storeStats (stats.ts)", () => {
|
|
55
|
+
let dir: string;
|
|
56
|
+
before(() => {
|
|
57
|
+
dir = mkdtempSync(join(tmpdir(), "mc-mech-stats-"));
|
|
58
|
+
process.env.MEGACOMPACT_STATE_DIR = dir;
|
|
59
|
+
});
|
|
60
|
+
after(() => {
|
|
61
|
+
closeStore(dir);
|
|
62
|
+
delete process.env.MEGACOMPACT_STATE_DIR;
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns numerically-max checkpoint id (not lexicographic) for 100+ checkpoints", () => {
|
|
67
|
+
const sid = "sess_stats";
|
|
68
|
+
// Insert chkpt_001..chkpt_100 (100 checkpoints) + chkpt_999 + chkpt_1000.
|
|
69
|
+
// Numeric max = 1000 → "chkpt_1000"; lexicographic max would be "chkpt_999"
|
|
70
|
+
// (because '9' > '1' at position 6 when comparing 8 vs 9-char strings).
|
|
71
|
+
for (let i = 1; i <= 100; i++) {
|
|
72
|
+
upsertCheckpoint(makeCp(`chkpt_${String(i).padStart(3, "0")}`, sid), dir);
|
|
73
|
+
}
|
|
74
|
+
upsertCheckpoint(makeCp("chkpt_999", sid), dir);
|
|
75
|
+
upsertCheckpoint(makeCp("chkpt_1000", sid), dir);
|
|
76
|
+
|
|
77
|
+
const s = storeStats(sid, dir);
|
|
78
|
+
assert.equal(s.checkpointCount, 102);
|
|
79
|
+
assert.equal(s.lastCheckpointId, "chkpt_1000");
|
|
80
|
+
assert.equal(s.lastSummary, "summary-chkpt_1000");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("excludes dedup_status='removed' rows from counts", () => {
|
|
84
|
+
const sid = "sess_dedup";
|
|
85
|
+
upsertCheckpoint(makeCp("chkpt_001", sid), dir);
|
|
86
|
+
upsertCheckpoint(makeCp("chkpt_002", sid), dir);
|
|
87
|
+
setDedupStatus("chkpt_001", sid, "removed", dir);
|
|
88
|
+
const s = storeStats(sid, dir);
|
|
89
|
+
assert.equal(s.checkpointCount, 1);
|
|
90
|
+
assert.equal(s.lastCheckpointId, "chkpt_002");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// addTokensSaved (meta.ts) — CAST AS REAL preserves fractional values
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe("mechanical-fix: addTokensSaved (meta.ts)", () => {
|
|
99
|
+
let dir: string;
|
|
100
|
+
before(() => {
|
|
101
|
+
dir = mkdtempSync(join(tmpdir(), "mc-mech-meta-"));
|
|
102
|
+
process.env.MEGACOMPACT_STATE_DIR = dir;
|
|
103
|
+
});
|
|
104
|
+
after(() => {
|
|
105
|
+
closeStore(dir);
|
|
106
|
+
delete process.env.MEGACOMPACT_STATE_DIR;
|
|
107
|
+
rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("preserves fractional values (CAST AS REAL, not INTEGER)", () => {
|
|
111
|
+
addTokensSaved(10.5, dir);
|
|
112
|
+
assert.equal(getTokensSaved(dir), 10.5);
|
|
113
|
+
addTokensSaved(10.5, dir);
|
|
114
|
+
// With INTEGER: CAST("10.5" AS INTEGER)=10, 10+10.5=20.5.
|
|
115
|
+
// With REAL: CAST("10.5" AS REAL)=10.5, 10.5+10.5=21.
|
|
116
|
+
assert.equal(getTokensSaved(dir), 21);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// evictMemoryLru (memories.ts) — named @param bindings
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("mechanical-fix: evictMemoryLru (memories.ts)", () => {
|
|
125
|
+
let dir: string;
|
|
126
|
+
before(() => {
|
|
127
|
+
dir = mkdtempSync(join(tmpdir(), "mc-mech-mem-"));
|
|
128
|
+
process.env.MEGACOMPACT_STATE_DIR = dir;
|
|
129
|
+
process.env.MEGACOMPACT_MEMORY_MAX_ROWS = "3";
|
|
130
|
+
});
|
|
131
|
+
after(() => {
|
|
132
|
+
closeStore(dir);
|
|
133
|
+
delete process.env.MEGACOMPACT_STATE_DIR;
|
|
134
|
+
delete process.env.MEGACOMPACT_MEMORY_MAX_ROWS;
|
|
135
|
+
rmSync(dir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("evicts oldest rows past cap (repo-scoped, @repo named binding)", () => {
|
|
139
|
+
const repo = "test-repo";
|
|
140
|
+
for (let i = 0; i < 5; i++) {
|
|
141
|
+
addMemory({ content: `memory-${i}` }, repo, dir);
|
|
142
|
+
}
|
|
143
|
+
const memories = listMemories(repo, 100, dir);
|
|
144
|
+
assert.equal(memories.length, 3);
|
|
145
|
+
// Oldest two (memory-0, memory-1) evicted; memory-2, 3, 4 survive.
|
|
146
|
+
const contents = memories.map((m) => m.content).sort();
|
|
147
|
+
assert.deepEqual(contents, ["memory-2", "memory-3", "memory-4"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("handles null-repo scope (repo IS NULL branch, @over only)", () => {
|
|
151
|
+
for (let i = 0; i < 4; i++) {
|
|
152
|
+
addMemory({ content: `nullmem-${i}` }, null, dir);
|
|
153
|
+
}
|
|
154
|
+
// listMemories(null, ...) returns ALL rows (no repo filter); narrow to
|
|
155
|
+
// null-repo rows to verify the IS NULL eviction branch in isolation.
|
|
156
|
+
const nullMemories = listMemories(null, 100, dir).filter((m) => m.repo === null);
|
|
157
|
+
assert.equal(nullMemories.length, 3);
|
|
158
|
+
// Oldest (nullmem-0) evicted.
|
|
159
|
+
const contents = nullMemories.map((m) => m.content).sort();
|
|
160
|
+
assert.deepEqual(contents, ["nullmem-1", "nullmem-2", "nullmem-3"]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -53,10 +53,10 @@ function evictMemoryLru(repo: string | null, stateDir: string): void {
|
|
|
53
53
|
const maxRows = memoryMaxRows();
|
|
54
54
|
// SQLite `= NULL` is never true, so the null-repo scope (memories are
|
|
55
55
|
// stateDir-scoped when repo is null — the applyMemoryOps path) needs `IS NULL`.
|
|
56
|
-
const where = repo == null ? "repo IS NULL" : "repo =
|
|
56
|
+
const where = repo == null ? "repo IS NULL" : "repo = @repo";
|
|
57
57
|
const countRow = repo == null
|
|
58
58
|
? db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE ${where}`).get()
|
|
59
|
-
: db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE ${where}`).get(repo);
|
|
59
|
+
: db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE ${where}`).get({ "@repo": repo });
|
|
60
60
|
const count = (countRow as { n: number }).n;
|
|
61
61
|
const over = count - maxRows;
|
|
62
62
|
if (over <= 0) return;
|
|
@@ -67,10 +67,10 @@ function evictMemoryLru(repo: string | null, stateDir: string): void {
|
|
|
67
67
|
`DELETE FROM memories WHERE ${where} AND id IN (
|
|
68
68
|
SELECT id FROM memories WHERE ${where}
|
|
69
69
|
ORDER BY COALESCE(last_referenced, last_recalled_at, created_at) ASC, id ASC
|
|
70
|
-
LIMIT
|
|
70
|
+
LIMIT @over
|
|
71
71
|
)`;
|
|
72
|
-
if (repo == null) db.prepare(sql).run(over);
|
|
73
|
-
else db.prepare(sql).run(
|
|
72
|
+
if (repo == null) db.prepare(sql).run({ "@over": over });
|
|
73
|
+
else db.prepare(sql).run({ "@over": over, "@repo": repo });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export interface MemoryRecord {
|
package/src/store/sqlite/meta.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function addTokensSaved(delta: number, stateDir: string = getStateDir()):
|
|
|
33
33
|
const db = openStore(stateDir);
|
|
34
34
|
db.prepare(
|
|
35
35
|
`INSERT INTO meta(key, value) VALUES('tokens_saved', ?)
|
|
36
|
-
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS
|
|
36
|
+
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS REAL) + ? AS TEXT)`,
|
|
37
37
|
).run(String(delta), delta);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raptor.test.ts — regression tests for RAPTOR node persistence.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* 1. saveRaptorTree atomicity (withTx + openStore nesting safety): rebuilding
|
|
6
|
+
* a DIFFERENT tree for the same session leaves no stale nodes from the
|
|
7
|
+
* first tree.
|
|
8
|
+
* 2. listRaptorNodes robustness: corrupt `children` JSON in a row does not
|
|
9
|
+
* throw and that row's children come back as [].
|
|
10
|
+
*
|
|
11
|
+
* No network. Uses node:sqlite directly via openStore.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { openStore, closeStore } from "./utils.js";
|
|
20
|
+
import { saveRaptorTree, listRaptorNodes, type StoredRaptorNode } from "./raptor.js";
|
|
21
|
+
|
|
22
|
+
let tmpDir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-raptor-persist-"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
closeStore(tmpDir);
|
|
30
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Build a simple tree map with the minimal shape saveRaptorTree expects. */
|
|
34
|
+
function makeTree(nodeSpecs: { id: string; level: number; children: string[] }[]) {
|
|
35
|
+
const nodes = new Map();
|
|
36
|
+
for (const spec of nodeSpecs) {
|
|
37
|
+
nodes.set(spec.id, {
|
|
38
|
+
id: spec.id,
|
|
39
|
+
level: spec.level,
|
|
40
|
+
parentId: spec.level === 0 ? null : "root",
|
|
41
|
+
children: spec.children,
|
|
42
|
+
summary: `summary for ${spec.id}`,
|
|
43
|
+
embedding: [1, 0, 0],
|
|
44
|
+
qualityMarker: "low",
|
|
45
|
+
tokenEstimate: 10,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return { nodes };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Test 1: rebuild + save a different tree → no stale nodes ──────────────────
|
|
52
|
+
|
|
53
|
+
test("saveRaptorTree: rebuilding a different tree for the same session leaves no stale nodes", () => {
|
|
54
|
+
const sid = "sess_replace";
|
|
55
|
+
const builtAt1 = 1000;
|
|
56
|
+
const builtAt2 = 2000;
|
|
57
|
+
|
|
58
|
+
// Tree A: nodes alpha, beta, root.
|
|
59
|
+
const treeA = makeTree([
|
|
60
|
+
{ id: "alpha", level: 1, children: ["leaf_0", "leaf_1"] },
|
|
61
|
+
{ id: "beta", level: 1, children: ["leaf_2", "leaf_3"] },
|
|
62
|
+
{ id: "root", level: 2, children: ["leaf_0", "leaf_1", "leaf_2", "leaf_3"] },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
saveRaptorTree(sid, treeA, builtAt1, tmpDir);
|
|
66
|
+
|
|
67
|
+
const afterA = listRaptorNodes(sid, tmpDir);
|
|
68
|
+
assert.equal(afterA.length, 3, "tree A has 3 nodes");
|
|
69
|
+
assert.deepEqual(
|
|
70
|
+
afterA.map((n) => n.id).sort(),
|
|
71
|
+
["alpha", "beta", "root"],
|
|
72
|
+
"tree A node ids match",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Tree B: completely different nodes gamma, delta, root.
|
|
76
|
+
const treeB = makeTree([
|
|
77
|
+
{ id: "gamma", level: 1, children: ["leaf_4", "leaf_5"] },
|
|
78
|
+
{ id: "delta", level: 1, children: ["leaf_6", "leaf_7"] },
|
|
79
|
+
{ id: "root", level: 2, children: ["leaf_4", "leaf_5", "leaf_6", "leaf_7"] },
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
saveRaptorTree(sid, treeB, builtAt2, tmpDir);
|
|
83
|
+
|
|
84
|
+
const afterB = listRaptorNodes(sid, tmpDir);
|
|
85
|
+
assert.equal(afterB.length, 3, "tree B has 3 nodes (stale A nodes gone)");
|
|
86
|
+
|
|
87
|
+
// No stale node ids from tree A survive.
|
|
88
|
+
const idsAfterB = new Set(afterB.map((n) => n.id));
|
|
89
|
+
assert.ok(!idsAfterB.has("alpha"), "stale node alpha is gone");
|
|
90
|
+
assert.ok(!idsAfterB.has("beta"), "stale node beta is gone");
|
|
91
|
+
assert.ok(idsAfterB.has("gamma"), "new node gamma present");
|
|
92
|
+
assert.ok(idsAfterB.has("delta"), "new node delta present");
|
|
93
|
+
assert.ok(idsAfterB.has("root"), "root present (upserted, not duplicated)");
|
|
94
|
+
|
|
95
|
+
// builtAt was updated to the new timestamp.
|
|
96
|
+
for (const n of afterB) {
|
|
97
|
+
assert.equal(n.builtAt, builtAt2, `node ${n.id} builtAt updated to tree B timestamp`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── Test 2: corrupt children JSON → no throw, children = [] ──────────────────
|
|
102
|
+
|
|
103
|
+
test("listRaptorNodes: corrupt children JSON does not throw and returns []", () => {
|
|
104
|
+
const sid = "sess_corrupt";
|
|
105
|
+
const builtAt = 5000;
|
|
106
|
+
|
|
107
|
+
const tree = makeTree([
|
|
108
|
+
{ id: "good", level: 1, children: ["leaf_0", "leaf_1"] },
|
|
109
|
+
{ id: "bad", level: 1, children: ["leaf_2", "leaf_3"] },
|
|
110
|
+
{ id: "root", level: 2, children: ["leaf_0", "leaf_1", "leaf_2", "leaf_3"] },
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
saveRaptorTree(sid, tree, builtAt, tmpDir);
|
|
114
|
+
|
|
115
|
+
// Corrupt the `children` JSON for the "bad" row directly in SQLite.
|
|
116
|
+
const db = openStore(tmpDir);
|
|
117
|
+
db.prepare(
|
|
118
|
+
"UPDATE raptor_nodes SET children = ? WHERE session_id = ? AND id = ?",
|
|
119
|
+
).run("{not valid json", sid, "bad");
|
|
120
|
+
|
|
121
|
+
// listRaptorNodes must not throw.
|
|
122
|
+
let nodes: StoredRaptorNode[];
|
|
123
|
+
assert.doesNotThrow(() => {
|
|
124
|
+
nodes = listRaptorNodes(sid, tmpDir);
|
|
125
|
+
}, "listRaptorNodes must not throw on corrupt children JSON");
|
|
126
|
+
|
|
127
|
+
const badNode = nodes!.find((n) => n.id === "bad");
|
|
128
|
+
assert.ok(badNode, "bad node still present");
|
|
129
|
+
assert.deepEqual(badNode!.children, [], "corrupt children parsed to []");
|
|
130
|
+
|
|
131
|
+
// The good node is unaffected.
|
|
132
|
+
const goodNode = nodes!.find((n) => n.id === "good");
|
|
133
|
+
assert.ok(goodNode, "good node present");
|
|
134
|
+
assert.deepEqual(
|
|
135
|
+
goodNode!.children,
|
|
136
|
+
["leaf_0", "leaf_1"],
|
|
137
|
+
"good node children intact",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
@@ -2,106 +2,160 @@
|
|
|
2
2
|
* raptor.ts — Sprint 13 RAPTOR node persistence.
|
|
3
3
|
*/
|
|
4
4
|
import { getStateDir, normalizeSessionId } from "../../store.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
openStore,
|
|
7
|
+
withTx,
|
|
8
|
+
jsonText,
|
|
9
|
+
encodeEmbedding,
|
|
10
|
+
decodeEmbedding,
|
|
11
|
+
} from "./utils.js";
|
|
6
12
|
|
|
7
13
|
export interface StoredRaptorNode {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
id: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
level: number;
|
|
17
|
+
parentId: string | null;
|
|
18
|
+
children: string[];
|
|
19
|
+
summary: string;
|
|
20
|
+
embedding: number[];
|
|
21
|
+
qualityMarker: string;
|
|
22
|
+
tokenEstimate: number;
|
|
23
|
+
/** S25: epoch ms when the tree containing this node was built. */
|
|
24
|
+
builtAt: number;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Persist a single RAPTOR node (upsert by (session_id, id)). */
|
|
22
|
-
export function upsertRaptorNode(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
export function upsertRaptorNode(
|
|
29
|
+
node: StoredRaptorNode,
|
|
30
|
+
stateDir: string = getStateDir(),
|
|
31
|
+
): void {
|
|
32
|
+
const db = openStore(stateDir);
|
|
33
|
+
db.prepare(
|
|
34
|
+
`INSERT INTO raptor_nodes(id, session_id, level, parent_id, children, summary, embedding_blob, quality_marker, token_estimate, built_at)
|
|
35
|
+
VALUES(@id, @session_id, @level, @parent_id, @children, @summary, @embedding_blob, @quality_marker, @token_estimate, @built_at)
|
|
27
36
|
ON CONFLICT(session_id, id) DO UPDATE SET
|
|
28
37
|
level=excluded.level, parent_id=excluded.parent_id, children=excluded.children,
|
|
29
38
|
summary=excluded.summary, embedding_blob=excluded.embedding_blob,
|
|
30
39
|
quality_marker=excluded.quality_marker, token_estimate=excluded.token_estimate,
|
|
31
40
|
built_at=excluded.built_at`,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
).run({
|
|
42
|
+
id: node.id,
|
|
43
|
+
session_id: node.sessionId,
|
|
44
|
+
level: node.level,
|
|
45
|
+
parent_id: node.parentId,
|
|
46
|
+
children: jsonText(node.children),
|
|
47
|
+
summary: node.summary,
|
|
48
|
+
embedding_blob: encodeEmbedding(node.embedding),
|
|
49
|
+
quality_marker: node.qualityMarker,
|
|
50
|
+
token_estimate: node.tokenEstimate,
|
|
51
|
+
built_at: node.builtAt,
|
|
52
|
+
});
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/** Persist an entire built RAPTOR tree for a session (shadow or live). */
|
|
47
56
|
export function saveRaptorTree(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
sessionId: string,
|
|
58
|
+
tree: {
|
|
59
|
+
nodes: Map<
|
|
60
|
+
string,
|
|
61
|
+
{
|
|
62
|
+
id: string;
|
|
63
|
+
level: number;
|
|
64
|
+
parentId: string | null;
|
|
65
|
+
children: string[];
|
|
66
|
+
summary: string;
|
|
67
|
+
embedding: number[];
|
|
68
|
+
qualityMarker: string;
|
|
69
|
+
tokenEstimate: number;
|
|
70
|
+
}
|
|
71
|
+
>;
|
|
72
|
+
},
|
|
73
|
+
builtAt: number,
|
|
74
|
+
stateDir: string = getStateDir(),
|
|
63
75
|
): void {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
const nsid = normalizeSessionId(sessionId);
|
|
77
|
+
const db = openStore(stateDir);
|
|
78
|
+
withTx(db, () => {
|
|
79
|
+
db.prepare("DELETE FROM raptor_nodes WHERE session_id = ?").run(nsid);
|
|
80
|
+
for (const node of tree.nodes.values()) {
|
|
81
|
+
upsertRaptorNode(
|
|
82
|
+
{
|
|
83
|
+
id: node.id,
|
|
84
|
+
sessionId: nsid,
|
|
85
|
+
level: node.level,
|
|
86
|
+
parentId: node.parentId,
|
|
87
|
+
children: node.children,
|
|
88
|
+
summary: node.summary,
|
|
89
|
+
embedding: node.embedding,
|
|
90
|
+
qualityMarker: node.qualityMarker,
|
|
91
|
+
tokenEstimate: node.tokenEstimate,
|
|
92
|
+
builtAt,
|
|
93
|
+
},
|
|
94
|
+
stateDir,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Safe JSON array parse — returns [] on corrupt input. */
|
|
101
|
+
function safeJsonArray(raw: unknown): string[] {
|
|
102
|
+
if (typeof raw !== "string" || !raw) return [];
|
|
103
|
+
try { return JSON.parse(raw) as string[]; }
|
|
104
|
+
catch { return []; }
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
/** Load all RAPTOR nodes for a session. */
|
|
84
|
-
export function listRaptorNodes(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
export function listRaptorNodes(
|
|
109
|
+
sessionId: string,
|
|
110
|
+
stateDir: string = getStateDir(),
|
|
111
|
+
): StoredRaptorNode[] {
|
|
112
|
+
const db = openStore(stateDir);
|
|
113
|
+
const rows = db
|
|
114
|
+
.prepare(
|
|
115
|
+
"SELECT * FROM raptor_nodes WHERE session_id = ? ORDER BY level ASC, id ASC",
|
|
116
|
+
)
|
|
117
|
+
.all(normalizeSessionId(sessionId)) as any[];
|
|
118
|
+
return rows.map((row) => ({
|
|
119
|
+
id: row.id,
|
|
120
|
+
sessionId: row.session_id,
|
|
121
|
+
level: row.level,
|
|
122
|
+
parentId: row.parent_id ?? null,
|
|
123
|
+
children: safeJsonArray(row.children),
|
|
124
|
+
summary: row.summary ?? "",
|
|
125
|
+
embedding: decodeEmbedding(row.embedding_blob),
|
|
126
|
+
qualityMarker: row.quality_marker ?? "low",
|
|
127
|
+
tokenEstimate: row.token_estimate ?? 0,
|
|
128
|
+
builtAt: Number(row.built_at ?? 0),
|
|
129
|
+
}));
|
|
101
130
|
}
|
|
102
131
|
|
|
103
132
|
/** Delete all RAPTOR nodes for a session (rollback/cleanup). */
|
|
104
|
-
export function clearRaptorNodes(
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
export function clearRaptorNodes(
|
|
134
|
+
sessionId: string,
|
|
135
|
+
stateDir: string = getStateDir(),
|
|
136
|
+
): void {
|
|
137
|
+
const db = openStore(stateDir);
|
|
138
|
+
db.prepare("DELETE FROM raptor_nodes WHERE session_id = ?").run(
|
|
139
|
+
normalizeSessionId(sessionId),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* S25: newest built_at for a session's RAPTOR tree (0 if no nodes exist).
|
|
145
|
+
* Cheap indexed MAX query used by raptorSearchHits to validate cache freshness
|
|
146
|
+
* without rehydrating the full node Map.
|
|
147
|
+
*/
|
|
148
|
+
export function maxRaptorNodeBuiltAt(
|
|
149
|
+
sessionId: string,
|
|
150
|
+
stateDir: string = getStateDir(),
|
|
151
|
+
): number {
|
|
152
|
+
const db = openStore(stateDir);
|
|
153
|
+
const row = db
|
|
154
|
+
.prepare(
|
|
155
|
+
`SELECT MAX(built_at) AS max_built FROM raptor_nodes WHERE session_id = ?`,
|
|
156
|
+
)
|
|
157
|
+
.get(normalizeSessionId(sessionId)) as
|
|
158
|
+
| { max_built: number | null }
|
|
159
|
+
| undefined;
|
|
160
|
+
return row?.max_built ?? 0;
|
|
107
161
|
}
|