@vortex-os/memory-extended 0.5.1 → 0.5.3

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 (60) hide show
  1. package/README.md +17 -17
  2. package/dist/mcp/recall-tool.d.ts +13 -1
  3. package/dist/mcp/recall-tool.d.ts.map +1 -1
  4. package/dist/mcp/recall-tool.js +37 -8
  5. package/dist/mcp/recall-tool.js.map +1 -1
  6. package/dist/mcp/server.d.ts.map +1 -1
  7. package/dist/mcp/server.js +4 -1
  8. package/dist/mcp/server.js.map +1 -1
  9. package/dist/recall/engine.d.ts +20 -18
  10. package/dist/recall/engine.d.ts.map +1 -1
  11. package/dist/recall/engine.js +248 -56
  12. package/dist/recall/engine.js.map +1 -1
  13. package/dist/recall/ftsQuery.d.ts +29 -0
  14. package/dist/recall/ftsQuery.d.ts.map +1 -0
  15. package/dist/recall/ftsQuery.js +36 -0
  16. package/dist/recall/ftsQuery.js.map +1 -0
  17. package/dist/recall/fusion.d.ts +58 -0
  18. package/dist/recall/fusion.d.ts.map +1 -0
  19. package/dist/recall/fusion.js +115 -0
  20. package/dist/recall/fusion.js.map +1 -0
  21. package/dist/recall/index.d.ts +3 -1
  22. package/dist/recall/index.d.ts.map +1 -1
  23. package/dist/recall/index.js +1 -0
  24. package/dist/recall/index.js.map +1 -1
  25. package/dist/recall/types.d.ts +24 -2
  26. package/dist/recall/types.d.ts.map +1 -1
  27. package/dist/sessionArchive/adapters/claude-code.d.ts.map +1 -1
  28. package/dist/sessionArchive/adapters/claude-code.js +38 -4
  29. package/dist/sessionArchive/adapters/claude-code.js.map +1 -1
  30. package/dist/sessionArchive/index.d.ts +1 -1
  31. package/dist/sessionArchive/index.d.ts.map +1 -1
  32. package/dist/sessionArchive/index.js.map +1 -1
  33. package/dist/sessionArchive/store.d.ts +22 -1
  34. package/dist/sessionArchive/store.d.ts.map +1 -1
  35. package/dist/sessionArchive/store.js +143 -12
  36. package/dist/sessionArchive/store.js.map +1 -1
  37. package/dist/sqlite/fts.d.ts +38 -0
  38. package/dist/sqlite/fts.d.ts.map +1 -0
  39. package/dist/sqlite/fts.js +102 -0
  40. package/dist/sqlite/fts.js.map +1 -0
  41. package/dist/sqlite/index.d.ts +2 -0
  42. package/dist/sqlite/index.d.ts.map +1 -1
  43. package/dist/sqlite/index.js +1 -0
  44. package/dist/sqlite/index.js.map +1 -1
  45. package/dist/sqlite/store.d.ts +8 -1
  46. package/dist/sqlite/store.d.ts.map +1 -1
  47. package/dist/sqlite/store.js +29 -7
  48. package/dist/sqlite/store.js.map +1 -1
  49. package/dist/vector/embedder.d.ts +11 -0
  50. package/dist/vector/embedder.d.ts.map +1 -1
  51. package/dist/vector/embedder.js +4 -1
  52. package/dist/vector/embedder.js.map +1 -1
  53. package/dist/vector/segment.d.ts +1 -1
  54. package/dist/vector/store.d.ts +12 -2
  55. package/dist/vector/store.d.ts.map +1 -1
  56. package/dist/vector/store.js +17 -2
  57. package/dist/vector/store.js.map +1 -1
  58. package/dist/vector/types.d.ts +1 -1
  59. package/dist/vector/types.js +1 -1
  60. package/package.json +3 -3
@@ -1,72 +1,203 @@
1
1
  import { parseIntent } from "./intent.js";
2
+ import { fuse, fetchK, fusionKey, TOOL_RESULT_MULTIPLIER, } from "./fusion.js";
2
3
  const DEFAULT_K = 5;
3
4
  const EXCERPT_CHARS = 280;
