@melandlabs/memory-store 1.2.4 → 1.3.0

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { isFactType, vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
3
- import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
4
- import { ChromaVectorStore } from '@melandlabs/rag';
3
+ import '@melandlabs/ai/memory';
4
+ import { ChromaVectorStore as ChromaVectorStore$1 } from '@melandlabs/rag';
5
5
  import { mkdirSync } from 'fs';
6
6
  import { dirname } from 'path';
7
7
  import { getOpenContextPath } from '@melandlabs/env-config';
@@ -12,6 +12,8 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
12
12
  import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
13
13
  import { isEntityKind } from '@melandlabs/contracts/entity-edge';
14
14
  import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
15
+ import { createHash } from 'crypto';
16
+ import { ChromaVectorStore } from '@melandlabs/rag/chroma-vector-store';
15
17
 
16
18
  var __defProp = Object.defineProperty;
17
19
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -22,27 +24,6 @@ var __export = (target, all) => {
22
24
  for (var name in all)
23
25
  __defProp(target, name, { get: all[name], enumerable: true });
24
26
  };
25
- function normalizeTimestampToMs(value) {
26
- if (!Number.isFinite(value)) {
27
- return 0;
28
- }
29
- if (value < 1e11) {
30
- return Math.floor(value * 1e3);
31
- }
32
- return Math.floor(value);
33
- }
34
- function rawMessageToMemoryRecord(message) {
35
- return {
36
- id: message.messageId,
37
- userId: message.userId,
38
- timestamp: normalizeTimestampToMs(message.timestamp),
39
- text: message.archivedAt ? void 0 : message.content,
40
- mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
41
- embedding: message.embedding,
42
- embeddingModel: message.embeddingModel,
43
- embeddingContentHash: message.embeddingContentHash
44
- };
45
- }
46
27
  var init_contracts = __esm({
47
28
  "src/contracts.ts"() {
48
29
  }
@@ -63,66 +44,14 @@ function getRawMessagesCollectionName() {
63
44
  return process.env.CHROMA_RAW_MESSAGES_COLLECTION || process.env.CHROMA_MEMORY_COLLECTION || DEFAULT_RAW_MESSAGES_COLLECTION;
64
45
  }
65
46
  function getChromaStore(collectionName) {
66
- return new ChromaVectorStore({
47
+ return new ChromaVectorStore$1({
67
48
  url: process.env.CHROMA_URL,
68
49
  collectionName
69
50
  });
70
51
  }
71
- function normalizeTimestampToMs2(value) {
72
- if (!Number.isFinite(value)) {
73
- return 0;
74
- }
75
- if (value < 1e11) {
76
- return Math.floor(value * 1e3);
77
- }
78
- return Math.floor(value);
79
- }
80
52
  function toBooleanValue(value) {
81
53
  return value === true || value === "true" || value === 1 || value === "1";
82
54
  }
83
- function rawMessageToChunk(message) {
84
- if (!message.embedding || message.embedding.length === 0) {
85
- return null;
86
- }
87
- const record = rawMessageToMemoryRecord(message);
88
- const document = buildMemoryRecordEmbeddingDocument(record);
89
- if (!document.content) {
90
- return null;
91
- }
92
- return {
93
- id: message.messageId,
94
- documentId: message.messageId,
95
- content: document.content,
96
- embedding: message.embedding,
97
- metadata: {
98
- sourceType: "raw_message",
99
- userId: message.userId,
100
- platform: message.platform,
101
- botId: message.botId,
102
- channel: message.channel,
103
- person: message.person,
104
- timestamp: normalizeTimestampToMs2(message.timestamp),
105
- memoryStage: message.memoryStage,
106
- embeddingModel: message.embeddingModel,
107
- embeddingDimensions: message.embeddingDimensions,
108
- contentHash: message.embeddingContentHash,
109
- archived: Boolean(message.archivedAt),
110
- role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
111
- }
112
- };
113
- }
114
- async function upsertRawMessagesToChroma(messages) {
115
- if (!isRawMessageChromaEnabled() || messages.length === 0) {
116
- return 0;
117
- }
118
- const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
119
- if (chunks.length === 0) {
120
- return 0;
121
- }
122
- const store = getChromaStore(getRawMessagesCollectionName());
123
- await store.addChunks(chunks);
124
- return chunks.length;
125
- }
126
55
  async function searchRawMessagesWithChroma(input) {
127
56
  if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
128
57
  return [];
@@ -324,59 +253,73 @@ var init_raw_message_store = __esm({
324
253
  }
325
254
  });
326
255
 
327
- // src/embed-on-insert.ts
328
- async function embedMissingMessages(messages, deps) {
329
- if (typeof deps.embedQuery !== "function") {
330
- return messages;
256
+ // src/search/unified-search.ts
257
+ init_chroma_memory_index();
258
+ init_raw_message_store();
259
+
260
+ // src/search/applicability.ts
261
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
262
+ "global",
263
+ "task",
264
+ "conversation",
265
+ "channel",
266
+ "project",
267
+ "custom"
268
+ ]);
269
+ function isRecord(value) {
270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
271
+ }
272
+ function invalidRuntimeContext(message) {
273
+ throw new TypeError(`memory-store search runtime context: ${message}`);
274
+ }
275
+ function validateApplicabilityContext(value, index) {
276
+ const path = `applicabilityContexts[${index}]`;
277
+ if (!isRecord(value)) {
278
+ invalidRuntimeContext(`${path} must be an object`);
331
279
  }
332
- const out = [];
333
- for (const message of messages) {
334
- if (Array.isArray(message.embedding) && message.embedding.length > 0) {
335
- out.push(message);
336
- continue;
337
- }
338
- if (typeof message.content !== "string" || message.content.length === 0) {
339
- out.push(message);
340
- continue;
280
+ const scope = value.scope;
281
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
282
+ invalidRuntimeContext(`${path}.scope is not supported`);
283
+ }
284
+ if (value.key !== void 0 && typeof value.key !== "string") {
285
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
286
+ }
287
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
288
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
289
+ }
290
+ for (const field of ["validFrom", "validUntil"]) {
291
+ const boundary = value[field];
292
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
293
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
341
294
  }
342
- const vector = await deps.embedQuery({
343
- userId: message.userId,
344
- query: message.content
345
- });
346
- out.push({
347
- ...message,
348
- embedding: vector,
349
- embeddingModel: message.embeddingModel ?? "server",
350
- embeddingDimensions: vector.length,
351
- embeddingUpdatedAt: Date.now()
352
- });
353
295
  }
354
- return out;
355
296
  }
356
- var AUTO_APPLIED_WARNING = {
357
- code: "embed_on_insert_auto_applied",
358
- message: "Caller set `embedOnInsert` falsy but the active vector backend needs an embedding per row. Server filled in the missing embeddings; pass `embedOnInsert: true` to opt in explicitly."
359
- };
360
- async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
361
- const messages = incoming;
362
- const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
363
- const embedderWired = typeof unified?.embedQuery === "function";
364
- if (embedOnInsert === true && anyMissing) {
365
- return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
366
- }
367
- if (embedOnInsert !== true && anyMissing && embedderWired) {
368
- return {
369
- messages: await embedMissingMessages(messages, unified ?? {}),
370
- warnings: [AUTO_APPLIED_WARNING]
371
- };
297
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
298
+ if (runtimeContext === void 0) {
299
+ return void 0;
300
+ }
301
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
302
+ invalidRuntimeContext("applicabilityContexts must be an array");
303
+ }
304
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
305
+ validateApplicabilityContext(context, index);
306
+ }
307
+ let applicabilityAt;
308
+ if (input.asOf === void 0) {
309
+ applicabilityAt = now();
310
+ } else {
311
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
312
+ if (!Number.isFinite(parsed)) {
313
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
314
+ }
315
+ applicabilityAt = parsed;
372
316
  }
373
- return { messages, warnings: [] };
317
+ return {
318
+ applicabilityContexts: runtimeContext.applicabilityContexts,
319
+ applicabilityAt
320
+ };
374
321
  }
375
322
 
376
- // src/search/unified-search.ts
377
- init_chroma_memory_index();
378
- init_raw_message_store();
379
-
380
323
  // src/search/reranker.ts
381
324
  async function applyReranker(reranker, query, results) {
382
325
  if (!reranker || results.length === 0) {
@@ -463,7 +406,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
463
406
  return value === "rrf" ? "rrf" : "similarity";
464
407
  }
465
408
  function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
466
- if (value === "rewrite" || value === "iterative") {
409
+ if (value === "rewrite" || value === "iterative" || value === "union") {
467
410
  return value;
468
411
  }
469
412
  return defaultStrategy;
@@ -570,6 +513,7 @@ function materializeSignals(lists, hit) {
570
513
  function listNameToChannel(name) {
571
514
  if (name === "memory-semantic") return "semantic";
572
515
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
516
+ if (name === "memory-hybrid") return "hybrid";
573
517
  if (name === "memory-entity") return "entity";
574
518
  return void 0;
575
519
  }
@@ -627,7 +571,6 @@ async function resolveScopePeer(input) {
627
571
 
628
572
  // src/search/gather-evidence.ts
629
573
  var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
630
- var MAX_EVIDENCE_CHARS = 600;
631
574
  function toSummaryResult(row) {
632
575
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
633
576
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -645,7 +588,7 @@ function toSummaryResult(row) {
645
588
  }
646
589
  };
647
590
  }
648
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
591
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
649
592
  if (typeof deps.searchSummaries !== "function") {
650
593
  return { hits: [], warnings: [] };
651
594
  }
@@ -658,7 +601,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
658
601
  limit,
659
602
  threshold,
660
603
  authToken: input.authToken,
661
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
604
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
605
+ ...runtimeContext ?? {}
662
606
  });
663
607
  const hits = rows.map(toSummaryResult);
664
608
  return { hits, warnings: [] };
@@ -676,12 +620,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
676
620
  };
677
621
  }
678
622
  }
