pi-mega-compact 0.8.24 → 0.8.26
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/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/dashboard-snapshot.js +4 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +4 -0
- package/dist/extensions/mega-runtime/runtime.js +58 -5
- 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/raptor-inject-summaries.test.js +162 -0
- package/dist/src/recall.js +153 -24
- 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 +179 -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/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/dashboard-snapshot.ts +8 -0
- package/extensions/mega-runtime/helpers.ts +25 -1
- package/extensions/mega-runtime/runtime-snapshot.ts +4 -0
- package/extensions/mega-runtime/runtime.ts +69 -23
- 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 +228 -0
- package/src/recall.test.ts +220 -4
- package/src/recall.ts +462 -265
- 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 +302 -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/extensions/mega-runtime/reset-runtime.ts +0 -80
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildHistory.ts — S42D RAPTOR build history + freshness.
|
|
3
|
+
*
|
|
4
|
+
* - Stores one structured row per tree build in `raptor_build_history`.
|
|
5
|
+
* - `computeCoherenceScore`: avg intra-cluster cosine similarity (0–1) for a
|
|
6
|
+
* built tree — a health/diagnostics metric, not a gating signal.
|
|
7
|
+
* - `isRaptorTreeFresh`: lets the build call site skip a rebuild when the last
|
|
8
|
+
* build is recent (within `freshnessHours`) and the checkpoint count has not
|
|
9
|
+
* drifted by more than 20%. Returns false when no history exists (forces a
|
|
10
|
+
* build) — the same default as a missing tree.
|
|
11
|
+
*
|
|
12
|
+
* No network. Pure SQLite + local cosine.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { openStore, withTx } from "../../store/sqlite/utils.js";
|
|
16
|
+
import { getStateDir } from "../../store.js";
|
|
17
|
+
import { normalizeSessionId } from "../../store.js";
|
|
18
|
+
import { cosineSimilarity } from "../../embedder.js";
|
|
19
|
+
/** Insert one build-history row. Idempotent on build_id (PK). */
|
|
20
|
+
export function insertBuildHistory(input, stateDir = getStateDir()) {
|
|
21
|
+
const db = openStore(stateDir);
|
|
22
|
+
const buildId = randomUUID();
|
|
23
|
+
const sid = normalizeSessionId(input.sessionId);
|
|
24
|
+
withTx(db, () => {
|
|
25
|
+
db.prepare(`INSERT INTO raptor_build_history
|
|
26
|
+
(build_id, session_id, state_dir, started_at, completed_at,
|
|
27
|
+
node_count, leaf_count, depth, config_json, coherence_score, timed_out)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
29
|
+
ON CONFLICT(build_id) DO NOTHING`).run(buildId, sid, input.stateDir, input.startedAt, input.completedAt, input.nodeCount, input.leafCount, input.depth, input.configJson, input.coherenceScore, input.timedOut ? 1 : 0);
|
|
30
|
+
});
|
|
31
|
+
return buildId;
|
|
32
|
+
}
|
|
33
|
+
/** The most recent build row for a session (by completed_at), or null. */
|
|
34
|
+
export function getLatestBuild(sessionId, stateDir = getStateDir()) {
|
|
35
|
+
const db = openStore(stateDir);
|
|
36
|
+
const sid = normalizeSessionId(sessionId);
|
|
37
|
+
const row = db
|
|
38
|
+
.prepare(`SELECT build_id, session_id, state_dir, started_at, completed_at,
|
|
39
|
+
node_count, leaf_count, depth, config_json, coherence_score, timed_out
|
|
40
|
+
FROM raptor_build_history
|
|
41
|
+
WHERE session_id = ?
|
|
42
|
+
ORDER BY completed_at DESC
|
|
43
|
+
LIMIT 1`)
|
|
44
|
+
.get(sid);
|
|
45
|
+
if (!row)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
buildId: row.build_id,
|
|
49
|
+
sessionId: row.session_id,
|
|
50
|
+
stateDir: row.state_dir,
|
|
51
|
+
startedAt: row.started_at,
|
|
52
|
+
completedAt: row.completed_at,
|
|
53
|
+
nodeCount: row.node_count,
|
|
54
|
+
leafCount: row.leaf_count,
|
|
55
|
+
depth: row.depth,
|
|
56
|
+
configJson: row.config_json,
|
|
57
|
+
coherenceScore: row.coherence_score,
|
|
58
|
+
timedOut: row.timed_out === 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** All build rows for a session, newest first. */
|
|
62
|
+
export function listBuildHistory(sessionId, stateDir = getStateDir()) {
|
|
63
|
+
const db = openStore(stateDir);
|
|
64
|
+
const sid = normalizeSessionId(sessionId);
|
|
65
|
+
const rows = db
|
|
66
|
+
.prepare(`SELECT build_id, session_id, state_dir, started_at, completed_at,
|
|
67
|
+
node_count, leaf_count, depth, config_json, coherence_score, timed_out
|
|
68
|
+
FROM raptor_build_history
|
|
69
|
+
WHERE session_id = ?
|
|
70
|
+
ORDER BY completed_at DESC`)
|
|
71
|
+
.all(sid);
|
|
72
|
+
return rows.map((row) => ({
|
|
73
|
+
buildId: row.build_id,
|
|
74
|
+
sessionId: row.session_id,
|
|
75
|
+
stateDir: row.state_dir,
|
|
76
|
+
startedAt: row.started_at,
|
|
77
|
+
completedAt: row.completed_at,
|
|
78
|
+
nodeCount: row.node_count,
|
|
79
|
+
leafCount: row.leaf_count,
|
|
80
|
+
depth: row.depth,
|
|
81
|
+
configJson: row.config_json,
|
|
82
|
+
coherenceScore: row.coherence_score,
|
|
83
|
+
timedOut: row.timed_out === 1,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compute the average intra-cluster cosine similarity for a RAPTOR tree.
|
|
88
|
+
* For each internal node, compute the mean pairwise cosine similarity of its
|
|
89
|
+
* children's embeddings. Average across all internal nodes. Returns 0–1
|
|
90
|
+
* (higher = more coherent clusters). A tree with no internal nodes returns 0.
|
|
91
|
+
*
|
|
92
|
+
* Child embeddings: a child that is itself an internal node contributes its own
|
|
93
|
+
* centroid. A child id not in `tree.nodes` is a raw leaf — leaves are not stored
|
|
94
|
+
* on RaptorNodes, so `leafEmbeddings` (id → embedding, from the build-time Leaf[]
|
|
95
|
+
* set) supplies the true leaf embedding; if absent, we fall back to the parent
|
|
96
|
+
* node's centroid. Passing `leafEmbeddings` is strongly recommended — without it
|
|
97
|
+
* every leaf collapses to its parent's centroid and the score inflates to ~1.0.
|
|
98
|
+
*/
|
|
99
|
+
export function computeCoherenceScore(tree, leafEmbeddings) {
|
|
100
|
+
let sum = 0;
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const node of tree.nodes.values()) {
|
|
103
|
+
if (node.embedding.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
const childEmbeddings = [];
|
|
106
|
+
for (const cid of node.children) {
|
|
107
|
+
const child = tree.nodes.get(cid);
|
|
108
|
+
if (child && child.embedding.length > 0) {
|
|
109
|
+
childEmbeddings.push(child.embedding);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Raw leaf: use its true embedding if known, else parent centroid.
|
|
113
|
+
const leafEmb = leafEmbeddings?.get(cid);
|
|
114
|
+
childEmbeddings.push(leafEmb && leafEmb.length > 0 ? leafEmb : node.embedding);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (childEmbeddings.length < 2)
|
|
118
|
+
continue; // need >=2 to measure spread
|
|
119
|
+
let pairSum = 0;
|
|
120
|
+
let pairCount = 0;
|
|
121
|
+
for (let i = 0; i < childEmbeddings.length; i++) {
|
|
122
|
+
for (let j = i + 1; j < childEmbeddings.length; j++) {
|
|
123
|
+
pairSum += cosineSimilarity(childEmbeddings[i], childEmbeddings[j]);
|
|
124
|
+
pairCount++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (pairCount > 0) {
|
|
128
|
+
sum += pairSum / pairCount;
|
|
129
|
+
count++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return count > 0 ? sum / count : 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if the RAPTOR tree is fresh enough to skip a rebuild.
|
|
136
|
+
* Returns true iff:
|
|
137
|
+
* - a build-history row exists, AND
|
|
138
|
+
* - it completed within `freshnessHours`, AND
|
|
139
|
+
* - the checkpoint count has not changed by more than 20% since the build.
|
|
140
|
+
* A `freshnessHours` of 0 always returns false (rebuild every time) — the spec's
|
|
141
|
+
* "0 disables" escape hatch, symmetric to the S38 retry caps.
|
|
142
|
+
*/
|
|
143
|
+
export function isRaptorTreeFresh(sessionId, stateDir, freshnessHours, currentCheckpointCount) {
|
|
144
|
+
if (freshnessHours <= 0)
|
|
145
|
+
return false;
|
|
146
|
+
const latest = getLatestBuild(sessionId, stateDir);
|
|
147
|
+
if (!latest)
|
|
148
|
+
return false;
|
|
149
|
+
const ageHours = (Date.now() - latest.completedAt) / 3_600_000;
|
|
150
|
+
if (ageHours > freshnessHours)
|
|
151
|
+
return false;
|
|
152
|
+
if (latest.leafCount <= 0)
|
|
153
|
+
return false;
|
|
154
|
+
const changeRatio = Math.abs(currentCheckpointCount - latest.leafCount) / latest.leafCount;
|
|
155
|
+
return changeRatio <= 0.2;
|
|
156
|
+
}
|
|
157
|
+
/** Clear build history for a session (used by tests + DR). */
|
|
158
|
+
export function clearBuildHistory(sessionId, stateDir = getStateDir()) {
|
|
159
|
+
const db = openStore(stateDir);
|
|
160
|
+
const sid = normalizeSessionId(sessionId);
|
|
161
|
+
withTx(db, () => {
|
|
162
|
+
db.prepare("DELETE FROM raptor_build_history WHERE session_id = ?").run(sid);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildHistory.test.ts — S42D RAPTOR build history + freshness tests.
|
|
3
|
+
*
|
|
4
|
+
* No network. Real stores with temp state dirs.
|
|
5
|
+
*/
|
|
6
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { openStore } from "../../store/sqlite/utils.js";
|
|
12
|
+
import { insertBuildHistory, getLatestBuild, listBuildHistory, computeCoherenceScore, isRaptorTreeFresh, clearBuildHistory, } from "./buildHistory.js";
|
|
13
|
+
import { runRaptor } from "./index.js";
|
|
14
|
+
import { VectorStore, vectorList } from "../../vectorStore.js";
|
|
15
|
+
import { compactSession } from "../../engine.js";
|
|
16
|
+
import { loadDedupConfig } from "../../config/dedup.js";
|
|
17
|
+
import { normalizeSessionId } from "../../store.js";
|
|
18
|
+
import { Logger } from "../../log.js";
|
|
19
|
+
let tmpDir;
|
|
20
|
+
let counter = 0;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-bh-"));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
function stateDir() {
|
|
28
|
+
return join(tmpDir, `run-${counter++}`);
|
|
29
|
+
}
|
|
30
|
+
function cfg(overrides) {
|
|
31
|
+
return {
|
|
32
|
+
...loadDedupConfig(),
|
|
33
|
+
RAPTOR_ENABLED: true,
|
|
34
|
+
L0_ENABLED: false,
|
|
35
|
+
L1_ENABLED: false,
|
|
36
|
+
L2_ENABLED: false,
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function msg(text, toolName) {
|
|
41
|
+
return toolName
|
|
42
|
+
? { role: "assistant", text, toolName, input: text, output: text }
|
|
43
|
+
: { role: "user", text };
|
|
44
|
+
}
|
|
45
|
+
// ─── 1. Build-history rows are created with correct metadata ──────────────
|
|
46
|
+
test("S42D-1: insertBuildHistory + getLatestBuild round-trip", () => {
|
|
47
|
+
const sd = stateDir();
|
|
48
|
+
openStore(sd); // initialize schema
|
|
49
|
+
const ts = Date.now();
|
|
50
|
+
const id = insertBuildHistory({
|
|
51
|
+
sessionId: "sess-foo",
|
|
52
|
+
stateDir: sd,
|
|
53
|
+
startedAt: ts - 500,
|
|
54
|
+
completedAt: ts,
|
|
55
|
+
nodeCount: 6,
|
|
56
|
+
leafCount: 10,
|
|
57
|
+
depth: 3,
|
|
58
|
+
configJson: '{"budgetMs":5000}',
|
|
59
|
+
coherenceScore: 0.82,
|
|
60
|
+
timedOut: false,
|
|
61
|
+
}, sd);
|
|
62
|
+
assert.ok(id, "build id returned");
|
|
63
|
+
const latest = getLatestBuild("sess-foo", sd);
|
|
64
|
+
assert.ok(latest, "latest build found");
|
|
65
|
+
assert.equal(latest.sessionId, normalizeSessionId("sess-foo"));
|
|
66
|
+
assert.equal(latest.nodeCount, 6);
|
|
67
|
+
assert.equal(latest.leafCount, 10);
|
|
68
|
+
assert.equal(latest.depth, 3);
|
|
69
|
+
assert.equal(latest.coherenceScore, 0.82);
|
|
70
|
+
assert.equal(latest.timedOut, false);
|
|
71
|
+
assert.equal(latest.configJson, '{"budgetMs":5000}');
|
|
72
|
+
// getLatest returns the newest by completed_at
|
|
73
|
+
insertBuildHistory({
|
|
74
|
+
sessionId: "sess-foo",
|
|
75
|
+
stateDir: sd,
|
|
76
|
+
startedAt: ts + 1000,
|
|
77
|
+
completedAt: ts + 2000,
|
|
78
|
+
nodeCount: 7,
|
|
79
|
+
leafCount: 11,
|
|
80
|
+
depth: 3,
|
|
81
|
+
configJson: "{}",
|
|
82
|
+
coherenceScore: null,
|
|
83
|
+
timedOut: true,
|
|
84
|
+
}, sd);
|
|
85
|
+
const newest = getLatestBuild("sess-foo", sd);
|
|
86
|
+
assert.equal(newest.nodeCount, 7);
|
|
87
|
+
assert.equal(newest.timedOut, true);
|
|
88
|
+
assert.equal(newest.coherenceScore, null);
|
|
89
|
+
// listBuildHistory returns all, newest first
|
|
90
|
+
const all = listBuildHistory("sess-foo", sd);
|
|
91
|
+
assert.equal(all.length, 2);
|
|
92
|
+
assert.equal(all[0].nodeCount, 7); // newest first
|
|
93
|
+
// No history for an unknown session
|
|
94
|
+
assert.equal(getLatestBuild("sess-none", sd), null);
|
|
95
|
+
assert.deepEqual(listBuildHistory("sess-none", sd), []);
|
|
96
|
+
// clearBuildHistory removes rows
|
|
97
|
+
clearBuildHistory("sess-foo", sd);
|
|
98
|
+
assert.equal(getLatestBuild("sess-foo", sd), null);
|
|
99
|
+
});
|
|
100
|
+
// ─── 2. Coherence score: 1.0 for identical, lower for diverse ───────────────
|
|
101
|
+
test("S42D-2: computeCoherenceScore is 1.0 for identical embeddings, lower for diverse", () => {
|
|
102
|
+
const identical = {
|
|
103
|
+
rootId: "root",
|
|
104
|
+
nodes: new Map([
|
|
105
|
+
["root", { id: "root", level: 0, parentId: null, children: ["c1", "c2"], summary: "r", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 2 }],
|
|
106
|
+
["c1", { id: "c1", level: 1, parentId: "root", children: [], summary: "c1", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 1 }],
|
|
107
|
+
["c2", { id: "c2", level: 1, parentId: "root", children: [], summary: "c2", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 1 }],
|
|
108
|
+
]),
|
|
109
|
+
levels: 2,
|
|
110
|
+
timedOut: false,
|
|
111
|
+
};
|
|
112
|
+
assert.equal(computeCoherenceScore(identical), 1.0);
|
|
113
|
+
// Diverse: orthogonal children → cosine 0
|
|
114
|
+
const diverse = {
|
|
115
|
+
rootId: "root",
|
|
116
|
+
nodes: new Map([
|
|
117
|
+
["root", { id: "root", level: 0, parentId: null, children: ["c1", "c2"], summary: "r", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 2 }],
|
|
118
|
+
["c1", { id: "c1", level: 1, parentId: "root", children: [], summary: "c1", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 1 }],
|
|
119
|
+
["c2", { id: "c2", level: 1, parentId: "root", children: [], summary: "c2", embedding: [0, 1], qualityMarker: "low", tokenEstimate: 1 }],
|
|
120
|
+
]),
|
|
121
|
+
levels: 2,
|
|
122
|
+
timedOut: false,
|
|
123
|
+
};
|
|
124
|
+
const diverseScore = computeCoherenceScore(diverse);
|
|
125
|
+
assert.ok(diverseScore < 1.0, `diverse score < 1.0 (got ${diverseScore})`);
|
|
126
|
+
assert.ok(diverseScore >= 0, "score is non-negative");
|
|
127
|
+
// No internal nodes with >=2 children → 0
|
|
128
|
+
const single = {
|
|
129
|
+
rootId: "root",
|
|
130
|
+
nodes: new Map([
|
|
131
|
+
["root", { id: "root", level: 0, parentId: null, children: ["c1"], summary: "r", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 1 }],
|
|
132
|
+
["c1", { id: "c1", level: 1, parentId: "root", children: [], summary: "c1", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 1 }],
|
|
133
|
+
]),
|
|
134
|
+
levels: 2,
|
|
135
|
+
timedOut: false,
|
|
136
|
+
};
|
|
137
|
+
assert.equal(computeCoherenceScore(single), 0);
|
|
138
|
+
});
|
|
139
|
+
// ─── 2b. Coherence with leaf embeddings (the real-tree shape) ──────────────
|
|
140
|
+
test("S42D-2b: computeCoherenceScore uses leafEmbeddings for leaf children (real-tree shape)", () => {
|
|
141
|
+
// Root whose children are raw leaves (the common collapse-path shape).
|
|
142
|
+
// Without leafEmbeddings, leaves fall back to the root centroid → score 1.0
|
|
143
|
+
// (degenerate). With true leaf embeddings, diverse leaves score < 1.0.
|
|
144
|
+
const tree = {
|
|
145
|
+
rootId: "root",
|
|
146
|
+
nodes: new Map([
|
|
147
|
+
["root", { id: "root", level: 0, parentId: null, children: ["l1", "l2"], summary: "r", embedding: [1, 0], qualityMarker: "low", tokenEstimate: 2 }],
|
|
148
|
+
]),
|
|
149
|
+
levels: 1,
|
|
150
|
+
timedOut: false,
|
|
151
|
+
};
|
|
152
|
+
// No leaf map → both leaves use root centroid [1,0] → cosine 1.0 (degenerate).
|
|
153
|
+
assert.equal(computeCoherenceScore(tree), 1.0);
|
|
154
|
+
// Leaf map with orthogonal leaves → cosine 0 (truly diverse).
|
|
155
|
+
const leafMap = new Map([
|
|
156
|
+
["l1", [1, 0]],
|
|
157
|
+
["l2", [0, 1]],
|
|
158
|
+
]);
|
|
159
|
+
const score = computeCoherenceScore(tree, leafMap);
|
|
160
|
+
assert.ok(score < 1.0, `diverse leaves score < 1.0 (got ${score})`);
|
|
161
|
+
assert.ok(score >= 0, "score non-negative");
|
|
162
|
+
// Identical leaves via the map → 1.0
|
|
163
|
+
const leafMapIdentical = new Map([
|
|
164
|
+
["l1", [1, 0]],
|
|
165
|
+
["l2", [1, 0]],
|
|
166
|
+
]);
|
|
167
|
+
assert.equal(computeCoherenceScore(tree, leafMapIdentical), 1.0);
|
|
168
|
+
});
|
|
169
|
+
// ─── 3. Freshness check: recent + stable → fresh ────────────────────────────
|
|
170
|
+
test("S42D-3: isRaptorTreeFresh true for recent build + stable count", () => {
|
|
171
|
+
const sd = stateDir();
|
|
172
|
+
openStore(sd);
|
|
173
|
+
insertBuildHistory({
|
|
174
|
+
sessionId: "sess-fresh",
|
|
175
|
+
stateDir: sd,
|
|
176
|
+
startedAt: Date.now() - 1000,
|
|
177
|
+
completedAt: Date.now() - 500, // < 4h old
|
|
178
|
+
nodeCount: 6,
|
|
179
|
+
leafCount: 10,
|
|
180
|
+
depth: 3,
|
|
181
|
+
configJson: "{}",
|
|
182
|
+
coherenceScore: 0.5,
|
|
183
|
+
timedOut: false,
|
|
184
|
+
}, sd);
|
|
185
|
+
// 10 checkpoints, latest had 10 → within 20% → fresh
|
|
186
|
+
assert.equal(isRaptorTreeFresh("sess-fresh", sd, 4, 10), true);
|
|
187
|
+
// 11 checkpoints → 10% change → still fresh
|
|
188
|
+
assert.equal(isRaptorTreeFresh("sess-fresh", sd, 4, 11), true);
|
|
189
|
+
// 9 checkpoints → 10% change → fresh
|
|
190
|
+
assert.equal(isRaptorTreeFresh("sess-fresh", sd, 4, 9), true);
|
|
191
|
+
});
|
|
192
|
+
// ─── 4. Freshness check: stale build → not fresh ────────────────────────────
|
|
193
|
+
test("S42D-4: isRaptorTreeFresh false for stale build (>4h old)", () => {
|
|
194
|
+
const sd = stateDir();
|
|
195
|
+
openStore(sd);
|
|
196
|
+
const oldTs = Date.now() - 5 * 3_600_000; // 5h ago
|
|
197
|
+
insertBuildHistory({
|
|
198
|
+
sessionId: "sess-stale",
|
|
199
|
+
stateDir: sd,
|
|
200
|
+
startedAt: oldTs - 1000,
|
|
201
|
+
completedAt: oldTs,
|
|
202
|
+
nodeCount: 6,
|
|
203
|
+
leafCount: 10,
|
|
204
|
+
depth: 3,
|
|
205
|
+
configJson: "{}",
|
|
206
|
+
coherenceScore: 0.5,
|
|
207
|
+
timedOut: false,
|
|
208
|
+
}, sd);
|
|
209
|
+
assert.equal(isRaptorTreeFresh("sess-stale", sd, 4, 10), false);
|
|
210
|
+
});
|
|
211
|
+
// ─── 5. Freshness check: count drift > 20% → not fresh ──────────────────────
|
|
212
|
+
test("S42D-5: isRaptorTreeFresh false when checkpoint count changed by >20%", () => {
|
|
213
|
+
const sd = stateDir();
|
|
214
|
+
openStore(sd);
|
|
215
|
+
insertBuildHistory({
|
|
216
|
+
sessionId: "sess-drift",
|
|
217
|
+
stateDir: sd,
|
|
218
|
+
startedAt: Date.now() - 1000,
|
|
219
|
+
completedAt: Date.now() - 500,
|
|
220
|
+
nodeCount: 6,
|
|
221
|
+
leafCount: 10,
|
|
222
|
+
depth: 3,
|
|
223
|
+
configJson: "{}",
|
|
224
|
+
coherenceScore: 0.5,
|
|
225
|
+
timedOut: false,
|
|
226
|
+
}, sd);
|
|
227
|
+
// 13 checkpoints vs 10 → 30% increase → not fresh
|
|
228
|
+
assert.equal(isRaptorTreeFresh("sess-drift", sd, 4, 13), false);
|
|
229
|
+
// 7 checkpoints vs 10 → 30% decrease → not fresh
|
|
230
|
+
assert.equal(isRaptorTreeFresh("sess-drift", sd, 4, 7), false);
|
|
231
|
+
});
|
|
232
|
+
// ─── 6. No build history → freshness false (forces build) ───────────────────
|
|
233
|
+
test("S42D-6: isRaptorTreeFresh false when no build history exists", () => {
|
|
234
|
+
const sd = stateDir();
|
|
235
|
+
openStore(sd);
|
|
236
|
+
assert.equal(isRaptorTreeFresh("sess-none", sd, 4, 50), false);
|
|
237
|
+
// freshnessHours=0 always false (disables the gate → always rebuild)
|
|
238
|
+
insertBuildHistory({
|
|
239
|
+
sessionId: "sess-zero",
|
|
240
|
+
stateDir: sd,
|
|
241
|
+
startedAt: Date.now() - 100,
|
|
242
|
+
completedAt: Date.now() - 50,
|
|
243
|
+
nodeCount: 1,
|
|
244
|
+
leafCount: 5,
|
|
245
|
+
depth: 1,
|
|
246
|
+
configJson: "{}",
|
|
247
|
+
coherenceScore: 0.5,
|
|
248
|
+
timedOut: false,
|
|
249
|
+
}, sd);
|
|
250
|
+
assert.equal(isRaptorTreeFresh("sess-zero", sd, 0, 5), false);
|
|
251
|
+
});
|
|
252
|
+
// ─── 7. runRaptor records build history end-to-end ──────────────────────────
|
|
253
|
+
test("S42D-7: runRaptor records a build-history row on real build", () => {
|
|
254
|
+
const sd = stateDir();
|
|
255
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
256
|
+
const sid = "bh-e2e";
|
|
257
|
+
// Seed 12 checkpoints and build a tree.
|
|
258
|
+
for (let i = 1; i <= 12; i++) {
|
|
259
|
+
compactSession({
|
|
260
|
+
sessionId: sid,
|
|
261
|
+
messages: [
|
|
262
|
+
msg(`build-history test checkpoint ${i} alpha beta`),
|
|
263
|
+
msg(`ack ${i}`, "Edit"),
|
|
264
|
+
],
|
|
265
|
+
keepFrom: 2,
|
|
266
|
+
timestamp: i + 1,
|
|
267
|
+
}, s);
|
|
268
|
+
}
|
|
269
|
+
const nsid = normalizeSessionId(sid);
|
|
270
|
+
const all = vectorList(s, nsid);
|
|
271
|
+
const leaves = all.map((cp) => ({
|
|
272
|
+
id: cp.checkpointId,
|
|
273
|
+
messages: [],
|
|
274
|
+
sourceText: cp.normalizedText ?? cp.summary ?? cp.regionHash,
|
|
275
|
+
embedding: cp.embedding,
|
|
276
|
+
}));
|
|
277
|
+
const tree = runRaptor(leaves, {
|
|
278
|
+
stateDir: sd,
|
|
279
|
+
sessionId: nsid,
|
|
280
|
+
logger: new Logger(),
|
|
281
|
+
});
|
|
282
|
+
assert.ok(tree, "tree built");
|
|
283
|
+
const latest = getLatestBuild(nsid, sd);
|
|
284
|
+
assert.ok(latest, "build history recorded by runRaptor");
|
|
285
|
+
assert.equal(latest.leafCount, leaves.length);
|
|
286
|
+
assert.equal(latest.depth, tree.levels);
|
|
287
|
+
assert.equal(latest.timedOut, tree.timedOut);
|
|
288
|
+
assert.equal(latest.nodeCount, tree.nodes.size);
|
|
289
|
+
assert.ok(latest.coherenceScore !== null && latest.coherenceScore >= 0, "coherence score recorded");
|
|
290
|
+
// The freshness gate should now report fresh for the same checkpoint count.
|
|
291
|
+
assert.equal(isRaptorTreeFresh(nsid, sd, 4, all.length), true, "tree is fresh immediately after build");
|
|
292
|
+
});
|
|
@@ -13,6 +13,7 @@ import { defaultEmbedder } from "../../embedder.js";
|
|
|
13
13
|
import { buildRaptorTree } from "./tree.js";
|
|
14
14
|
import { stagedExpansion } from "./retrieval.js";
|
|
15
15
|
import { saveRaptorTree, listRaptorNodes } from "../../store/sqlite.js";
|
|
16
|
+
import { insertBuildHistory, computeCoherenceScore } from "./buildHistory.js";
|
|
16
17
|
/** Shadow mode is on by default; set RAPTOR_SHADOW_MODE=false to serve live. */
|
|
17
18
|
export function isShadowMode() {
|
|
18
19
|
return process.env.RAPTOR_SHADOW_MODE !== "false";
|
|
@@ -28,6 +29,7 @@ export function isShadowMode() {
|
|
|
28
29
|
export function runRaptor(leaves, opts) {
|
|
29
30
|
const embedder = opts.embedder ?? defaultEmbedder();
|
|
30
31
|
const logger = opts.logger;
|
|
32
|
+
const startedAt = opts.builtAt ?? Date.now();
|
|
31
33
|
try {
|
|
32
34
|
const tree = buildRaptorTree(leaves, {
|
|
33
35
|
embedder,
|
|
@@ -45,6 +47,42 @@ export function runRaptor(leaves, opts) {
|
|
|
45
47
|
timedOut: tree.timedOut,
|
|
46
48
|
shadow: isShadowMode(),
|
|
47
49
|
});
|
|
50
|
+
// S42D: record structured build history (coherence score + leaf count) so
|
|
51
|
+
// the freshness check can skip the next rebuild when the tree is recent
|
|
52
|
+
// and the chunk count is stable. Best-effort: a history-write failure must
|
|
53
|
+
// never break the build.
|
|
54
|
+
try {
|
|
55
|
+
// Map leaf id → embedding so computeCoherenceScore can score real
|
|
56
|
+
// intra-cluster spread (without this, leaves collapse to parent centroids
|
|
57
|
+
// and the score inflates to ~1.0 for every tree).
|
|
58
|
+
const leafEmbs = new Map();
|
|
59
|
+
for (const l of leaves)
|
|
60
|
+
if (l.embedding.length > 0)
|
|
61
|
+
leafEmbs.set(l.id, l.embedding);
|
|
62
|
+
const coherence = computeCoherenceScore(tree, leafEmbs);
|
|
63
|
+
insertBuildHistory({
|
|
64
|
+
sessionId: opts.sessionId,
|
|
65
|
+
stateDir: opts.stateDir,
|
|
66
|
+
startedAt,
|
|
67
|
+
completedAt: Date.now(),
|
|
68
|
+
nodeCount: tree.nodes.size,
|
|
69
|
+
leafCount: leaves.length,
|
|
70
|
+
depth: tree.levels,
|
|
71
|
+
configJson: JSON.stringify({
|
|
72
|
+
budgetMs: opts.budgetMs,
|
|
73
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
74
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
75
|
+
}),
|
|
76
|
+
coherenceScore: coherence,
|
|
77
|
+
timedOut: tree.timedOut,
|
|
78
|
+
}, opts.stateDir);
|
|
79
|
+
}
|
|
80
|
+
catch (histErr) {
|
|
81
|
+
logger?.warn("raptor_build_history_failed", {
|
|
82
|
+
sessionId: opts.sessionId,
|
|
83
|
+
error: String(histErr instanceof Error ? histErr.message : histErr),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
48
86
|
if (isShadowMode()) {
|
|
49
87
|
// Build + log only. Do NOT replace retrieval.
|
|
50
88
|
logger?.info("raptor_shadow", { sessionId: opts.sessionId, served: false });
|