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.
Files changed (111) hide show
  1. package/README.md +26 -0
  2. package/dist/extensions/mega-compact-s38.test.js +263 -14
  3. package/dist/extensions/mega-compact.js +15 -0
  4. package/dist/extensions/mega-config.js +3 -0
  5. package/dist/extensions/mega-events/agent-handlers.js +211 -26
  6. package/dist/extensions/mega-events/context-handler.js +45 -7
  7. package/dist/extensions/mega-events/error-classifier.js +125 -18
  8. package/dist/extensions/mega-pipeline/compact.js +24 -13
  9. package/dist/extensions/mega-pipeline/recall.js +31 -2
  10. package/dist/extensions/mega-runtime/dashboard-snapshot.js +4 -0
  11. package/dist/extensions/mega-runtime/runtime-snapshot.js +4 -0
  12. package/dist/extensions/mega-runtime/runtime.js +58 -5
  13. package/dist/src/boundary.js +79 -43
  14. package/dist/src/boundary.test.js +119 -2
  15. package/dist/src/canary.js +10 -0
  16. package/dist/src/config/dedup.js +14 -0
  17. package/dist/src/config.js +3 -1
  18. package/dist/src/dedup/raptor/buildHistory.js +164 -0
  19. package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
  20. package/dist/src/dedup/raptor/index.js +38 -0
  21. package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
  22. package/dist/src/dedup/raptor/multilevel.js +17 -5
  23. package/dist/src/dedup/raptor/multilevel.test.js +36 -1
  24. package/dist/src/dedup/raptor/raptor.test.js +43 -0
  25. package/dist/src/dedup/raptor/retrieval.js +14 -2
  26. package/dist/src/dedup/raptor/retrieval.test.js +95 -0
  27. package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
  28. package/dist/src/dedup/raptor/summarizer.js +1 -0
  29. package/dist/src/dedup/raptor/tree.js +16 -2
  30. package/dist/src/engine.js +18 -2
  31. package/dist/src/httpEmbedder.js +96 -6
  32. package/dist/src/httpEmbedder.test.js +277 -0
  33. package/dist/src/mechanical-fix.test.js +65 -0
  34. package/dist/src/raptor-inject-summaries.test.js +162 -0
  35. package/dist/src/recall.js +153 -24
  36. package/dist/src/recall.test.js +179 -4
  37. package/dist/src/store/sqlite/dedup-mirror.js +32 -15
  38. package/dist/src/store/sqlite/maintenance.js +2 -2
  39. package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
  40. package/dist/src/store/sqlite/memories.js +5 -5
  41. package/dist/src/store/sqlite/meta.js +1 -1
  42. package/dist/src/store/sqlite/raptor.js +56 -17
  43. package/dist/src/store/sqlite/raptor.test.js +106 -0
  44. package/dist/src/store/sqlite/schema.js +90 -1
  45. package/dist/src/store/sqlite/session-state.js +9 -3
  46. package/dist/src/store/sqlite/stats.js +9 -5
  47. package/dist/src/store/sqlite/turns.js +179 -0
  48. package/dist/src/store/sqlite/turns.test.js +183 -0
  49. package/dist/src/store/sqlite/utils.js +15 -4
  50. package/dist/src/store/sqlite.js +1 -0
  51. package/dist/src/store.js +2 -2
  52. package/dist/src/vector-search-cache.test.js +157 -0
  53. package/dist/src/vector-search.js +107 -15
  54. package/dist/src/vectorStore.js +36 -8
  55. package/extensions/mega-compact-s38.test.ts +259 -14
  56. package/extensions/mega-compact.ts +15 -0
  57. package/extensions/mega-config.ts +18 -0
  58. package/extensions/mega-dashboard.ts +10 -1
  59. package/extensions/mega-events/agent-handlers.ts +211 -26
  60. package/extensions/mega-events/context-handler.ts +43 -7
  61. package/extensions/mega-events/error-classifier.ts +125 -17
  62. package/extensions/mega-pipeline/compact.ts +28 -16
  63. package/extensions/mega-pipeline/recall.ts +34 -2
  64. package/extensions/mega-runtime/dashboard-snapshot.ts +8 -0
  65. package/extensions/mega-runtime/helpers.ts +25 -1
  66. package/extensions/mega-runtime/runtime-snapshot.ts +4 -0
  67. package/extensions/mega-runtime/runtime.ts +69 -23
  68. package/package.json +1 -1
  69. package/src/boundary.test.ts +128 -2
  70. package/src/boundary.ts +75 -39
  71. package/src/canary.ts +10 -0
  72. package/src/config/dedup.ts +25 -0
  73. package/src/config.ts +3 -1
  74. package/src/dedup/raptor/buildHistory.test.ts +353 -0
  75. package/src/dedup/raptor/buildHistory.ts +259 -0
  76. package/src/dedup/raptor/index.ts +38 -0
  77. package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
  78. package/src/dedup/raptor/multilevel.test.ts +47 -0
  79. package/src/dedup/raptor/multilevel.ts +18 -8
  80. package/src/dedup/raptor/raptor.test.ts +59 -0
  81. package/src/dedup/raptor/retrieval.test.ts +118 -0
  82. package/src/dedup/raptor/retrieval.ts +14 -2
  83. package/src/dedup/raptor/serve-gate.test.ts +348 -0
  84. package/src/dedup/raptor/summarizer.ts +1 -0
  85. package/src/dedup/raptor/tree.ts +17 -2
  86. package/src/engine.ts +32 -3
  87. package/src/httpEmbedder.test.ts +286 -0
  88. package/src/httpEmbedder.ts +98 -8
  89. package/src/mechanical-fix.test.ts +70 -0
  90. package/src/raptor-inject-summaries.test.ts +228 -0
  91. package/src/recall.test.ts +220 -4
  92. package/src/recall.ts +462 -265
  93. package/src/store/sqlite/dedup-mirror.ts +35 -18
  94. package/src/store/sqlite/maintenance.ts +2 -2
  95. package/src/store/sqlite/mechanical-fix.test.ts +162 -0
  96. package/src/store/sqlite/memories.ts +5 -5
  97. package/src/store/sqlite/meta.ts +1 -1
  98. package/src/store/sqlite/raptor.test.ts +139 -0
  99. package/src/store/sqlite/raptor.ts +135 -81
  100. package/src/store/sqlite/schema.ts +90 -1
  101. package/src/store/sqlite/session-state.ts +9 -3
  102. package/src/store/sqlite/stats.ts +10 -8
  103. package/src/store/sqlite/turns.test.ts +218 -0
  104. package/src/store/sqlite/turns.ts +302 -0
  105. package/src/store/sqlite/utils.ts +14 -4
  106. package/src/store/sqlite.ts +1 -0
  107. package/src/store.ts +9 -2
  108. package/src/vector-search-cache.test.ts +190 -0
  109. package/src/vector-search.ts +273 -156
  110. package/src/vectorStore.ts +443 -382
  111. package/extensions/mega-runtime/reset-runtime.ts +0 -80