679
- function truncate(input, max = MAX_EVIDENCE_CHARS) {
680
- if (input.length <= max) {
681
- return input;
682
- }
683
- return `${input.slice(0, max - 1)}\u2026`;
684
- }
685
623
  function buildSynthesisPrompt(input) {
686
624
  const grouped = /* @__PURE__ */ new Map();
687
625
  for (const item of input.evidence) {
@@ -695,9 +633,10 @@ function buildSynthesisPrompt(input) {
695
633
  if (items.length === 0) {
696
634
  continue;
697
635
  }
698
- const lines = items.map(
699
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
700
- );
636
+ const lines = items.map((item, index) => {
637
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
638
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
639
+ });
701
640
  sections.push(`## ${tier}
702
641
  ${lines.join("\n")}`);
703
642
  }
@@ -779,12 +718,15 @@ function coerceAnswer(text, schema) {
779
718
  return text;
780
719
  }
781
720
  async function synthesizeAnswer(input) {
782
- const { query, hits, responseSchema, deps, logger } = input;
783
- const evidence = hits.map((hit) => ({
784
- id: hit.id,
785
- source: mapHitToTier(hit),
786
- snippet: truncate(hit.content),
787
- score: hit.similarity
721
+ const { query, responseSchema, deps, logger } = input;
722
+ const evidence = input.evidence.map((item) => ({
723
+ id: item.id,
724
+ source: mapEvidenceSourceToTier(item.source),
725
+ snippet: item.snippet,
726
+ score: item.score,
727
+ // Forward per-fact provenance (e.g. `metadata.source` from
728
+ // `opencontext add --source`) so the synthesis prompt can cite it.
729
+ metadata: item.metadata
788
730
  }));
789
731
  const warnings = [];
790
732
  const complete = deps.reasoning?.complete;
@@ -815,21 +757,10 @@ async function synthesizeAnswer(input) {
815
757
  return { answer: "", warnings };
816
758
  }
817
759
  }
818
- function mapHitToTier(hit) {
819
- const tierMarker = hit.metadata?.tier;
820
- if (tierMarker === "summary") {
821
- return "summary";
822
- }
823
- if (hit.type === "memory") {
824
- return "raw";
825
- }
826
- if (hit.type === "insight") {
827
- return "insight";
828
- }
829
- if (hit.type === "knowledge") {
830
- return "knowledge";
831
- }
832
- return hit.type;
760
+ function mapEvidenceSourceToTier(source) {
761
+ if (source === "memory") return "raw";
762
+ if (source === "insights") return "insight";
763
+ return source;
833
764
  }
834
765
  async function resolveSearchScopePeers(input) {
835
766
  return resolveScopePeer({
@@ -853,7 +784,7 @@ function deriveEntityKeywords(query) {
853
784
  }
854
785
  return out.slice(0, 16);
855
786
  }
856
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
787
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
857
788
  if (typeof deps.entitySearch !== "function") {
858
789
  return [];
859
790
  }
@@ -871,7 +802,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
871
802
  keywords,
872
803
  limit: Math.ceil(limit / filters.length),
873
804
  botId,
874
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
805
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
806
+ ...runtimeContext ?? {}
875
807
  })
876
808
  )
877
809
  )).flat();
@@ -959,6 +891,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
959
891
  return true;
960
892
  });
961
893
  }
