pi-mega-compact 0.11.9 → 0.11.10
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/mega-events/context-handler.js +1 -1
- package/dist/src/config/dedup.js +1 -0
- package/dist/src/dedup/raptor/incremental.js +276 -0
- package/dist/src/dedup/raptor/index.js +32 -8
- package/dist/src/dedup/raptor/summarizer.js +1 -1
- package/dist/src/httpEmbedder.js +1 -1
- package/extensions/mega-events/context-handler.ts +1 -1
- package/package.json +1 -1
- package/src/config/dedup.ts +2 -0
- package/src/dedup/raptor/incremental.ts +339 -0
- package/src/dedup/raptor/index.ts +33 -8
- package/src/dedup/raptor/summarizer.ts +1 -1
- package/src/httpEmbedder.ts +1 -1
|
@@ -240,7 +240,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
240
240
|
if (runtime.trimCache &&
|
|
241
241
|
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
242
242
|
runtime.trimCache.cut <= messages.length) {
|
|
243
|
-
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002
|
|
243
|
+
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized by computeLiveTrimCut (src/boundary.ts); replayed verbatim, transcript only grows within an epoch.
|
|
244
244
|
runtime.diagLiveTrimFires++;
|
|
245
245
|
runtime.diagLiveTrimReplays++;
|
|
246
246
|
runtime.snapshot(ctx);
|
package/dist/src/config/dedup.js
CHANGED
|
@@ -64,6 +64,7 @@ export function loadDedupConfig() {
|
|
|
64
64
|
RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
|
|
65
65
|
RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
|
|
66
66
|
RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
|
|
67
|
+
RAPTOR_INCREMENTAL: envBool("MEGACOMPACT_RAPTOR_INCREMENTAL", true),
|
|
67
68
|
RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
|
|
68
69
|
RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
|
|
69
70
|
RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* incremental.ts — incremental RAPTOR tree update (Sprint 26, #7).
|
|
3
|
+
*
|
|
4
|
+
* Instead of rebuilding the full tree (~2s per compaction), insert only the new
|
|
5
|
+
* checkpoints since the last build and recompute only the affected cluster
|
|
6
|
+
* assignments up the tree. Falls back to a full rebuild when the tree is
|
|
7
|
+
* missing, corrupted, or >50% of nodes are new.
|
|
8
|
+
*
|
|
9
|
+
* Non-fatal: every failure logs and returns null (caller falls back to full
|
|
10
|
+
* rebuild). PREVENT-PI-004: zero network; all operations are local SQLite + CPU.
|
|
11
|
+
*/
|
|
12
|
+
import { cosineSimilarity } from "../../embedder.js";
|
|
13
|
+
import { defaultEmbedder as getDefaultEmbedder } from "../../embedder.js";
|
|
14
|
+
import { buildRaptorTree } from "./tree.js";
|
|
15
|
+
import { meanVector } from "./kmeans.js";
|
|
16
|
+
import { summarizeCluster } from "./summarizer.js";
|
|
17
|
+
import { saveRaptorTree, listRaptorNodes, } from "../../store/sqlite.js";
|
|
18
|
+
/** When >FRESH_THRESHOLD of existing leaves are new, a full rebuild is cheaper. */
|
|
19
|
+
const FRESH_THRESHOLD = 0.5;
|
|
20
|
+
/**
|
|
21
|
+
* Incrementally update a persisted RAPTOR tree with new leaves.
|
|
22
|
+
*
|
|
23
|
+
* 1. Reads the existing tree from the store.
|
|
24
|
+
* 2. Filters `newLeaves` to those NOT already covered by the tree.
|
|
25
|
+
* 3. If no new leaves → early-return (existing tree).
|
|
26
|
+
* 4. If no existing tree or >50% new → full rebuild.
|
|
27
|
+
* 5. For each new leaf, finds the best-matching Level-0 cluster node,
|
|
28
|
+
* reassigns (inserts into its children), updates that node's summary
|
|
29
|
+
* and centroid embedding.
|
|
30
|
+
* 6. Propagates changes up through parent levels to the root.
|
|
31
|
+
* 7. Persists the updated tree (full delete + reinsert in a tx).
|
|
32
|
+
*
|
|
33
|
+
* Never throws. Returns the updated tree, or null on failure/fallback.
|
|
34
|
+
*/
|
|
35
|
+
export function incrementRaptorTree(existingLeaves, newLeaves, opts) {
|
|
36
|
+
const logger = opts.logger;
|
|
37
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
38
|
+
try {
|
|
39
|
+
// 1. Read existing tree.
|
|
40
|
+
const storedNodes = listRaptorNodes(opts.sessionId, opts.stateDir);
|
|
41
|
+
if (storedNodes.length === 0) {
|
|
42
|
+
logger?.info("raptor_incremental_no_tree", { sessionId: opts.sessionId });
|
|
43
|
+
return null; // no existing tree — caller falls back to full buildRaptorTree
|
|
44
|
+
}
|
|
45
|
+
// 2. Find new leaf ids not already in the tree.
|
|
46
|
+
const coveredLeafIds = new Set();
|
|
47
|
+
for (const n of storedNodes) {
|
|
48
|
+
for (const cid of n.children)
|
|
49
|
+
coveredLeafIds.add(cid);
|
|
50
|
+
}
|
|
51
|
+
const reallyNewLeaves = newLeaves.filter((l) => !coveredLeafIds.has(l.id));
|
|
52
|
+
const totalCovered = coveredLeafIds.size;
|
|
53
|
+
if (reallyNewLeaves.length === 0) {
|
|
54
|
+
logger?.info("raptor_incremental_no_new", { sessionId: opts.sessionId });
|
|
55
|
+
return rehydrateFromStored(storedNodes);
|
|
56
|
+
}
|
|
57
|
+
// 4. Fallback guard: >50% new → full rebuild.
|
|
58
|
+
if (totalCovered > 0 &&
|
|
59
|
+
reallyNewLeaves.length / totalCovered > FRESH_THRESHOLD) {
|
|
60
|
+
logger?.info("raptor_incremental_fallback_ratio", {
|
|
61
|
+
sessionId: opts.sessionId,
|
|
62
|
+
coveredNodes: totalCovered,
|
|
63
|
+
newLeaves: reallyNewLeaves.length,
|
|
64
|
+
threshold: FRESH_THRESHOLD,
|
|
65
|
+
});
|
|
66
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
67
|
+
}
|
|
68
|
+
// 5. Build the in-memory tree from stored nodes.
|
|
69
|
+
const tree = rehydrateFromStored(storedNodes);
|
|
70
|
+
if (!tree || tree.nodes.size === 0) {
|
|
71
|
+
logger?.info("raptor_incremental_rehydrate_failed", {
|
|
72
|
+
sessionId: opts.sessionId,
|
|
73
|
+
});
|
|
74
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
75
|
+
}
|
|
76
|
+
// Group stored nodes by level.
|
|
77
|
+
const levelGroups = new Map();
|
|
78
|
+
for (const n of storedNodes) {
|
|
79
|
+
const g = levelGroups.get(n.level);
|
|
80
|
+
if (g)
|
|
81
|
+
g.push(n);
|
|
82
|
+
else
|
|
83
|
+
levelGroups.set(n.level, [n]);
|
|
84
|
+
}
|
|
85
|
+
// Level-0 covers leaf ids directly.
|
|
86
|
+
const level0Nodes = levelGroups.get(0) ??
|
|
87
|
+
storedNodes.filter((n) => {
|
|
88
|
+
// A node is level 0 if its level is the minimum.
|
|
89
|
+
const minLevel = Math.min(...storedNodes.map((x) => x.level));
|
|
90
|
+
return n.level === minLevel;
|
|
91
|
+
});
|
|
92
|
+
if (level0Nodes.length === 0) {
|
|
93
|
+
// Single-root tree with no hierarchy: cannot increment, fall back.
|
|
94
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
95
|
+
}
|
|
96
|
+
// For each new leaf, assign to the nearest Level-0 cluster node.
|
|
97
|
+
const leafEmbeddings = new Map();
|
|
98
|
+
for (const l of reallyNewLeaves)
|
|
99
|
+
leafEmbeddings.set(l.id, l.embedding);
|
|
100
|
+
// Track which Level-0 nodes changed (by their stored id).
|
|
101
|
+
const affectedNodeIds = new Set();
|
|
102
|
+
for (const leaf of reallyNewLeaves) {
|
|
103
|
+
const emb = leafEmbeddings.get(leaf.id);
|
|
104
|
+
if (!emb || emb.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
// Find best matching Level-0 node.
|
|
107
|
+
let bestNode = null;
|
|
108
|
+
let bestSim = -Infinity;
|
|
109
|
+
for (const cn of level0Nodes) {
|
|
110
|
+
const sim = cosineSimilarity(emb, cn.embedding);
|
|
111
|
+
if (sim > bestSim) {
|
|
112
|
+
bestSim = sim;
|
|
113
|
+
bestNode = cn;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!bestNode)
|
|
117
|
+
continue;
|
|
118
|
+
// Add the leaf id to this node's children.
|
|
119
|
+
const updated = tree.nodes.get(bestNode.id);
|
|
120
|
+
if (updated) {
|
|
121
|
+
if (!updated.children.includes(leaf.id)) {
|
|
122
|
+
updated.children.push(leaf.id);
|
|
123
|
+
}
|
|
124
|
+
affectedNodeIds.add(bestNode.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (affectedNodeIds.size === 0) {
|
|
128
|
+
// No matches found — fall back to full rebuild.
|
|
129
|
+
logger?.info("raptor_incremental_no_matches", {
|
|
130
|
+
sessionId: opts.sessionId,
|
|
131
|
+
});
|
|
132
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
133
|
+
}
|
|
134
|
+
// 6. Recompute embeddings and summaries for affected nodes, bottom-up.
|
|
135
|
+
// Build a map: leafId → leaf info for summarization.
|
|
136
|
+
const leafMap = new Map();
|
|
137
|
+
for (const l of existingLeaves)
|
|
138
|
+
leafMap.set(l.id, l);
|
|
139
|
+
for (const l of reallyNewLeaves)
|
|
140
|
+
leafMap.set(l.id, l);
|
|
141
|
+
// Recompute affected Level-0 nodes.
|
|
142
|
+
for (const nid of affectedNodeIds) {
|
|
143
|
+
const node = tree.nodes.get(nid);
|
|
144
|
+
if (!node)
|
|
145
|
+
continue;
|
|
146
|
+
const coveredLeaves = node.children
|
|
147
|
+
.map((cid) => leafMap.get(cid))
|
|
148
|
+
.filter((l) => !!l);
|
|
149
|
+
if (coveredLeaves.length > 0) {
|
|
150
|
+
node.embedding = meanVector(coveredLeaves.map((l) => l.embedding));
|
|
151
|
+
// Summarize: flatten messages from all covered leaves.
|
|
152
|
+
const messages = coveredLeaves.flatMap((l) => l.messages);
|
|
153
|
+
if (messages.length > 0) {
|
|
154
|
+
const cs = summarizeCluster(messages);
|
|
155
|
+
node.summary = cs.summary;
|
|
156
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Propagate up: for each higher level, recompute nodes whose leaf set
|
|
161
|
+
// includes any newly inserted leaf. Since `children` stores flattened leaf
|
|
162
|
+
// ids (not node ids), we check intersection with the new leaf id set.
|
|
163
|
+
const newLeafIds = new Set(reallyNewLeaves.map((l) => l.id));
|
|
164
|
+
const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b);
|
|
165
|
+
for (const level of sortedLevels.slice(1)) {
|
|
166
|
+
const levelNodes = levelGroups.get(level) ?? [];
|
|
167
|
+
for (const stored of levelNodes) {
|
|
168
|
+
const node = tree.nodes.get(stored.id);
|
|
169
|
+
if (!node)
|
|
170
|
+
continue;
|
|
171
|
+
if (node.children.some((cid) => newLeafIds.has(cid))) {
|
|
172
|
+
// Recompute embedding as centroid of covered leaves.
|
|
173
|
+
const covered = node.children
|
|
174
|
+
.map((cid) => leafMap.get(cid))
|
|
175
|
+
.filter((l) => !!l);
|
|
176
|
+
if (covered.length > 0) {
|
|
177
|
+
node.embedding = meanVector(covered.map((l) => l.embedding));
|
|
178
|
+
const messages = covered.flatMap((l) => l.messages);
|
|
179
|
+
if (messages.length > 0) {
|
|
180
|
+
const cs = summarizeCluster(messages);
|
|
181
|
+
node.summary = cs.summary;
|
|
182
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 7. Persist the full updated tree (delete + reinsert in a tx).
|
|
189
|
+
tree.builtAt = builtAt;
|
|
190
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
191
|
+
logger?.info("raptor_incremental_success", {
|
|
192
|
+
sessionId: opts.sessionId,
|
|
193
|
+
nodeCount: tree.nodes.size,
|
|
194
|
+
newLeaves: reallyNewLeaves.length,
|
|
195
|
+
affectedNodes: affectedNodeIds.size,
|
|
196
|
+
});
|
|
197
|
+
return tree;
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
logger?.error("raptor_incremental_failed", {
|
|
201
|
+
sessionId: opts.sessionId,
|
|
202
|
+
error: String(e instanceof Error ? e.message : e),
|
|
203
|
+
});
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Helpers
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* Full rebuild fallback: merges existingLeaves + newLeaves, builds from scratch.
|
|
212
|
+
*/
|
|
213
|
+
function fullRebuild(existingLeaves, newLeaves, opts, logger) {
|
|
214
|
+
logger?.info("raptor_incremental_full_rebuild", {
|
|
215
|
+
sessionId: opts.sessionId,
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
// Merge: deduplicate by leaf id.
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
const allLeaves = [];
|
|
221
|
+
for (const l of existingLeaves) {
|
|
222
|
+
if (!seen.has(l.id)) {
|
|
223
|
+
seen.add(l.id);
|
|
224
|
+
allLeaves.push(l);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const l of newLeaves) {
|
|
228
|
+
if (!seen.has(l.id)) {
|
|
229
|
+
seen.add(l.id);
|
|
230
|
+
allLeaves.push(l);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const tree = buildRaptorTree(allLeaves, {
|
|
234
|
+
embedder: opts.embedder ?? getDefaultEmbedder(),
|
|
235
|
+
budgetMs: opts.budgetMs,
|
|
236
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
237
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
238
|
+
});
|
|
239
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
240
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
241
|
+
return tree;
|
|
242
|
+
}
|
|
243
|
+
catch (e2) {
|
|
244
|
+
logger?.error("raptor_incremental_full_rebuild_failed", {
|
|
245
|
+
sessionId: opts.sessionId,
|
|
246
|
+
error: String(e2 instanceof Error ? e2.message : e2),
|
|
247
|
+
});
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Rebuild the in-memory RaptorTree from stored nodes. */
|
|
252
|
+
function rehydrateFromStored(nodes) {
|
|
253
|
+
if (nodes.length === 0)
|
|
254
|
+
return null;
|
|
255
|
+
const root = nodes.reduce((best, n) => (!best || n.level > (best?.level ?? -1) ? n : best), null);
|
|
256
|
+
const tree = {
|
|
257
|
+
nodes: new Map(nodes.map((n) => [
|
|
258
|
+
n.id,
|
|
259
|
+
{
|
|
260
|
+
id: n.id,
|
|
261
|
+
level: n.level,
|
|
262
|
+
parentId: n.parentId,
|
|
263
|
+
children: n.children,
|
|
264
|
+
summary: n.summary,
|
|
265
|
+
embedding: n.embedding,
|
|
266
|
+
qualityMarker: n.qualityMarker,
|
|
267
|
+
tokenEstimate: n.tokenEstimate,
|
|
268
|
+
},
|
|
269
|
+
])),
|
|
270
|
+
rootId: root?.id ?? null,
|
|
271
|
+
levels: Math.max(1, ...nodes.map((n) => n.level + 1)),
|
|
272
|
+
timedOut: root != null && root.level >= 99,
|
|
273
|
+
builtAt: nodes.reduce((max, n) => Math.max(max, n.builtAt), 0),
|
|
274
|
+
};
|
|
275
|
+
return tree;
|
|
276
|
+
}
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { defaultEmbedder } from "../../embedder.js";
|
|
13
13
|
import { buildRaptorTree } from "./tree.js";
|
|
14
|
+
import { incrementRaptorTree } from "./incremental.js";
|
|
14
15
|
import { stagedExpansion } from "./retrieval.js";
|
|
15
16
|
import { saveRaptorTree, listRaptorNodes } from "../../store/sqlite.js";
|
|
16
17
|
import { insertBuildHistory, computeCoherenceScore } from "./buildHistory.js";
|
|
18
|
+
import { loadDedupConfig } from "../../config/dedup.js";
|
|
17
19
|
/** Shadow mode is on by default; set RAPTOR_SHADOW_MODE=false to serve live. */
|
|
18
20
|
export function isShadowMode() {
|
|
19
21
|
return process.env.RAPTOR_SHADOW_MODE !== "false";
|
|
@@ -30,15 +32,37 @@ export function runRaptor(leaves, opts) {
|
|
|
30
32
|
const embedder = opts.embedder ?? defaultEmbedder();
|
|
31
33
|
const logger = opts.logger;
|
|
32
34
|
const startedAt = opts.builtAt ?? Date.now();
|
|
35
|
+
const builtAt = startedAt;
|
|
33
36
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Try incremental update first when the flag is ON (Sprint 26, #7).
|
|
38
|
+
// The incremental path reads the existing tree, diffs by leaf id, and
|
|
39
|
+
// inserts only new leaves into the nearest clusters. Falls back to a full
|
|
40
|
+
// rebuild when the tree is missing, corrupted, or >50% of nodes are new.
|
|
41
|
+
const cfg = loadDedupConfig();
|
|
42
|
+
let tree = null;
|
|
43
|
+
if (cfg.RAPTOR_INCREMENTAL) {
|
|
44
|
+
tree = incrementRaptorTree(leaves, leaves, {
|
|
45
|
+
embedder,
|
|
46
|
+
stateDir: opts.stateDir,
|
|
47
|
+
sessionId: opts.sessionId,
|
|
48
|
+
budgetMs: opts.budgetMs,
|
|
49
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
50
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
51
|
+
logger,
|
|
52
|
+
builtAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Fall back to full rebuild when incremental returned null (not attempted,
|
|
56
|
+
// tree missing, ratio exceeded, or internal error).
|
|
57
|
+
if (!tree) {
|
|
58
|
+
tree = buildRaptorTree(leaves, {
|
|
59
|
+
embedder,
|
|
60
|
+
budgetMs: opts.budgetMs,
|
|
61
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
62
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
63
|
+
});
|
|
64
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
65
|
+
}
|
|
42
66
|
logger?.info("raptor_build", {
|
|
43
67
|
sessionId: opts.sessionId,
|
|
44
68
|
nodes: tree.nodes.size,
|
|
@@ -17,7 +17,7 @@ function ollamaEndpoint() {
|
|
|
17
17
|
const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
|
|
18
18
|
if (!model)
|
|
19
19
|
return null;
|
|
20
|
-
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434";
|
|
20
|
+
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434"; // guardrails-allow PREVENT-PI-004: loopback-only Ollama endpoint for local RAPTOR summarization
|
|
21
21
|
// Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
|
|
22
22
|
if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
|
|
23
23
|
throw new Error(`MEGACOMPACT_RAPTOR_URL must be localhost/127.0.0.1 (got ${base}). ` +
|
package/dist/src/httpEmbedder.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { l2Normalize } from "./embedder.js";
|
|
28
28
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
29
|
-
import { isIP } from "node:net";
|
|
29
|
+
import { isIP } from "node:net"; // guardrails-allow PREVENT-PI-004: localhost-only loopback address validation in BYO embedding server
|
|
30
30
|
// Inline worker script: resolves a hostname via dns.lookup in a child process
|
|
31
31
|
// (dns.lookup is callback-async; the child has its own event loop). Used to
|
|
32
32
|
// verify that a hostname in the embedding URL resolves to loopback ONLY.
|
|
@@ -293,7 +293,7 @@ export function registerContextHandler(
|
|
|
293
293
|
runtime.trimCache.checkpointId === runtime.rt.lastCheckpointId &&
|
|
294
294
|
runtime.trimCache.cut <= messages.length
|
|
295
295
|
) {
|
|
296
|
-
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002
|
|
296
|
+
const recent = messages.slice(runtime.trimCache.cut); // guardrails-allow PREVENT-PI-002: cached `cut` was sanitized by computeLiveTrimCut (src/boundary.ts); replayed verbatim, transcript only grows within an epoch.
|
|
297
297
|
runtime.diagLiveTrimFires++;
|
|
298
298
|
runtime.diagLiveTrimReplays++;
|
|
299
299
|
runtime.snapshot(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.10",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
package/src/config/dedup.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface DedupConfigShape {
|
|
|
66
66
|
RAPTOR_MULTILEVEL_ENABLED: boolean;
|
|
67
67
|
RAPTOR_LEVEL_WEIGHTS: number[]; // per-level weights, index 0 = leaves (uncalibrated)
|
|
68
68
|
RAPTOR_LEAF_EXPANSION: boolean;
|
|
69
|
+
RAPTOR_INCREMENTAL: boolean;
|
|
69
70
|
RAPTOR_MAX_LEAF_EXPANSION: number; // uncalibrated
|
|
70
71
|
RAPTOR_FRESHNESS_HOURS: number; // S42D: skip rebuild when tree is fresh (uncalibrated)
|
|
71
72
|
/** S25 Phase-2: inject top-level RAPTOR summary nodes (root + level-1
|
|
@@ -109,6 +110,7 @@ export function loadDedupConfig(): DedupConfigShape {
|
|
|
109
110
|
RAPTOR_MULTILEVEL_ENABLED: envBool("MEGACOMPACT_RAPTOR_MULTILEVEL", true),
|
|
110
111
|
RAPTOR_LEVEL_WEIGHTS: envNumArray("MEGACOMPACT_RAPTOR_LEVEL_WEIGHTS", [1.0, 0.9, 0.8, 0.7, 0.5]),
|
|
111
112
|
RAPTOR_LEAF_EXPANSION: envBool("MEGACOMPACT_RAPTOR_LEAF_EXPANSION", true),
|
|
113
|
+
RAPTOR_INCREMENTAL: envBool("MEGACOMPACT_RAPTOR_INCREMENTAL", true),
|
|
112
114
|
RAPTOR_MAX_LEAF_EXPANSION: envNum("MEGACOMPACT_RAPTOR_MAX_LEAF_EXP", 10),
|
|
113
115
|
RAPTOR_FRESHNESS_HOURS: envNum("MEGACOMPACT_RAPTOR_FRESHNESS_HOURS", 4),
|
|
114
116
|
RAPTOR_INJECT_SUMMARIES: envBool("MEGACOMPACT_RAPTOR_INJECT_SUMMARIES", true),
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* incremental.ts — incremental RAPTOR tree update (Sprint 26, #7).
|
|
3
|
+
*
|
|
4
|
+
* Instead of rebuilding the full tree (~2s per compaction), insert only the new
|
|
5
|
+
* checkpoints since the last build and recompute only the affected cluster
|
|
6
|
+
* assignments up the tree. Falls back to a full rebuild when the tree is
|
|
7
|
+
* missing, corrupted, or >50% of nodes are new.
|
|
8
|
+
*
|
|
9
|
+
* Non-fatal: every failure logs and returns null (caller falls back to full
|
|
10
|
+
* rebuild). PREVENT-PI-004: zero network; all operations are local SQLite + CPU.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Embedder, Vector } from "../../embedder.js";
|
|
14
|
+
import { cosineSimilarity } from "../../embedder.js";
|
|
15
|
+
import { defaultEmbedder as getDefaultEmbedder } from "../../embedder.js";
|
|
16
|
+
import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
|
|
17
|
+
import type { QualityMarker } from "./guardrails.js";
|
|
18
|
+
import { meanVector } from "./kmeans.js";
|
|
19
|
+
import { summarizeCluster } from "./summarizer.js";
|
|
20
|
+
import type { Logger } from "../../log.js";
|
|
21
|
+
import {
|
|
22
|
+
saveRaptorTree,
|
|
23
|
+
listRaptorNodes,
|
|
24
|
+
type StoredRaptorNode,
|
|
25
|
+
} from "../../store/sqlite.js";
|
|
26
|
+
|
|
27
|
+
/** When >FRESH_THRESHOLD of existing leaves are new, a full rebuild is cheaper. */
|
|
28
|
+
const FRESH_THRESHOLD = 0.5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Incrementally update a persisted RAPTOR tree with new leaves.
|
|
32
|
+
*
|
|
33
|
+
* 1. Reads the existing tree from the store.
|
|
34
|
+
* 2. Filters `newLeaves` to those NOT already covered by the tree.
|
|
35
|
+
* 3. If no new leaves → early-return (existing tree).
|
|
36
|
+
* 4. If no existing tree or >50% new → full rebuild.
|
|
37
|
+
* 5. For each new leaf, finds the best-matching Level-0 cluster node,
|
|
38
|
+
* reassigns (inserts into its children), updates that node's summary
|
|
39
|
+
* and centroid embedding.
|
|
40
|
+
* 6. Propagates changes up through parent levels to the root.
|
|
41
|
+
* 7. Persists the updated tree (full delete + reinsert in a tx).
|
|
42
|
+
*
|
|
43
|
+
* Never throws. Returns the updated tree, or null on failure/fallback.
|
|
44
|
+
*/
|
|
45
|
+
export function incrementRaptorTree(
|
|
46
|
+
existingLeaves: Leaf[],
|
|
47
|
+
newLeaves: Leaf[],
|
|
48
|
+
opts: {
|
|
49
|
+
embedder?: Embedder;
|
|
50
|
+
stateDir: string;
|
|
51
|
+
sessionId: string;
|
|
52
|
+
budgetMs?: number;
|
|
53
|
+
clustersPerLevel?: number;
|
|
54
|
+
consistencyThreshold?: number;
|
|
55
|
+
logger?: Logger;
|
|
56
|
+
builtAt?: number;
|
|
57
|
+
},
|
|
58
|
+
): RaptorTree | null {
|
|
59
|
+
const logger = opts.logger;
|
|
60
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 1. Read existing tree.
|
|
64
|
+
const storedNodes = listRaptorNodes(opts.sessionId, opts.stateDir);
|
|
65
|
+
if (storedNodes.length === 0) {
|
|
66
|
+
logger?.info("raptor_incremental_no_tree", { sessionId: opts.sessionId });
|
|
67
|
+
return null; // no existing tree — caller falls back to full buildRaptorTree
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Find new leaf ids not already in the tree.
|
|
71
|
+
const coveredLeafIds = new Set<string>();
|
|
72
|
+
for (const n of storedNodes) {
|
|
73
|
+
for (const cid of n.children) coveredLeafIds.add(cid);
|
|
74
|
+
}
|
|
75
|
+
const reallyNewLeaves = newLeaves.filter(
|
|
76
|
+
(l) => !coveredLeafIds.has(l.id),
|
|
77
|
+
);
|
|
78
|
+
const totalCovered = coveredLeafIds.size;
|
|
79
|
+
|
|
80
|
+
if (reallyNewLeaves.length === 0) {
|
|
81
|
+
logger?.info("raptor_incremental_no_new", { sessionId: opts.sessionId });
|
|
82
|
+
return rehydrateFromStored(storedNodes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. Fallback guard: >50% new → full rebuild.
|
|
86
|
+
if (
|
|
87
|
+
totalCovered > 0 &&
|
|
88
|
+
reallyNewLeaves.length / totalCovered > FRESH_THRESHOLD
|
|
89
|
+
) {
|
|
90
|
+
logger?.info("raptor_incremental_fallback_ratio", {
|
|
91
|
+
sessionId: opts.sessionId,
|
|
92
|
+
coveredNodes: totalCovered,
|
|
93
|
+
newLeaves: reallyNewLeaves.length,
|
|
94
|
+
threshold: FRESH_THRESHOLD,
|
|
95
|
+
});
|
|
96
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Build the in-memory tree from stored nodes.
|
|
100
|
+
const tree = rehydrateFromStored(storedNodes);
|
|
101
|
+
if (!tree || tree.nodes.size === 0) {
|
|
102
|
+
logger?.info("raptor_incremental_rehydrate_failed", {
|
|
103
|
+
sessionId: opts.sessionId,
|
|
104
|
+
});
|
|
105
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Group stored nodes by level.
|
|
109
|
+
const levelGroups = new Map<number, StoredRaptorNode[]>();
|
|
110
|
+
for (const n of storedNodes) {
|
|
111
|
+
const g = levelGroups.get(n.level);
|
|
112
|
+
if (g) g.push(n);
|
|
113
|
+
else levelGroups.set(n.level, [n]);
|
|
114
|
+
}
|
|
115
|
+
// Level-0 covers leaf ids directly.
|
|
116
|
+
const level0Nodes =
|
|
117
|
+
levelGroups.get(0) ??
|
|
118
|
+
storedNodes.filter((n) => {
|
|
119
|
+
// A node is level 0 if its level is the minimum.
|
|
120
|
+
const minLevel = Math.min(...storedNodes.map((x) => x.level));
|
|
121
|
+
return n.level === minLevel;
|
|
122
|
+
});
|
|
123
|
+
if (level0Nodes.length === 0) {
|
|
124
|
+
// Single-root tree with no hierarchy: cannot increment, fall back.
|
|
125
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// For each new leaf, assign to the nearest Level-0 cluster node.
|
|
129
|
+
const leafEmbeddings = new Map<string, Vector>();
|
|
130
|
+
for (const l of reallyNewLeaves) leafEmbeddings.set(l.id, l.embedding);
|
|
131
|
+
|
|
132
|
+
// Track which Level-0 nodes changed (by their stored id).
|
|
133
|
+
const affectedNodeIds = new Set<string>();
|
|
134
|
+
|
|
135
|
+
for (const leaf of reallyNewLeaves) {
|
|
136
|
+
const emb = leafEmbeddings.get(leaf.id);
|
|
137
|
+
if (!emb || emb.length === 0) continue;
|
|
138
|
+
|
|
139
|
+
// Find best matching Level-0 node.
|
|
140
|
+
let bestNode: StoredRaptorNode | null = null;
|
|
141
|
+
let bestSim = -Infinity;
|
|
142
|
+
for (const cn of level0Nodes) {
|
|
143
|
+
const sim = cosineSimilarity(emb, cn.embedding);
|
|
144
|
+
if (sim > bestSim) {
|
|
145
|
+
bestSim = sim;
|
|
146
|
+
bestNode = cn;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!bestNode) continue;
|
|
150
|
+
|
|
151
|
+
// Add the leaf id to this node's children.
|
|
152
|
+
const updated = tree.nodes.get(bestNode.id);
|
|
153
|
+
if (updated) {
|
|
154
|
+
if (!updated.children.includes(leaf.id)) {
|
|
155
|
+
updated.children.push(leaf.id);
|
|
156
|
+
}
|
|
157
|
+
affectedNodeIds.add(bestNode.id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (affectedNodeIds.size === 0) {
|
|
162
|
+
// No matches found — fall back to full rebuild.
|
|
163
|
+
logger?.info("raptor_incremental_no_matches", {
|
|
164
|
+
sessionId: opts.sessionId,
|
|
165
|
+
});
|
|
166
|
+
return fullRebuild(existingLeaves, reallyNewLeaves, opts, logger);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 6. Recompute embeddings and summaries for affected nodes, bottom-up.
|
|
170
|
+
// Build a map: leafId → leaf info for summarization.
|
|
171
|
+
const leafMap = new Map<string, Leaf>();
|
|
172
|
+
for (const l of existingLeaves) leafMap.set(l.id, l);
|
|
173
|
+
for (const l of reallyNewLeaves) leafMap.set(l.id, l);
|
|
174
|
+
|
|
175
|
+
// Recompute affected Level-0 nodes.
|
|
176
|
+
for (const nid of affectedNodeIds) {
|
|
177
|
+
const node = tree.nodes.get(nid);
|
|
178
|
+
if (!node) continue;
|
|
179
|
+
const coveredLeaves = node.children
|
|
180
|
+
.map((cid) => leafMap.get(cid))
|
|
181
|
+
.filter((l): l is Leaf => !!l);
|
|
182
|
+
if (coveredLeaves.length > 0) {
|
|
183
|
+
node.embedding = meanVector(
|
|
184
|
+
coveredLeaves.map((l) => l.embedding),
|
|
185
|
+
);
|
|
186
|
+
// Summarize: flatten messages from all covered leaves.
|
|
187
|
+
const messages = coveredLeaves.flatMap((l) => l.messages);
|
|
188
|
+
if (messages.length > 0) {
|
|
189
|
+
const cs = summarizeCluster(messages);
|
|
190
|
+
node.summary = cs.summary;
|
|
191
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Propagate up: for each higher level, recompute nodes whose leaf set
|
|
197
|
+
// includes any newly inserted leaf. Since `children` stores flattened leaf
|
|
198
|
+
// ids (not node ids), we check intersection with the new leaf id set.
|
|
199
|
+
const newLeafIds = new Set(reallyNewLeaves.map((l) => l.id));
|
|
200
|
+
const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b);
|
|
201
|
+
for (const level of sortedLevels.slice(1)) {
|
|
202
|
+
const levelNodes = levelGroups.get(level) ?? [];
|
|
203
|
+
for (const stored of levelNodes) {
|
|
204
|
+
const node = tree.nodes.get(stored.id);
|
|
205
|
+
if (!node) continue;
|
|
206
|
+
|
|
207
|
+
if (node.children.some((cid) => newLeafIds.has(cid))) {
|
|
208
|
+
// Recompute embedding as centroid of covered leaves.
|
|
209
|
+
const covered = node.children
|
|
210
|
+
.map((cid) => leafMap.get(cid))
|
|
211
|
+
.filter((l): l is Leaf => !!l);
|
|
212
|
+
if (covered.length > 0) {
|
|
213
|
+
node.embedding = meanVector(
|
|
214
|
+
covered.map((l) => l.embedding),
|
|
215
|
+
);
|
|
216
|
+
const messages = covered.flatMap((l) => l.messages);
|
|
217
|
+
if (messages.length > 0) {
|
|
218
|
+
const cs = summarizeCluster(messages);
|
|
219
|
+
node.summary = cs.summary;
|
|
220
|
+
node.tokenEstimate = cs.tokenEstimate;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 7. Persist the full updated tree (delete + reinsert in a tx).
|
|
228
|
+
tree.builtAt = builtAt;
|
|
229
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
230
|
+
|
|
231
|
+
logger?.info("raptor_incremental_success", {
|
|
232
|
+
sessionId: opts.sessionId,
|
|
233
|
+
nodeCount: tree.nodes.size,
|
|
234
|
+
newLeaves: reallyNewLeaves.length,
|
|
235
|
+
affectedNodes: affectedNodeIds.size,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return tree;
|
|
239
|
+
} catch (e) {
|
|
240
|
+
logger?.error("raptor_incremental_failed", {
|
|
241
|
+
sessionId: opts.sessionId,
|
|
242
|
+
error: String(e instanceof Error ? e.message : e),
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Helpers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Full rebuild fallback: merges existingLeaves + newLeaves, builds from scratch.
|
|
254
|
+
*/
|
|
255
|
+
function fullRebuild(
|
|
256
|
+
existingLeaves: Leaf[],
|
|
257
|
+
newLeaves: Leaf[],
|
|
258
|
+
opts: {
|
|
259
|
+
embedder?: Embedder;
|
|
260
|
+
budgetMs?: number;
|
|
261
|
+
clustersPerLevel?: number;
|
|
262
|
+
consistencyThreshold?: number;
|
|
263
|
+
stateDir: string;
|
|
264
|
+
sessionId: string;
|
|
265
|
+
logger?: Logger;
|
|
266
|
+
builtAt?: number;
|
|
267
|
+
},
|
|
268
|
+
logger?: Logger,
|
|
269
|
+
): RaptorTree | null {
|
|
270
|
+
logger?.info("raptor_incremental_full_rebuild", {
|
|
271
|
+
sessionId: opts.sessionId,
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
// Merge: deduplicate by leaf id.
|
|
275
|
+
const seen = new Set<string>();
|
|
276
|
+
const allLeaves: Leaf[] = [];
|
|
277
|
+
for (const l of existingLeaves) {
|
|
278
|
+
if (!seen.has(l.id)) {
|
|
279
|
+
seen.add(l.id);
|
|
280
|
+
allLeaves.push(l);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const l of newLeaves) {
|
|
284
|
+
if (!seen.has(l.id)) {
|
|
285
|
+
seen.add(l.id);
|
|
286
|
+
allLeaves.push(l);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const tree = buildRaptorTree(allLeaves, {
|
|
290
|
+
embedder: opts.embedder ?? getDefaultEmbedder(),
|
|
291
|
+
budgetMs: opts.budgetMs,
|
|
292
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
293
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
294
|
+
});
|
|
295
|
+
const builtAt = opts.builtAt ?? Date.now();
|
|
296
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
297
|
+
return tree;
|
|
298
|
+
} catch (e2) {
|
|
299
|
+
logger?.error("raptor_incremental_full_rebuild_failed", {
|
|
300
|
+
sessionId: opts.sessionId,
|
|
301
|
+
error: String(e2 instanceof Error ? e2.message : e2),
|
|
302
|
+
});
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Rebuild the in-memory RaptorTree from stored nodes. */
|
|
308
|
+
function rehydrateFromStored(
|
|
309
|
+
nodes: StoredRaptorNode[],
|
|
310
|
+
): RaptorTree | null {
|
|
311
|
+
if (nodes.length === 0) return null;
|
|
312
|
+
const root = nodes.reduce<StoredRaptorNode | null>(
|
|
313
|
+
(best, n) => (!best || n.level > (best?.level ?? -1) ? n : best),
|
|
314
|
+
null,
|
|
315
|
+
);
|
|
316
|
+
const tree: RaptorTree = {
|
|
317
|
+
nodes: new Map(
|
|
318
|
+
nodes.map((n) => [
|
|
319
|
+
n.id,
|
|
320
|
+
{
|
|
321
|
+
id: n.id,
|
|
322
|
+
level: n.level,
|
|
323
|
+
parentId: n.parentId,
|
|
324
|
+
children: n.children,
|
|
325
|
+
summary: n.summary,
|
|
326
|
+
embedding: n.embedding,
|
|
327
|
+
qualityMarker: n.qualityMarker as QualityMarker,
|
|
328
|
+
tokenEstimate: n.tokenEstimate,
|
|
329
|
+
},
|
|
330
|
+
]),
|
|
331
|
+
),
|
|
332
|
+
rootId: root?.id ?? null,
|
|
333
|
+
levels: Math.max(1, ...nodes.map((n) => n.level + 1)),
|
|
334
|
+
timedOut: root != null && root.level >= 99,
|
|
335
|
+
builtAt: nodes.reduce((max, n) => Math.max(max, n.builtAt), 0),
|
|
336
|
+
};
|
|
337
|
+
return tree;
|
|
338
|
+
}
|
|
339
|
+
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import type { Embedder } from "../../embedder.js";
|
|
14
14
|
import { defaultEmbedder } from "../../embedder.js";
|
|
15
15
|
import { buildRaptorTree, type Leaf, type RaptorTree } from "./tree.js";
|
|
16
|
+
import { incrementRaptorTree } from "./incremental.js";
|
|
16
17
|
import { stagedExpansion } from "./retrieval.js";
|
|
17
18
|
import { Logger } from "../../log.js";
|
|
18
19
|
import { saveRaptorTree, listRaptorNodes } from "../../store/sqlite.js";
|
|
19
20
|
import { insertBuildHistory, computeCoherenceScore } from "./buildHistory.js";
|
|
21
|
+
import { loadDedupConfig } from "../../config/dedup.js";
|
|
20
22
|
|
|
21
23
|
/** Shadow mode is on by default; set RAPTOR_SHADOW_MODE=false to serve live. */
|
|
22
24
|
export function isShadowMode(): boolean {
|
|
@@ -51,15 +53,38 @@ export function runRaptor(
|
|
|
51
53
|
const embedder = opts.embedder ?? defaultEmbedder();
|
|
52
54
|
const logger = opts.logger;
|
|
53
55
|
const startedAt = opts.builtAt ?? Date.now();
|
|
56
|
+
const builtAt = startedAt;
|
|
54
57
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
// Try incremental update first when the flag is ON (Sprint 26, #7).
|
|
59
|
+
// The incremental path reads the existing tree, diffs by leaf id, and
|
|
60
|
+
// inserts only new leaves into the nearest clusters. Falls back to a full
|
|
61
|
+
// rebuild when the tree is missing, corrupted, or >50% of nodes are new.
|
|
62
|
+
const cfg = loadDedupConfig();
|
|
63
|
+
let tree: RaptorTree | null = null;
|
|
64
|
+
if (cfg.RAPTOR_INCREMENTAL) {
|
|
65
|
+
tree = incrementRaptorTree(leaves, leaves, {
|
|
66
|
+
embedder,
|
|
67
|
+
stateDir: opts.stateDir,
|
|
68
|
+
sessionId: opts.sessionId,
|
|
69
|
+
budgetMs: opts.budgetMs,
|
|
70
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
71
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
72
|
+
logger,
|
|
73
|
+
builtAt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fall back to full rebuild when incremental returned null (not attempted,
|
|
78
|
+
// tree missing, ratio exceeded, or internal error).
|
|
79
|
+
if (!tree) {
|
|
80
|
+
tree = buildRaptorTree(leaves, {
|
|
81
|
+
embedder,
|
|
82
|
+
budgetMs: opts.budgetMs,
|
|
83
|
+
clustersPerLevel: opts.clustersPerLevel,
|
|
84
|
+
consistencyThreshold: opts.consistencyThreshold,
|
|
85
|
+
});
|
|
86
|
+
saveRaptorTree(opts.sessionId, tree, builtAt, opts.stateDir);
|
|
87
|
+
}
|
|
63
88
|
logger?.info("raptor_build", {
|
|
64
89
|
sessionId: opts.sessionId,
|
|
65
90
|
nodes: tree.nodes.size,
|
|
@@ -24,7 +24,7 @@ export interface ClusterSummary {
|
|
|
24
24
|
function ollamaEndpoint(): { url: string; model: string } | null {
|
|
25
25
|
const model = process.env.MEGACOMPACT_RAPTOR_MODEL;
|
|
26
26
|
if (!model) return null;
|
|
27
|
-
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434";
|
|
27
|
+
const base = process.env.MEGACOMPACT_RAPTOR_URL ?? "http://127.0.0.1:11434"; // guardrails-allow PREVENT-PI-004: loopback-only Ollama endpoint for local RAPTOR summarization
|
|
28
28
|
// Guard: only loopback is permitted (remote Ollama would violate PREVENT-PI-004).
|
|
29
29
|
if (!/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(base)) {
|
|
30
30
|
throw new Error(
|
package/src/httpEmbedder.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import type { Embedder, Vector } from "./embedder.js";
|
|
29
29
|
import { l2Normalize } from "./embedder.js";
|
|
30
30
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
31
|
-
import { isIP } from "node:net";
|
|
31
|
+
import { isIP } from "node:net"; // guardrails-allow PREVENT-PI-004: localhost-only loopback address validation in BYO embedding server
|
|
32
32
|
|
|
33
33
|
export interface HttpEmbedderOptions {
|
|
34
34
|
url: string;
|