4
5
  /**
5
- * Two-stage hybrid recall (operator decision 5 / 2026-05-29). Returns
6
- * structured {@link RecallResult} data never a pre-rendered report. The
7
- * caller decides whether to list the hits (explicit `/recall`) or phrase
8
- * one in conversation (ambient use).
6
+ * Hybrid recall (P3). Branches on `mode`:
7
+ * - `semantic` the original cosine pipeline (memory hard-filter vector
8
+ * rerank), unchanged in behavior.
9
+ * - `keyword` — FTS5 over memories + session events.
10
+ * - `hybrid` (default) — both lanes, fused by Reciprocal Rank Fusion.
9
11
  *
10
- * Pipeline:
11
- * 1. **Parse intent** pull obvious filters (type / tag / date) out of the
12
- * query (regex, no LLM).
13
- * 2. **Hard filter (loose)**if filters were found, restrict candidates
14
- * via SQLite. *But* if that leaves fewer than `k` candidates, the
15
- * filter is dropped and the semantic stage runs over the whole corpus.
16
- * This is the safeguard against the over-narrowing risk: a guessed
17
- * filter never costs you a relevant-but-unfiltered memory.
18
- * 3. **Semantic rerank** — embed the (filter-stripped) query text and ask
19
- * the vector store for the closest candidates.
20
- * 4. **Hydrate** — join vector hits back to their memory rows for name,
21
- * description, tags, and a body excerpt.
12
+ * Returns structured {@link RecallResult} data — never a pre-rendered report.
13
+ * The memory hard-filter (parsed type/tag/date) constrains the MEMORY lanes
14
+ * only; session lanes filter by their own semantics (§12 R8). Session hits fuse
15
+ * at the SESSION level the semantic lane is de-duped to one best chunk per
16
+ * session before ranking (§13). The tool_result keyword downweight applies in
17
+ * `hybrid` only (§12 R6). `score` stays cosine for any semantically-matched
18
+ * hit; a keyword-only hit gets `1/(1+rank)` (§12 R7).
22
19
  */
