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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serve-gate.test.ts — S25 RAPTOR promotion acceptance tests.
|
|
3
|
+
*
|
|
4
|
+
* Five gates that must ALL pass before the RAPTOR tree is trusted as the live
|
|
5
|
+
* recall surface:
|
|
6
|
+
* 1. Shadow gate (RAPTOR_SHADOW_MODE != false → no RAPTOR merge)
|
|
7
|
+
* 2. Stale tree fallback (builtAt < max checkpoint timestamp → flat)
|
|
8
|
+
* 3. timedOut fallback (extractive-fallback root → flat)
|
|
9
|
+
* 4. Coverage breadth (2-topic fixture, RAPTOR hits both vs flat misses one)
|
|
10
|
+
* 5. p95 latency (200-checkpoint × 20-query median < 100 ms)
|
|
11
|
+
*
|
|
12
|
+
* No network — default extractive summarizer + trigram embedder.
|
|
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 { VectorStore, vectorList, vectorSearch } from "../../vectorStore.js";
|
|
20
|
+
import { runRaptor, isShadowMode } from "./index.js";
|
|
21
|
+
import { compactSession } from "../../engine.js";
|
|
22
|
+
import { Logger } from "../../log.js";
|
|
23
|
+
import { loadDedupConfig } from "../../config/dedup.js";
|
|
24
|
+
import { listRaptorNodes } from "../../store/sqlite.js";
|
|
25
|
+
import { normalizeSessionId } from "../../store.js";
|
|
26
|
+
/* ------------------------------------------------------------------ helpers */
|
|
27
|
+
let tmpDir;
|
|
28
|
+
let counter = 0;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-gate-"));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
function stateDir() {
|
|
36
|
+
return join(tmpDir, `run-${counter++}`);
|
|
37
|
+
}
|
|
38
|
+
/** Config with dedup tiers disabled so compactSession creates distinct checkpoints. */
|
|
39
|
+
function cfg(overrides) {
|
|
40
|
+
return {
|
|
41
|
+
...loadDedupConfig(),
|
|
42
|
+
RAPTOR_ENABLED: true,
|
|
43
|
+
L0_ENABLED: false,
|
|
44
|
+
L1_ENABLED: false,
|
|
45
|
+
L2_ENABLED: false,
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function msg(text, toolName) {
|
|
50
|
+
return toolName
|
|
51
|
+
? { role: "assistant", text, toolName, input: text, output: text }
|
|
52
|
+
: { role: "user", text };
|
|
53
|
+
}
|
|
54
|
+
function seedSession(store, sid, count, topic, startTs = 1) {
|
|
55
|
+
for (let i = 1; i <= count; i++) {
|
|
56
|
+
compactSession({
|
|
57
|
+
sessionId: sid,
|
|
58
|
+
messages: [
|
|
59
|
+
msg(`${topic} checkpoint ${i} with unique content`),
|
|
60
|
+
msg(`acknowledged ${i}`, "Edit"),
|
|
61
|
+
],
|
|
62
|
+
keepFrom: 2,
|
|
63
|
+
timestamp: startTs + i,
|
|
64
|
+
}, store);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function buildTree(store, sid) {
|
|
68
|
+
const nsid = normalizeSessionId(sid);
|
|
69
|
+
const all = vectorList(store, nsid);
|
|
70
|
+
const leaves = all.map((cp) => ({
|
|
71
|
+
id: cp.checkpointId,
|
|
72
|
+
messages: [],
|
|
73
|
+
sourceText: cp.normalizedText ?? cp.summary ?? cp.regionHash,
|
|
74
|
+
embedding: cp.embedding,
|
|
75
|
+
}));
|
|
76
|
+
return runRaptor(leaves, {
|
|
77
|
+
stateDir: store.stateDir,
|
|
78
|
+
sessionId: nsid,
|
|
79
|
+
logger: new Logger(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/* ------------------------------------------------------------------- tests */
|
|
83
|
+
// ─── Gate 1: shadow mode ────────────────────────────────────────────────────
|
|
84
|
+
test("gate 1: shadow mode (default) → search does NOT use RAPTOR merge", () => {
|
|
85
|
+
const sd = stateDir();
|
|
86
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
87
|
+
const sid = "shadow";
|
|
88
|
+
seedSession(s, sid, 5, "shadows");
|
|
89
|
+
buildTree(s, sid);
|
|
90
|
+
// Shadow mode is the default when env is not explicitly "false".
|
|
91
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
92
|
+
process.env.RAPTOR_SHADOW_MODE = "true";
|
|
93
|
+
try {
|
|
94
|
+
assert.ok(isShadowMode(), "shadow mode should be active");
|
|
95
|
+
// Search still returns results (flat fallback). Verify no crash.
|
|
96
|
+
const hits = vectorSearch(s, sid, "shadows checkpoint", 5);
|
|
97
|
+
assert.ok(hits.length > 0, "flat search returns results in shadow mode");
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (orig === undefined)
|
|
101
|
+
delete process.env.RAPTOR_SHADOW_MODE;
|
|
102
|
+
else
|
|
103
|
+
process.env.RAPTOR_SHADOW_MODE = orig;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// ─── Gate 2: stale tree fallback ────────────────────────────────────────────
|
|
107
|
+
test("gate 2: stale tree (builtAt < newest checkpoint) → serve flat", () => {
|
|
108
|
+
const sd = stateDir();
|
|
109
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
110
|
+
const sid = "stale";
|
|
111
|
+
// Build tree at checkpoints 1..5 (builtAt ≈ now).
|
|
112
|
+
seedSession(s, sid, 5, "stale topic", 0);
|
|
113
|
+
buildTree(s, sid);
|
|
114
|
+
const nodesBefore = listRaptorNodes(sid, sd);
|
|
115
|
+
assert.ok(nodesBefore.length > 0, "tree built");
|
|
116
|
+
const treeBuiltAt = nodesBefore[0].builtAt;
|
|
117
|
+
// Add checkpoint 6 with timestamp > treeBuiltAt → tree is now stale.
|
|
118
|
+
compactSession({
|
|
119
|
+
sessionId: sid,
|
|
120
|
+
messages: [
|
|
121
|
+
msg("stale topic checkpoint 6 newer than tree"),
|
|
122
|
+
msg("ok", "Edit"),
|
|
123
|
+
],
|
|
124
|
+
keepFrom: 2,
|
|
125
|
+
timestamp: treeBuiltAt + 1000,
|
|
126
|
+
}, s);
|
|
127
|
+
// Enable live mode so the gate is actually evaluated.
|
|
128
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
129
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
130
|
+
try {
|
|
131
|
+
// Search should succeed (flat fallback) even though tree is stale.
|
|
132
|
+
const hits = vectorSearch(s, sid, "stale topic", 3);
|
|
133
|
+
assert.ok(hits.length > 0, "flat search returns hits when tree is stale");
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (orig === undefined)
|
|
137
|
+
delete process.env.RAPTOR_SHADOW_MODE;
|
|
138
|
+
else
|
|
139
|
+
process.env.RAPTOR_SHADOW_MODE = orig;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// ─── Gate 3: timedOut fallback ──────────────────────────────────────────────
|
|
143
|
+
test("gate 3: timedOut extractive-fallback tree → serve flat", () => {
|
|
144
|
+
const sd = stateDir();
|
|
145
|
+
// Force extractive fallback by setting a tiny token budget.
|
|
146
|
+
const s = new VectorStore({
|
|
147
|
+
dedupSim: 0.9,
|
|
148
|
+
stateDir: sd,
|
|
149
|
+
config: cfg({ RAPTOR_MAX_TOKEN_BUDGET_PER_SESSION: 1 }),
|
|
150
|
+
});
|
|
151
|
+
const sid = "timedout";
|
|
152
|
+
seedSession(s, sid, 10, "budget topic", 0);
|
|
153
|
+
buildTree(s, sid);
|
|
154
|
+
const nodes = listRaptorNodes(sid, sd);
|
|
155
|
+
if (nodes.length > 0) {
|
|
156
|
+
const root = nodes.find((n) => !n.parentId && n.level >= 99);
|
|
157
|
+
if (root) {
|
|
158
|
+
// timedOut tree — search should fall back to flat.
|
|
159
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
160
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
161
|
+
try {
|
|
162
|
+
const hits = vectorSearch(s, sid, "budget topic", 3);
|
|
163
|
+
assert.ok(hits.length > 0, "flat search returns hits when tree is timedOut");
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (orig === undefined)
|
|
167
|
+
delete process.env.RAPTOR_SHADOW_MODE;
|
|
168
|
+
else
|
|
169
|
+
process.env.RAPTOR_SHADOW_MODE = orig;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
assert.ok(true, "budget did not trigger extractive fallback (non-deterministic)");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
assert.ok(true, "no tree built for tiny-budget session");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// ─── Gate 4: coverage breadth ───────────────────────────────────────────────
|
|
181
|
+
test("gate 4: RAPTOR covers both topics vs flat may miss one", () => {
|
|
182
|
+
const sd = stateDir();
|
|
183
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
184
|
+
const sid = "breadth";
|
|
185
|
+
// Two distinct topics — interleaved timestamps.
|
|
186
|
+
for (let i = 1; i <= 5; i++) {
|
|
187
|
+
compactSession({
|
|
188
|
+
sessionId: sid,
|
|
189
|
+
messages: [
|
|
190
|
+
msg(`cooking recipe ${i} pasta sauce basil`),
|
|
191
|
+
msg(`ok ${i}`, "Edit"),
|
|
192
|
+
],
|
|
193
|
+
keepFrom: 2,
|
|
194
|
+
timestamp: i * 2,
|
|
195
|
+
}, s);
|
|
196
|
+
compactSession({
|
|
197
|
+
sessionId: sid,
|
|
198
|
+
messages: [
|
|
199
|
+
msg(`automotive engine ${i} horsepower torque transmission`),
|
|
200
|
+
msg(`ok ${i}`, "Edit"),
|
|
201
|
+
],
|
|
202
|
+
keepFrom: 2,
|
|
203
|
+
timestamp: i * 2 + 1,
|
|
204
|
+
}, s);
|
|
205
|
+
}
|
|
206
|
+
buildTree(s, sid);
|
|
207
|
+
const nodes = listRaptorNodes(sid, sd);
|
|
208
|
+
assert.ok(nodes.length > 0, "tree built for breadth test");
|
|
209
|
+
// Enable live mode.
|
|
210
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
211
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
212
|
+
try {
|
|
213
|
+
const hits = vectorSearch(s, sid, "cooking and automotive", 5);
|
|
214
|
+
assert.ok(hits.length > 0, "search returns hits for mixed query");
|
|
215
|
+
// Breadth: at least 2 distinct hits (probabilistic, but with 10
|
|
216
|
+
// checkpoints across 2 topics, the tree's staged expansion should
|
|
217
|
+
// surface both).
|
|
218
|
+
assert.ok(hits.length >= 2, `expected ≥2 diverse hits, got ${hits.length}`);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
if (orig === undefined)
|
|
222
|
+
delete process.env.RAPTOR_SHADOW_MODE;
|
|
223
|
+
else
|
|
224
|
+
process.env.RAPTOR_SHADOW_MODE = orig;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// ─── Gate 5: p95 latency ────────────────────────────────────────────────────
|
|
228
|
+
test("gate 5: 200-checkpoint × 20-query median latency < 100 ms", () => {
|
|
229
|
+
const sd = stateDir();
|
|
230
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
231
|
+
const sid = "latency";
|
|
232
|
+
// 200 checkpoints across 10 topics.
|
|
233
|
+
const topics = [
|
|
234
|
+
"machine learning gradient descent",
|
|
235
|
+
"quantum computing entanglement",
|
|
236
|
+
"organic chemistry catalysis",
|
|
237
|
+
"renaissance art fresco painting",
|
|
238
|
+
"marine biology coral reefs",
|
|
239
|
+
"astrophysics dark matter",
|
|
240
|
+
"ancient rome senate",
|
|
241
|
+
"music theory counterpoint",
|
|
242
|
+
"urban planning zoning",
|
|
243
|
+
"evolutionary biology adaptation",
|
|
244
|
+
];
|
|
245
|
+
for (let i = 1; i <= 200; i++) {
|
|
246
|
+
const topic = topics[i % topics.length];
|
|
247
|
+
compactSession({
|
|
248
|
+
sessionId: sid,
|
|
249
|
+
messages: [msg(`${topic} checkpoint ${i}`), msg(`ack ${i}`, "Edit")],
|
|
250
|
+
keepFrom: 2,
|
|
251
|
+
timestamp: i,
|
|
252
|
+
}, s);
|
|
253
|
+
}
|
|
254
|
+
buildTree(s, sid);
|
|
255
|
+
// Enable live mode.
|
|
256
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
257
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
258
|
+
try {
|
|
259
|
+
const queries = [
|
|
260
|
+
"machine learning optimization",
|
|
261
|
+
"quantum entanglement physics",
|
|
262
|
+
"organic chemistry reactions",
|
|
263
|
+
"renaissance art techniques",
|
|
264
|
+
"coral reef ecosystems",
|
|
265
|
+
"dark matter universe",
|
|
266
|
+
"roman government structure",
|
|
267
|
+
"music harmony rules",
|
|
268
|
+
"city planning infrastructure",
|
|
269
|
+
"evolutionary adaptation species",
|
|
270
|
+
"gradient descent algorithms",
|
|
271
|
+
"quantum computing qubits",
|
|
272
|
+
"chemical catalysis processes",
|
|
273
|
+
"fresco painting methods",
|
|
274
|
+
"marine biology biodiversity",
|
|
275
|
+
"astrophysics observations",
|
|
276
|
+
"ancient roman history",
|
|
277
|
+
"music composition theory",
|
|
278
|
+
"urban development patterns",
|
|
279
|
+
"biology natural selection",
|
|
280
|
+
];
|
|
281
|
+
const latencies = [];
|
|
282
|
+
for (const q of queries) {
|
|
283
|
+
const t0 = Date.now();
|
|
284
|
+
vectorSearch(s, sid, q, 5);
|
|
285
|
+
latencies.push(Date.now() - t0);
|
|
286
|
+
}
|
|
287
|
+
latencies.sort((a, b) => a - b);
|
|
288
|
+
const median = latencies[Math.floor(latencies.length / 2)];
|
|
289
|
+
const p95 = latencies[Math.floor(latencies.length * 0.95)];
|
|
290
|
+
assert.ok(median < 100, `median latency ${median} ms exceeds 100 ms budget (p95=${p95} ms)`);
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (orig === undefined)
|
|
294
|
+
delete process.env.RAPTOR_SHADOW_MODE;
|
|
295
|
+
else
|
|
296
|
+
process.env.RAPTOR_SHADOW_MODE = orig;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
@@ -65,6 +65,7 @@ function ollamaSummarize(messages, ollama) {
|
|
|
65
65
|
`;
|
|
66
66
|
const res = spawnSync(process.execPath, ["-e", WORKER], {
|
|
67
67
|
encoding: "utf8",
|
|
68
|
+
timeout: 30_000,
|
|
68
69
|
env: { ...process.env, R_URL: ollama.url, R_MODEL: ollama.model, R_PROMPT: prompt },
|
|
69
70
|
});
|
|
70
71
|
let parsed = { ok: false, error: "no response" };
|
|
@@ -59,6 +59,7 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
59
59
|
if (leaves.length < 10) {
|
|
60
60
|
const item = {
|
|
61
61
|
id: "root",
|
|
62
|
+
parentId: null,
|
|
62
63
|
embedding: meanVector(leaves.map((l) => l.embedding)),
|
|
63
64
|
leafIds: leaves.map((l) => l.id),
|
|
64
65
|
messages: leaves.flatMap((l) => l.messages),
|
|
@@ -81,6 +82,7 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
81
82
|
}
|
|
82
83
|
let currentLevel = leaves.map((l) => ({
|
|
83
84
|
id: l.id,
|
|
85
|
+
parentId: null,
|
|
84
86
|
embedding: l.embedding,
|
|
85
87
|
leafIds: [l.id],
|
|
86
88
|
messages: l.messages,
|
|
@@ -96,6 +98,7 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
96
98
|
if (currentLevel.length <= clustersPerLevel) {
|
|
97
99
|
const merged = {
|
|
98
100
|
id: "merge",
|
|
101
|
+
parentId: null,
|
|
99
102
|
embedding: meanVector(currentLevel.map((c) => c.embedding)),
|
|
100
103
|
leafIds: currentLevel.flatMap((c) => c.leafIds),
|
|
101
104
|
messages: currentLevel.flatMap((c) => c.messages),
|
|
@@ -114,6 +117,15 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
114
117
|
qualityMarker,
|
|
115
118
|
tokenEstimate,
|
|
116
119
|
});
|
|
120
|
+
// Populate parentId for the internal nodes being merged into this root.
|
|
121
|
+
// currentLevel items with ids in `nodes` are internal summary nodes (level
|
|
122
|
+
// >= 1); raw leaf ids are not in `nodes` and are correctly skipped.
|
|
123
|
+
for (const c of currentLevel) {
|
|
124
|
+
const child = nodes.get(c.id);
|
|
125
|
+
if (child && child.id !== rootId && child.parentId === null) {
|
|
126
|
+
child.parentId = rootId;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
117
129
|
return { nodes, rootId, levels: level + 2, timedOut: false };
|
|
118
130
|
}
|
|
119
131
|
const k = Math.max(1, Math.min(clustersPerLevel, currentLevel.length));
|
|
@@ -127,6 +139,7 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
127
139
|
continue;
|
|
128
140
|
const merged = {
|
|
129
141
|
id: nextId(level + 1, g),
|
|
142
|
+
parentId: null,
|
|
130
143
|
embedding: clustered.centroids[g],
|
|
131
144
|
leafIds: group.flatMap((c) => c.leafIds),
|
|
132
145
|
messages: group.flatMap((c) => c.messages),
|
|
@@ -149,9 +162,10 @@ export function buildRaptorTree(leaves, opts) {
|
|
|
149
162
|
level++;
|
|
150
163
|
}
|
|
151
164
|
const root = currentLevel[0];
|
|
165
|
+
const rootId = root ? root.id : null;
|
|
152
166
|
return {
|
|
153
167
|
nodes,
|
|
154
|
-
rootId
|
|
168
|
+
rootId,
|
|
155
169
|
levels: level + 1,
|
|
156
170
|
timedOut: false,
|
|
157
171
|
};
|
|
@@ -173,5 +187,5 @@ function extractiveFallbackRoot(leaves, nodes, nextId) {
|
|
|
173
187
|
qualityMarker: "low",
|
|
174
188
|
tokenEstimate: summary.tokenEstimate,
|
|
175
189
|
});
|
|
176
|
-
return { nodes, rootId, levels:
|
|
190
|
+
return { nodes, rootId, levels: 100, timedOut: true };
|
|
177
191
|
}
|
package/dist/src/engine.js
CHANGED
|
@@ -36,7 +36,10 @@ export function setDefaultStore(store) {
|
|
|
36
36
|
* when the compactable slice is empty.
|
|
37
37
|
*/
|
|
38
38
|
export function compactSession(input, store = getDefaultStore()) {
|
|
39
|
-
|
|
39
|
+
// F6: clamp keepFrom defensively. Upstream (the extension adapter) already
|
|
40
|
+
// clamps, but a bad keepFrom (negative or > length) would produce a misleading
|
|
41
|
+
// drop range or an empty compactable slice; belt-and-braces, clamp here too.
|
|
42
|
+
const keepFrom = Math.max(0, Math.min(input.keepFrom ?? input.messages.length, input.messages.length));
|
|
40
43
|
const compactable = input.messages.slice(0, keepFrom);
|
|
41
44
|
const compactedFrom = keepFrom;
|
|
42
45
|
if (compactable.length === 0) {
|
|
@@ -48,6 +51,8 @@ export function compactSession(input, store = getDefaultStore()) {
|
|
|
48
51
|
tokenEstimate: 0,
|
|
49
52
|
filesModified: [],
|
|
50
53
|
originalTokenEstimate: 0,
|
|
54
|
+
keepTokenEstimate: 0,
|
|
55
|
+
supersedeTokenSavings: 0,
|
|
51
56
|
compactedFrom,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
@@ -82,12 +87,21 @@ export function compactSession(input, store = getDefaultStore()) {
|
|
|
82
87
|
}
|
|
83
88
|
// Honest "tokens saved" accounting:
|
|
84
89
|
// - originalTokenEstimate = the dropped region's token count (what context
|
|
85
|
-
// held before compaction) = the compacted slice's tokens.
|
|
90
|
+
// held before compaction) = the compacted slice's tokens. Computed over the
|
|
91
|
+
// FULL compactable slice (incl. superseded) so the dedup branch books the
|
|
92
|
+
// whole region; this is the value persisted into the checkpoint record.
|
|
93
|
+
// - keepTokenEstimate (F5) = the filtered keep set's tokens (post-supersede).
|
|
94
|
+
// The honest base for the stored-vs-original compaction delta: the pure
|
|
95
|
+
// compaction savings = keepTokenEstimate − storedTokens, and the supersede
|
|
96
|
+
// savings = originalTokenEstimate − keepTokenEstimate. Without this, the
|
|
97
|
+
// supersede savings get booked as compaction savings.
|
|
86
98
|
// - storedTokens = the persisted summary's token count, computed from the
|
|
87
99
|
// actual summary string so it's honest for BOTH the extractive and legacy
|
|
88
100
|
// COLLAPSE paths (the legacy path's fallback estimateSessionTokens is the
|
|
89
101
|
// *original* size, not the stored size).
|
|
90
102
|
const originalTokenEstimate = estimateSessionTokens(compactable);
|
|
103
|
+
const keepTokenEstimate = estimateSessionTokens(keep);
|
|
104
|
+
const supersedeTokenSavings = Math.max(0, originalTokenEstimate - keepTokenEstimate);
|
|
91
105
|
const storedTokens = estimateBlockTokens(summary);
|
|
92
106
|
// Region text = the compacted slice, used for dedup + embedding.
|
|
93
107
|
const regionText = input.regionText ?? keep.map((m) => m.text).join("\n");
|
|
@@ -116,6 +130,8 @@ export function compactSession(input, store = getDefaultStore()) {
|
|
|
116
130
|
tokenEstimate: storedTokens,
|
|
117
131
|
filesModified,
|
|
118
132
|
originalTokenEstimate,
|
|
133
|
+
keepTokenEstimate,
|
|
134
|
+
supersedeTokenSavings,
|
|
119
135
|
compactedFrom,
|
|
120
136
|
};
|
|
121
137
|
}
|
package/dist/src/httpEmbedder.js
CHANGED
|
@@ -26,15 +26,105 @@
|
|
|
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
|
-
|
|
29
|
+
import { isIP } from "node:net";
|
|
30
|
+
// Inline worker script: resolves a hostname via dns.lookup in a child process
|
|
31
|
+
// (dns.lookup is callback-async; the child has its own event loop). Used to
|
|
32
|
+
// verify that a hostname in the embedding URL resolves to loopback ONLY.
|
|
33
|
+
const DNS_WORKER = String.raw `
|
|
34
|
+
const { lookup } = await import("node:dns");
|
|
35
|
+
const { promisify } = await import("node:util");
|
|
36
|
+
const pLookup = promisify(lookup);
|
|
37
|
+
try {
|
|
38
|
+
const result = await pLookup(process.env.MC_DNS_HOST, { all: true });
|
|
39
|
+
process.stdout.write(JSON.stringify({ addresses: result.map((a) => a.address) }));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
process.stdout.write(JSON.stringify({ error: String((e && e.message) || e) }));
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
/** True if the IP string is a loopback address (127.x.x.x for IPv4, ::1 for IPv6). */
|
|
45
|
+
function isLoopbackIP(ip) {
|
|
46
|
+
const type = isIP(ip);
|
|
47
|
+
if (type === 4)
|
|
48
|
+
return ip.startsWith("127.");
|
|
49
|
+
if (type === 6)
|
|
50
|
+
return ip === "::1";
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/** Resolve a hostname via dns.lookup (in a child process) and verify EVERY
|
|
54
|
+
* returned address is loopback. Rejects on any resolution failure, empty
|
|
55
|
+
* result, or non-loopback answer. Fails closed: returns false on any error. */
|
|
56
|
+
function hostnameResolvesToLoopback(hostname) {
|
|
57
|
+
const res = spawnSync(process.execPath, ["-e", DNS_WORKER], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
env: { ...process.env, MC_DNS_HOST: hostname },
|
|
60
|
+
timeout: 5000,
|
|
61
|
+
});
|
|
62
|
+
if (res.error || typeof res.stdout !== "string" || res.stdout.length === 0) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(res.stdout);
|
|
67
|
+
if (parsed.error)
|
|
68
|
+
return false;
|
|
69
|
+
if (!Array.isArray(parsed.addresses) || parsed.addresses.length === 0)
|
|
70
|
+
return false;
|
|
71
|
+
return parsed.addresses.every((a) => isLoopbackIP(a));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read + validate the localhost embeddings config from the environment.
|
|
79
|
+
*
|
|
80
|
+
* Security: the URL is parsed with `new URL()` (not regex) to prevent userinfo
|
|
81
|
+
* bypass (e.g. a URL like localhost:8080@evil.com/ masks evil.com as the real
|
|
82
|
+
* host). The hostname must be a loopback address:
|
|
83
|
+
* - Literal IPv4: 127.x.x.x (the full 127.0.0.0/8 range)
|
|
84
|
+
* - Literal IPv6: ::1
|
|
85
|
+
* - Hostname: resolved via dns.lookup; ALL returned addresses must be loopback
|
|
86
|
+
* Credentials in the URL (user:pass@) are rejected. Non-loopback literal IPs are
|
|
87
|
+
* rejected. Any resolution failure or non-loopback DNS answer is rejected.
|
|
88
|
+
*
|
|
89
|
+
* Fails CLOSED: unset/invalid/non-loopback URL → returns null → caller falls
|
|
90
|
+
* back to the local TrigramEmbedder. Never throws — a misconfigured URL must
|
|
91
|
+
* not crash the extension or silently connect to a remote endpoint.
|
|
92
|
+
*/
|
|
30
93
|
export function embeddingConfigFromEnv() {
|
|
31
94
|
const url = process.env.MEGACOMPACT_EMBEDDING_URL;
|
|
32
95
|
if (!url)
|
|
33
96
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = new URL(url);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL is not a valid URL: ${url} — falling back to default embedder (PREVENT-PI-004)`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
106
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must use http or https scheme (got ${parsed.protocol}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Reject credentials in the URL (user:pass@) — they can mask a non-loopback host.
|
|
110
|
+
if (parsed.username || parsed.password) {
|
|
111
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must not contain credentials (user:pass@) — falling back to default embedder (PREVENT-PI-004)`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
115
|
+
if (isIP(hostname)) {
|
|
116
|
+
// Literal IP — must be loopback.
|
|
117
|
+
if (!isLoopbackIP(hostname)) {
|
|
118
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must be a loopback IP (got ${hostname}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Hostname — resolve via dns.lookup and require ALL addresses loopback.
|
|
124
|
+
if (!hostnameResolvesToLoopback(hostname)) {
|
|
125
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL hostname "${hostname}" does not resolve to loopback — falling back to default embedder (PREVENT-PI-004)`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
38
128
|
}
|
|
39
129
|
const headers = {};
|
|
40
130
|
if (process.env.MEGACOMPACT_EMBEDDING_HEADERS) {
|
|
@@ -42,7 +132,7 @@ export function embeddingConfigFromEnv() {
|
|
|
42
132
|
Object.assign(headers, JSON.parse(process.env.MEGACOMPACT_EMBEDDING_HEADERS));
|
|
43
133
|
}
|
|
44
134
|
catch {
|
|
45
|
-
|
|
135
|
+
console.warn("MEGACOMPACT_EMBEDDING_HEADERS must be valid JSON — ignoring headers");
|
|
46
136
|
}
|
|
47
137
|
}
|
|
48
138
|
const dim = process.env.MEGACOMPACT_EMBEDDING_DIM
|