894
+ function warnApplicabilityNotEnforced(warnings) {
895
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
896
+ return;
897
+ }
898
+ warnings.push({
899
+ source: "memory",
900
+ code: "memory_applicability_not_enforced",
901
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
902
+ });
903
+ }
962
904
  async function embedQueryVariant(embedQuery, input, query) {
963
905
  return embedQuery({
964
906
  userId: input.userId,
@@ -966,10 +908,29 @@ async function embedQueryVariant(embedQuery, input, query) {
966
908
  authToken: input.authToken
967
909
  });
968
910
  }
969
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
911
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
970
912
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
971
913
  let semantic = [];
972
- if (isRawMessageChromaEnabled()) {
914
+ if (typeof deps.searchRawMessagesAnn === "function") {
915
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
916
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
917
+ const includeDeprecated = input.includeDeprecated === true;
918
+ semantic = (await Promise.all(
919
+ filters.map(
920
+ (filter) => searchRawMessagesAnn({
921
+ userId: input.userId,
922
+ queryEmbedding,
923
+ limit,
924
+ threshold,
925
+ botId: "botId" in filter ? filter.botId : void 0,
926
+ includeDeprecated,
927
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
928
+ ...factTypes ? { factTypes } : {},
929
+ ...runtimeContext ?? {}
930
+ })
931
+ )
932
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
933
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
973
934
  try {
974
935
  semantic = (await Promise.all(
975
936
  filters.map((filter) => {
@@ -994,24 +955,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
994
955
  error
995
956
  );
996
957
  }
997
- } else if (typeof deps.searchRawMessagesAnn === "function") {
998
- const searchRawMessagesAnn = deps.searchRawMessagesAnn;
999
- const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1000
- semantic = (await Promise.all(
1001
- filters.map(
1002
- (filter) => searchRawMessagesAnn({
1003
- userId: input.userId,
1004
- queryEmbedding,
1005
- limit,
1006
- threshold,
1007
- botId: "botId" in filter ? filter.botId : void 0,
1008
- ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1009
- ...factTypes ? { factTypes } : {}
1010
- })
1011
- )
1012
- )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1013
958
  }
1014
- if (semantic.length === 0) {
959
+ if (semantic.length === 0 && runtimeContext !== void 0) {
960
+ warnApplicabilityNotEnforced(warnings);
961
+ return [];
962
+ }
963
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1015
964
  try {
1016
965
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1017
966
  const manager2 = await getRawMessageManager2();
@@ -1022,6 +971,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1022
971
  queryEmbedding,
1023
972
  limit,
1024
973
  threshold,
974
+ includeDeprecated: input.includeDeprecated === true,
1025
975
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1026
976
  ...factTypes ? { factTypes } : {}
1027
977
  });
@@ -1051,6 +1001,18 @@ function mergeByMaxScore(lists) {
1051
1001
  }
1052
1002
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1053
1003
  }
1004
+ function dedupeChannelByParent(hits) {
1005
+ const strongest = /* @__PURE__ */ new Map();
1006
+ for (const hit of hits) {
1007
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1008
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1009
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1010
+ const key = `${normalized.type}:${parentId}`;
1011
+ const current = strongest.get(key);
1012
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1013
+ }
1014
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1015
+ }
1054
1016
  function toIterativeRecallCandidate(hit) {
1055
1017
  return {
1056
1018
  id: hit.id,
@@ -1059,7 +1021,7 @@ function toIterativeRecallCandidate(hit) {
1059
1021
  metadata: hit.metadata
1060
1022
  };
1061
1023
  }
1062
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1024
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1063
1025
  if (keywords.length === 0) {
1064
1026
  return [];
1065
1027
  }
@@ -1068,6 +1030,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1068
1030
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1069
1031
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1070
1032
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1033
+ const includeDeprecated = input.includeDeprecated === true;
1071
1034
  return (await Promise.all(
1072
1035
  filters.map(
1073
1036
  (botId) => searchRawMessagesLexical({
@@ -1075,8 +1038,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1075
1038
  keywords,
1076
1039
  limit: Math.ceil(limit / filters.length),
1077
1040
  botId,
1041
+ includeDeprecated,
1078
1042
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1079
- ...factTypes ? { factTypes } : {}
1043
+ ...factTypes ? { factTypes } : {},
1044
+ ...runtimeContext ?? {}
1080
1045
  })
1081
1046
  )
1082
1047
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1087,10 +1052,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1087
1052
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1088
1053
  }
1089
1054
  }
1055
+ if (runtimeContext !== void 0) {
1056
+ warnApplicabilityNotEnforced(warnings);
1057
+ return [];
1058
+ }
1090
1059
  try {
1091
1060
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1092
1061
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1093
1062
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1063
+ const includeDeprecated = input.includeDeprecated === true;
1094
1064
  return (await Promise.all(
1095
1065
  filters.map(
1096
1066
  (botId) => lexicalSearchRawMessages2({
@@ -1098,6 +1068,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1098
1068
  keywords,
1099
1069
  limit: Math.ceil(limit / filters.length),
1100
1070
  botId,
1071
+ includeDeprecated,
1101
1072
  ...factTypes ? { factTypes } : {}
1102
1073
  })
1103
1074
  )
@@ -1128,7 +1099,9 @@ function searchInputToUnified(input) {
1128
1099
  mergeStrategy: input.mergeStrategy,
1129
1100
  peerFilter: input.peerFilter,
1130
1101
  reasoningStrategy: input.reasoningStrategy,
1131
- factTypes: input.factTypes
1102
+ factTypes: input.factTypes,
1103
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1104
+ includeDeprecated: input.includeDeprecated
1132
1105
  };
1133
1106
  }
1134
1107
  function mapHitToEvidenceSource(hit) {
@@ -1156,8 +1129,8 @@ function createUnifiedSearch(deps = {}) {
1156
1129
  }
1157
1130
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1158
1131
  }
1159
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1160
- if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
1132
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1133
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1161
1134
  warnings.push({
1162
1135
  source: "memory",
1163
1136
  code: "memory_iterative_planner_not_configured",
@@ -1173,7 +1146,8 @@ function createUnifiedSearch(deps = {}) {
1173
1146
  });
1174
1147
  if (reasoningInfo) reasoningInfo.degraded = true;
1175
1148
  }
1176
- if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
1149
+ let unionEvidence = null;
1150
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
1177
1151
  const planner = deps.reasoning.iterativePlanner;
1178
1152
  const result = await planner.plan({
1179
1153
  query: input.query,
@@ -1188,7 +1162,9 @@ function createUnifiedSearch(deps = {}) {
1188
1162
  keywords2,
1189
1163
  limit,
1190
1164
  logger,
1191
- peerPeers
1165
+ peerPeers,
1166
+ warnings,
1167
+ runtimeContext
1192
1168
  );
1193
1169
  let semanticHits = [];
1194
1170
  if (typeof deps.embedQuery === "function") {
@@ -1202,7 +1178,9 @@ function createUnifiedSearch(deps = {}) {
1202
1178
  limit,
1203
1179
  threshold,
1204
1180
  logger,
1205
- peerPeers
1181
+ peerPeers,
1182
+ warnings,
1183
+ runtimeContext
1206
1184
  );
1207
1185
  } catch (error) {
1208
1186
  logger.warn?.(
@@ -1230,16 +1208,24 @@ function createUnifiedSearch(deps = {}) {
1230
1208
  reasoningInfo.degraded = true;
1231
1209
  }
1232
1210
  }
1233
- const semantic2 = result.evidence.map((candidate) => ({
1211
+ const evidence = result.evidence.map((candidate) => ({
1234
1212
  type: "memory",
1235
1213
  id: candidate.id,
1236
1214
  content: candidate.content,
1237
1215
  similarity: candidate.similarity,
1238
- metadata: { ...candidate.metadata, reasoning: "iterative" }
1216
+ metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
1239
1217
  }));
1240
- return { semantic: semantic2, lexical: [] };
1218
+ if (reasoningStrategy === "iterative") {
1219
+ return { semantic: evidence, lexical: [] };
1220
+ }
1221
+ unionEvidence = evidence;
1241
1222
  }
1242
1223
  if (typeof deps.embedQuery !== "function") {
1224
+ warnings.push({
1225
+ source: "memory",
1226
+ code: "semantic_unavailable",
1227
+ message: "Semantic search is unavailable because no embedding provider is configured."
1228
+ });
1243
1229
  warnings.push({
1244
1230
  source: "memory",
1245
1231
  code: "memory_lexical_search_fallback",
@@ -1247,7 +1233,16 @@ function createUnifiedSearch(deps = {}) {
1247
1233
  });
1248
1234
  const keywords2 = deriveLexicalKeywords(input.query);
1249
1235
  const lexical2 = filterByDateRange(
1250
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1236
+ await runLexicalSearchForKeywords(
1237
+ deps,
1238
+ input,
1239
+ keywords2,
1240
+ limit,
1241
+ logger,
1242
+ peerPeers,
1243
+ warnings,
1244
+ runtimeContext
1245
+ ),
1251
1246
  input.dateFrom,
1252
1247
  input.dateTo
1253
1248
  );
@@ -1255,6 +1250,26 @@ function createUnifiedSearch(deps = {}) {
1255
1250
  }
1256
1251
  let semantic = [];
1257
1252
  const embedQuery = deps.embedQuery;
1253
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1254
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1255
+ );
1256
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1257
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1258
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1259
+ const hybrid = (await Promise.all(
1260
+ filters.map(
1261
+ (botId) => deps.searchRawMessagesHybrid?.({
1262
+ userId: input.userId,
1263
+ query: input.query,
1264
+ queryEmbedding,
1265
+ limit: Math.ceil(limit / filters.length),
1266
+ botId,
1267
+ ...runtimeContext ?? {}
1268
+ })
1269
+ )
1270
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1271
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1272
+ }
1258
1273
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1259
1274
  try {
1260
1275
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1273,7 +1288,17 @@ function createUnifiedSearch(deps = {}) {
1273
1288
  );
1274
1289
  const lists = await Promise.all(
1275
1290
  embeddings.map(
1276
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1291
+ (embedding) => runSemanticSearchForEmbedding(
1292
+ deps,
1293
+ input,
1294
+ embedding,
1295
+ limit,
1296
+ threshold,
1297
+ logger,
1298
+ peerPeers,
1299
+ warnings,
1300
+ runtimeContext
1301
+ )
1277
1302
  )
1278
1303
  );
1279
1304
  semantic = mergeByMaxScore(lists);
@@ -1296,7 +1321,9 @@ function createUnifiedSearch(deps = {}) {
1296
1321
  limit,
1297
1322
  threshold,
1298
1323
  logger,
1299
- peerPeers
1324
+ peerPeers,
1325
+ warnings,
1326
+ runtimeContext
1300
1327
  );
1301
1328
  }
1302
1329
  let lexical = [];
@@ -1315,7 +1342,8 @@ function createUnifiedSearch(deps = {}) {
1315
1342
  limit,
1316
1343
  botId,
1317
1344
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1318
- ...factTypes ? { factTypes } : {}
1345
+ ...factTypes ? { factTypes } : {},
1346
+ ...runtimeContext ?? {}
1319
1347
  })
1320
1348
  )
1321
1349
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1341,7 +1369,7 @@ function createUnifiedSearch(deps = {}) {
1341
1369
  let entity;
1342
1370
  if (typeof deps.entitySearch === "function") {
1343
1371
  entity = filterByDateRange(
1344
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1372
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1345
1373
  input.dateFrom,
1346
1374
  input.dateTo
1347
1375
  );
@@ -1363,6 +1391,37 @@ function createUnifiedSearch(deps = {}) {
1363
1391
  message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1364
1392
  });
1365
1393
  }
1394
+ if (unionEvidence) {
1395
+ const plannerHits = unionEvidence;
1396
+ const base = mergeByMaxScore([
1397
+ filterByDateRange(semantic, input.dateFrom, input.dateTo),
1398
+ filterByDateRange(lexical, input.dateFrom, input.dateTo)
1399
+ ]);
1400
+ const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
1401
+ const seen = /* @__PURE__ */ new Set();
1402
+ const merged = [];
1403
+ plannerHits.forEach((hit, i) => {
1404
+ if (seen.has(hit.id)) {
1405
+ return;
1406
+ }
1407
+ seen.add(hit.id);
1408
+ merged.push({
1409
+ ...hit,
1410
+ similarity: maxBase + (plannerHits.length - i) * 1e-6
1411
+ });
1412
+ });
1413
+ for (const hit of base) {
1414
+ if (merged.length >= limit) {
1415
+ break;
1416
+ }
1417
+ if (seen.has(hit.id)) {
1418
+ continue;
1419
+ }
1420
+ seen.add(hit.id);
1421
+ merged.push(hit);
1422
+ }
1423
+ return { semantic: merged.slice(0, limit), lexical: [] };
1424
+ }
1366
1425
  const out = {
1367
1426
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1368
1427
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
@@ -1372,14 +1431,16 @@ function createUnifiedSearch(deps = {}) {
1372
1431
  }
1373
1432
  return out;
1374
1433
  }
1375
- async function searchUnifiedMemory(input) {
1434
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1376
1435
  const query = input.query.trim();
1377
1436
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1378
1437
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1379
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1380
1438
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1381
1439
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1382
1440
  );
1441
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1442
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1443
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1383
1444
  const reasoningStrategy = resolveReasoningStrategy(input);
1384
1445
  const warnings = [];
1385
1446
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1399,23 +1460,46 @@ function createUnifiedSearch(deps = {}) {
1399
1460
  };
1400
1461
  }
1401
1462
  let memorySubs;
1463
+ let retrievalStatus;
1402
1464
  const reasoningInfo = { strategy: reasoningStrategy };
1403
1465
  if (input.dateFrom || input.dateTo) {
1404
1466
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1405
1467
  }
1406
1468
  if (sources.includes("memory")) {
1407
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1469
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1408
1470
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1409
1471
  try {
1410
1472
  memorySubs = await runMemorySource(
1411
1473
  input,
1412
- limit,
1413
- threshold,
1474
+ candidateLimit,
1475
+ memoryThreshold,
1414
1476
  warnings,
1415
1477
  reasoningStrategy,
1416
1478
  reasoningInfo,
1417
- peerPeers
1479
+ peerPeers,
1480
+ runtimeContext
1418
1481
  );
1482
+ memorySubs = {
1483
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1484
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1485
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1486
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1487
+ };
1488
+ if (deps.getRawMessageRetrievalStatus) {
1489
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1490
+ }
1491
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1492
+ let code = "semantic_candidates_empty";
1493
+ let message = "Semantic search was available but returned no candidates.";
1494
+ if (retrievalStatus?.embeddedChildCount === 0) {
1495
+ code = "semantic_unavailable";
1496
+ message = "No embedded RawMessage children are available for semantic search.";
1497
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1498
+ code = "semantic_dimension_mismatch";
1499
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1500
+ }
1501
+ warnings.push({ source: "memory", code, message });
1502
+ }
1419
1503
  } catch (error) {
1420
1504
  logger.warn?.("[memory-store] memory source failed:", error);
1421
1505
  warnings.push({
@@ -1438,12 +1522,13 @@ function createUnifiedSearch(deps = {}) {
1438
1522
  const hits = await deps.searchInsights({
1439
1523
  userId: input.userId,
1440
1524
  query: input.query,
1441
- limit,
1525
+ limit: candidateLimit,
1442
1526
  threshold,
1443
1527
  botIds: input.botIds,
1444
1528
  includeArchived: input.includeArchivedInsights,
1445
1529
  authToken: input.authToken,
1446
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1530
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1531
+ ...runtimeContext ?? {}
1447
1532
  });
1448
1533
  for (const hit of hits) {
1449
1534
  insightHits.push({
@@ -1470,12 +1555,13 @@ function createUnifiedSearch(deps = {}) {
1470
1555
  userId: input.userId,
1471
1556
  query: input.query,
1472
1557
  options: {
1473
- limit,
1558
+ limit: candidateLimit,
1474
1559
  threshold,
1475
1560
  documentIds: input.documentIds
1476
1561
  },
1477
1562
  authToken: input.authToken,
1478
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1563
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1564
+ ...runtimeContext ?? {}
1479
1565
  });
1480
1566
  for (const hit of hits) {
1481
1567
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1493,10 +1579,14 @@ function createUnifiedSearch(deps = {}) {
1493
1579
  memorySubs,
1494
1580
  insightHits,
1495
1581
  knowledgeHits,
1496
- limit,
1582
+ limit: candidateLimit,
1497
1583
  strategy: mergeStrategy
1498
1584
  });
1499
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1585
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1586
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1587
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1588
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1589
+ const ranked = reranked.slice(0, limit);
1500
1590
  const output = {
1501
1591
  query: input.query,
1502
1592
  sources,
@@ -1504,28 +1594,51 @@ function createUnifiedSearch(deps = {}) {
1504
1594
  count: ranked.length,
1505
1595
  warnings
1506
1596
  };
1597
+ if (input.includeRetrievalDiagnostics) {
1598
+ output.retrievalDiagnostics = {
1599
+ mergeStrategy,
1600
+ candidateLimit,
1601
+ backend: retrievalStatus?.backend,
1602
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1603
+ candidateCounts: {
1604
+ semantic: memorySubs?.semantic.length ?? 0,
1605
+ lexical: memorySubs?.lexical.length ?? 0,
1606
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1607
+ entity: memorySubs?.entity?.length ?? 0,
1608
+ fused: merged.length,
1609
+ final: ranked.length
1610
+ },
1611
+ channels: {
1612
+ semantic: memorySubs?.semantic ?? [],
1613
+ lexical: memorySubs?.lexical ?? [],
1614
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1615
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1616
+ },
1617
+ fusedBeforeRerank: merged,
1618
+ reranker: {
1619
+ enabled: Boolean(deps.reranker),
1620
+ provider: deps.rerankerInfo?.provider,
1621
+ model: deps.rerankerInfo?.model,
1622
+ inputCount: merged.length,
1623
+ outputCount: reranked.length,
1624
+ latencyMs: rerankerLatencyMs,
1625
+ orderChanged: rerankerOrderChanged
1626
+ },
1627
+ final: ranked
1628
+ };
1629
+ }
1507
1630
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1508
1631
  output.reasoning = reasoningInfo;
1509
1632
  }
1510
1633
  return output;
1511
1634
  }
1635
+ async function searchUnifiedMemory(input) {
1636
+ return searchUnifiedMemoryWithRuntime(input);
1637
+ }
1512
1638
  async function searchRawMemorySemantically(input) {
1513
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1514
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1515
- const reasoningStrategy = resolveReasoningStrategy(input);
1516
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1517
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1518
- );
1519
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1520
- return mergeAcrossSources({
1521
- memorySubs: sub,
1522
- insightHits: [],
1523
- knowledgeHits: [],
1524
- limit,
1525
- strategy: mergeStrategy
1526
- });
1639
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1527
1640
  }
1528
- async function search(input) {
1641
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1529
1642
  const query = input.query.trim();
1530
1643
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1531
1644
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1557,19 +1670,24 @@ function createUnifiedSearch(deps = {}) {
1557
1670
  let unifiedHits = [];
1558
1671
  let sources = input.sources ? [...input.sources] : [];
1559
1672
  let unifiedReasoning;
1673
+ let retrievalDiagnostics;
1560
1674
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1561
1675
  const inferredSources = sources.length > 0 ? sources : [
1562
1676
  ...wantsRawTier ? ["memory"] : [],
1563
1677
  ...wantsInsightTier ? ["insights"] : [],
1564
1678
  ...wantsKnowledgeTier ? ["knowledge"] : []
1565
1679
  ];
1566
- const unified = await searchUnifiedMemory({
1567
- ...searchInputToUnified(input),
1568
- sources: inferredSources.length > 0 ? inferredSources : void 0
1569
- });
1680
+ const unified = await searchUnifiedMemoryWithRuntime(
1681
+ {
1682
+ ...searchInputToUnified(input),
1683
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1684
+ },
1685
+ runtimeContext
1686
+ );
1570
1687
  unifiedHits = unified.results;
1571
1688
  sources = unified.sources;
1572
1689
  unifiedReasoning = unified.reasoning;
1690
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1573
1691
  warnings.push(...unified.warnings);
1574
1692
  }
1575
1693
  let summaryHits = [];
@@ -1580,7 +1698,8 @@ function createUnifiedSearch(deps = {}) {
1580
1698
  limit,
1581
1699
  threshold,
1582
1700
  logger,
1583
- peerScope.peers
1701
+ peerScope.peers,
1702
+ runtimeContext
1584
1703
  );
1585
1704
  warnings.push(...summaryBucket.warnings);
1586
1705
  summaryHits = summaryBucket.hits;
@@ -1605,7 +1724,8 @@ function createUnifiedSearch(deps = {}) {
1605
1724
  source: mapHitToEvidenceSource(hit),
1606
1725
  snippet: hit.content,
1607
1726
  score: hit.similarity,
1608
- timestamp: getCandidateTimestamp(hit.metadata)
1727
+ timestamp: getCandidateTimestamp(hit.metadata),
1728
+ metadata: hit.metadata
1609
1729
  }));
1610
1730
  const base = {
1611
1731
  query,
@@ -1618,12 +1738,15 @@ function createUnifiedSearch(deps = {}) {
1618
1738
  if (unifiedReasoning) {
1619
1739
  base.reasoning = unifiedReasoning;
1620
1740
  }
1741
+ if (retrievalDiagnostics) {
1742
+ base.retrievalDiagnostics = retrievalDiagnostics;
1743
+ }
1621
1744
  if (!wantsSynthesis) {
1622
1745
  return base;
1623
1746
  }
1624
1747
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1625
1748
  query,
1626
- hits,
1749
+ evidence,
1627
1750
  responseSchema: synthesisSchema,
1628
1751
  deps,
1629
1752
  logger
@@ -1632,6 +1755,10 @@ function createUnifiedSearch(deps = {}) {
1632
1755
  base.warnings = [...base.warnings, ...synthWarnings];
1633
1756
  return base;
1634
1757
  }
1758
+ async function search(input, runtimeContext) {
1759
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1760
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1761
+ }
1635
1762
  return {
1636
1763
  search,
1637
1764
  searchUnifiedMemory,
@@ -1647,6 +1774,7 @@ function mergeAcrossSources(input) {
1647
1774
  for (const hit of [
1648
1775
  ...memorySubs.semantic,
1649
1776
  ...memorySubs.lexical,
1777
+ ...memorySubs.hybrid ?? [],
1650
1778
  ...input.insightHits,
1651
1779
  ...input.knowledgeHits
1652
1780
  ]) {
@@ -1662,6 +1790,7 @@ function mergeAcrossSources(input) {
1662
1790
  const all = [
1663
1791
  ...memorySubs.semantic,
1664
1792
  ...memorySubs.lexical,
1793
+ ...memorySubs.hybrid ?? [],
1665
1794
  ...memorySubs.entity ?? [],
1666
1795
  ...input.insightHits,
1667
1796
  ...input.knowledgeHits
@@ -1673,6 +1802,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1673
1802
  const lists = [];
1674
1803
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1675
1804
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1805
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1806
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1807
+ }
1676
1808
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1677
1809
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1678
1810
  }
@@ -2137,8 +2269,250 @@ function createVsaRecall(storage) {
2137
2269
  };
2138
2270
  }
2139
2271
 
2272
+ // ../shared/src/tokens.ts
2273
+ var estimateTokens = (text) => {
2274
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
2275
+ const otherChars = text.length - chineseChars;
2276
+ const wordCount = Math.ceil(otherChars / 5);
2277
+ return chineseChars + wordCount;
2278
+ };
2279
+
2280
+ // ../shared/src/text-chunking.ts
2281
+ var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
2282
+ var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
2283
+ var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
2284
+ var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
2285
+ function largestEndWithinTokenBudget(text, start, maxTokens) {
2286
+ let low = start + 1;
2287
+ let high = text.length;
2288
+ let best = low;
2289
+ while (low <= high) {
2290
+ const middle = Math.floor((low + high) / 2);
2291
+ if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
2292
+ best = middle;
2293
+ low = middle + 1;
2294
+ } else {
2295
+ high = middle - 1;
2296
+ }
2297
+ }
2298
+ return best;
2299
+ }
2300
+ function preferNaturalEnd(text, start, hardEnd) {
2301
+ if (hardEnd >= text.length) return text.length;
2302
+ const searchStart = start + Math.floor((hardEnd - start) * 0.6);
2303
+ const candidate = text.slice(searchStart, hardEnd);
2304
+ for (const boundary of PARAGRAPH_BOUNDARIES) {
2305
+ const index = candidate.lastIndexOf(boundary);
2306
+ if (index >= 0) return searchStart + index + boundary.length;
2307
+ }
2308
+ for (let offset = candidate.length; offset > 0; offset -= 1) {
2309
+ const prefix = candidate.slice(0, offset);
2310
+ if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
2311
+ }
2312
+ for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
2313
+ if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
2314
+ }
2315
+ return hardEnd;
2316
+ }
2317
+ function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
2318
+ if (overlapTokens <= 0) return chunkEnd;
2319
+ let low = chunkStart + 1;
2320
+ let high = chunkEnd;
2321
+ let best = chunkEnd;
2322
+ while (low <= high) {
2323
+ const middle = Math.floor((low + high) / 2);
2324
+ if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
2325
+ best = middle;
2326
+ high = middle - 1;
2327
+ } else {
2328
+ low = middle + 1;
2329
+ }
2330
+ }
2331
+ return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
2332
+ }
2333
+ function chunkTextByEstimatedTokens(text, options = {}) {
2334
+ if (text.length === 0) return [];
2335
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
2336
+ const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
2337
+ if (overlapTokens >= maxTokens) {
2338
+ throw new Error("overlapTokens must be smaller than maxTokens");
2339
+ }
2340
+ const chunks = [];
2341
+ let start = 0;
2342
+ while (start < text.length) {
2343
+ const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
2344
+ const end = preferNaturalEnd(text, start, hardEnd);
2345
+ chunks.push({
2346
+ chunkIndex: chunks.length,
2347
+ startPosition: start,
2348
+ endPosition: end,
2349
+ content: text.slice(start, end)
2350
+ });
2351
+ if (end >= text.length) break;
2352
+ start = overlapStart(text, start, end, overlapTokens);
2353
+ }
2354
+ return chunks;
2355
+ }
2356
+
2357
+ // src/embed-on-insert.ts
2358
+ var AUTO_APPLIED_WARNING = {
2359
+ code: "embed_on_insert_auto_applied",
2360
+ message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
2361
+ };
2362
+ var SEMANTIC_UNAVAILABLE_WARNING = {
2363
+ code: "semantic_index_unavailable",
2364
+ message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
2365
+ };
2366
+ function sha256(value) {
2367
+ return createHash("sha256").update(value).digest("hex");
2368
+ }
2369
+ function isUsableEmbedding(embedding) {
2370
+ return Boolean(
2371
+ embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
2372
+ );
2373
+ }
2374
+ async function embedText(text, userId, deps) {
2375
+ if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
2376
+ const embedding = await deps.embedQuery({ userId, query: text });
2377
+ return isUsableEmbedding(embedding) ? embedding : void 0;
2378
+ }
2379
+ async function embedMissingMessages(messages, deps) {
2380
+ if (typeof deps.embedQuery !== "function") return messages;
2381
+ const out = [];
2382
+ for (const message of messages) {
2383
+ if (isUsableEmbedding(message.embedding) || !message.content) {
2384
+ out.push(message);
2385
+ continue;
2386
+ }
2387
+ const embedding = await embedText(message.content, message.userId, deps);
2388
+ out.push(
2389
+ embedding ? {
2390
+ ...message,
2391
+ embedding,
2392
+ embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
2393
+ embeddingContentHash: sha256(message.content),
2394
+ embeddingDimensions: embedding.length,
2395
+ embeddingUpdatedAt: Date.now()
2396
+ } : message
2397
+ );
2398
+ }
2399
+ return out;
2400
+ }
2401
+ async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
2402
+ const deps = unified ?? {};
2403
+ const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
2404
+ const shouldEmbed = embedOnInsert === true || embedderAvailable;
2405
+ const messages = [];
2406
+ const chunks = [];
2407
+ let missingSemanticEmbedding = false;
2408
+ let generatedEmbedding = false;
2409
+ const plans = incoming.map((message) => ({
2410
+ message,
2411
+ pieces: chunkTextByEstimatedTokens(message.content)
2412
+ }));
2413
+ const generatedEmbeddings = /* @__PURE__ */ new Map();
2414
+ const pendingByUser = /* @__PURE__ */ new Map();
2415
+ for (const [messageIndex, plan] of plans.entries()) {
2416
+ for (const piece of plan.pieces) {
2417
+ const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
2418
+ if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
2419
+ const pending = pendingByUser.get(plan.message.userId) ?? [];
2420
+ pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
2421
+ pendingByUser.set(plan.message.userId, pending);
2422
+ }
2423
+ }
2424
+ for (const [userId, pending] of pendingByUser) {
2425
+ let embeddings;
2426
+ if (typeof deps.embedDocuments === "function") {
2427
+ const batch = await deps.embedDocuments({
2428
+ userId,
2429
+ texts: pending.map((item) => item.text)
2430
+ });
2431
+ if (batch.length !== pending.length) {
2432
+ throw new Error(
2433
+ `Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
2434
+ );
2435
+ }
2436
+ embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
2437
+ } else {
2438
+ embeddings = [];
2439
+ for (const item of pending) {
2440
+ embeddings.push(await embedText(item.text, userId, deps));
2441
+ }
2442
+ }
2443
+ for (const [index, item] of pending.entries()) {
2444
+ const embedding = embeddings[index];
2445
+ if (embedding) generatedEmbeddings.set(item.key, embedding);
2446
+ }
2447
+ }
2448
+ for (const [messageIndex, plan] of plans.entries()) {
2449
+ const incomingMessage = plan.message;
2450
+ const pieces = plan.pieces;
2451
+ let message = incomingMessage;
2452
+ for (const piece of pieces) {
2453
+ const contentHash = sha256(piece.content);
2454
+ const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
2455
+ const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
2456
+ generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
2457
+ if (!embedding) missingSemanticEmbedding = true;
2458
+ const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
2459
+ chunks.push({
2460
+ chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
2461
+ messageId: incomingMessage.messageId,
2462
+ userId: incomingMessage.userId,
2463
+ chunkIndex: piece.chunkIndex,
2464
+ chunkCount: pieces.length,
2465
+ startPosition: piece.startPosition,
2466
+ endPosition: piece.endPosition,
2467
+ content: piece.content,
2468
+ contentHash,
2469
+ embedding,
2470
+ embeddingModel,
2471
+ embeddingDimensions: embedding?.length,
2472
+ embeddingUpdatedAt: embedding ? Date.now() : void 0
2473
+ });
2474
+ if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
2475
+ message = {
2476
+ ...incomingMessage,
2477
+ embedding,
2478
+ embeddingModel,
2479
+ embeddingContentHash: contentHash,
2480
+ embeddingDimensions: embedding.length,
2481
+ embeddingUpdatedAt: Date.now()
2482
+ };
2483
+ }
2484
+ }
2485
+ messages.push(message);
2486
+ }
2487
+ const warnings = [];
2488
+ if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
2489
+ if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
2490
+ return { messages, chunks, warnings };
2491
+ }
2492
+
2493
+ // src/storage/raw-message-ingest.ts
2494
+ async function persistRawMessages(input) {
2495
+ const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
2496
+ let ids;
2497
+ if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
2498
+ ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
2499
+ } else {
2500
+ const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
2501
+ if (typeof input.manager.upsertRawMessages === "function") {
2502
+ await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
2503
+ } else if (typeof input.manager.storeMessages === "function") {
2504
+ ids = await input.manager.storeMessages(legacyMessages);
2505
+ } else {
2506
+ throw new Error("active raw-message manager exposes no supported write method");
2507
+ }
2508
+ }
2509
+ if (input.externalIndex) {
2510
+ await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
2511
+ }
2512
+ return { count: prepared.messages.length, ids, warnings: prepared.warnings };
2513
+ }
2514
+
2140
2515
  // src/http.ts
2141
- init_chroma_memory_index();
2142
2516
  init_raw_message_store();
2143
2517
  init_sqlite_raw_message_store();
2144
2518
  async function startHttpServer(options = {}) {
@@ -2168,7 +2542,18 @@ async function startHttpServer(options = {}) {
2168
2542
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2169
2543
  const vsa = createVsaRecall(vsaStorage);
2170
2544
  const app = new Hono();
2171
- app.get("/health", (c) => c.json({ ok: true, store: "memory", ts: Date.now() }));
2545
+ app.get("/health", async (c) => {
2546
+ const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
2547
+ backend: "unknown",
2548
+ childCount: 0,
2549
+ embeddedChildCount: 0,
2550
+ indexedDimensions: [],
2551
+ semanticReady: false,
2552
+ lexicalReady: false,
2553
+ semanticDegradedReason: "health_check_failed"
2554
+ })) : void 0;
2555
+ return c.json({ ok: true, store: "memory", ts: Date.now(), retrieval });
2556
+ });
2172
2557
  app.post("/v1/search", async (c) => {
2173
2558
  const body = await c.req.json().catch(() => ({}));
2174
2559
  const userId = typeof body.userId === "string" ? body.userId : null;
@@ -2191,7 +2576,9 @@ async function startHttpServer(options = {}) {
2191
2576
  authToken: typeof body.authToken === "string" ? body.authToken : void 0,
2192
2577
  dateFrom: typeof body.dateFrom === "string" ? body.dateFrom : void 0,
2193
2578
  dateTo: typeof body.dateTo === "string" ? body.dateTo : void 0,
2194
- reasoningStrategy: body.reasoningStrategy === "rewrite" || body.reasoningStrategy === "iterative" ? body.reasoningStrategy : void 0,
2579
+ reasoningStrategy: body.reasoningStrategy === "rewrite" || body.reasoningStrategy === "iterative" || body.reasoningStrategy === "union" ? body.reasoningStrategy : void 0,
2580
+ mergeStrategy: body.mergeStrategy === "rrf" || body.mergeStrategy === "similarity" ? body.mergeStrategy : void 0,
2581
+ includeRetrievalDiagnostics: body.includeRetrievalDiagnostics === true,
2195
2582
  ...wantsSynthesis ? {
2196
2583
  synthesize: {
2197
2584
  ...responseSchema ? { responseSchema } : {}
@@ -2295,57 +2682,20 @@ async function startHttpServer(options = {}) {
2295
2682
  ...m,
2296
2683
  userId: m.userId ?? body.userId
2297
2684
  }));
2298
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2299
- incoming,
2300
- body.embedOnInsert,
2301
- options.unified
2302
- );
2303
- let result;
2304
- if (typeof manager3.upsertRawMessages === "function") {
2305
- result = await manager3.upsertRawMessages({
2306
- userId: body.userId,
2307
- messages
2308
- });
2309
- } else if (typeof manager3.storeMessages === "function") {
2310
- const ids = await manager3.storeMessages(messages);
2311
- result = { inserted: ids.length, ids };
2312
- } else {
2313
- return c.json(
2314
- {
2315
- error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
2316
- },
2317
- 500
2318
- );
2319
- }
2320
- try {
2321
- await upsertRawMessagesToChroma(messages);
2322
- } catch (error) {
2323
- console.warn("[memory-store/http] chroma upsert failed:", error);
2324
- }
2325
- try {
2326
- const messagesWithEmbeddings = messages.filter(
2327
- (m) => Array.isArray(m.embedding) && m.embedding.length > 0
2328
- );
2329
- if (messagesWithEmbeddings.length > 0) {
2330
- for (const message of messagesWithEmbeddings) {
2331
- if (typeof manager3.upsertVectorForMessage === "function") {
2332
- manager3.upsertVectorForMessage(message.messageId, message.embedding);
2333
- }
2334
- }
2335
- console.log(
2336
- "[memory-store/http] Updated sqlite-vec vector table for",
2337
- messagesWithEmbeddings.length,
2338
- "message(s)"
2339
- );
2340
- }
2341
- } catch (error) {
2342
- console.warn("[memory-store/http] sqlite-vec vector update failed:", error);
2343
- }
2685
+ const persisted = await persistRawMessages({
2686
+ manager: manager3,
2687
+ userId: body.userId,
2688
+ messages: incoming,
2689
+ embedOnInsert: body.embedOnInsert,
2690
+ unified: options.unified,
2691
+ externalIndex: options.unified?.rawMessageChildIndex
2692
+ });
2693
+ const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
2344
2694
  return c.json({
2345
2695
  ok: true,
2346
- count: messages.length,
2696
+ count: persisted.count,
2347
2697
  result,
2348
- ...warnings.length > 0 ? { warnings } : {}
2698
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2349
2699
  });
2350
2700
  });
2351
2701
  app.get("/v1/raw-messages/:id", async (c) => {
@@ -2444,7 +2794,17 @@ async function startHttpServer(options = {}) {
2444
2794
  });
2445
2795
  const okfHttpSpecifier = "@melandlabs/okf/http";
2446
2796
  const { registerOkfRoutes } = await import(okfHttpSpecifier);
2447
- registerOkfRoutes(app, rawStore);
2797
+ registerOkfRoutes(app, rawStore, {
2798
+ writeMessages: async (userId, messages) => {
2799
+ await persistRawMessages({
2800
+ manager: manager2,
2801
+ userId,
2802
+ messages,
2803
+ unified: options.unified,
2804
+ externalIndex: options.unified?.rawMessageChildIndex
2805
+ });
2806
+ }
2807
+ });
2448
2808
  const server = serve({ fetch: app.fetch, port, hostname: host });
2449
2809
  return {
2450
2810
  url: `http://${host}:${port}`,
@@ -2796,6 +3156,121 @@ function createUserVoiceRewriter(options) {
2796
3156
  };
2797
3157
  }
2798
3158
 
3159
+ // src/storage/raw-message-child-vector-index.ts
3160
+ var RawMessageChildVectorIndex = class {
3161
+ backend;
3162
+ store;
3163
+ catalog;
3164
+ constructor(options) {
3165
+ this.backend = options.backend;
3166
+ this.store = options.store;
3167
+ this.catalog = options.catalog;
3168
+ }
3169
+ async replaceMessages(messages, chunks) {
3170
+ const chunksByMessage = /* @__PURE__ */ new Map();
3171
+ for (const chunk of chunks) {
3172
+ const group = chunksByMessage.get(chunk.messageId) ?? [];
3173
+ group.push(chunk);
3174
+ chunksByMessage.set(chunk.messageId, group);
3175
+ }
3176
+ for (const message of messages) {
3177
+ await this.store.deleteDocument(message.messageId);
3178
+ const vectorChunks = (chunksByMessage.get(message.messageId) ?? []).filter(
3179
+ (chunk) => Boolean(chunk.embedding?.length) && !chunk.embedding?.every((value) => value === 0)
3180
+ ).map((chunk) => ({
3181
+ id: chunk.chunkId,
3182
+ documentId: message.messageId,
3183
+ content: chunk.content,
3184
+ embedding: chunk.embedding,
3185
+ metadata: {
3186
+ userId: message.userId,
3187
+ botId: message.botId,
3188
+ platform: message.platform,
3189
+ timestamp: message.timestamp,
3190
+ chunkIndex: chunk.chunkIndex,
3191
+ chunkCount: chunk.chunkCount,
3192
+ startPosition: chunk.startPosition,
3193
+ endPosition: chunk.endPosition,
3194
+ contentHash: chunk.contentHash,
3195
+ embeddingModel: chunk.embeddingModel
3196
+ }
3197
+ }));
3198
+ if (vectorChunks.length > 0) await this.store.addChunks(vectorChunks);
3199
+ }
3200
+ }
3201
+ async search(input) {
3202
+ const results = input.hybrid && this.store.hybridSearch ? await this.store.hybridSearch({
3203
+ text: input.query ?? "",
3204
+ vector: input.queryEmbedding,
3205
+ limit: input.limit,
3206
+ candidateLimit: input.limit,
3207
+ fusion: "rrf",
3208
+ filter: { userId: input.userId }
3209
+ }) : await this.store.similaritySearch(input.queryEmbedding, input.limit, input.userId);
3210
+ return this.hydrate(results, input);
3211
+ }
3212
+ async getChunkCount() {
3213
+ return this.store.getChunkCount();
3214
+ }
3215
+ async hydrate(results, input) {
3216
+ const matchedChunks = await this.catalog.getRawMessageSearchChunks({
3217
+ chunkIds: results.map((result) => result.id),
3218
+ userId: input.userId
3219
+ });
3220
+ const matchedById = new Map(matchedChunks.map((chunk) => [chunk.chunkId, chunk]));
3221
+ const messageIds = [...new Set(matchedChunks.map((chunk) => chunk.messageId))];
3222
+ const [allChunks, parents] = await Promise.all([
3223
+ this.catalog.getRawMessageSearchChunks({ messageIds, userId: input.userId }),
3224
+ Promise.all(messageIds.map((messageId) => this.catalog.getMessageById(messageId)))
3225
+ ]);
3226
+ const parentById = new Map(
3227
+ parents.filter((parent) => Boolean(parent)).map((parent) => [parent.messageId, parent])
3228
+ );
3229
+ const chunksByMessage = /* @__PURE__ */ new Map();
3230
+ for (const chunk of allChunks) {
3231
+ const group = chunksByMessage.get(chunk.messageId) ?? [];
3232
+ group.push(chunk);
3233
+ chunksByMessage.set(chunk.messageId, group);
3234
+ }
3235
+ const strongest = /* @__PURE__ */ new Map();
3236
+ for (const result of results) {
3237
+ const chunk = matchedById.get(result.id);
3238
+ if (!chunk) continue;
3239
+ const parent = parentById.get(chunk.messageId);
3240
+ if (!parent || input.botId && parent.botId !== input.botId) continue;
3241
+ if (input.threshold !== void 0 && result.score < input.threshold) continue;
3242
+ const siblings = (chunksByMessage.get(parent.messageId) ?? []).sort(
3243
+ (a, b) => a.chunkIndex - b.chunkIndex
3244
+ );
3245
+ const content = chunk.chunkCount <= 1 ? parent.content : parent.content.slice(
3246
+ siblings[Math.max(0, chunk.chunkIndex - 1)]?.startPosition ?? chunk.startPosition,
3247
+ siblings[Math.min(siblings.length - 1, chunk.chunkIndex + 1)]?.endPosition ?? chunk.endPosition
3248
+ );
3249
+ const hit = {
3250
+ id: parent.messageId,
3251
+ content,
3252
+ similarity: result.score,
3253
+ metadata: {
3254
+ ...parent.metadata ?? {},
3255
+ userId: parent.userId,
3256
+ botId: parent.botId,
3257
+ platform: parent.platform,
3258
+ timestamp: parent.timestamp,
3259
+ sourceMessageId: parent.messageId,
3260
+ sourceChunkId: chunk.chunkId,
3261
+ sourceChunkIndex: chunk.chunkIndex,
3262
+ sourceChunkCount: chunk.chunkCount,
3263
+ backend: this.backend,
3264
+ scoring: input.hybrid ? "native-hybrid" : "dense"
3265
+ }
3266
+ };
3267
+ const existing = strongest.get(parent.messageId);
3268
+ if (!existing || hit.similarity > existing.similarity) strongest.set(parent.messageId, hit);
3269
+ }
3270
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity).slice(0, input.limit);
3271
+ }
3272
+ };
3273
+
2799
3274
  // src/server/cli-shared.ts
2800
3275
  init_raw_message_store();
2801
3276
 
@@ -2810,7 +3285,7 @@ function isInsightSQLiteVecEnabled(_env) {
2810
3285
  }
2811
3286
  async function getInsightSQLiteVecStore(_env) {
2812
3287
  const dbPath = resolveSQLiteRawMessageDbPath();
2813
- const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
3288
+ const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
2814
3289
  return await getSQLiteVecStore(dbPath, void 0, {
2815
3290
  collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
2816
3291
  });
@@ -2834,17 +3309,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
2834
3309
  // src/server/cli-shared.ts
2835
3310
  async function loadAiRag() {
2836
3311
  try {
2837
- const [local, chroma] = await Promise.all([
3312
+ const [local, reranker, chroma] = await Promise.all([
2838
3313
  import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
3314
+ import('@melandlabs/ai-rag/local-transformers-reranker'),
2839
3315
  import('@melandlabs/ai-rag/chroma-store')
2840
3316
  ]);
2841
3317
  return {
2842
3318
  LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
3319
+ LocalTransformersReranker: reranker.LocalTransformersReranker,
2843
3320
  ChromaVectorStore: chroma.ChromaVectorStore
2844
3321
  };
2845
3322
  } catch (error) {
2846
3323
  throw new Error(
2847
- `--embedding-provider local / --*-backend=chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
3324
+ `Local embedding/reranking and insight/knowledge Chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
2848
3325
  );
2849
3326
  }
2850
3327
  }
@@ -2853,8 +3330,21 @@ function parseUnifiedArgs(argv) {
2853
3330
  const args = {
2854
3331
  embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
2855
3332
  embeddingModel: env.EMBEDDING_MODEL,
3333
+ embeddingCacheDir: env.LOCAL_EMBEDDING_CACHE_DIR,
3334
+ rerankerProvider: env.RERANKER_PROVIDER ?? "none",
3335
+ rerankerModel: env.LOCAL_RERANKER_MODEL,
3336
+ rerankerCacheDir: env.LOCAL_RERANKER_CACHE_DIR,
3337
+ rerankerBatchSize: env.LOCAL_RERANKER_BATCH_SIZE ? Number.parseInt(env.LOCAL_RERANKER_BATCH_SIZE, 10) : void 0,
3338
+ rerankerMaxTokens: env.LOCAL_RERANKER_MAX_TOKENS ? Number.parseInt(env.LOCAL_RERANKER_MAX_TOKENS, 10) : void 0,
2856
3339
  chromaUrl: env.CHROMA_URL,
2857
3340
  memoryBackend: env.MEMORY_BACKEND ?? "none",
3341
+ lancedbUri: env.LANCEDB_URI,
3342
+ lancedbTable: env.LANCEDB_TABLE,
3343
+ milvusAddress: env.MILVUS_ADDRESS,
3344
+ milvusToken: env.MILVUS_TOKEN,
3345
+ milvusDatabase: env.MILVUS_DATABASE,
3346
+ milvusCollection: env.MILVUS_COLLECTION,
3347
+ milvusDimension: env.MILVUS_DIMENSION ? Number.parseInt(env.MILVUS_DIMENSION, 10) : void 0,
2858
3348
  insightsBackend: env.INSIGHTS_BACKEND ?? "none",
2859
3349
  insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
2860
3350
  knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
@@ -2879,12 +3369,51 @@ function parseUnifiedArgs(argv) {
2879
3369
  case "--embedding-model":
2880
3370
  args.embeddingModel = takeValue();
2881
3371
  break;
3372
+ case "--embedding-cache-dir":
3373
+ args.embeddingCacheDir = takeValue();
3374
+ break;
3375
+ case "--reranker-provider":
3376
+ args.rerankerProvider = takeValue();
3377
+ break;
3378
+ case "--reranker-model":
3379
+ args.rerankerModel = takeValue();
3380
+ break;
3381
+ case "--reranker-cache-dir":
3382
+ args.rerankerCacheDir = takeValue();
3383
+ break;
3384
+ case "--reranker-batch-size":
3385
+ args.rerankerBatchSize = Number.parseInt(takeValue(), 10);
3386
+ break;
3387
+ case "--reranker-max-tokens":
3388
+ args.rerankerMaxTokens = Number.parseInt(takeValue(), 10);
3389
+ break;
2882
3390
  case "--chroma-url":
2883
3391
  args.chromaUrl = takeValue();
2884
3392
  break;
2885
3393
  case "--memory-backend":
2886
3394
  args.memoryBackend = takeValue();
2887
3395
  break;
3396
+ case "--lancedb-uri":
3397
+ args.lancedbUri = takeValue();
3398
+ break;
3399
+ case "--lancedb-table":
3400
+ args.lancedbTable = takeValue();
3401
+ break;
3402
+ case "--milvus-address":
3403
+ args.milvusAddress = takeValue();
3404
+ break;
3405
+ case "--milvus-token":
3406
+ args.milvusToken = takeValue();
3407
+ break;
3408
+ case "--milvus-database":
3409
+ args.milvusDatabase = takeValue();
3410
+ break;
3411
+ case "--milvus-collection":
3412
+ args.milvusCollection = takeValue();
3413
+ break;
3414
+ case "--milvus-dimension":
3415
+ args.milvusDimension = Number.parseInt(takeValue(), 10);
3416
+ break;
2888
3417
  case "--insights-backend":
2889
3418
  args.insightsBackend = takeValue();
2890
3419
  break;
@@ -2914,31 +3443,60 @@ function parseUnifiedArgs(argv) {
2914
3443
  break;
2915
3444
  }
2916
3445
  }
2917
- for (const [name, value] of [
2918
- ["--embedding-provider", args.embeddingProvider],
2919
- ["--memory-backend", args.memoryBackend],
2920
- ["--insights-backend", args.insightsBackend],
2921
- ["--knowledge-backend", args.knowledgeBackend]
2922
- ]) {
2923
- const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
3446
+ const validate = (name, value, allowed) => {
2924
3447
  if (!allowed.includes(value)) {
2925
3448
  throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
2926
3449
  }
3450
+ };
3451
+ validate("--embedding-provider", args.embeddingProvider, ["local", "openrouter", "none"]);
3452
+ validate("--reranker-provider", args.rerankerProvider, ["local", "none"]);
3453
+ validate("--memory-backend", args.memoryBackend, ["sqlite-vec", "chroma", "lancedb", "milvus", "none"]);
3454
+ validate("--insights-backend", args.insightsBackend, ["sqlite-vec", "chroma", "none"]);
3455
+ validate("--knowledge-backend", args.knowledgeBackend, ["chroma", "none"]);
3456
+ if (args.milvusDimension !== void 0 && (!Number.isInteger(args.milvusDimension) || args.milvusDimension <= 0)) {
3457
+ throw new Error("[memory-store/cli] --milvus-dimension must be a positive integer");
3458
+ }
3459
+ for (const [name, value] of [
3460
+ ["--reranker-batch-size", args.rerankerBatchSize],
3461
+ ["--reranker-max-tokens", args.rerankerMaxTokens]
3462
+ ]) {
3463
+ if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) {
3464
+ throw new Error(`[memory-store/cli] ${name} must be a positive integer`);
3465
+ }
2927
3466
  }
2928
3467
  return args;
2929
3468
  }
2930
3469
  async function buildUnified(args) {
2931
3470
  const unified = {};
2932
3471
  const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
2933
- const wantsAiRag = args.embeddingProvider === "local" || args.memoryBackend === "chroma" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
3472
+ const wantsAiRag = args.embeddingProvider === "local" || args.rerankerProvider === "local" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
2934
3473
  const aiRag = wantsAiRag ? await loadAiRag() : null;
2935
3474
  if (args.embeddingProvider === "local") {
2936
3475
  try {
2937
3476
  if (!aiRag) {
2938
3477
  throw new Error("ai-rag modules not loaded");
2939
3478
  }
2940
- const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
2941
- unified.embedQuery = async ({ query }) => provider.embedQuery(query);
3479
+ const provider = new aiRag.LocalTransformersEmbeddingProvider({
3480
+ modelName: args.embeddingModel,
3481
+ cacheDir: args.embeddingCacheDir
3482
+ });
3483
+ unified.embeddingInfo = {
3484
+ provider: "local",
3485
+ model: args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2",
3486
+ maxTokens: 512
3487
+ };
3488
+ unified.embedQuery = async ({ query }) => {
3489
+ const embedding = await provider.embedQuery(query);
3490
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
3491
+ return embedding;
3492
+ };
3493
+ unified.embedDocuments = async ({ texts }) => {
3494
+ const embeddings = await provider.embedDocuments(texts);
3495
+ if (unified.embeddingInfo && embeddings[0]) {
3496
+ unified.embeddingInfo.dimensions = embeddings[0].length;
3497
+ }
3498
+ return embeddings;
3499
+ };
2942
3500
  log(
2943
3501
  `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
2944
3502
  );
@@ -2954,6 +3512,7 @@ async function buildUnified(args) {
2954
3512
  }
2955
3513
  const model = args.embeddingModel ?? "text-embedding-3-small";
2956
3514
  const baseURL = "https://openrouter.ai/api/v1";
3515
+ unified.embeddingInfo = { provider: "openrouter", model };
2957
3516
  unified.embedQuery = async ({ query }) => {
2958
3517
  const res = await fetch(`${baseURL}/embeddings`, {
2959
3518
  method: "POST",
@@ -2971,49 +3530,150 @@ async function buildUnified(args) {
2971
3530
  const body = await res.json();
2972
3531
  const embedding = body.data?.[0]?.embedding;
2973
3532
  if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
3533
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
2974
3534
  return embedding;
2975
3535
  };
2976
3536
  log(`embedQuery wired via openrouter (model=${model})`);
2977
3537
  }
2978
- if (args.memoryBackend === "sqlite-vec") {
2979
- const store = createRawMessageStore({ env: void 0 });
2980
- const manager2 = await store.getManager();
2981
- if (typeof manager2.searchMessagesSemantically !== "function") {
3538
+ if (args.rerankerProvider === "local") {
3539
+ if (!aiRag) throw new Error("Local reranker modules were not loaded");
3540
+ const reranker = new aiRag.LocalTransformersReranker({
3541
+ modelName: args.rerankerModel,
3542
+ cacheDir: args.rerankerCacheDir,
3543
+ batchSize: args.rerankerBatchSize,
3544
+ maxTokens: args.rerankerMaxTokens
3545
+ });
3546
+ await reranker.warmup();
3547
+ unified.reranker = reranker;
3548
+ unified.rerankerInfo = {
3549
+ provider: "local",
3550
+ model: reranker.getModelName(),
3551
+ maxTokens: reranker.getMaxTokens(),
3552
+ ready: true
3553
+ };
3554
+ log(`reranker wired via local Transformers.js (model=${reranker.getModelName()})`);
3555
+ } else {
3556
+ unified.rerankerInfo = { provider: "none", ready: false };
3557
+ }
3558
+ if (args.memoryBackend !== "none") {
3559
+ if (args.memoryBackend === "chroma" && !args.chromaUrl) {
3560
+ throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3561
+ }
3562
+ if (args.memoryBackend === "lancedb" && !args.lancedbUri) {
3563
+ throw new Error("--memory-backend=lancedb requires --lancedb-uri <uri> (or LANCEDB_URI env)");
3564
+ }
3565
+ if (args.memoryBackend === "milvus" && !args.milvusAddress) {
3566
+ throw new Error("--memory-backend=milvus requires --milvus-address <address> (or MILVUS_ADDRESS env)");
3567
+ }
3568
+ if (args.memoryBackend === "lancedb") {
3569
+ try {
3570
+ await import('@lancedb/lancedb');
3571
+ } catch (error) {
3572
+ throw new Error(
3573
+ `LanceDB requires the optional dependency @lancedb/lancedb (${error.message})`
3574
+ );
3575
+ }
3576
+ }
3577
+ if (args.memoryBackend === "milvus") {
3578
+ try {
3579
+ await import('@zilliz/milvus2-sdk-node');
3580
+ } catch (error) {
3581
+ throw new Error(
3582
+ `Milvus requires the optional dependency @zilliz/milvus2-sdk-node (${error.message})`
3583
+ );
3584
+ }
3585
+ }
3586
+ const rawStore = createRawMessageStore({ env: void 0 });
3587
+ const manager2 = await rawStore.getManager();
3588
+ if (typeof manager2.getRawMessageSearchChunks !== "function" || typeof manager2.getRawMessageSearchIndexStats !== "function") {
2982
3589
  throw new Error(
2983
- "the active raw-message manager does not implement searchMessagesSemantically; sqlite-vec ANN for the memory source is unavailable in this build"
3590
+ `${args.memoryBackend} child retrieval requires the SQLite raw-message catalog; the Postgres manager remains on the legacy parent fallback in this release`
2984
3591
  );
2985
3592
  }
2986
- unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
2987
- const rows = await manager2.searchMessagesSemantically({
2988
- userId,
2989
- queryEmbedding,
2990
- limit,
2991
- threshold,
2992
- botId
3593
+ if (typeof manager2.lexicalSearchMessages === "function") {
3594
+ unified.searchRawMessagesLexical = async (input) => {
3595
+ const rows = await manager2.lexicalSearchMessages(input);
3596
+ return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
3597
+ };
3598
+ }
3599
+ let externalStore;
3600
+ if (args.memoryBackend === "chroma") {
3601
+ externalStore = new ChromaVectorStore({
3602
+ url: args.chromaUrl,
3603
+ collectionName: "opencontext_raw_message_chunks"
3604
+ });
3605
+ } else if (args.memoryBackend === "lancedb") {
3606
+ const { LanceDBStore } = await import('@melandlabs/rag/lancedb-store');
3607
+ externalStore = new LanceDBStore({
3608
+ uri: args.lancedbUri,
3609
+ tableName: args.lancedbTable ?? "opencontext_raw_message_chunks",
3610
+ defaultFusion: "rrf",
3611
+ candidateMultiplier: 4
3612
+ });
3613
+ } else if (args.memoryBackend === "milvus") {
3614
+ const { MilvusStore } = await import('@melandlabs/rag/milvus-store');
3615
+ externalStore = new MilvusStore({
3616
+ address: args.milvusAddress,
3617
+ token: args.milvusToken,
3618
+ database: args.milvusDatabase,
3619
+ collectionName: args.milvusCollection ?? "opencontext_raw_message_chunks",
3620
+ dimension: args.milvusDimension,
3621
+ defaultFusion: "rrf",
3622
+ candidateMultiplier: 4
2993
3623
  });
2994
- return rows.map((r) => ({
2995
- id: r.id,
2996
- content: r.content,
2997
- similarity: r.similarity,
2998
- metadata: r.metadata ?? {}
2999
- }));
3000
- };
3001
- log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
3002
- } else if (args.memoryBackend === "chroma") {
3003
- if (!args.chromaUrl)
3004
- throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3005
- if (!aiRag) {
3006
- throw new Error("ai-rag modules not loaded");
3007
3624
  }
3008
- const store = new aiRag.ChromaVectorStore({
3009
- url: args.chromaUrl,
3010
- collectionName: "opencontext_raw_messages"
3011
- });
3012
- unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
3013
- const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
3014
- return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
3625
+ if (args.memoryBackend === "sqlite-vec") {
3626
+ if (typeof manager2.searchMessagesSemantically !== "function") {
3627
+ throw new Error("sqlite-vec child search is unavailable in the active raw-message manager");
3628
+ }
3629
+ unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
3630
+ const rows = await manager2.searchMessagesSemantically({
3631
+ userId,
3632
+ queryEmbedding,
3633
+ limit,
3634
+ threshold,
3635
+ botId
3636
+ });
3637
+ return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
3638
+ };
3639
+ } else if (externalStore) {
3640
+ const childIndex = new RawMessageChildVectorIndex({
3641
+ backend: args.memoryBackend,
3642
+ store: externalStore,
3643
+ catalog: {
3644
+ getMessageById: (messageId) => manager2.getMessageById(messageId),
3645
+ // biome-ignore lint/style/noNonNullAssertion: guarded above
3646
+ getRawMessageSearchChunks: (input) => manager2.getRawMessageSearchChunks(input)
3647
+ }
3648
+ });
3649
+ unified.rawMessageChildIndex = childIndex;
3650
+ unified.searchRawMessagesAnn = (input) => childIndex.search(input);
3651
+ if (args.memoryBackend !== "chroma") {
3652
+ unified.searchRawMessagesHybrid = (input) => childIndex.search({ ...input, hybrid: true });
3653
+ }
3654
+ }
3655
+ unified.getRawMessageRetrievalStatus = async () => {
3656
+ const stats = await manager2.getRawMessageSearchIndexStats();
3657
+ const embeddingDimensions = unified.embeddingInfo?.dimensions;
3658
+ const dimensionMismatch = embeddingDimensions !== void 0 && stats.embeddingDimensions.length > 0 && !stats.embeddingDimensions.includes(embeddingDimensions);
3659
+ const semanticReady = Boolean(unified.embedQuery) && (args.memoryBackend === "sqlite-vec" ? stats.semanticReady : stats.embeddedChunkCount > 0) && !dimensionMismatch;
3660
+ return {
3661
+ backend: args.memoryBackend,
3662
+ embeddingProvider: unified.embeddingInfo?.provider,
3663
+ embeddingModel: unified.embeddingInfo?.model,
3664
+ embeddingDimensions,
3665
+ childCount: stats.chunkCount,
3666
+ embeddedChildCount: stats.embeddedChunkCount,
3667
+ indexedDimensions: stats.embeddingDimensions,
3668
+ semanticReady,
3669
+ lexicalReady: stats.lexicalReady,
3670
+ semanticDegradedReason: dimensionMismatch ? "semantic_dimension_mismatch" : !unified.embedQuery ? "semantic_unavailable" : stats.embeddedChunkCount === 0 ? "semantic_child_vectors_empty" : void 0,
3671
+ rerankerProvider: unified.rerankerInfo?.provider,
3672
+ rerankerModel: unified.rerankerInfo?.model,
3673
+ rerankerReady: unified.rerankerInfo?.ready ?? false
3674
+ };
3015
3675
  };
3016
- log("memory backend wired via chroma (collection=opencontext_raw_messages)");
3676
+ log(`memory backend wired via ${args.memoryBackend} with SQLite child catalog and lexical search`);
3017
3677
  }
3018
3678
  if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
3019
3679
  if (!unified.embedQuery) {
@@ -3140,20 +3800,41 @@ function printUnifiedHelp() {
3140
3800
  --embedding-model <name> Model name
3141
3801
  (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
3142
3802
  openrouter \u2192 text-embedding-3-small)
3803
+ --embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
3143
3804
  Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
3144
3805
 
3806
+ Reranking (after RRF/source fusion, before final Top-K):
3807
+ --reranker-provider <name> local | none
3808
+ (env: RERANKER_PROVIDER, default: none)
3809
+ --reranker-model <name> Local sequence-classification model
3810
+ (env: LOCAL_RERANKER_MODEL,
3811
+ default: Xenova/ms-marco-MiniLM-L-6-v2)
3812
+ --reranker-cache-dir <path> Persistent model cache (env: LOCAL_RERANKER_CACHE_DIR)
3813
+ --reranker-batch-size <int> Pair scoring batch size (env: LOCAL_RERANKER_BATCH_SIZE,
3814
+ default: 8)
3815
+ --reranker-max-tokens <int> Query/document pair token limit
3816
+ (env: LOCAL_RERANKER_MAX_TOKENS, default: 512)
3817
+
3145
3818
  Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
3146
3819
  --chroma-url <url> Chroma server URL
3147
3820
  (env: CHROMA_URL; required when any *-backend=chroma)
3148
- --memory-backend <name> sqlite-vec | chroma | none
3821
+ --memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
3149
3822
  (env: MEMORY_BACKEND, default: none)
3823
+ --lancedb-uri <uri> LanceDB directory or URI (env: LANCEDB_URI)
3824
+ --lancedb-table <name> Optional table name (env: LANCEDB_TABLE)
3825
+ --milvus-address <address> Milvus endpoint (env: MILVUS_ADDRESS)
3826
+ --milvus-token <token> Optional Milvus token (env: MILVUS_TOKEN)
3827
+ --milvus-database <name> Optional database (env: MILVUS_DATABASE)
3828
+ --milvus-collection <name> Optional collection (env: MILVUS_COLLECTION)
3829
+ --milvus-dimension <int> Optional vector dimension (env: MILVUS_DIMENSION)
3150
3830
  --insights-backend <name> sqlite-vec | chroma | none
3151
3831
  (env: INSIGHTS_BACKEND, default: none)
3152
3832
  --insights-collection <name> Chroma collection (default: opencontext_insights)
3153
3833
  --knowledge-backend <name> chroma | none
3154
3834
  (env: KNOWLEDGE_BACKEND, default: none)
3155
3835
  --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
3156
- Note: the chroma backends dynamically import @melandlabs/ai-rag.
3836
+ Notes: memory Chroma uses @melandlabs/rag; insight/knowledge Chroma use
3837
+ @melandlabs/ai-rag. LanceDB and Milvus require their optional peer dependencies.
3157
3838
 
3158
3839
  Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
3159
3840
  --reasoning Enable the LLM reasoning layer so /v1/search and