23
20
  export async function recall(params, deps) {
24
21
  const { sqlite, vector, embed } = deps;
25
22
  const k = params.k ?? DEFAULT_K;
23
+ const mode = params.mode ?? "hybrid";
26
24
  const intent = parseIntent(params.query);
27
25
  const corpusSize = vector.count(params.source);
26
+ const fk = fetchK(k);
27
+ // ---- memory hard-filter — MEMORY lanes only (never session lanes, §12 R8) ----
28
28
  let appliedFilters = {};
29
- let ids;
29
+ let memIds;
30
30
  let hardFilterCandidates = corpusSize;
31
31
  let hardFilterDropped = false;
32
- const hasFilters = hasAnyFilter(intent.filters);
33
- if (hasFilters && !params.noHardFilter) {
32
+ if (hasAnyFilter(intent.filters) && !params.noHardFilter) {
34
33
  const filtered = sqlite.query(intent.filters);
35
34
  if (filtered.length < k) {
36
- // Too narrow — drop it. Searching the full corpus is safer than
37
- // excluding a relevant memory the guessed filter happened to miss.
35
+ // Too narrow — drop it; searching the full corpus is safer than excluding
36
+ // a relevant memory the guessed filter happened to miss.
38
37
  hardFilterDropped = true;
39
- hardFilterCandidates = corpusSize;
40
38
  }
41
39
  else {
42
40
  appliedFilters = intent.filters;
43
- ids = new Set(filtered.map((r) => r.id));
41
+ memIds = new Set(filtered.map((r) => r.id));
44
42
  hardFilterCandidates = filtered.length;
45
43
  }
46
44
  }
47
- const vectorHits = await vector.search(intent.semanticText, embed, {
48
- k,
49
- source: params.source,
50
- ids,
51
- });
45
+ const runKeyword = mode !== "semantic";
46
+ const runSemantic = mode !== "keyword";
47
+ const wantMemory = params.source !== "session-archive";
48
+ const wantSession = params.source !== "memory";
49
+ const hydration = new Map();
50
+ const semanticItems = [];
51
+ const keywordItems = [];
52
+ // ---- SEMANTIC lane (cosine) ----
53
+ if (runSemantic) {
54
+ if (wantMemory) {
55
+ const hits = await vector.search(intent.semanticText, embed, {
56
+ source: "memory",
57
+ ids: memIds,
58
+ k: fk,
59
+ });
60
+ for (const vh of hits) {
61
+ const row = sqlite.getById(vh.id);
62
+ if (!row)
63
+ continue; // drift: vector present, memory deleted
64
+ const key = fusionKey("memory", vh.id);
65
+ if (!hydration.has(key))
66
+ hydration.set(key, { kind: "memory", row });
67
+ semanticItems.push({ key, weight: 1, score: vh.score, recency: row.updated });
68
+ }
69
+ }
70
+ if (wantSession && deps.sessionChunks) {
71
+ const sessCount = vector.count("session-archive");
72
+ const vhits = await vector.search(intent.semanticText, embed, {
73
+ source: "session-archive",
74
+ k: sessCount > 0 ? sessCount : fk,
75
+ });
76
+ if (mode === "semantic") {
77
+ // Legacy behavior: PER-CHUNK session hits (id = chunk id), no collapse —
78
+ // mode:"semantic" reproduces the original cosine pipeline exactly (§12 R1).
79
+ for (const vh of vhits.slice(0, fk)) {
80
+ const chunk = deps.sessionChunks.getById(vh.id);
81
+ if (!chunk)
82
+ continue;
83
+ const key = fusionKey("session-archive", chunk.id);
84
+ hydration.set(key, { kind: "session", chunk, perChunk: true });
85
+ semanticItems.push({ key, weight: 1, score: vh.score, recency: chunk.startedAt });
86
+ }
87
+ }
88
+ else {
89
+ // Hybrid: collapse to the single best chunk per session so the keyword and
90
+ // semantic lanes fuse on a shared SESSION-level key (§13). Over-fetch in
91
+ // DISTINCT sessions (§14) — search broadly, collapse, then take fk sessions.
92
+ const best = new Map();
93
+ for (const vh of vhits) {
94
+ const chunk = deps.sessionChunks.getById(vh.id);
95
+ if (!chunk)
96
+ continue;
97
+ const sk = `${chunk.host}/${chunk.sessionId}`;
98
+ const prev = best.get(sk);
99
+ if (!prev ||
100
+ vh.score > prev.score ||
101
+ (vh.score === prev.score && chunk.chunkIdx < prev.chunk.chunkIdx)) {
102
+ best.set(sk, { chunk, score: vh.score });
103
+ }
104
+ }
105
+ const sessions = [...best.entries()]
106
+ .map(([sk, v]) => ({ sk, chunk: v.chunk, score: v.score }))
107
+ .sort(compareSessionSemantic)
108
+ .slice(0, fk);
109
+ for (const { sk, chunk, score } of sessions) {
110
+ const key = fusionKey("session-archive", sk);
111
+ const ref = hydration.get(key);
112
+ if (ref)
113
+ ref.chunk = chunk;
114
+ else
115
+ hydration.set(key, { kind: "session", chunk });
116
+ semanticItems.push({ key, weight: 1, score, recency: chunk.startedAt });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ // ---- KEYWORD lane (FTS5) ----
122
+ if (runKeyword) {
123
+ if (wantMemory) {
124
+ const mids = sqlite.keywordSearch(intent.semanticText, { limit: fk, ids: memIds });
125
+ mids.forEach((mid, i) => {
126
+ const key = fusionKey("memory", mid);
127
+ let ref = hydration.get(key);
128
+ if (!ref) {
129
+ const row = sqlite.getById(mid);
130
+ if (!row)
131
+ return; // drift
132
+ ref = { kind: "memory", row };
133
+ hydration.set(key, ref);
134
+ }
135
+ keywordItems.push({ key, weight: 1, score: rankConfidence(i), recency: ref.row.updated });
136
+ });
137
+ }
138
+ if (wantSession && deps.sessionArchive) {
139
+ // Fetch broadly so the per-session collapse below still yields fk DISTINCT
140
+ // sessions even when one chatty session owns many of the top events (Codex F3).
141
+ // O(window) at current archive scale; a SQL GROUP-BY per session is the scale follow-up.
142
+ const evHits = deps.sessionArchive.searchEvents(intent.semanticText, {
143
+ limit: Math.max(fk * 50, 500),
144
+ });
145
+ // Collapse to the best (first = best-ranked) event per session.
146
+ const bestEv = new Map();
147
+ for (const ev of evHits) {
148
+ const sk = `${ev.host}/${ev.sessionId}`;
149
+ if (!bestEv.has(sk))
150
+ bestEv.set(sk, ev);
151
+ }
152
+ let r = 0;
153
+ for (const [sk, ev] of bestEv) {
154
+ if (r >= fk)
155
+ break; // distinct-session over-fetch budget
156
+ const key = fusionKey("session-archive", sk);
157
+ // tool_result downweight is HYBRID-only — keyword-only forensic recall
158
+ // must not be penalized (§12 R6).
159
+ const weight = mode === "hybrid" && ev.kind === "tool_result" ? TOOL_RESULT_MULTIPLIER : 1;
160
+ keywordItems.push({ key, weight, score: rankConfidence(r), recency: ev.at });
161
+ const ref = hydration.get(key);
162
+ if (ref)
163
+ ref.keywordEvent = ev; // §12 R2: prefer the matched event's snippet
164
+ else
165
+ hydration.set(key, { kind: "session", keywordEvent: ev });
166
+ r++;
167
+ }
168
+ }
169
+ }
170
+ // ---- total per-lane ordering before RRF rank assignment (§13 R3) ----
171
+ semanticItems.sort(compareLaneItem);
172
+ keywordItems.sort(compareLaneItem);
173
+ const lanes = [];
174
+ if (runSemantic)
175
+ lanes.push({ name: "semantic", items: semanticItems });
176
+ if (runKeyword)
177
+ lanes.push({ name: "keyword", items: keywordItems });
178
+ const fused = fuse(lanes, k);
52
179
  const hits = [];
53
- for (const vh of vectorHits) {
54
- if (vh.source === "session-archive") {
55
- const chunk = deps.sessionChunks?.getById(vh.id);
56
- if (!chunk)
57
- continue; // no session-chunk metadata (not hydratable) — skip
58
- hits.push(toSessionHit(chunk, vh.score));
180
+ for (const entry of fused) {
181
+ const ref = hydration.get(entry.key);
182
+ if (!ref)
59
183
  continue;
60
- }
61
- const row = sqlite.getById(vh.id);
62
- if (!row)
63
- continue; // vector exists but memory was deleted drift; skip
64
- hits.push(toHit(row, vh.source, vh.score));
184
+ const score = entry.scoreByLane.semantic ?? entry.scoreByLane.keyword ?? 0;
185
+ // mode:"semantic" keeps the original hit shape — no fusion metadata (§12 R1).
186
+ const meta = mode === "semantic" ? {} : { lanes: entry.lanes, fusedScore: entry.fusedScore };
187
+ hits.push(ref.kind === "memory" ? hydrateMemory(ref, score, meta) : hydrateSession(ref, score, meta));
65
188
  }
66
189
  return {
67
190
  query: params.query,
68
191
  intent,
69
- stage: { appliedFilters, hardFilterCandidates, hardFilterDropped, corpusSize },
192
+ stage: {
193
+ appliedFilters,
194
+ hardFilterCandidates,
195
+ hardFilterDropped,
196
+ corpusSize,
197
+ mode,
198
+ lanesRun: lanes.map((l) => l.name),
199
+ laneCandidates: { semantic: semanticItems.length, keyword: keywordItems.length },
200
+ },
70
201
  hits,
71
202
  };
72
203
  }
@@ -76,10 +207,11 @@ function hasAnyFilter(q) {
76
207
  (q.privacy && q.privacy.length) ||
77
208
  q.updatedSinceMs !== undefined);
78
209
  }
79
- function toHit(row, source, score) {
210
+ function hydrateMemory(ref, score, meta) {
211
+ const row = ref.row;
80
212
  return {
81
213
  id: row.id,
82
- source,
214
+ source: "memory",
83
215
  score,
84
216
  name: row.name,
85
217
  description: row.description,
@@ -87,27 +219,87 @@ function toHit(row, source, score) {
87
219
  updated: row.updated,
88
220
  tags: row.tags,
89
221
  excerpt: excerpt(row.body),
222
+ ...meta,
90
223
  };
91
224
  }
92
- function excerpt(body) {
93
- const collapsed = body.replace(/\s+/g, " ").trim();
94
- return collapsed.length <= EXCERPT_CHARS
95
- ? collapsed
96
- : `${collapsed.slice(0, EXCERPT_CHARS).trimEnd()}…`;
97
- }
98
- /** Hydrate a session-archive hit from its chunk metadata row. */
99
- function toSessionHit(chunk, score) {
100
- const day = chunk.startedAt ? chunk.startedAt.slice(0, 10) : null;
225
+ function hydrateSession(ref, score, meta) {
226
+ const chunk = ref.chunk;
227
+ const ev = ref.keywordEvent;
228
+ if (ref.perChunk && chunk) {
229
+ // Legacy per-CHUNK session hit (mode:"semantic") — original id/name shape (§12 R1).
230
+ const cday = chunk.startedAt ? chunk.startedAt.slice(0, 10) : null;
231
+ return {
232
+ id: chunk.id,
233
+ source: "session-archive",
234
+ score,
235
+ name: `${chunk.host} session${cday ? ` (${cday})` : ""} · part ${chunk.chunkIdx + 1}`,
236
+ description: "",
237
+ type: "session",
238
+ updated: chunk.startedAt,
239
+ tags: [chunk.host],
240
+ excerpt: chunk.excerpt,
241
+ ...meta,
242
+ };
243
+ }
244
+ const host = chunk?.host ?? ev?.host ?? "session";
245
+ const sessionId = chunk?.sessionId ?? ev?.sessionId ?? "";
246
+ const when = chunk?.startedAt ?? ev?.at ?? null;
247
+ const day = when ? when.slice(0, 10) : null;
248
+ // §12 R2: when the keyword lane matched, prefer the matched event's text as
249
+ // the excerpt (the actual evidence — e.g. the error line) over an unrelated
250
+ // semantic chunk; otherwise use the chunk excerpt.
251
+ const excerptText = ev?.text ? excerpt(ev.text) : chunk ? chunk.excerpt : "";
101
252
  return {
102
- id: chunk.id,
253
+ id: `${host}/${sessionId}`,
103
254
  source: "session-archive",
104
255
  score,
105
- name: `${chunk.host} session${day ? ` (${day})` : ""} · part ${chunk.chunkIdx + 1}`,
256
+ name: `${host} session${day ? ` (${day})` : ""}`,
106
257
  description: "",
107
258
  type: "session",
108
- updated: chunk.startedAt,
109
- tags: [chunk.host],
110
- excerpt: chunk.excerpt,
259
+ updated: when,
260
+ tags: [host],
261
+ excerpt: excerptText,
262
+ ...meta,
111
263
  };
112
264
  }
265
+ function excerpt(body) {
266
+ const collapsed = body.replace(/\s+/g, " ").trim();
267
+ return collapsed.length <= EXCERPT_CHARS
268
+ ? collapsed
269
+ : `${collapsed.slice(0, EXCERPT_CHARS).trimEnd()}…`;
270
+ }
271
+ /** Display/tie-break confidence for a keyword hit at 0-based position `i` → `1/(1+rank)` (§12 R7). */
272
+ function rankConfidence(i) {
273
+ return 1 / (2 + i);
274
+ }
275
+ /** Total order for a lane's items before RRF (§13 R3): score desc, recency desc (null last), key asc. */
276
+ function compareLaneItem(a, b) {
277
+ if (b.score !== a.score)
278
+ return b.score - a.score;
279
+ const r = compareRecencyDesc(a.recency, b.recency);
280
+ if (r !== 0)
281
+ return r;
282
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
283
+ }
284
+ /** Total order for best-chunk-per-session: score desc, startedAt desc (null last), chunkIdx asc, key asc. */
285
+ function compareSessionSemantic(a, b) {
286
+ if (b.score !== a.score)
287
+ return b.score - a.score;
288
+ const r = compareRecencyDesc(a.chunk.startedAt, b.chunk.startedAt);
289
+ if (r !== 0)
290
+ return r;
291
+ if (a.chunk.chunkIdx !== b.chunk.chunkIdx)
292
+ return a.chunk.chunkIdx - b.chunk.chunkIdx;
293
+ return a.sk < b.sk ? -1 : a.sk > b.sk ? 1 : 0;
294
+ }
295
+ /** Order newer-first; null sorts last (ISO strings compare chronologically). */
296
+ function compareRecencyDesc(a, b) {
297
+ if (a === b)
298
+ return 0;
299
+ if (a === null)
300
+ return 1;
301
+ if (b === null)
302
+ return -1;
303
+ return a < b ? 1 : -1;
304
+ }
113
305
  //# sourceMappingURL=engine.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/recall/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/recall/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /** Result of parsing a recall query into FTS5-safe tokens (P3 hybrid search). */
2
+ export interface FtsQuery {
3
+ /**
4
+ * FTS5 MATCH string: each >=3-codepoint token as a double-quoted phrase,
5
+ * space-joined (implicit AND). Empty string when no long token is present.
6
+ */
7
+ readonly match: string;
8
+ /**
9
+ * Sub-3-codepoint terms the `trigram` tokenizer cannot match (a trigram
10
+ * needs >=3 codepoints — verified on SQLite 3.53.1). These need a LIKE
11
+ * fallback over the source text instead.
12
+ */
13
+ readonly shortTerms: readonly string[];
14
+ /** Whether the query yielded any usable token at all (long or short). */
15
+ readonly hasTokens: boolean;
16
+ }
17
+ /**
18
+ * Parse a natural-language query into an FTS5-safe MATCH string plus the short
19
+ * terms that need a LIKE fallback. Pure + deterministic — the one shared query
20
+ * builder for BOTH the memory and session keyword lanes (design §2-B / §11).
21
+ *
22
+ * Each whitespace-delimited token of >=3 codepoints becomes a double-quoted
23
+ * phrase (internal `"` doubled) so paths, `:`, `-`, and error codes are treated
24
+ * as literal phrases, never FTS5 operators — an unquoted `:`/path throws
25
+ * (`no such column` — verified), so the quoting is load-bearing. Shorter tokens
26
+ * go to `shortTerms` for the LIKE branch.
27
+ */
28
+ export declare function buildFtsQuery(query: string): FtsQuery;
29
+ //# sourceMappingURL=ftsQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ftsQuery.d.ts","sourceRoot":"","sources":["../../src/recall/ftsQuery.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAsBrD"}
@@ -0,0 +1,36 @@
1
+ /** The `trigram` tokenizer cannot form a trigram from fewer than 3 codepoints. */
2
+ const TRIGRAM_MIN = 3;
3
+ /**
4
+ * Parse a natural-language query into an FTS5-safe MATCH string plus the short
5
+ * terms that need a LIKE fallback. Pure + deterministic — the one shared query
6
+ * builder for BOTH the memory and session keyword lanes (design §2-B / §11).
7
+ *
8
+ * Each whitespace-delimited token of >=3 codepoints becomes a double-quoted
9
+ * phrase (internal `"` doubled) so paths, `:`, `-`, and error codes are treated
10
+ * as literal phrases, never FTS5 operators — an unquoted `:`/path throws
11
+ * (`no such column` — verified), so the quoting is load-bearing. Shorter tokens
12
+ * go to `shortTerms` for the LIKE branch.
13
+ */
14
+ export function buildFtsQuery(query) {
15
+ const tokens = query
16
+ .split(/\s+/)
17
+ .map((t) => t.trim())
18
+ .filter((t) => t.length > 0);
19
+ const phrases = [];
20
+ const shortTerms = [];
21
+ for (const tok of tokens) {
22
+ const codepoints = [...tok].length; // surrogate-safe codepoint count
23
+ if (codepoints >= TRIGRAM_MIN) {
24
+ phrases.push(`"${tok.replace(/"/g, '""')}"`);
25
+ }
26
+ else {
27
+ shortTerms.push(tok);
28
+ }
29
+ }
30
+ return {
31
+ match: phrases.join(" "),
32
+ shortTerms: [...new Set(shortTerms)],
33
+ hasTokens: phrases.length > 0 || shortTerms.length > 0,
34
+ };
35
+ }
36
+ //# sourceMappingURL=ftsQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ftsQuery.js","sourceRoot":"","sources":["../../src/recall/ftsQuery.ts"],"names":[],"mappings":"AAiBA,kFAAkF;AAClF,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,MAAM,GAAG,KAAK;SACjB,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,iCAAiC;QACrE,IAAI,UAAU,IAAI,WAAW,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QACxB,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;KACvD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Reciprocal Rank Fusion (RRF) for P3 hybrid search.
3
+ *
4
+ * Merges the keyword and semantic lanes by RANK, so their incomparable scores
5
+ * (bm25 is negative/unbounded; e5 cosine sits in a high, narrow band) never need
6
+ * per-query normalization. Pure + deterministic. See
7
+ * `docs/P3-hybrid-search-design.md` §2-A and the §11-§14 revisions.
8
+ */
9
+ export type LaneName = "keyword" | "semantic";
10
+ /** RRF rank-bias constant — the established default. */
11
+ export declare const RRF_K = 60;
12
+ /** Per-item keyword-contribution multiplier for a tool_result hit (hybrid only; §11 C / §12 R6). */
13
+ export declare const TOOL_RESULT_MULTIPLIER = 0.5;
14
+ /** Over-fetch budget per lane so a hit ranked low in one lane still surfaces after fusion. */
15
+ export declare function fetchK(k: number): number;
16
+ export interface LaneItem {
17
+ /** Fusion identity — the SAME key across lanes denotes the SAME result. */
18
+ readonly key: string;
19
+ /** Per-item contribution multiplier (1, or {@link TOOL_RESULT_MULTIPLIER}). */
20
+ readonly weight: number;
21
+ /** Raw lane score: cosine for semantic, a rank-confidence proxy for keyword. Tie-break + display. */
22
+ readonly score: number;
23
+ /** Recency (ISO) for the deterministic tie-break; null sorts last. */
24
+ readonly recency: string | null;
25
+ }
26
+ export interface Lane {
27
+ readonly name: LaneName;
28
+ /** Items best-first, ALREADY totally ordered by the caller (§13 R3). */
29
+ readonly items: readonly LaneItem[];
30
+ }
31
+ export interface FusedEntry {
32
+ readonly key: string;
33
+ readonly fusedScore: number;
34
+ /** Which lanes matched this key (provenance). */
35
+ readonly lanes: readonly LaneName[];
36
+ /** 1-based rank of this key within each lane that matched it. */
37
+ readonly rankByLane: Partial<Record<LaneName, number>>;
38
+ /** Raw score of this key within each lane that matched it. */
39
+ readonly scoreByLane: Partial<Record<LaneName, number>>;
40
+ /** Max raw score across matching lanes (tie-break + a sane display value). */
41
+ readonly bestScore: number;
42
+ readonly recency: string | null;
43
+ }
44
+ /**
45
+ * Fuse the given lanes and return the top-`k` entries, deterministically
46
+ * ordered. Each item contributes `weight / (RRF_K + rank)` to its key, **at most
47
+ * once per lane** — a key repeated within one lane keeps its best (first) rank,
48
+ * the "one contribution per lane per item" invariant (§13 HIGH). Tie-break is
49
+ * total: fusedScore desc, then #lanes desc, then bestScore desc, then recency
50
+ * desc (nulls last), then key asc — never depends on input/DB row order.
51
+ */
52
+ export declare function fuse(lanes: readonly Lane[], k: number): FusedEntry[];
53
+ /**
54
+ * Canonical fusion key — a value-equal string usable directly as a Map key
55
+ * (§13 LOW: raw arrays compare by object identity, not value).
56
+ */
57
+ export declare function fusionKey(source: string, id: string): string;
58
+ //# sourceMappingURL=fusion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion.d.ts","sourceRoot":"","sources":["../../src/recall/fusion.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;AAE9C,wDAAwD;AACxD,eAAO,MAAM,KAAK,KAAK,CAAC;AAExB,oGAAoG;AACpG,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE,CAmDpE;AAkCD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Reciprocal Rank Fusion (RRF) for P3 hybrid search.
3
+ *
4
+ * Merges the keyword and semantic lanes by RANK, so their incomparable scores
5
+ * (bm25 is negative/unbounded; e5 cosine sits in a high, narrow band) never need
6
+ * per-query normalization. Pure + deterministic. See
7
+ * `docs/P3-hybrid-search-design.md` §2-A and the §11-§14 revisions.
8
+ */
9
+ /** RRF rank-bias constant — the established default. */
10
+ export const RRF_K = 60;
11
+ /** Per-item keyword-contribution multiplier for a tool_result hit (hybrid only; §11 C / §12 R6). */
12
+ export const TOOL_RESULT_MULTIPLIER = 0.5;
13
+ /** Over-fetch budget per lane so a hit ranked low in one lane still surfaces after fusion. */
14
+ export function fetchK(k) {
15
+ return Math.max(k * 4, 20);
16
+ }
17
+ /**
18
+ * Fuse the given lanes and return the top-`k` entries, deterministically
19
+ * ordered. Each item contributes `weight / (RRF_K + rank)` to its key, **at most
20
+ * once per lane** — a key repeated within one lane keeps its best (first) rank,
21
+ * the "one contribution per lane per item" invariant (§13 HIGH). Tie-break is
22
+ * total: fusedScore desc, then #lanes desc, then bestScore desc, then recency
23
+ * desc (nulls last), then key asc — never depends on input/DB row order.
24
+ */
25
+ export function fuse(lanes, k) {
26
+ const acc = new Map();
27
+ for (const lane of lanes) {
28
+ const seenInLane = new Set();
29
+ lane.items.forEach((item, i) => {
30
+ if (seenInLane.has(item.key))
31
+ return; // one contribution per lane per item
32
+ seenInLane.add(item.key);
33
+ const rank = i + 1;
34
+ let e = acc.get(item.key);
35
+ if (!e) {
36
+ e = {
37
+ fusedScore: 0,
38
+ lanes: new Set(),
39
+ rankByLane: {},
40
+ scoreByLane: {},
41
+ bestScore: -Infinity,
42
+ recency: null,
43
+ };
44
+ acc.set(item.key, e);
45
+ }
46
+ e.fusedScore += item.weight / (RRF_K + rank);
47
+ e.lanes.add(lane.name);
48
+ e.rankByLane[lane.name] = rank;
49
+ e.scoreByLane[lane.name] = item.score;
50
+ if (item.score > e.bestScore)
51
+ e.bestScore = item.score;
52
+ if (isNewer(item.recency, e.recency))
53
+ e.recency = item.recency;
54
+ });
55
+ }
56
+ const entries = [];
57
+ for (const [key, e] of acc) {
58
+ entries.push({
59
+ key,
60
+ fusedScore: e.fusedScore,
61
+ lanes: [...e.lanes],
62
+ rankByLane: e.rankByLane,
63
+ scoreByLane: e.scoreByLane,
64
+ bestScore: e.bestScore,
65
+ recency: e.recency,
66
+ });
67
+ }
68
+ entries.sort(compareFused);
69
+ return entries.slice(0, k);
70
+ }
71
+ function compareFused(a, b) {
72
+ if (b.fusedScore !== a.fusedScore)
73
+ return b.fusedScore - a.fusedScore;
74
+ if (b.lanes.length !== a.lanes.length)
75
+ return b.lanes.length - a.lanes.length;
76
+ if (b.bestScore !== a.bestScore)
77
+ return b.bestScore - a.bestScore;
78
+ const r = compareRecencyDesc(a.recency, b.recency);
79
+ if (r !== 0)
80
+ return r;
81
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
82
+ }
83
+ /** True iff `cand` is strictly more recent than `cur` (null = oldest). */
84
+ function isNewer(cand, cur) {
85
+ if (cand === null)
86
+ return false;
87
+ if (cur === null)
88
+ return true;
89
+ return cand > cur;
90
+ }
91
+ /** Order newer-first; null sorts last (ISO strings compare chronologically). */
92
+ function compareRecencyDesc(a, b) {
93
+ if (a === b)
94
+ return 0;
95
+ if (a === null)
96
+ return 1;
97
+ if (b === null)
98
+ return -1;
99
+ return a < b ? 1 : -1;
100
+ }
101
+ /**
102
+ * Fusion-key delimiter: a NUL (U+0000), built with `String.fromCharCode` so the
103
+ * SOURCE holds no literal control byte (see feedback_no_literal_control_bytes).
104
+ * NUL cannot occur in `source` (a fixed enum) or `id` (a memory slug, a
105
+ * `host/sessionId`, or a chunk id), so the joined key is collision-free.
106
+ */
107
+ const KEY_SEP = String.fromCharCode(0);
108
+ /**
109
+ * Canonical fusion key — a value-equal string usable directly as a Map key
110
+ * (§13 LOW: raw arrays compare by object identity, not value).
111
+ */
112
+ export function fusionKey(source, id) {
113
+ return source + KEY_SEP + id;
114
+ }
115
+ //# sourceMappingURL=fusion.js.map