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
package/src/vector-search.ts
CHANGED
|
@@ -16,18 +16,24 @@ import { normalizeSessionId } from "./store.js";
|
|
|
16
16
|
import { mmrRerank, type MmrItem } from "./dedup/mmr.js";
|
|
17
17
|
import { topK } from "./dedup/topk.js";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
listCheckpoints,
|
|
20
|
+
getCheckpoint,
|
|
21
|
+
maxCheckpointTimestamp,
|
|
22
|
+
maxRaptorNodeBuiltAt,
|
|
22
23
|
} from "./store/sqlite.js";
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
initVectorIndex,
|
|
26
|
+
searchAsync as vectorIndexSearch,
|
|
27
|
+
type VectorIndexHit,
|
|
27
28
|
} from "./store/vectorIndex.js";
|
|
28
29
|
import { rehydrateRaptorTree, isShadowMode } from "./dedup/raptor/index.js";
|
|
29
30
|
import { stagedExpansion } from "./dedup/raptor/retrieval.js";
|
|
31
|
+
import { multilevelRetrieval } from "./dedup/raptor/multilevel.js";
|
|
32
|
+
import { Logger } from "./log.js";
|
|
30
33
|
import type { SearchHit, VectorStore } from "./vectorStore.js";
|
|
34
|
+
import type { StoredCheckpoint } from "./store.js";
|
|
35
|
+
|
|
36
|
+
const logger = new Logger();
|
|
31
37
|
|
|
32
38
|
// ---------------------------------------------------------------------------
|
|
33
39
|
// raptorSearchHits — internal helper (NOT exported)
|
|
@@ -37,50 +43,152 @@ import type { SearchHit, VectorStore } from "./vectorStore.js";
|
|
|
37
43
|
* Serve the RAPTOR tree for a query (Fix D): rehydrate the persisted tree and
|
|
38
44
|
* return its staged-expansion leaf hits as SearchHits. Returns [] when no tree
|
|
39
45
|
* exists (small sessions — flat search remains the path). Best-effort/non-fatal.
|
|
46
|
+
*
|
|
47
|
+
* `checkpoints` is the caller's already-loaded, dedup-status-filtered checkpoint
|
|
48
|
+
* list — pass it in so the flat-search path and the RAPTOR merge path share ONE
|
|
49
|
+
* `listCheckpoints` call per `vectorSearch` (eliminates a redundant full scan +
|
|
50
|
+
* N-row hydration at 500+ checkpoints — QA perf review).
|
|
40
51
|
*/
|
|
41
|
-
function raptorSearchHits(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
function raptorSearchHits(
|
|
53
|
+
store: VectorStore,
|
|
54
|
+
sid: string,
|
|
55
|
+
query: string,
|
|
56
|
+
k: number,
|
|
57
|
+
checkpoints: StoredCheckpoint[],
|
|
58
|
+
): SearchHit[] {
|
|
59
|
+
const t0 = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
const stateDir = store.stateDir;
|
|
62
|
+
const cfg = store.cfg;
|
|
63
|
+
const embedder = store.embedder;
|
|
64
|
+
const record = store.record;
|
|
65
|
+
|
|
66
|
+
// S25 gate (a): honor the shadow contract at SERVE time. The tree is still
|
|
67
|
+
// built + persisted (logging-only) but NOT merged into recall while
|
|
68
|
+
// RAPTOR_SHADOW_MODE is anything other than "false".
|
|
69
|
+
if (isShadowMode()) return [];
|
|
70
|
+
// S25 gate (b): freshness + fallback guards with per-session cache.
|
|
71
|
+
// The cache avoids O(n·leaves) rehydrate on every search when the tree
|
|
72
|
+
// hasn't changed. Freshness is validated via maxRaptorNodeBuiltAt (cheap
|
|
73
|
+
// indexed MAX query) — if the persisted builtAt matches the cached builtAt,
|
|
74
|
+
// the tree hasn't been rebuilt and can be served from cache.
|
|
75
|
+
const cacheKey = `${stateDir}::${sid}`;
|
|
76
|
+
const cached = store.raptorCache.get(cacheKey);
|
|
77
|
+
const latestBuiltAt = maxRaptorNodeBuiltAt(sid, stateDir);
|
|
78
|
+
|
|
79
|
+
let tree: ReturnType<typeof rehydrateRaptorTree>;
|
|
80
|
+
if (cached && cached.builtAt === latestBuiltAt && latestBuiltAt > 0) {
|
|
81
|
+
tree = cached.tree;
|
|
82
|
+
} else {
|
|
83
|
+
tree = rehydrateRaptorTree(sid, stateDir);
|
|
84
|
+
if (tree && tree.rootId && !tree.timedOut) {
|
|
85
|
+
store.raptorCache.set(cacheKey, { tree, builtAt: tree.builtAt ?? 0 });
|
|
86
|
+
} else {
|
|
87
|
+
store.raptorCache.delete(cacheKey);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!tree || !tree.rootId) return [];
|
|
92
|
+
|
|
93
|
+
// Freshness: skip persisted tree if the session has been compacted since
|
|
94
|
+
// the tree was built. The next runRaptor will rebuild.
|
|
95
|
+
const maxTs = maxCheckpointTimestamp(sid, stateDir);
|
|
96
|
+
if (tree.builtAt && tree.builtAt < maxTs) return [];
|
|
97
|
+
|
|
98
|
+
// S25: skip timedOut extractive-fallback trees (level === 99).
|
|
99
|
+
if (tree.timedOut) return [];
|
|
100
|
+
// Use the caller's already-loaded checkpoint list (no second scan).
|
|
101
|
+
const all = checkpoints;
|
|
102
|
+
const qv = embedder.embed(query);
|
|
103
|
+
const hits: SearchHit[] = [];
|
|
104
|
+
|
|
105
|
+
// S42B: multi-level retrieval — score all tree levels (leaves + internal
|
|
106
|
+
// clusters), expand cluster hits to their leaf descendants, dedup overlaps,
|
|
107
|
+
// and MMR-rerank. Default ON (RAPTOR_MULTILEVEL_ENABLED). When OFF, the
|
|
108
|
+
// leaf-only stagedExpansion path below is used (identical to pre-S42B).
|
|
109
|
+
if (cfg.RAPTOR_MULTILEVEL_ENABLED) {
|
|
110
|
+
const mlHits = multilevelRetrieval(query, tree, {
|
|
111
|
+
embedder,
|
|
112
|
+
levelWeights: cfg.RAPTOR_LEVEL_WEIGHTS,
|
|
113
|
+
leafExpansion: cfg.RAPTOR_LEAF_EXPANSION,
|
|
114
|
+
maxLeafExpansion: cfg.RAPTOR_MAX_LEAF_EXPANSION,
|
|
115
|
+
k,
|
|
116
|
+
mmrLambda: cfg.MMR_LAMBDA,
|
|
117
|
+
});
|
|
118
|
+
if (mlHits.length === 0) return [];
|
|
119
|
+
for (const mh of mlHits) {
|
|
120
|
+
// Leaf hits: nodeId IS the checkpointId — hydrate from the stored list.
|
|
121
|
+
const cp = all.find((c) => c.checkpointId === mh.nodeId);
|
|
122
|
+
if (cp) {
|
|
123
|
+
hits.push({ checkpoint: cp, score: mh.score });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Orphaned leaf (checkpoint missing from the stored list, e.g. SemDeDup
|
|
127
|
+
// removed) — skip; synthesizing it would yield an empty cluster block.
|
|
128
|
+
if (mh.isLeaf) continue;
|
|
129
|
+
// Cluster hit: synthesize a SearchHit so the recall block can surface
|
|
130
|
+
// the hierarchical summary (not a stored checkpoint). The minimal
|
|
131
|
+
// StoredCheckpoint carries the node's centroid embedding for any
|
|
132
|
+
// downstream cosine / MMR the caller does.
|
|
133
|
+
hits.push({
|
|
134
|
+
checkpoint: {
|
|
135
|
+
checkpointId: mh.nodeId,
|
|
136
|
+
sessionId: sid,
|
|
137
|
+
summary: mh.summary,
|
|
138
|
+
keyDecisions: [],
|
|
139
|
+
nextSteps: [],
|
|
140
|
+
filesModified: [],
|
|
141
|
+
tokenEstimate: 0,
|
|
142
|
+
regionHash: `raptor:${mh.nodeId}`,
|
|
143
|
+
embedding: mh.embedding,
|
|
144
|
+
timestamp: tree.builtAt ?? Date.now(),
|
|
145
|
+
},
|
|
146
|
+
score: mh.score,
|
|
147
|
+
raptorSummary: mh.summary,
|
|
148
|
+
raptorLevel: mh.level,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
record(
|
|
152
|
+
"RAPTOR",
|
|
153
|
+
hits.length > 0 ? "new" : "mark_only",
|
|
154
|
+
`ml=${mlHits.length}`,
|
|
155
|
+
Date.now() - t0,
|
|
156
|
+
);
|
|
157
|
+
return hits;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// S42B: flag OFF — leaf-only stagedExpansion (pre-S42B production path).
|
|
161
|
+
const leafIds = stagedExpansion(query, tree, {
|
|
162
|
+
embedder,
|
|
163
|
+
k,
|
|
164
|
+
topM: cfg.RAPTOR_CLUSTERS_PER_LEVEL,
|
|
165
|
+
mmrLambda: cfg.MMR_LAMBDA,
|
|
166
|
+
});
|
|
167
|
+
if (leafIds.length === 0) return [];
|
|
168
|
+
for (const id of leafIds) {
|
|
169
|
+
const cp = all.find((c) => c.checkpointId === id);
|
|
170
|
+
if (cp)
|
|
171
|
+
hits.push({
|
|
172
|
+
checkpoint: cp,
|
|
173
|
+
score: cosineSimilarity(qv, cp.embedding),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// S25 monitoring: emit a raptor_serve decision so canary.ts can track
|
|
177
|
+
// p95 latency + the tier's live traffic (non-fatal, best-effort).
|
|
178
|
+
record(
|
|
179
|
+
"RAPTOR",
|
|
180
|
+
hits.length > 0 ? "new" : "mark_only",
|
|
181
|
+
`leaves=${leafIds.length}`,
|
|
182
|
+
Date.now() - t0,
|
|
183
|
+
);
|
|
184
|
+
return hits;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
logger.warn("raptor_search_error", {
|
|
187
|
+
error: String(e),
|
|
188
|
+
sessionId: sid,
|
|
189
|
+
});
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
84
192
|
}
|
|
85
193
|
|
|
86
194
|
// ---------------------------------------------------------------------------
|
|
@@ -96,65 +204,69 @@ function raptorSearchHits(store: VectorStore, sid: string, query: string, k: num
|
|
|
96
204
|
* reranks the candidate window for diversity.
|
|
97
205
|
*/
|
|
98
206
|
export function vectorSearch(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
207
|
+
store: VectorStore,
|
|
208
|
+
sessionId: string,
|
|
209
|
+
query: string,
|
|
210
|
+
k = 3,
|
|
103
211
|
): SearchHit[] {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
212
|
+
const stateDir = store.stateDir;
|
|
213
|
+
const cfg = store.cfg;
|
|
214
|
+
const embedder = store.embedder;
|
|
215
|
+
|
|
216
|
+
const sid = normalizeSessionId(sessionId);
|
|
217
|
+
const checkpoints = listCheckpoints(sid, stateDir).filter(
|
|
218
|
+
(cp) => cp.dedupStatus !== "removed", // SemDeDup: exclude removed rows
|
|
219
|
+
);
|
|
220
|
+
if (checkpoints.length === 0) return [];
|
|
221
|
+
const qv = embedder.embed(query);
|
|
222
|
+
|
|
223
|
+
const scored: SearchHit[] = checkpoints.map((cp) => ({
|
|
224
|
+
checkpoint: cp,
|
|
225
|
+
score: cosineSimilarity(qv, cp.embedding),
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
// Heap top-K over a widened window (2k) so MMR has diverse candidates.
|
|
229
|
+
const window = topK(
|
|
230
|
+
scored.map((h) => ({ item: h, score: h.score })),
|
|
231
|
+
Math.max(k * 2, k),
|
|
232
|
+
).map((s) => s.item);
|
|
233
|
+
// MMR (QA #10) is part of the L2 semantic tier: skip it when L2 is disabled
|
|
234
|
+
// (Sprint 14 flag), returning the plain relevance-ranked window instead.
|
|
235
|
+
if (!cfg.L2_ENABLED) return window.slice(0, k);
|
|
236
|
+
|
|
237
|
+
// Fix D: when RAPTOR is promoted, ALSO recall high-level tree summaries and
|
|
238
|
+
// merge them with the flat hits via MMR so RAPTOR + flat don't double-cover.
|
|
239
|
+
// RAPTOR returns fewer, broader hits (O(log n) high-level nodes) than the
|
|
240
|
+
// O(n) flat leaves, tightening the block at read time.
|
|
241
|
+
if (cfg.RAPTOR_ENABLED) {
|
|
242
|
+
const rh = raptorSearchHits(store, sid, query, k, checkpoints);
|
|
243
|
+
if (rh.length > 0) {
|
|
244
|
+
const merged: SearchHit[] = [...window];
|
|
245
|
+
for (const h of rh) {
|
|
246
|
+
if (
|
|
247
|
+
!merged.some(
|
|
248
|
+
(m) => m.checkpoint.checkpointId === h.checkpoint.checkpointId,
|
|
249
|
+
)
|
|
250
|
+
) {
|
|
251
|
+
merged.push(h);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const mmrItems: MmrItem<SearchHit>[] = merged.map((h) => ({
|
|
255
|
+
item: h,
|
|
256
|
+
vector: h.checkpoint.embedding,
|
|
257
|
+
relevance: h.score,
|
|
258
|
+
}));
|
|
259
|
+
return mmrRerank(mmrItems, k, cfg.MMR_LAMBDA);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const mmrItems: MmrItem<SearchHit>[] = window.map((h) => ({
|
|
264
|
+
item: h,
|
|
265
|
+
vector: h.checkpoint.embedding,
|
|
266
|
+
relevance: h.score,
|
|
267
|
+
}));
|
|
268
|
+
const ranked = mmrRerank(mmrItems, k, cfg.MMR_LAMBDA);
|
|
269
|
+
return ranked;
|
|
158
270
|
}
|
|
159
271
|
|
|
160
272
|
// ---------------------------------------------------------------------------
|
|
@@ -162,10 +274,10 @@ export function vectorSearch(
|
|
|
162
274
|
// ---------------------------------------------------------------------------
|
|
163
275
|
|
|
164
276
|
export interface VectorSearchOptions {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
277
|
+
/** Scope to a specific repo (omit for cross-repo NN). */
|
|
278
|
+
repoId?: string;
|
|
279
|
+
/** Include hits from all repos (cross-repo NN). Mutually exclusive with repoId. */
|
|
280
|
+
crossRepo?: boolean;
|
|
169
281
|
}
|
|
170
282
|
|
|
171
283
|
/**
|
|
@@ -183,49 +295,54 @@ export interface VectorSearchOptions {
|
|
|
183
295
|
* MMR-dedupes the merged set.
|
|
184
296
|
*/
|
|
185
297
|
export async function vectorSearchAsync(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
298
|
+
store: VectorStore,
|
|
299
|
+
sessionId: string,
|
|
300
|
+
query: string,
|
|
301
|
+
k = 3,
|
|
302
|
+
opts: VectorSearchOptions = {},
|
|
191
303
|
): Promise<SearchHit[]> {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
304
|
+
const cfg = store.cfg;
|
|
305
|
+
const embedder = store.embedder;
|
|
306
|
+
|
|
307
|
+
const sid = normalizeSessionId(sessionId);
|
|
308
|
+
const qv = embedder.embed(query);
|
|
309
|
+
// repoId filter: explicit opts.repoId wins; else this repo unless crossRepo.
|
|
310
|
+
const selfRepo = store.repoId;
|
|
311
|
+
const repoId = opts.repoId ?? (opts.crossRepo ? undefined : selfRepo);
|
|
312
|
+
let indexHits: VectorIndexHit[] = [];
|
|
313
|
+
try {
|
|
314
|
+
await initVectorIndex();
|
|
315
|
+
indexHits = await vectorIndexSearch(qv, { k: Math.max(k * 2, k), repoId });
|
|
316
|
+
} catch {
|
|
317
|
+
indexHits = [];
|
|
318
|
+
}
|
|
319
|
+
if (indexHits.length === 0) {
|
|
320
|
+
// Index empty/unavailable → synchronous per-session fallback (this repo).
|
|
321
|
+
return vectorSearch(store, sid, query, k);
|
|
322
|
+
}
|
|
323
|
+
// Hydrate each index hit from the authoritative node:sqlite store. repoId is
|
|
324
|
+
// that repo's stateDir, so cross-repo hits resolve against their own store.
|
|
325
|
+
// Tag cross-repo hits with their source repoId so the recall block can label
|
|
326
|
+
// them ("from repo <name>"); same-repo hits stay unlabeled.
|
|
327
|
+
const hydrated: SearchHit[] = [];
|
|
328
|
+
for (const h of indexHits) {
|
|
329
|
+
const cp = getCheckpoint(h.sessionId, h.checkpointId, h.repoId);
|
|
330
|
+
if (cp && cp.dedupStatus !== "removed") {
|
|
331
|
+
const crossRepo =
|
|
332
|
+
opts.crossRepo && selfRepo && h.repoId && h.repoId !== selfRepo;
|
|
333
|
+
hydrated.push({
|
|
334
|
+
checkpoint: cp,
|
|
335
|
+
score: h.score,
|
|
336
|
+
repoId: crossRepo ? h.repoId : undefined,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (hydrated.length === 0) return vectorSearch(store, sid, query, k);
|
|
341
|
+
// MMR-dedupe the merged candidate set for diversity (mirrors sync search).
|
|
342
|
+
const mmrItems: MmrItem<SearchHit>[] = hydrated.map((h) => ({
|
|
343
|
+
item: h,
|
|
344
|
+
vector: h.checkpoint.embedding,
|
|
345
|
+
relevance: h.score,
|
|
346
|
+
}));
|
|
347
|
+
return mmrRerank(mmrItems, k, cfg.MMR_LAMBDA);
|
|
348
|
+
}
|