@@ -0,0 +1,228 @@
1
+ /**
2
+ * raptor-inject-summaries.test.ts — S25 Phase-2 acceptance tests.
3
+ *
4
+ * Verifies the RAPTOR_INJECT_SUMMARIES optional phase (default ON): when
5
+ * enabled, recallAndInline prepends a hierarchical overview header built from
6
+ * the tree's top-level summary nodes (root + top level-1 clusters); when
7
+ * disabled, the recall block is unchanged (detail-only). Also covers the
8
+ * formatRaptorBlock formatter directly.
9
+ *
10
+ * No network. Real stores, temp state dirs.
11
+ */
12
+
13
+ import { test, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { VectorStore, vectorList } from "./vectorStore.js";
19
+ import { runRaptor } from "./dedup/raptor/index.js";
20
+ import { compactSession } from "./engine.js";
21
+ import { loadDedupConfig } from "./config/dedup.js";
22
+ import { normalizeSessionId } from "./store.js";
23
+ import { recallAndInline, formatRaptorBlock } from "./recall.js";
24
+ import type { EngineMessage } from "./types.js";
25
+ import type { DedupConfigShape } from "./config/dedup.js";
26
+
27
+ let tmpDir: string;
28
+ let counter = 0;
29
+
30
+ beforeEach(() => {
31
+ tmpDir = mkdtempSync(join(tmpdir(), "mc-ris-"));
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(tmpDir, { recursive: true, force: true });
36
+ });
37
+
38
+ function stateDir(): string {
39
+ return join(tmpDir, `run-${counter++}`);
40
+ }
41
+
42
+ function cfg(overrides?: Record<string, unknown>): DedupConfigShape {
43
+ return {
44
+ ...loadDedupConfig(),
45
+ RAPTOR_ENABLED: true,
46
+ RAPTOR_MULTILEVEL_ENABLED: true,
47
+ L0_ENABLED: false,
48
+ L1_ENABLED: false,
49
+ L2_ENABLED: true,
50
+ RAPTOR_INJECT_SUMMARIES: true,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function msg(text: string, toolName?: string): EngineMessage {
56
+ return toolName
57
+ ? { role: "assistant", text, toolName, input: text, output: text }
58
+ : { role: "user", text };
59
+ }
60
+
61
+ function seedTwoTopics(store: VectorStore, sid: string, per: number) {
62
+ const topics = [
63
+ "database connection pool postgres",
64
+ "user interface button react",
65
+ ];
66
+ for (let i = 1; i <= per * 2; i++) {
67
+ compactSession(
68
+ {
69
+ sessionId: sid,
70
+ messages: [
71
+ msg(`${topics[i % 2]} checkpoint ${i} alpha beta`),
72
+ msg(`ack ${i}`, "Edit"),
73
+ ],
74
+ keepFrom: 2,
75
+ timestamp: i,
76
+ },
77
+ store,
78
+ );
79
+ }
80
+ }
81
+
82
+ function buildTree(store: VectorStore, sid: string) {
83
+ const nsid = normalizeSessionId(sid);
84
+ runRaptor(
85
+ vectorList(store, nsid).map((cp) => ({
86
+ id: cp.checkpointId,
87
+ messages: [],
88
+ sourceText: cp.normalizedText ?? cp.summary ?? cp.regionHash,
89
+ embedding: cp.embedding,
90
+ })),
91
+ { stateDir: store.stateDir, sessionId: nsid },
92
+ );
93
+ }
94
+
95
+ // ─── 1. Flag ON (default) → overview header prepended ───────────────────────
96
+
97
+ test("S25-P2: RAPTOR_INJECT_SUMMARIES=true prepends a hierarchical overview header", () => {
98
+ const sd = stateDir();
99
+ const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
100
+ const sid = "on";
101
+ seedTwoTopics(s, sid, 8);
102
+ buildTree(s, sid);
103
+ process.env.RAPTOR_SHADOW_MODE = "false";
104
+ const r = recallAndInline(
105
+ { sessionId: sid, query: "database pool", source: "resume", limit: 3 },
106
+ s,
107
+ );
108
+ assert.ok(r.block.length > 0, "recall block is non-empty");
109
+ assert.ok(
110
+ r.block.includes("hierarchical overview"),
111
+ "overview header present when flag ON",
112
+ );
113
+ assert.ok(
114
+ r.block.includes("Recalled context"),
115
+ "detailed recall block still present after overview",
116
+ );
117
+ assert.ok(
118
+ r.block.indexOf("hierarchical overview") <
119
+ r.block.indexOf("Recalled context"),
120
+ "overview precedes detail",
121
+ );
122
+ });
123
+
124
+ // ─── 2. Flag OFF → no overview (detail-only, unchanged behavior) ────────────
125
+
126
+ test("S25-P2: RAPTOR_INJECT_SUMMARIES=false → no overview (detail-only)", () => {
127
+ const sd = stateDir();
128
+ const s = new VectorStore({
129
+ dedupSim: 0.9,
130
+ stateDir: sd,
131
+ config: cfg({ RAPTOR_INJECT_SUMMARIES: false }),
132
+ });
133
+ const sid = "off";
134
+ seedTwoTopics(s, sid, 8);
135
+ buildTree(s, sid);
136
+ process.env.RAPTOR_SHADOW_MODE = "false";
137
+ const r = recallAndInline(
138
+ { sessionId: sid, query: "database pool", source: "resume", limit: 3 },
139
+ s,
140
+ );
141
+ if (r.block.length > 0) {
142
+ assert.ok(
143
+ !r.block.includes("hierarchical overview"),
144
+ "no overview header when flag OFF",
145
+ );
146
+ assert.ok(
147
+ r.block.includes("Recalled context") || r.block.length > 0,
148
+ "detail block present",
149
+ );
150
+ }
151
+ });
152
+
153
+ // ─── 3. No tree → no overview (graceful, detail-only) ────────────────────────
154
+
155
+ test("S25-P2: no RAPTOR tree → no overview (graceful degradation)", () => {
156
+ const sd = stateDir();
157
+ const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
158
+ const sid = "notree";
159
+ seedTwoTopics(s, sid, 4); // checkpoints exist but no tree built
160
+ process.env.RAPTOR_SHADOW_MODE = "false";
161
+ const r = recallAndInline(
162
+ { sessionId: sid, query: "database pool", source: "resume", limit: 3 },
163
+ s,
164
+ );
165
+ assert.ok(
166
+ !r.block.includes("hierarchical overview"),
167
+ "no overview when no tree exists",
168
+ );
169
+ });
170
+
171
+ // ─── 4. formatRaptorBlock formatter contract ────────────────────────────────
172
+
173
+ test("S25-P2: formatRaptorBlock labels root as 'Session overview' + clusters as 'Cluster summary'", () => {
174
+ const block = formatRaptorBlock([
175
+ { summary: "root overview text", level: 0, score: 0.9 },
176
+ { summary: "cluster A text", level: 1, score: 0.7 },
177
+ ]);
178
+ assert.ok(
179
+ /The following hierarchical overview/.test(block),
180
+ "preamble present",
181
+ );
182
+ assert.ok(
183
+ /Session overview \[1\] \(relevance 90%\)/.test(block),
184
+ "root labeled",
185
+ );
186
+ assert.ok(
187
+ /Cluster summary \[2\] \(level 1\) \(relevance 70%\)/.test(block),
188
+ "cluster labeled",
189
+ );
190
+ assert.ok(block.includes("root overview text"), "root body present");
191
+ assert.ok(block.includes("cluster A text"), "cluster body present");
192
+ assert.equal(formatRaptorBlock([]), "", "empty input → empty string");
193
+ });
194
+
195
+ // ─── 5. overview is skipped when shadows mode is on (tree not served) ──────
196
+ //
197
+ // Actually the overview reads rehydrateRaptorTree directly (not raptorSearchHits),
198
+ // so shadow mode does NOT suppress it via the serve gate. Verify the behavior:
199
+ // the overview IS emitted in shadow mode (the tree exists and is rehydrated).
200
+ // This is acceptable — the overview is a session map, not a live-serve decision.
201
+ // QA finding #1 (v0.8.25): the overview previously bypassed the shadow gate —
202
+ // the tree was injected into recall while RAPTOR_SERVE was logging-only. The
203
+ // gate now applies to the inject path too: shadow mode = no overview.
204
+
205
+ test("S25-P2: overview is suppressed in shadow mode (QA fix)", () => {
206
+ const sd = stateDir();
207
+ const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
208
+ const sid = "shadow";
209
+ seedTwoTopics(s, sid, 8);
210
+ buildTree(s, sid);
211
+ const orig = process.env.RAPTOR_SHADOW_MODE;
212
+ process.env.RAPTOR_SHADOW_MODE = "true";
213
+ try {
214
+ const r = recallAndInline(
215
+ { sessionId: sid, query: "database pool", source: "resume", limit: 3 },
216
+ s,
217
+ );
218
+ // Shadow mode = build + persist + log only; nothing RAPTOR-derived is
219
+ // injected into recall, including the overview header.
220
+ assert.ok(
221
+ !r.block.includes("hierarchical overview"),
222
+ "overview suppressed in shadow mode",
223
+ );
224
+ } finally {
225
+ if (orig === undefined) delete process.env.RAPTOR_SHADOW_MODE;
226
+ else process.env.RAPTOR_SHADOW_MODE = orig;
227
+ }
228
+ });
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { VectorStore } from "./vectorStore.js";
7
7
  import { compactSession } from "./engine.js";
8
8
  import { recallAndInline, recallAndInlineAsync, formatRecallBlock } from "./recall.js";
9
- import { vectorList } from "./vectorStore.js";
9
+ import { vectorList, vectorWasInjected } from "./vectorStore.js";
10
10
  import { markInjectedGlobal, wasInjectedGlobal, closeIndexStore } from "./store/sqlite.js";
11
11
  import {
12
12
  closeVectorIndex,
@@ -88,10 +88,12 @@ test("Fix C: recallMaxTokens caps the injected block", () => {
88
88
  compactSession({ sessionId: SESS, messages: [msg("user", "beta module config and env resolution"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
89
89
  compactSession({ sessionId: SESS, messages: [msg("user", "gamma module shutdown and cleanup hooks"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 3 }, s);
90
90
 
91
- // A ceiling of 100 tokens fits the first checkpoint (~82) but stops before the
92
- // second (~163 cumulative) — proving the cap bites mid-stream.
91
+ // A ceiling of 50 tokens fits the first checkpoint (~33 body tokens) but
92
+ // stops before the second (~65 cumulative) — proving the cap bites mid-stream.
93
+ // (F3: the cap now counts body tokens only — one preamble at format time, not
94
+ // N — matching the async path. The old per-hit preamble counting needed ~100.)
93
95
  const r = recallAndInline(
94
- { sessionId: SESS, query: "module wiring config shutdown", limit: 5, source: "command", recallMaxTokens: 100, skipInjected: false },
96
+ { sessionId: SESS, query: "module wiring config shutdown", limit: 5, source: "command", recallMaxTokens: 50, skipInjected: false },
95
97
  s as any,
96
98
  );
97
99
  assert.ok(r.toInject.length >= 1, "at least one injected under the cap");
@@ -246,6 +248,220 @@ test("S18: a fresh foreign checkpoint is injected AND recorded globally", async
246
248
  }
247
249
  });
248
250
 
251
+ // ---- F3: format-once — one preamble, [1..n] numbering, cap respected ----
252
+ //
253
+ // The fix (landed async, extended to sync here) accumulates the hit list and
254
+ // calls formatRecallBlock ONCE at the end, so a multi-hit block carries exactly
255
+ // one preamble and contiguous [1..n] labels instead of one preamble per hit.
256
+
257
+ test("F3 (sync): multi-hit injection has exactly one preamble and [1..n] numbering", () => {
258
+ const s = store();
259
+ compactSession({ sessionId: SESS, messages: [msg("user", "alpha module wiring and bootstrap sequence"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
260
+ compactSession({ sessionId: SESS, messages: [msg("user", "beta module config and env resolution"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
261
+ compactSession({ sessionId: SESS, messages: [msg("user", "gamma module shutdown and cleanup hooks"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 3 }, s);
262
+
263
+ const r = recallAndInline(
264
+ { sessionId: SESS, query: "module wiring config shutdown", limit: 5, source: "command", skipInjected: false },
265
+ s as any,
266
+ );
267
+ assert.ok(r.toInject.length >= 2, "at least two hits injected (got " + r.toInject.length + ")");
268
+ // Exactly one preamble (the old per-hit format produced one per hit).
269
+ const preamble = "The following compacted context was recalled";
270
+ assert.equal(r.block.split(preamble).length - 1, 1, "exactly one preamble for a multi-hit block");
271
+ // Contiguous [1..n] numbering.
272
+ for (let i = 1; i <= r.toInject.length; i++) {
273
+ assert.ok(r.block.includes(`[${i}]`), `block includes [${i}]`);
274
+ }
275
+ // No out-of-range label (e.g. [n+1]) leaks in.
276
+ assert.ok(!r.block.includes(`[${r.toInject.length + 1}]`), "no extra-numbered label");
277
+ });
278
+
279
+ test("F3 (sync): recallMaxTokens caps mid-stream with exactly one preamble", () => {
280
+ const s = store();
281
+ compactSession({ sessionId: SESS, messages: [msg("user", "alpha module wiring and bootstrap sequence"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 1 }, s);
282
+ compactSession({ sessionId: SESS, messages: [msg("user", "beta module config and env resolution"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 2 }, s);
283
+ compactSession({ sessionId: SESS, messages: [msg("user", "gamma module shutdown and cleanup hooks"), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: 3 }, s);
284
+
285
+ // A tight ceiling that fits the first checkpoint (~33 body tokens) but stops
286
+ // before the second (~65 cumulative). F3: body-only counting (one preamble at
287
+ // format time, not per hit), matching the async path.
288
+ const r = recallAndInline(
289
+ { sessionId: SESS, query: "module wiring config shutdown", limit: 5, source: "command", skipInjected: false, recallMaxTokens: 50 },
290
+ s as any,
291
+ );
292
+ assert.ok(r.toInject.length < 3, "cap stopped before all three injected");
293
+ assert.ok(r.toInject.length >= 1, "at least one injected under the cap");
294
+ assert.equal(
295
+ r.block.split("The following compacted context was recalled").length - 1,
296
+ 1,
297
+ "still exactly one preamble under the cap",
298
+ );
299
+ });
300
+
301
+ test("F3 (async): multi-hit cross-repo injection has exactly one preamble and [1..n] numbering", async () => {
302
+ if (process.env.MEGACOMPACT_PGLITE_DISABLED === "true") { return; } // skip when WASM index is off
303
+ const indexDir = mkdtempSync(join(tmpdir(), "mc-f3a-"));
304
+ const foreignStateDir = mkdtempSync(join(tmpdir(), "mc-f3a-foreign-"));
305
+ const selfStateDir = mkdtempSync(join(tmpdir(), "mc-f3a-self-"));
306
+ process.env.MEGACOMPACT_VECTOR_INDEX_DIR = mkdtempSync(join(tmpdir(), "mc-f3a-vidx-"));
307
+ try {
308
+ await closeVectorIndex();
309
+ // Seed multiple distinct foreign checkpoints so the cross-repo query yields
310
+ // more than one hit (needed to assert [1..n] numbering).
311
+ const foreign = new VectorStore({ stateDir: foreignStateDir, dedupSim: 0.9 });
312
+ const fsess = "sess_foreign_f3";
313
+ const summaries = [
314
+ "foreign repo authentication jwt token validation",
315
+ "foreign repo database connection pooling and retry",
316
+ "foreign repo logging telemetry and tracing spans",
317
+ ];
318
+ const cids: string[] = [];
319
+ for (let i = 0; i < summaries.length; i++) {
320
+ const res = compactSession(
321
+ { sessionId: fsess, messages: [msg("user", summaries[i]), msg("assistant", "ok", "Edit")], keepFrom: 2, timestamp: i + 1 },
322
+ foreign,
323
+ );
324
+ if (res.checkpointId) cids.push(res.checkpointId);
325
+ }
326
+ await rebuildFromSqlite(
327
+ () => [{ repoId: foreignStateDir, stateDir: foreignStateDir }],
328
+ (sd) => {
329
+ const st = new VectorStore({ stateDir: sd, dedupSim: 0.9 });
330
+ return vectorList(st, fsess).map((cp) => ({
331
+ sessionId: fsess, checkpointId: cp.checkpointId, embedding: cp.embedding,
332
+ }));
333
+ },
334
+ );
335
+ const pg = await initVectorIndex();
336
+ assert.ok(pg, "PGlite index should initialize");
337
+
338
+ const selfStore = new VectorStore({ stateDir: selfStateDir, dedupSim: 0.9 });
339
+ const r = await recallAndInlineAsync(
340
+ { sessionId: "sess_f3", query: "foreign repo", limit: 5, source: "command", crossRepo: true, skipInjected: false, globalIndexDir: indexDir },
341
+ selfStore,
342
+ );
343
+ assert.ok(r.toInject.length >= 2, "at least two cross-repo hits (got " + r.toInject.length + ")");
344
+ assert.equal(
345
+ r.block.split("The following compacted context was recalled").length - 1,
346
+ 1,
347
+ "exactly one preamble for a multi-hit async block",
348
+ );
349
+ for (let i = 1; i <= r.toInject.length; i++) {
350
+ assert.ok(r.block.includes(`[${i}]`), `async block includes [${i}]`);
351
+ }
352
+ assert.ok(!r.block.includes(`[${r.toInject.length + 1}]`), "no extra-numbered label (async)");
353
+ } finally {
354
+ await closeVectorIndex();
355
+ closeIndexStore();
356
+ delete process.env.MEGACOMPACT_VECTOR_INDEX_DIR;
357
+ rmSync(indexDir, { recursive: true, force: true });
358
+ rmSync(foreignStateDir, { recursive: true, force: true });
359
+ rmSync(selfStateDir, { recursive: true, force: true });
360
+ }
361
+ });
362
+
363
+ // ---- F2: cross-repo injected-set bypass ----------------------------------
364
+ //
365
+ // When the same session resumes in a DIFFERENT repo, the per-repo session_state
366
+ // has no injection marker for the foreign checkpoint (different stateDir), so
367
+ // only the machine-wide injected-set (shared globalIndexDir) can block a
368
+ // re-inject. The F2 fix ensures globalIndexDir is always resolved (via
369
+ // getIndexDir in the extension), so wasInjectedGlobal is consulted; without
370
+ // the fix, globalIndexDir was undefined and foreign checkpoints re-injected
371
+ // every resume. This test passes globalIndexDir explicitly (simulating the
372
+ // resolver default) and does NOT set MEGACOMPACT_INDEX_DIR.
373
+
374
+ test("F2: cross-repo hit is not re-injected when resuming the same session in a different repo (shared index dir)", async () => {
375
+ if (process.env.MEGACOMPACT_PGLITE_DISABLED === "true") { return; } // skip when WASM index is off
376
+ const indexDir = mkdtempSync(join(tmpdir(), "mc-f2-"));
377
+ const foreignStateDir = mkdtempSync(join(tmpdir(), "mc-f2-foreign-"));
378
+ const selfStateDir1 = mkdtempSync(join(tmpdir(), "mc-f2-self1-"));
379
+ const selfStateDir2 = mkdtempSync(join(tmpdir(), "mc-f2-self2-"));
380
+ process.env.MEGACOMPACT_VECTOR_INDEX_DIR = mkdtempSync(join(tmpdir(), "mc-f2-vidx-"));
381
+ try {
382
+ await closeVectorIndex();
383
+ const seed = await seedForeignRepo(foreignStateDir);
384
+ await rebuildFromSqlite(
385
+ () => [{ repoId: foreignStateDir, stateDir: foreignStateDir }],
386
+ (sd) => {
387
+ const st = new VectorStore({ stateDir: sd, dedupSim: 0.9 });
388
+ return vectorList(st, seed.sessionId).map((cp) => ({
389
+ sessionId: seed.sessionId, checkpointId: cp.checkpointId, embedding: cp.embedding,
390
+ }));
391
+ },
392
+ );
393
+ const pg = await initVectorIndex();
394
+ assert.ok(pg, "PGlite index should initialize");
395
+
396
+ const sess = "sess_resume";
397
+ // Repo B: first recall — fresh foreign checkpoint is injected AND recorded
398
+ // in the shared machine-wide index.
399
+ const storeB = new VectorStore({ stateDir: selfStateDir1, dedupSim: 0.9 });
400
+ const r1 = await recallAndInlineAsync(
401
+ { sessionId: sess, query: "foreign repo authentication jwt", limit: 3, source: "command", crossRepo: true, globalIndexDir: indexDir },
402
+ storeB,
403
+ );
404
+ assert.equal(r1.toInject.length, 1, "first recall (repo B) injects the foreign checkpoint");
405
+ assert.equal(wasInjectedGlobal(seed.checkpointId, sess, indexDir), true, "recorded in the shared index");
406
+
407
+ // Repo C: SAME session resumed in a DIFFERENT repo. The per-session injected
408
+ // set in repo C's store is empty (different stateDir), so only the shared
409
+ // global injected-set can block re-injection.
410
+ const storeC = new VectorStore({ stateDir: selfStateDir2, dedupSim: 0.9 });
411
+ assert.equal(vectorWasInjected(storeC, sess, seed.checkpointId), false, "repo C per-session set is empty (different stateDir)");
412
+ const r2 = await recallAndInlineAsync(
413
+ { sessionId: sess, query: "foreign repo authentication jwt", limit: 3, source: "command", crossRepo: true, globalIndexDir: indexDir },
414
+ storeC,
415
+ );
416
+ assert.equal(r2.toInject.length, 0, "second recall (repo C) does NOT re-inject (machine-wide global dedup)");
417
+ } finally {
418
+ await closeVectorIndex();
419
+ closeIndexStore();
420
+ delete process.env.MEGACOMPACT_VECTOR_INDEX_DIR;
421
+ rmSync(indexDir, { recursive: true, force: true });
422
+ rmSync(foreignStateDir, { recursive: true, force: true });
423
+ rmSync(selfStateDir1, { recursive: true, force: true });
424
+ rmSync(selfStateDir2, { recursive: true, force: true });
425
+ }
426
+ });
427
+
428
+ test("F2: without globalIndexDir, cross-repo hits are skipped (not injected undeduped)", async () => {
429
+ if (process.env.MEGACOMPACT_PGLITE_DISABLED === "true") { return; } // skip when WASM index is off
430
+ const foreignStateDir = mkdtempSync(join(tmpdir(), "mc-f2b-foreign-"));
431
+ const selfStateDir = mkdtempSync(join(tmpdir(), "mc-f2b-self-"));
432
+ process.env.MEGACOMPACT_VECTOR_INDEX_DIR = mkdtempSync(join(tmpdir(), "mc-f2b-vidx-"));
433
+ try {
434
+ await closeVectorIndex();
435
+ const seed = await seedForeignRepo(foreignStateDir);
436
+ await rebuildFromSqlite(
437
+ () => [{ repoId: foreignStateDir, stateDir: foreignStateDir }],
438
+ (sd) => {
439
+ const st = new VectorStore({ stateDir: sd, dedupSim: 0.9 });
440
+ return vectorList(st, seed.sessionId).map((cp) => ({
441
+ sessionId: seed.sessionId, checkpointId: cp.checkpointId, embedding: cp.embedding,
442
+ }));
443
+ },
444
+ );
445
+ const pg = await initVectorIndex();
446
+ assert.ok(pg, "PGlite index should initialize");
447
+
448
+ // No globalIndexDir → the F2 guard skips foreign hits rather than injecting
449
+ // them undeduped. Same-repo hits (none here) would still pass.
450
+ const selfStore = new VectorStore({ stateDir: selfStateDir, dedupSim: 0.9 });
451
+ const r = await recallAndInlineAsync(
452
+ { sessionId: "sess_nodir", query: "foreign repo authentication jwt", limit: 3, source: "command", crossRepo: true },
453
+ selfStore,
454
+ );
455
+ assert.equal(r.toInject.length, 0, "foreign hit skipped when globalIndexDir is unset (no undeduped injection)");
456
+ } finally {
457
+ await closeVectorIndex();
458
+ closeIndexStore();
459
+ delete process.env.MEGACOMPACT_VECTOR_INDEX_DIR;
460
+ rmSync(foreignStateDir, { recursive: true, force: true });
461
+ rmSync(selfStateDir, { recursive: true, force: true });
462
+ }
463
+ });
464
+
249
465
  test("cleanup", () => {
250
466
  rmSync(baseTmp, { recursive: true, force: true });
251
467
  });