@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';
@@ -13,6 +13,8 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
13
13
  import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
14
14
  import { isEntityKind } from '@melandlabs/contracts/entity-edge';
15
15
  import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
16
+ import { createHash } from 'crypto';
17
+ import { ChromaVectorStore } from '@melandlabs/rag/chroma-vector-store';
16
18
 
17
19
  var __defProp = Object.defineProperty;
18
20
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -23,27 +25,6 @@ var __export = (target, all) => {
23
25
  for (var name in all)
24
26
  __defProp(target, name, { get: all[name], enumerable: true });
25
27
  };
26
- function normalizeTimestampToMs(value) {
27
- if (!Number.isFinite(value)) {
28
- return 0;
29
- }
30
- if (value < 1e11) {
31
- return Math.floor(value * 1e3);
32
- }
33
- return Math.floor(value);
34
- }
35
- function rawMessageToMemoryRecord(message) {
36
- return {
37
- id: message.messageId,
38
- userId: message.userId,
39
- timestamp: normalizeTimestampToMs(message.timestamp),
40
- text: message.archivedAt ? void 0 : message.content,
41
- mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
42
- embedding: message.embedding,
43
- embeddingModel: message.embeddingModel,
44
- embeddingContentHash: message.embeddingContentHash
45
- };
46
- }
47
28
  var init_contracts = __esm({
48
29
  "src/contracts.ts"() {
49
30
  }
@@ -64,66 +45,14 @@ function getRawMessagesCollectionName() {
64
45
  return process.env.CHROMA_RAW_MESSAGES_COLLECTION || process.env.CHROMA_MEMORY_COLLECTION || DEFAULT_RAW_MESSAGES_COLLECTION;
65
46
  }
66
47
  function getChromaStore(collectionName) {
67
- return new ChromaVectorStore({
48
+ return new ChromaVectorStore$1({
68
49
  url: process.env.CHROMA_URL,
69
50
  collectionName
70
51
  });
71
52
  }
72
- function normalizeTimestampToMs2(value) {
73
- if (!Number.isFinite(value)) {
74
- return 0;
75
- }
76
- if (value < 1e11) {
77
- return Math.floor(value * 1e3);
78
- }
79
- return Math.floor(value);
80
- }
81
53
  function toBooleanValue(value) {
82
54
  return value === true || value === "true" || value === 1 || value === "1";
83
55
  }
84
- function rawMessageToChunk(message) {
85
- if (!message.embedding || message.embedding.length === 0) {
86
- return null;
87
- }
88
- const record = rawMessageToMemoryRecord(message);
89
- const document = buildMemoryRecordEmbeddingDocument(record);
90
- if (!document.content) {
91
- return null;
92
- }
93
- return {
94
- id: message.messageId,
95
- documentId: message.messageId,
96
- content: document.content,
97
- embedding: message.embedding,
98
- metadata: {
99
- sourceType: "raw_message",
100
- userId: message.userId,
101
- platform: message.platform,
102
- botId: message.botId,
103
- channel: message.channel,
104
- person: message.person,
105
- timestamp: normalizeTimestampToMs2(message.timestamp),
106
- memoryStage: message.memoryStage,
107
- embeddingModel: message.embeddingModel,
108
- embeddingDimensions: message.embeddingDimensions,
109
- contentHash: message.embeddingContentHash,
110
- archived: Boolean(message.archivedAt),
111
- role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
112
- }
113
- };
114
- }
115
- async function upsertRawMessagesToChroma(messages) {
116
- if (!isRawMessageChromaEnabled() || messages.length === 0) {
117
- return 0;
118
- }
119
- const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
120
- if (chunks.length === 0) {
121
- return 0;
122
- }
123
- const store = getChromaStore(getRawMessagesCollectionName());
124
- await store.addChunks(chunks);
125
- return chunks.length;
126
- }
127
56
  async function searchRawMessagesWithChroma(input) {
128
57
  if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
129
58
  return [];
@@ -325,59 +254,73 @@ var init_raw_message_store = __esm({
325
254
  }
326
255
  });
327
256
 
328
- // src/embed-on-insert.ts
329
- async function embedMissingMessages(messages, deps) {
330
- if (typeof deps.embedQuery !== "function") {
331
- return messages;
257
+ // src/search/unified-search.ts
258
+ init_chroma_memory_index();
259
+ init_raw_message_store();
260
+
261
+ // src/search/applicability.ts
262
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
263
+ "global",
264
+ "task",
265
+ "conversation",
266
+ "channel",
267
+ "project",
268
+ "custom"
269
+ ]);
270
+ function isRecord(value) {
271
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
+ }
273
+ function invalidRuntimeContext(message) {
274
+ throw new TypeError(`memory-store search runtime context: ${message}`);
275
+ }
276
+ function validateApplicabilityContext(value, index) {
277
+ const path = `applicabilityContexts[${index}]`;
278
+ if (!isRecord(value)) {
279
+ invalidRuntimeContext(`${path} must be an object`);
332
280
  }
333
- const out = [];
334
- for (const message of messages) {
335
- if (Array.isArray(message.embedding) && message.embedding.length > 0) {
336
- out.push(message);
337
- continue;
338
- }
339
- if (typeof message.content !== "string" || message.content.length === 0) {
340
- out.push(message);
341
- continue;
281
+ const scope = value.scope;
282
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
283
+ invalidRuntimeContext(`${path}.scope is not supported`);
284
+ }
285
+ if (value.key !== void 0 && typeof value.key !== "string") {
286
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
287
+ }
288
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
289
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
290
+ }
291
+ for (const field of ["validFrom", "validUntil"]) {
292
+ const boundary = value[field];
293
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
294
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
342
295
  }
343
- const vector = await deps.embedQuery({
344
- userId: message.userId,
345
- query: message.content
346
- });
347
- out.push({
348
- ...message,
349
- embedding: vector,
350
- embeddingModel: message.embeddingModel ?? "server",
351
- embeddingDimensions: vector.length,
352
- embeddingUpdatedAt: Date.now()
353
- });
354
296
  }
355
- return out;
356
297
  }
357
- var AUTO_APPLIED_WARNING = {
358
- code: "embed_on_insert_auto_applied",
359
- 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."
360
- };
361
- async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
362
- const messages = incoming;
363
- const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
364
- const embedderWired = typeof unified?.embedQuery === "function";
365
- if (embedOnInsert === true && anyMissing) {
366
- return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
367
- }
368
- if (embedOnInsert !== true && anyMissing && embedderWired) {
369
- return {
370
- messages: await embedMissingMessages(messages, unified ?? {}),
371
- warnings: [AUTO_APPLIED_WARNING]
372
- };
298
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
299
+ if (runtimeContext === void 0) {
300
+ return void 0;
373
301
  }
374
- return { messages, warnings: [] };
302
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
303
+ invalidRuntimeContext("applicabilityContexts must be an array");
304
+ }
305
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
306
+ validateApplicabilityContext(context, index);
307
+ }
308
+ let applicabilityAt;
309
+ if (input.asOf === void 0) {
310
+ applicabilityAt = now();
311
+ } else {
312
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
313
+ if (!Number.isFinite(parsed)) {
314
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
315
+ }
316
+ applicabilityAt = parsed;
317
+ }
318
+ return {
319
+ applicabilityContexts: runtimeContext.applicabilityContexts,
320
+ applicabilityAt
321
+ };
375
322
  }
376
323
 
377
- // src/search/unified-search.ts
378
- init_chroma_memory_index();
379
- init_raw_message_store();
380
-
381
324
  // src/search/reranker.ts
382
325
  async function applyReranker(reranker, query, results) {
383
326
  if (!reranker || results.length === 0) {
@@ -464,7 +407,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
464
407
  return value === "rrf" ? "rrf" : "similarity";
465
408
  }
466
409
  function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
467
- if (value === "rewrite" || value === "iterative") {
410
+ if (value === "rewrite" || value === "iterative" || value === "union") {
468
411
  return value;
469
412
  }
470
413
  return defaultStrategy;
@@ -571,6 +514,7 @@ function materializeSignals(lists, hit) {
571
514
  function listNameToChannel(name) {
572
515
  if (name === "memory-semantic") return "semantic";
573
516
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
517
+ if (name === "memory-hybrid") return "hybrid";
574
518
  if (name === "memory-entity") return "entity";
575
519
  return void 0;
576
520
  }
@@ -628,7 +572,6 @@ async function resolveScopePeer(input) {
628
572
 
629
573
  // src/search/gather-evidence.ts
630
574
  var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
631
- var MAX_EVIDENCE_CHARS = 600;
632
575
  function toSummaryResult(row) {
633
576
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
634
577
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -646,7 +589,7 @@ function toSummaryResult(row) {
646
589
  }
647
590
  };
648
591
  }
649
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
592
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
650
593
  if (typeof deps.searchSummaries !== "function") {
651
594
  return { hits: [], warnings: [] };
652
595
  }
@@ -659,7 +602,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
659
602
  limit,
660
603
  threshold,
661
604
  authToken: input.authToken,
662
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
605
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
606
+ ...runtimeContext ?? {}
663
607
  });
664
608
  const hits = rows.map(toSummaryResult);
665
609
  return { hits, warnings: [] };
@@ -677,12 +621,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
677
621
  };
678
622
  }
679
623
  }
680
- function truncate(input, max = MAX_EVIDENCE_CHARS) {
681
- if (input.length <= max) {
682
- return input;
683
- }
684
- return `${input.slice(0, max - 1)}\u2026`;
685
- }
686
624
  function buildSynthesisPrompt(input) {
687
625
  const grouped = /* @__PURE__ */ new Map();
688
626
  for (const item of input.evidence) {
@@ -696,9 +634,10 @@ function buildSynthesisPrompt(input) {
696
634
  if (items.length === 0) {
697
635
  continue;
698
636
  }
699
- const lines = items.map(
700
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
701
- );
637
+ const lines = items.map((item, index) => {
638
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
639
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
640
+ });
702
641
  sections.push(`## ${tier}
703
642
  ${lines.join("\n")}`);
704
643
  }
@@ -780,12 +719,15 @@ function coerceAnswer(text, schema) {
780
719
  return text;
781
720
  }
782
721
  async function synthesizeAnswer(input) {
783
- const { query, hits, responseSchema, deps, logger } = input;
784
- const evidence = hits.map((hit) => ({
785
- id: hit.id,
786
- source: mapHitToTier(hit),
787
- snippet: truncate(hit.content),
788
- score: hit.similarity
722
+ const { query, responseSchema, deps, logger } = input;
723
+ const evidence = input.evidence.map((item) => ({
724
+ id: item.id,
725
+ source: mapEvidenceSourceToTier(item.source),
726
+ snippet: item.snippet,
727
+ score: item.score,
728
+ // Forward per-fact provenance (e.g. `metadata.source` from
729
+ // `opencontext add --source`) so the synthesis prompt can cite it.
730
+ metadata: item.metadata
789
731
  }));
790
732
  const warnings = [];
791
733
  const complete = deps.reasoning?.complete;
@@ -816,21 +758,10 @@ async function synthesizeAnswer(input) {
816
758
  return { answer: "", warnings };
817
759
  }
818
760
  }
819
- function mapHitToTier(hit) {
820
- const tierMarker = hit.metadata?.tier;
821
- if (tierMarker === "summary") {
822
- return "summary";
823
- }
824
- if (hit.type === "memory") {
825
- return "raw";
826
- }
827
- if (hit.type === "insight") {
828
- return "insight";
829
- }
830
- if (hit.type === "knowledge") {
831
- return "knowledge";
832
- }
833
- return hit.type;
761
+ function mapEvidenceSourceToTier(source) {
762
+ if (source === "memory") return "raw";
763
+ if (source === "insights") return "insight";
764
+ return source;
834
765
  }
835
766
  async function resolveSearchScopePeers(input) {
836
767
  return resolveScopePeer({
@@ -854,7 +785,7 @@ function deriveEntityKeywords(query) {
854
785
  }
855
786
  return out.slice(0, 16);
856
787
  }
857
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
788
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
858
789
  if (typeof deps.entitySearch !== "function") {
859
790
  return [];
860
791
  }
@@ -872,7 +803,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
872
803
  keywords,
873
804
  limit: Math.ceil(limit / filters.length),
874
805
  botId,
875
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
806
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
807
+ ...runtimeContext ?? {}
876
808
  })
877
809
  )
878
810
  )).flat();
@@ -960,6 +892,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
960
892
  return true;
961
893
  });
962
894
  }
895
+ function warnApplicabilityNotEnforced(warnings) {
896
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
897
+ return;
898
+ }
899
+ warnings.push({
900
+ source: "memory",
901
+ code: "memory_applicability_not_enforced",
902
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
903
+ });
904
+ }
963
905
  async function embedQueryVariant(embedQuery, input, query) {
964
906
  return embedQuery({
965
907
  userId: input.userId,
@@ -967,10 +909,29 @@ async function embedQueryVariant(embedQuery, input, query) {
967
909
  authToken: input.authToken
968
910
  });
969
911
  }
970
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
912
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
971
913
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
972
914
  let semantic = [];
973
- if (isRawMessageChromaEnabled()) {
915
+ if (typeof deps.searchRawMessagesAnn === "function") {
916
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
917
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
918
+ const includeDeprecated = input.includeDeprecated === true;
919
+ semantic = (await Promise.all(
920
+ filters.map(
921
+ (filter) => searchRawMessagesAnn({
922
+ userId: input.userId,
923
+ queryEmbedding,
924
+ limit,
925
+ threshold,
926
+ botId: "botId" in filter ? filter.botId : void 0,
927
+ includeDeprecated,
928
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
929
+ ...factTypes ? { factTypes } : {},
930
+ ...runtimeContext ?? {}
931
+ })
932
+ )
933
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
934
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
974
935
  try {
975
936
  semantic = (await Promise.all(
976
937
  filters.map((filter) => {
@@ -995,24 +956,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
995
956
  error
996
957
  );
997
958
  }
998
- } else if (typeof deps.searchRawMessagesAnn === "function") {
999
- const searchRawMessagesAnn = deps.searchRawMessagesAnn;
1000
- const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1001
- semantic = (await Promise.all(
1002
- filters.map(
1003
- (filter) => searchRawMessagesAnn({
1004
- userId: input.userId,
1005
- queryEmbedding,
1006
- limit,
1007
- threshold,
1008
- botId: "botId" in filter ? filter.botId : void 0,
1009
- ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1010
- ...factTypes ? { factTypes } : {}
1011
- })
1012
- )
1013
- )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1014
959
  }
1015
- if (semantic.length === 0) {
960
+ if (semantic.length === 0 && runtimeContext !== void 0) {
961
+ warnApplicabilityNotEnforced(warnings);
962
+ return [];
963
+ }
964
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1016
965
  try {
1017
966
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1018
967
  const manager2 = await getRawMessageManager2();
@@ -1023,6 +972,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1023
972
  queryEmbedding,
1024
973
  limit,
1025
974
  threshold,
975
+ includeDeprecated: input.includeDeprecated === true,
1026
976
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1027
977
  ...factTypes ? { factTypes } : {}
1028
978
  });
@@ -1052,6 +1002,18 @@ function mergeByMaxScore(lists) {
1052
1002
  }
1053
1003
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1054
1004
  }
1005
+ function dedupeChannelByParent(hits) {
1006
+ const strongest = /* @__PURE__ */ new Map();
1007
+ for (const hit of hits) {
1008
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1009
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1010
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1011
+ const key = `${normalized.type}:${parentId}`;
1012
+ const current = strongest.get(key);
1013
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1014
+ }
1015
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1016
+ }
1055
1017
  function toIterativeRecallCandidate(hit) {
1056
1018
  return {
1057
1019
  id: hit.id,
@@ -1060,7 +1022,7 @@ function toIterativeRecallCandidate(hit) {
1060
1022
  metadata: hit.metadata
1061
1023
  };
1062
1024
  }
1063
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1025
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1064
1026
  if (keywords.length === 0) {
1065
1027
  return [];
1066
1028
  }
@@ -1069,6 +1031,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1069
1031
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1070
1032
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1071
1033
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1034
+ const includeDeprecated = input.includeDeprecated === true;
1072
1035
  return (await Promise.all(
1073
1036
  filters.map(
1074
1037
  (botId) => searchRawMessagesLexical({
@@ -1076,8 +1039,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1076
1039
  keywords,
1077
1040
  limit: Math.ceil(limit / filters.length),
1078
1041
  botId,
1042
+ includeDeprecated,
1079
1043
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1080
- ...factTypes ? { factTypes } : {}
1044
+ ...factTypes ? { factTypes } : {},
1045
+ ...runtimeContext ?? {}
1081
1046
  })
1082
1047
  )
1083
1048
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1088,10 +1053,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1088
1053
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1089
1054
  }
1090
1055
  }
1056
+ if (runtimeContext !== void 0) {
1057
+ warnApplicabilityNotEnforced(warnings);
1058
+ return [];
1059
+ }
1091
1060
  try {
1092
1061
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1093
1062
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1094
1063
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1064
+ const includeDeprecated = input.includeDeprecated === true;
1095
1065
  return (await Promise.all(
1096
1066
  filters.map(
1097
1067
  (botId) => lexicalSearchRawMessages2({
@@ -1099,6 +1069,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1099
1069
  keywords,
1100
1070
  limit: Math.ceil(limit / filters.length),
1101
1071
  botId,
1072
+ includeDeprecated,
1102
1073
  ...factTypes ? { factTypes } : {}
1103
1074
  })
1104
1075
  )
@@ -1129,7 +1100,9 @@ function searchInputToUnified(input) {
1129
1100
  mergeStrategy: input.mergeStrategy,
1130
1101
  peerFilter: input.peerFilter,
1131
1102
  reasoningStrategy: input.reasoningStrategy,
1132
- factTypes: input.factTypes
1103
+ factTypes: input.factTypes,
1104
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1105
+ includeDeprecated: input.includeDeprecated
1133
1106
  };
1134
1107
  }
1135
1108
  function mapHitToEvidenceSource(hit) {
@@ -1157,8 +1130,8 @@ function createUnifiedSearch(deps = {}) {
1157
1130
  }
1158
1131
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1159
1132
  }
1160
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1161
- if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
1133
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1134
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1162
1135
  warnings.push({
1163
1136
  source: "memory",
1164
1137
  code: "memory_iterative_planner_not_configured",
@@ -1174,7 +1147,8 @@ function createUnifiedSearch(deps = {}) {
1174
1147
  });
1175
1148
  if (reasoningInfo) reasoningInfo.degraded = true;
1176
1149
  }
1177
- if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
1150
+ let unionEvidence = null;
1151
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
1178
1152
  const planner = deps.reasoning.iterativePlanner;
1179
1153
  const result = await planner.plan({
1180
1154
  query: input.query,
@@ -1189,7 +1163,9 @@ function createUnifiedSearch(deps = {}) {
1189
1163
  keywords2,
1190
1164
  limit,
1191
1165
  logger,
1192
- peerPeers
1166
+ peerPeers,
1167
+ warnings,
1168
+ runtimeContext
1193
1169
  );
1194
1170
  let semanticHits = [];
1195
1171
  if (typeof deps.embedQuery === "function") {
@@ -1203,7 +1179,9 @@ function createUnifiedSearch(deps = {}) {
1203
1179
  limit,
1204
1180
  threshold,
1205
1181
  logger,
1206
- peerPeers
1182
+ peerPeers,
1183
+ warnings,
1184
+ runtimeContext
1207
1185
  );
1208
1186
  } catch (error) {
1209
1187
  logger.warn?.(
@@ -1231,16 +1209,24 @@ function createUnifiedSearch(deps = {}) {
1231
1209
  reasoningInfo.degraded = true;
1232
1210
  }
1233
1211
  }
1234
- const semantic2 = result.evidence.map((candidate) => ({
1212
+ const evidence = result.evidence.map((candidate) => ({
1235
1213
  type: "memory",
1236
1214
  id: candidate.id,
1237
1215
  content: candidate.content,
1238
1216
  similarity: candidate.similarity,
1239
- metadata: { ...candidate.metadata, reasoning: "iterative" }
1217
+ metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
1240
1218
  }));
1241
- return { semantic: semantic2, lexical: [] };
1219
+ if (reasoningStrategy === "iterative") {
1220
+ return { semantic: evidence, lexical: [] };
1221
+ }
1222
+ unionEvidence = evidence;
1242
1223
  }
1243
1224
  if (typeof deps.embedQuery !== "function") {
1225
+ warnings.push({
1226
+ source: "memory",
1227
+ code: "semantic_unavailable",
1228
+ message: "Semantic search is unavailable because no embedding provider is configured."
1229
+ });
1244
1230
  warnings.push({
1245
1231
  source: "memory",
1246
1232
  code: "memory_lexical_search_fallback",
@@ -1248,7 +1234,16 @@ function createUnifiedSearch(deps = {}) {
1248
1234
  });
1249
1235
  const keywords2 = deriveLexicalKeywords(input.query);
1250
1236
  const lexical2 = filterByDateRange(
1251
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1237
+ await runLexicalSearchForKeywords(
1238
+ deps,
1239
+ input,
1240
+ keywords2,
1241
+ limit,
1242
+ logger,
1243
+ peerPeers,
1244
+ warnings,
1245
+ runtimeContext
1246
+ ),
1252
1247
  input.dateFrom,
1253
1248
  input.dateTo
1254
1249
  );
@@ -1256,6 +1251,26 @@ function createUnifiedSearch(deps = {}) {
1256
1251
  }
1257
1252
  let semantic = [];
1258
1253
  const embedQuery = deps.embedQuery;
1254
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1255
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1256
+ );
1257
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1258
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1259
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1260
+ const hybrid = (await Promise.all(
1261
+ filters.map(
1262
+ (botId) => deps.searchRawMessagesHybrid?.({
1263
+ userId: input.userId,
1264
+ query: input.query,
1265
+ queryEmbedding,
1266
+ limit: Math.ceil(limit / filters.length),
1267
+ botId,
1268
+ ...runtimeContext ?? {}
1269
+ })
1270
+ )
1271
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1272
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1273
+ }
1259
1274
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1260
1275
  try {
1261
1276
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1274,7 +1289,17 @@ function createUnifiedSearch(deps = {}) {
1274
1289
  );
1275
1290
  const lists = await Promise.all(
1276
1291
  embeddings.map(
1277
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1292
+ (embedding) => runSemanticSearchForEmbedding(
1293
+ deps,
1294
+ input,
1295
+ embedding,
1296
+ limit,
1297
+ threshold,
1298
+ logger,
1299
+ peerPeers,
1300
+ warnings,
1301
+ runtimeContext
1302
+ )
1278
1303
  )
1279
1304
  );
1280
1305
  semantic = mergeByMaxScore(lists);
@@ -1297,7 +1322,9 @@ function createUnifiedSearch(deps = {}) {
1297
1322
  limit,
1298
1323
  threshold,
1299
1324
  logger,
1300
- peerPeers
1325
+ peerPeers,
1326
+ warnings,
1327
+ runtimeContext
1301
1328
  );
1302
1329
  }
1303
1330
  let lexical = [];
@@ -1316,7 +1343,8 @@ function createUnifiedSearch(deps = {}) {
1316
1343
  limit,
1317
1344
  botId,
1318
1345
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1319
- ...factTypes ? { factTypes } : {}
1346
+ ...factTypes ? { factTypes } : {},
1347
+ ...runtimeContext ?? {}
1320
1348
  })
1321
1349
  )
1322
1350
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1342,7 +1370,7 @@ function createUnifiedSearch(deps = {}) {
1342
1370
  let entity;
1343
1371
  if (typeof deps.entitySearch === "function") {
1344
1372
  entity = filterByDateRange(
1345
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1373
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1346
1374
  input.dateFrom,
1347
1375
  input.dateTo
1348
1376
  );
@@ -1364,6 +1392,37 @@ function createUnifiedSearch(deps = {}) {
1364
1392
  message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1365
1393
  });
1366
1394
  }
1395
+ if (unionEvidence) {
1396
+ const plannerHits = unionEvidence;
1397
+ const base = mergeByMaxScore([
1398
+ filterByDateRange(semantic, input.dateFrom, input.dateTo),
1399
+ filterByDateRange(lexical, input.dateFrom, input.dateTo)
1400
+ ]);
1401
+ const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
1402
+ const seen = /* @__PURE__ */ new Set();
1403
+ const merged = [];
1404
+ plannerHits.forEach((hit, i) => {
1405
+ if (seen.has(hit.id)) {
1406
+ return;
1407
+ }
1408
+ seen.add(hit.id);
1409
+ merged.push({
1410
+ ...hit,
1411
+ similarity: maxBase + (plannerHits.length - i) * 1e-6
1412
+ });
1413
+ });
1414
+ for (const hit of base) {
1415
+ if (merged.length >= limit) {
1416
+ break;
1417
+ }
1418
+ if (seen.has(hit.id)) {
1419
+ continue;
1420
+ }
1421
+ seen.add(hit.id);
1422
+ merged.push(hit);
1423
+ }
1424
+ return { semantic: merged.slice(0, limit), lexical: [] };
1425
+ }
1367
1426
  const out = {
1368
1427
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1369
1428
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
@@ -1373,14 +1432,16 @@ function createUnifiedSearch(deps = {}) {
1373
1432
  }
1374
1433
  return out;
1375
1434
  }
1376
- async function searchUnifiedMemory(input) {
1435
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1377
1436
  const query = input.query.trim();
1378
1437
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1379
1438
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1380
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1381
1439
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1382
1440
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1383
1441
  );
1442
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1443
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1444
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1384
1445
  const reasoningStrategy = resolveReasoningStrategy(input);
1385
1446
  const warnings = [];
1386
1447
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1400,23 +1461,46 @@ function createUnifiedSearch(deps = {}) {
1400
1461
  };
1401
1462
  }
1402
1463
  let memorySubs;
1464
+ let retrievalStatus;
1403
1465
  const reasoningInfo = { strategy: reasoningStrategy };
1404
1466
  if (input.dateFrom || input.dateTo) {
1405
1467
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1406
1468
  }
1407
1469
  if (sources.includes("memory")) {
1408
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1470
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1409
1471
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1410
1472
  try {
1411
1473
  memorySubs = await runMemorySource(
1412
1474
  input,
1413
- limit,
1414
- threshold,
1475
+ candidateLimit,
1476
+ memoryThreshold,
1415
1477
  warnings,
1416
1478
  reasoningStrategy,
1417
1479
  reasoningInfo,
1418
- peerPeers
1480
+ peerPeers,
1481
+ runtimeContext
1419
1482
  );
1483
+ memorySubs = {
1484
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1485
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1486
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1487
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1488
+ };
1489
+ if (deps.getRawMessageRetrievalStatus) {
1490
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1491
+ }
1492
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1493
+ let code = "semantic_candidates_empty";
1494
+ let message = "Semantic search was available but returned no candidates.";
1495
+ if (retrievalStatus?.embeddedChildCount === 0) {
1496
+ code = "semantic_unavailable";
1497
+ message = "No embedded RawMessage children are available for semantic search.";
1498
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1499
+ code = "semantic_dimension_mismatch";
1500
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1501
+ }
1502
+ warnings.push({ source: "memory", code, message });
1503
+ }
1420
1504
  } catch (error) {
1421
1505
  logger.warn?.("[memory-store] memory source failed:", error);
1422
1506
  warnings.push({
@@ -1439,12 +1523,13 @@ function createUnifiedSearch(deps = {}) {
1439
1523
  const hits = await deps.searchInsights({
1440
1524
  userId: input.userId,
1441
1525
  query: input.query,
1442
- limit,
1526
+ limit: candidateLimit,
1443
1527
  threshold,
1444
1528
  botIds: input.botIds,
1445
1529
  includeArchived: input.includeArchivedInsights,
1446
1530
  authToken: input.authToken,
1447
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1531
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1532
+ ...runtimeContext ?? {}
1448
1533
  });
1449
1534
  for (const hit of hits) {
1450
1535
  insightHits.push({
@@ -1471,12 +1556,13 @@ function createUnifiedSearch(deps = {}) {
1471
1556
  userId: input.userId,
1472
1557
  query: input.query,
1473
1558
  options: {
1474
- limit,
1559
+ limit: candidateLimit,
1475
1560
  threshold,
1476
1561
  documentIds: input.documentIds
1477
1562
  },
1478
1563
  authToken: input.authToken,
1479
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1564
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1565
+ ...runtimeContext ?? {}
1480
1566
  });
1481
1567
  for (const hit of hits) {
1482
1568
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1494,10 +1580,14 @@ function createUnifiedSearch(deps = {}) {
1494
1580
  memorySubs,
1495
1581
  insightHits,
1496
1582
  knowledgeHits,
1497
- limit,
1583
+ limit: candidateLimit,
1498
1584
  strategy: mergeStrategy
1499
1585
  });
1500
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1586
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1587
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1588
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1589
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1590
+ const ranked = reranked.slice(0, limit);
1501
1591
  const output = {
1502
1592
  query: input.query,
1503
1593
  sources,
@@ -1505,28 +1595,51 @@ function createUnifiedSearch(deps = {}) {
1505
1595
  count: ranked.length,
1506
1596
  warnings
1507
1597
  };
1598
+ if (input.includeRetrievalDiagnostics) {
1599
+ output.retrievalDiagnostics = {
1600
+ mergeStrategy,
1601
+ candidateLimit,
1602
+ backend: retrievalStatus?.backend,
1603
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1604
+ candidateCounts: {
1605
+ semantic: memorySubs?.semantic.length ?? 0,
1606
+ lexical: memorySubs?.lexical.length ?? 0,
1607
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1608
+ entity: memorySubs?.entity?.length ?? 0,
1609
+ fused: merged.length,
1610
+ final: ranked.length
1611
+ },
1612
+ channels: {
1613
+ semantic: memorySubs?.semantic ?? [],
1614
+ lexical: memorySubs?.lexical ?? [],
1615
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1616
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1617
+ },
1618
+ fusedBeforeRerank: merged,
1619
+ reranker: {
1620
+ enabled: Boolean(deps.reranker),
1621
+ provider: deps.rerankerInfo?.provider,
1622
+ model: deps.rerankerInfo?.model,
1623
+ inputCount: merged.length,
1624
+ outputCount: reranked.length,
1625
+ latencyMs: rerankerLatencyMs,
1626
+ orderChanged: rerankerOrderChanged
1627
+ },
1628
+ final: ranked
1629
+ };
1630
+ }
1508
1631
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1509
1632
  output.reasoning = reasoningInfo;
1510
1633
  }
1511
1634
  return output;
1512
1635
  }
1636
+ async function searchUnifiedMemory(input) {
1637
+ return searchUnifiedMemoryWithRuntime(input);
1638
+ }
1513
1639
  async function searchRawMemorySemantically(input) {
1514
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1515
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1516
- const reasoningStrategy = resolveReasoningStrategy(input);
1517
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1518
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1519
- );
1520
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1521
- return mergeAcrossSources({
1522
- memorySubs: sub,
1523
- insightHits: [],
1524
- knowledgeHits: [],
1525
- limit,
1526
- strategy: mergeStrategy
1527
- });
1640
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1528
1641
  }
1529
- async function search(input) {
1642
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1530
1643
  const query = input.query.trim();
1531
1644
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1532
1645
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1558,19 +1671,24 @@ function createUnifiedSearch(deps = {}) {
1558
1671
  let unifiedHits = [];
1559
1672
  let sources = input.sources ? [...input.sources] : [];
1560
1673
  let unifiedReasoning;
1674
+ let retrievalDiagnostics;
1561
1675
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1562
1676
  const inferredSources = sources.length > 0 ? sources : [
1563
1677
  ...wantsRawTier ? ["memory"] : [],
1564
1678
  ...wantsInsightTier ? ["insights"] : [],
1565
1679
  ...wantsKnowledgeTier ? ["knowledge"] : []
1566
1680
  ];
1567
- const unified = await searchUnifiedMemory({
1568
- ...searchInputToUnified(input),
1569
- sources: inferredSources.length > 0 ? inferredSources : void 0
1570
- });
1681
+ const unified = await searchUnifiedMemoryWithRuntime(
1682
+ {
1683
+ ...searchInputToUnified(input),
1684
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1685
+ },
1686
+ runtimeContext
1687
+ );
1571
1688
  unifiedHits = unified.results;
1572
1689
  sources = unified.sources;
1573
1690
  unifiedReasoning = unified.reasoning;
1691
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1574
1692
  warnings.push(...unified.warnings);
1575
1693
  }
1576
1694
  let summaryHits = [];
@@ -1581,7 +1699,8 @@ function createUnifiedSearch(deps = {}) {
1581
1699
  limit,
1582
1700
  threshold,
1583
1701
  logger,
1584
- peerScope.peers
1702
+ peerScope.peers,
1703
+ runtimeContext
1585
1704
  );
1586
1705
  warnings.push(...summaryBucket.warnings);
1587
1706
  summaryHits = summaryBucket.hits;
@@ -1606,7 +1725,8 @@ function createUnifiedSearch(deps = {}) {
1606
1725
  source: mapHitToEvidenceSource(hit),
1607
1726
  snippet: hit.content,
1608
1727
  score: hit.similarity,
1609
- timestamp: getCandidateTimestamp(hit.metadata)
1728
+ timestamp: getCandidateTimestamp(hit.metadata),
1729
+ metadata: hit.metadata
1610
1730
  }));
1611
1731
  const base = {
1612
1732
  query,
@@ -1619,12 +1739,15 @@ function createUnifiedSearch(deps = {}) {
1619
1739
  if (unifiedReasoning) {
1620
1740
  base.reasoning = unifiedReasoning;
1621
1741
  }
1742
+ if (retrievalDiagnostics) {
1743
+ base.retrievalDiagnostics = retrievalDiagnostics;
1744
+ }
1622
1745
  if (!wantsSynthesis) {
1623
1746
  return base;
1624
1747
  }
1625
1748
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1626
1749
  query,
1627
- hits,
1750
+ evidence,
1628
1751
  responseSchema: synthesisSchema,
1629
1752
  deps,
1630
1753
  logger
@@ -1633,6 +1756,10 @@ function createUnifiedSearch(deps = {}) {
1633
1756
  base.warnings = [...base.warnings, ...synthWarnings];
1634
1757
  return base;
1635
1758
  }
1759
+ async function search(input, runtimeContext) {
1760
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1761
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1762
+ }
1636
1763
  return {
1637
1764
  search,
1638
1765
  searchUnifiedMemory,
@@ -1648,6 +1775,7 @@ function mergeAcrossSources(input) {
1648
1775
  for (const hit of [
1649
1776
  ...memorySubs.semantic,
1650
1777
  ...memorySubs.lexical,
1778
+ ...memorySubs.hybrid ?? [],
1651
1779
  ...input.insightHits,
1652
1780
  ...input.knowledgeHits
1653
1781
  ]) {
@@ -1663,6 +1791,7 @@ function mergeAcrossSources(input) {
1663
1791
  const all = [
1664
1792
  ...memorySubs.semantic,
1665
1793
  ...memorySubs.lexical,
1794
+ ...memorySubs.hybrid ?? [],
1666
1795
  ...memorySubs.entity ?? [],
1667
1796
  ...input.insightHits,
1668
1797
  ...input.knowledgeHits
@@ -1674,6 +1803,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1674
1803
  const lists = [];
1675
1804
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1676
1805
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1806
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1807
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1808
+ }
1677
1809
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1678
1810
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1679
1811
  }
@@ -2138,8 +2270,250 @@ function createVsaRecall(storage) {
2138
2270
  };
2139
2271
  }
2140
2272
 
2273
+ // ../shared/src/tokens.ts
2274
+ var estimateTokens = (text) => {
2275
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
2276
+ const otherChars = text.length - chineseChars;
2277
+ const wordCount = Math.ceil(otherChars / 5);
2278
+ return chineseChars + wordCount;
2279
+ };
2280
+
2281
+ // ../shared/src/text-chunking.ts
2282
+ var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
2283
+ var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
2284
+ var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
2285
+ var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
2286
+ function largestEndWithinTokenBudget(text, start, maxTokens) {
2287
+ let low = start + 1;
2288
+ let high = text.length;
2289
+ let best = low;
2290
+ while (low <= high) {
2291
+ const middle = Math.floor((low + high) / 2);
2292
+ if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
2293
+ best = middle;
2294
+ low = middle + 1;
2295
+ } else {
2296
+ high = middle - 1;
2297
+ }
2298
+ }
2299
+ return best;
2300
+ }
2301
+ function preferNaturalEnd(text, start, hardEnd) {
2302
+ if (hardEnd >= text.length) return text.length;
2303
+ const searchStart = start + Math.floor((hardEnd - start) * 0.6);
2304
+ const candidate = text.slice(searchStart, hardEnd);
2305
+ for (const boundary of PARAGRAPH_BOUNDARIES) {
2306
+ const index = candidate.lastIndexOf(boundary);
2307
+ if (index >= 0) return searchStart + index + boundary.length;
2308
+ }
2309
+ for (let offset = candidate.length; offset > 0; offset -= 1) {
2310
+ const prefix = candidate.slice(0, offset);
2311
+ if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
2312
+ }
2313
+ for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
2314
+ if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
2315
+ }
2316
+ return hardEnd;
2317
+ }
2318
+ function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
2319
+ if (overlapTokens <= 0) return chunkEnd;
2320
+ let low = chunkStart + 1;
2321
+ let high = chunkEnd;
2322
+ let best = chunkEnd;
2323
+ while (low <= high) {
2324
+ const middle = Math.floor((low + high) / 2);
2325
+ if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
2326
+ best = middle;
2327
+ high = middle - 1;
2328
+ } else {
2329
+ low = middle + 1;
2330
+ }
2331
+ }
2332
+ return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
2333
+ }
2334
+ function chunkTextByEstimatedTokens(text, options = {}) {
2335
+ if (text.length === 0) return [];
2336
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
2337
+ const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
2338
+ if (overlapTokens >= maxTokens) {
2339
+ throw new Error("overlapTokens must be smaller than maxTokens");
2340
+ }
2341
+ const chunks = [];
2342
+ let start = 0;
2343
+ while (start < text.length) {
2344
+ const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
2345
+ const end = preferNaturalEnd(text, start, hardEnd);
2346
+ chunks.push({
2347
+ chunkIndex: chunks.length,
2348
+ startPosition: start,
2349
+ endPosition: end,
2350
+ content: text.slice(start, end)
2351
+ });
2352
+ if (end >= text.length) break;
2353
+ start = overlapStart(text, start, end, overlapTokens);
2354
+ }
2355
+ return chunks;
2356
+ }
2357
+
2358
+ // src/embed-on-insert.ts
2359
+ var AUTO_APPLIED_WARNING = {
2360
+ code: "embed_on_insert_auto_applied",
2361
+ message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
2362
+ };
2363
+ var SEMANTIC_UNAVAILABLE_WARNING = {
2364
+ code: "semantic_index_unavailable",
2365
+ message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
2366
+ };
2367
+ function sha256(value) {
2368
+ return createHash("sha256").update(value).digest("hex");
2369
+ }
2370
+ function isUsableEmbedding(embedding) {
2371
+ return Boolean(
2372
+ embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
2373
+ );
2374
+ }
2375
+ async function embedText(text, userId, deps) {
2376
+ if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
2377
+ const embedding = await deps.embedQuery({ userId, query: text });
2378
+ return isUsableEmbedding(embedding) ? embedding : void 0;
2379
+ }
2380
+ async function embedMissingMessages(messages, deps) {
2381
+ if (typeof deps.embedQuery !== "function") return messages;
2382
+ const out = [];
2383
+ for (const message of messages) {
2384
+ if (isUsableEmbedding(message.embedding) || !message.content) {
2385
+ out.push(message);
2386
+ continue;
2387
+ }
2388
+ const embedding = await embedText(message.content, message.userId, deps);
2389
+ out.push(
2390
+ embedding ? {
2391
+ ...message,
2392
+ embedding,
2393
+ embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
2394
+ embeddingContentHash: sha256(message.content),
2395
+ embeddingDimensions: embedding.length,
2396
+ embeddingUpdatedAt: Date.now()
2397
+ } : message
2398
+ );
2399
+ }
2400
+ return out;
2401
+ }
2402
+ async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
2403
+ const deps = unified ?? {};
2404
+ const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
2405
+ const shouldEmbed = embedOnInsert === true || embedderAvailable;
2406
+ const messages = [];
2407
+ const chunks = [];
2408
+ let missingSemanticEmbedding = false;
2409
+ let generatedEmbedding = false;
2410
+ const plans = incoming.map((message) => ({
2411
+ message,
2412
+ pieces: chunkTextByEstimatedTokens(message.content)
2413
+ }));
2414
+ const generatedEmbeddings = /* @__PURE__ */ new Map();
2415
+ const pendingByUser = /* @__PURE__ */ new Map();
2416
+ for (const [messageIndex, plan] of plans.entries()) {
2417
+ for (const piece of plan.pieces) {
2418
+ const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
2419
+ if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
2420
+ const pending = pendingByUser.get(plan.message.userId) ?? [];
2421
+ pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
2422
+ pendingByUser.set(plan.message.userId, pending);
2423
+ }
2424
+ }
2425
+ for (const [userId, pending] of pendingByUser) {
2426
+ let embeddings;
2427
+ if (typeof deps.embedDocuments === "function") {
2428
+ const batch = await deps.embedDocuments({
2429
+ userId,
2430
+ texts: pending.map((item) => item.text)
2431
+ });
2432
+ if (batch.length !== pending.length) {
2433
+ throw new Error(
2434
+ `Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
2435
+ );
2436
+ }
2437
+ embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
2438
+ } else {
2439
+ embeddings = [];
2440
+ for (const item of pending) {
2441
+ embeddings.push(await embedText(item.text, userId, deps));
2442
+ }
2443
+ }
2444
+ for (const [index, item] of pending.entries()) {
2445
+ const embedding = embeddings[index];
2446
+ if (embedding) generatedEmbeddings.set(item.key, embedding);
2447
+ }
2448
+ }
2449
+ for (const [messageIndex, plan] of plans.entries()) {
2450
+ const incomingMessage = plan.message;
2451
+ const pieces = plan.pieces;
2452
+ let message = incomingMessage;
2453
+ for (const piece of pieces) {
2454
+ const contentHash = sha256(piece.content);
2455
+ const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
2456
+ const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
2457
+ generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
2458
+ if (!embedding) missingSemanticEmbedding = true;
2459
+ const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
2460
+ chunks.push({
2461
+ chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
2462
+ messageId: incomingMessage.messageId,
2463
+ userId: incomingMessage.userId,
2464
+ chunkIndex: piece.chunkIndex,
2465
+ chunkCount: pieces.length,
2466
+ startPosition: piece.startPosition,
2467
+ endPosition: piece.endPosition,
2468
+ content: piece.content,
2469
+ contentHash,
2470
+ embedding,
2471
+ embeddingModel,
2472
+ embeddingDimensions: embedding?.length,
2473
+ embeddingUpdatedAt: embedding ? Date.now() : void 0
2474
+ });
2475
+ if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
2476
+ message = {
2477
+ ...incomingMessage,
2478
+ embedding,
2479
+ embeddingModel,
2480
+ embeddingContentHash: contentHash,
2481
+ embeddingDimensions: embedding.length,
2482
+ embeddingUpdatedAt: Date.now()
2483
+ };
2484
+ }
2485
+ }
2486
+ messages.push(message);
2487
+ }
2488
+ const warnings = [];
2489
+ if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
2490
+ if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
2491
+ return { messages, chunks, warnings };
2492
+ }
2493
+
2494
+ // src/storage/raw-message-ingest.ts
2495
+ async function persistRawMessages(input) {
2496
+ const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
2497
+ let ids;
2498
+ if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
2499
+ ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
2500
+ } else {
2501
+ const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
2502
+ if (typeof input.manager.upsertRawMessages === "function") {
2503
+ await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
2504
+ } else if (typeof input.manager.storeMessages === "function") {
2505
+ ids = await input.manager.storeMessages(legacyMessages);
2506
+ } else {
2507
+ throw new Error("active raw-message manager exposes no supported write method");
2508
+ }
2509
+ }
2510
+ if (input.externalIndex) {
2511
+ await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
2512
+ }
2513
+ return { count: prepared.messages.length, ids, warnings: prepared.warnings };
2514
+ }
2515
+
2141
2516
  // src/mcp.ts
2142
- init_chroma_memory_index();
2143
2517
  init_raw_message_store();
2144
2518
  init_sqlite_raw_message_store();
2145
2519
  var DEFAULT_NAME = "@melandlabs/memory-store";
@@ -2152,13 +2526,37 @@ async function startMcpServer(options = {}) {
2152
2526
  const rawStore = createRawMessageStore({
2153
2527
  env: options.env
2154
2528
  });
2155
- const search = createUnifiedSearch(options.unified);
2529
+ const rawMessageManager = await rawStore.getManager();
2530
+ const search = createUnifiedSearch({
2531
+ ...options.unified,
2532
+ searchRawMessagesLexical: async (input) => {
2533
+ if (typeof rawMessageManager.lexicalSearchMessages !== "function") return [];
2534
+ const results = await rawMessageManager.lexicalSearchMessages(input);
2535
+ return results.filter(Boolean).map((result) => ({
2536
+ id: result.id,
2537
+ content: result.content,
2538
+ similarity: result.similarity,
2539
+ metadata: result.metadata ?? {}
2540
+ }));
2541
+ }
2542
+ });
2156
2543
  const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
2157
2544
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2158
2545
  const vsa = createVsaRecall(vsaStorage);
2159
- server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
2160
- content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
2161
- }));
2546
+ server.tool("memory.health", "Returns the health status of the memory store.", async () => {
2547
+ const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
2548
+ backend: "unknown",
2549
+ childCount: 0,
2550
+ embeddedChildCount: 0,
2551
+ indexedDimensions: [],
2552
+ semanticReady: false,
2553
+ lexicalReady: false,
2554
+ semanticDegradedReason: "health_check_failed"
2555
+ })) : void 0;
2556
+ return {
2557
+ content: [{ type: "text", text: JSON.stringify({ ok: true, retrieval }) }]
2558
+ };
2559
+ });
2162
2560
  const searchSchema = {
2163
2561
  userId: z.string(),
2164
2562
  query: z.string().min(1),
@@ -2166,8 +2564,8 @@ async function startMcpServer(options = {}) {
2166
2564
  tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
2167
2565
  "Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
2168
2566
  ),
2169
- reasoningStrategy: z.enum(["none", "rewrite", "iterative"]).optional().describe(
2170
- "LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again. Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
2567
+ reasoningStrategy: z.enum(["none", "rewrite", "iterative", "union"]).optional().describe(
2568
+ "LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again; `union` merges the planner's evidence with the baseline top-k hits (evidence first, deduped, capped at limit). Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
2171
2569
  ),
2172
2570
  synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
2173
2571
  "Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
@@ -2176,7 +2574,11 @@ async function startMcpServer(options = {}) {
2176
2574
  // coerces it into `synthesize.responseSchema` when present.
2177
2575
  responseSchema: z.record(z.string(), z.unknown()).optional(),
2178
2576
  limit: z.number().int().min(1).max(50).default(10),
2179
- threshold: z.number().min(-1).max(1).default(0.7),
2577
+ threshold: z.number().min(-1).max(1).optional().describe(
2578
+ "Optional similarity floor. Omit it to let the default RRF pipeline fuse the full candidate window."
2579
+ ),
2580
+ mergeStrategy: z.enum(["rrf", "similarity"]).optional(),
2581
+ includeRetrievalDiagnostics: z.boolean().optional(),
2180
2582
  botIds: z.array(z.string()).optional(),
2181
2583
  documentIds: z.array(z.string()).optional(),
2182
2584
  includeArchivedInsights: z.boolean().default(false),
@@ -2240,11 +2642,13 @@ async function startMcpServer(options = {}) {
2240
2642
  sources: a.sources,
2241
2643
  limit: a.limit,
2242
2644
  threshold: a.threshold,
2645
+ mergeStrategy: a.mergeStrategy,
2646
+ includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
2243
2647
  botIds: a.botIds,
2244
2648
  documentIds: a.documentIds,
2245
2649
  authToken: a.authToken,
2246
2650
  includeArchivedInsights: a.includeArchivedInsights,
2247
- reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" ? a.reasoningStrategy : void 0,
2651
+ reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" || a.reasoningStrategy === "union" ? a.reasoningStrategy : void 0,
2248
2652
  ...a.tiers ? { tiers: a.tiers } : {},
2249
2653
  ...wantsSynthesis ? {
2250
2654
  synthesize: {
@@ -2270,37 +2674,24 @@ async function startMcpServer(options = {}) {
2270
2674
  const a = args;
2271
2675
  const manager2 = await rawStore.getManager();
2272
2676
  const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
2273
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2274
- incoming,
2275
- a.embedOnInsert,
2276
- options.unified
2277
- );
2278
- let result;
2279
- if (typeof manager2.upsertRawMessages === "function") {
2280
- result = await manager2.upsertRawMessages({
2281
- userId: a.userId,
2282
- messages
2283
- });
2284
- } else if (typeof manager2.storeMessages === "function") {
2285
- const ids = await manager2.storeMessages(messages);
2286
- result = { inserted: ids.length, ids };
2287
- } else {
2288
- throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
2289
- }
2290
- try {
2291
- await upsertRawMessagesToChroma(messages);
2292
- } catch (error) {
2293
- console.warn("[memory-store/mcp] chroma upsert failed:", error);
2294
- }
2677
+ const persisted = await persistRawMessages({
2678
+ manager: manager2,
2679
+ userId: a.userId,
2680
+ messages: incoming,
2681
+ embedOnInsert: a.embedOnInsert,
2682
+ unified: options.unified,
2683
+ externalIndex: options.unified?.rawMessageChildIndex
2684
+ });
2685
+ const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
2295
2686
  return {
2296
2687
  content: [
2297
2688
  {
2298
2689
  type: "text",
2299
2690
  text: JSON.stringify({
2300
2691
  ok: true,
2301
- count: messages.length,
2692
+ count: persisted.count,
2302
2693
  result,
2303
- ...warnings.length > 0 ? { warnings } : {}
2694
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2304
2695
  })
2305
2696
  }
2306
2697
  ]
@@ -2640,7 +3031,17 @@ async function startMcpServer(options = {}) {
2640
3031
  );
2641
3032
  const okfMcpSpecifier = "@melandlabs/okf/mcp";
2642
3033
  const { registerOkfTools } = await import(okfMcpSpecifier);
2643
- registerOkfTools(server, rawStore);
3034
+ registerOkfTools(server, rawStore, {
3035
+ writeMessages: async (userId, messages) => {
3036
+ await persistRawMessages({
3037
+ manager: rawMessageManager,
3038
+ userId,
3039
+ messages,
3040
+ unified: options.unified,
3041
+ externalIndex: options.unified?.rawMessageChildIndex
3042
+ });
3043
+ }
3044
+ });
2644
3045
  const transport = new StdioServerTransport();
2645
3046
  await server.connect(transport);
2646
3047
  return server;
@@ -2985,6 +3386,121 @@ function createUserVoiceRewriter(options) {
2985
3386
  };
2986
3387
  }
2987
3388
 
3389
+ // src/storage/raw-message-child-vector-index.ts
3390
+ var RawMessageChildVectorIndex = class {
3391
+ backend;
3392
+ store;
3393
+ catalog;
3394
+ constructor(options) {
3395
+ this.backend = options.backend;
3396
+ this.store = options.store;
3397
+ this.catalog = options.catalog;
3398
+ }
3399
+ async replaceMessages(messages, chunks) {
3400
+ const chunksByMessage = /* @__PURE__ */ new Map();
3401
+ for (const chunk of chunks) {
3402
+ const group = chunksByMessage.get(chunk.messageId) ?? [];
3403
+ group.push(chunk);
3404
+ chunksByMessage.set(chunk.messageId, group);
3405
+ }
3406
+ for (const message of messages) {
3407
+ await this.store.deleteDocument(message.messageId);
3408
+ const vectorChunks = (chunksByMessage.get(message.messageId) ?? []).filter(
3409
+ (chunk) => Boolean(chunk.embedding?.length) && !chunk.embedding?.every((value) => value === 0)
3410
+ ).map((chunk) => ({
3411
+ id: chunk.chunkId,
3412
+ documentId: message.messageId,
3413
+ content: chunk.content,
3414
+ embedding: chunk.embedding,
3415
+ metadata: {
3416
+ userId: message.userId,
3417
+ botId: message.botId,
3418
+ platform: message.platform,
3419
+ timestamp: message.timestamp,
3420
+ chunkIndex: chunk.chunkIndex,
3421
+ chunkCount: chunk.chunkCount,
3422
+ startPosition: chunk.startPosition,
3423
+ endPosition: chunk.endPosition,
3424
+ contentHash: chunk.contentHash,
3425
+ embeddingModel: chunk.embeddingModel
3426
+ }
3427
+ }));
3428
+ if (vectorChunks.length > 0) await this.store.addChunks(vectorChunks);
3429
+ }
3430
+ }
3431
+ async search(input) {
3432
+ const results = input.hybrid && this.store.hybridSearch ? await this.store.hybridSearch({
3433
+ text: input.query ?? "",
3434
+ vector: input.queryEmbedding,
3435
+ limit: input.limit,
3436
+ candidateLimit: input.limit,
3437
+ fusion: "rrf",
3438
+ filter: { userId: input.userId }
3439
+ }) : await this.store.similaritySearch(input.queryEmbedding, input.limit, input.userId);
3440
+ return this.hydrate(results, input);
3441
+ }
3442
+ async getChunkCount() {
3443
+ return this.store.getChunkCount();
3444
+ }
3445
+ async hydrate(results, input) {
3446
+ const matchedChunks = await this.catalog.getRawMessageSearchChunks({
3447
+ chunkIds: results.map((result) => result.id),
3448
+ userId: input.userId
3449
+ });
3450
+ const matchedById = new Map(matchedChunks.map((chunk) => [chunk.chunkId, chunk]));
3451
+ const messageIds = [...new Set(matchedChunks.map((chunk) => chunk.messageId))];
3452
+ const [allChunks, parents] = await Promise.all([
3453
+ this.catalog.getRawMessageSearchChunks({ messageIds, userId: input.userId }),
3454
+ Promise.all(messageIds.map((messageId) => this.catalog.getMessageById(messageId)))
3455
+ ]);
3456
+ const parentById = new Map(
3457
+ parents.filter((parent) => Boolean(parent)).map((parent) => [parent.messageId, parent])
3458
+ );
3459
+ const chunksByMessage = /* @__PURE__ */ new Map();
3460
+ for (const chunk of allChunks) {
3461
+ const group = chunksByMessage.get(chunk.messageId) ?? [];
3462
+ group.push(chunk);
3463
+ chunksByMessage.set(chunk.messageId, group);
3464
+ }
3465
+ const strongest = /* @__PURE__ */ new Map();
3466
+ for (const result of results) {
3467
+ const chunk = matchedById.get(result.id);
3468
+ if (!chunk) continue;
3469
+ const parent = parentById.get(chunk.messageId);
3470
+ if (!parent || input.botId && parent.botId !== input.botId) continue;
3471
+ if (input.threshold !== void 0 && result.score < input.threshold) continue;
3472
+ const siblings = (chunksByMessage.get(parent.messageId) ?? []).sort(
3473
+ (a, b) => a.chunkIndex - b.chunkIndex
3474
+ );
3475
+ const content = chunk.chunkCount <= 1 ? parent.content : parent.content.slice(
3476
+ siblings[Math.max(0, chunk.chunkIndex - 1)]?.startPosition ?? chunk.startPosition,
3477
+ siblings[Math.min(siblings.length - 1, chunk.chunkIndex + 1)]?.endPosition ?? chunk.endPosition
3478
+ );
3479
+ const hit = {
3480
+ id: parent.messageId,
3481
+ content,
3482
+ similarity: result.score,
3483
+ metadata: {
3484
+ ...parent.metadata ?? {},
3485
+ userId: parent.userId,
3486
+ botId: parent.botId,
3487
+ platform: parent.platform,
3488
+ timestamp: parent.timestamp,
3489
+ sourceMessageId: parent.messageId,
3490
+ sourceChunkId: chunk.chunkId,
3491
+ sourceChunkIndex: chunk.chunkIndex,
3492
+ sourceChunkCount: chunk.chunkCount,
3493
+ backend: this.backend,
3494
+ scoring: input.hybrid ? "native-hybrid" : "dense"
3495
+ }
3496
+ };
3497
+ const existing = strongest.get(parent.messageId);
3498
+ if (!existing || hit.similarity > existing.similarity) strongest.set(parent.messageId, hit);
3499
+ }
3500
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity).slice(0, input.limit);
3501
+ }
3502
+ };
3503
+
2988
3504
  // src/server/cli-shared.ts
2989
3505
  init_raw_message_store();
2990
3506
 
@@ -2999,7 +3515,7 @@ function isInsightSQLiteVecEnabled(_env) {
2999
3515
  }
3000
3516
  async function getInsightSQLiteVecStore(_env) {
3001
3517
  const dbPath = resolveSQLiteRawMessageDbPath();
3002
- const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
3518
+ const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
3003
3519
  return await getSQLiteVecStore(dbPath, void 0, {
3004
3520
  collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
3005
3521
  });
@@ -3023,17 +3539,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
3023
3539
  // src/server/cli-shared.ts
3024
3540
  async function loadAiRag() {
3025
3541
  try {
3026
- const [local, chroma] = await Promise.all([
3542
+ const [local, reranker, chroma] = await Promise.all([
3027
3543
  import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
3544
+ import('@melandlabs/ai-rag/local-transformers-reranker'),
3028
3545
  import('@melandlabs/ai-rag/chroma-store')
3029
3546
  ]);
3030
3547
  return {
3031
3548
  LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
3549
+ LocalTransformersReranker: reranker.LocalTransformersReranker,
3032
3550
  ChromaVectorStore: chroma.ChromaVectorStore
3033
3551
  };
3034
3552
  } catch (error) {
3035
3553
  throw new Error(
3036
- `--embedding-provider local / --*-backend=chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
3554
+ `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})`
3037
3555
  );
3038
3556
  }
3039
3557
  }
@@ -3042,8 +3560,21 @@ function parseUnifiedArgs(argv) {
3042
3560
  const args = {
3043
3561
  embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
3044
3562
  embeddingModel: env.EMBEDDING_MODEL,
3563
+ embeddingCacheDir: env.LOCAL_EMBEDDING_CACHE_DIR,
3564
+ rerankerProvider: env.RERANKER_PROVIDER ?? "none",
3565
+ rerankerModel: env.LOCAL_RERANKER_MODEL,
3566
+ rerankerCacheDir: env.LOCAL_RERANKER_CACHE_DIR,
3567
+ rerankerBatchSize: env.LOCAL_RERANKER_BATCH_SIZE ? Number.parseInt(env.LOCAL_RERANKER_BATCH_SIZE, 10) : void 0,
3568
+ rerankerMaxTokens: env.LOCAL_RERANKER_MAX_TOKENS ? Number.parseInt(env.LOCAL_RERANKER_MAX_TOKENS, 10) : void 0,
3045
3569
  chromaUrl: env.CHROMA_URL,
3046
3570
  memoryBackend: env.MEMORY_BACKEND ?? "none",
3571
+ lancedbUri: env.LANCEDB_URI,
3572
+ lancedbTable: env.LANCEDB_TABLE,
3573
+ milvusAddress: env.MILVUS_ADDRESS,
3574
+ milvusToken: env.MILVUS_TOKEN,
3575
+ milvusDatabase: env.MILVUS_DATABASE,
3576
+ milvusCollection: env.MILVUS_COLLECTION,
3577
+ milvusDimension: env.MILVUS_DIMENSION ? Number.parseInt(env.MILVUS_DIMENSION, 10) : void 0,
3047
3578
  insightsBackend: env.INSIGHTS_BACKEND ?? "none",
3048
3579
  insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
3049
3580
  knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
@@ -3068,12 +3599,51 @@ function parseUnifiedArgs(argv) {
3068
3599
  case "--embedding-model":
3069
3600
  args.embeddingModel = takeValue();
3070
3601
  break;
3602
+ case "--embedding-cache-dir":
3603
+ args.embeddingCacheDir = takeValue();
3604
+ break;
3605
+ case "--reranker-provider":
3606
+ args.rerankerProvider = takeValue();
3607
+ break;
3608
+ case "--reranker-model":
3609
+ args.rerankerModel = takeValue();
3610
+ break;
3611
+ case "--reranker-cache-dir":
3612
+ args.rerankerCacheDir = takeValue();
3613
+ break;
3614
+ case "--reranker-batch-size":
3615
+ args.rerankerBatchSize = Number.parseInt(takeValue(), 10);
3616
+ break;
3617
+ case "--reranker-max-tokens":
3618
+ args.rerankerMaxTokens = Number.parseInt(takeValue(), 10);
3619
+ break;
3071
3620
  case "--chroma-url":
3072
3621
  args.chromaUrl = takeValue();
3073
3622
  break;
3074
3623
  case "--memory-backend":
3075
3624
  args.memoryBackend = takeValue();
3076
3625
  break;
3626
+ case "--lancedb-uri":
3627
+ args.lancedbUri = takeValue();
3628
+ break;
3629
+ case "--lancedb-table":
3630
+ args.lancedbTable = takeValue();
3631
+ break;
3632
+ case "--milvus-address":
3633
+ args.milvusAddress = takeValue();
3634
+ break;
3635
+ case "--milvus-token":
3636
+ args.milvusToken = takeValue();
3637
+ break;
3638
+ case "--milvus-database":
3639
+ args.milvusDatabase = takeValue();
3640
+ break;
3641
+ case "--milvus-collection":
3642
+ args.milvusCollection = takeValue();
3643
+ break;
3644
+ case "--milvus-dimension":
3645
+ args.milvusDimension = Number.parseInt(takeValue(), 10);
3646
+ break;
3077
3647
  case "--insights-backend":
3078
3648
  args.insightsBackend = takeValue();
3079
3649
  break;
@@ -3103,31 +3673,60 @@ function parseUnifiedArgs(argv) {
3103
3673
  break;
3104
3674
  }
3105
3675
  }
3106
- for (const [name, value] of [
3107
- ["--embedding-provider", args.embeddingProvider],
3108
- ["--memory-backend", args.memoryBackend],
3109
- ["--insights-backend", args.insightsBackend],
3110
- ["--knowledge-backend", args.knowledgeBackend]
3111
- ]) {
3112
- const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
3676
+ const validate = (name, value, allowed) => {
3113
3677
  if (!allowed.includes(value)) {
3114
3678
  throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
3115
3679
  }
3680
+ };
3681
+ validate("--embedding-provider", args.embeddingProvider, ["local", "openrouter", "none"]);
3682
+ validate("--reranker-provider", args.rerankerProvider, ["local", "none"]);
3683
+ validate("--memory-backend", args.memoryBackend, ["sqlite-vec", "chroma", "lancedb", "milvus", "none"]);
3684
+ validate("--insights-backend", args.insightsBackend, ["sqlite-vec", "chroma", "none"]);
3685
+ validate("--knowledge-backend", args.knowledgeBackend, ["chroma", "none"]);
3686
+ if (args.milvusDimension !== void 0 && (!Number.isInteger(args.milvusDimension) || args.milvusDimension <= 0)) {
3687
+ throw new Error("[memory-store/cli] --milvus-dimension must be a positive integer");
3688
+ }
3689
+ for (const [name, value] of [
3690
+ ["--reranker-batch-size", args.rerankerBatchSize],
3691
+ ["--reranker-max-tokens", args.rerankerMaxTokens]
3692
+ ]) {
3693
+ if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) {
3694
+ throw new Error(`[memory-store/cli] ${name} must be a positive integer`);
3695
+ }
3116
3696
  }
3117
3697
  return args;
3118
3698
  }
3119
3699
  async function buildUnified(args) {
3120
3700
  const unified = {};
3121
3701
  const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
3122
- const wantsAiRag = args.embeddingProvider === "local" || args.memoryBackend === "chroma" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
3702
+ const wantsAiRag = args.embeddingProvider === "local" || args.rerankerProvider === "local" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
3123
3703
  const aiRag = wantsAiRag ? await loadAiRag() : null;
3124
3704
  if (args.embeddingProvider === "local") {
3125
3705
  try {
3126
3706
  if (!aiRag) {
3127
3707
  throw new Error("ai-rag modules not loaded");
3128
3708
  }
3129
- const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
3130
- unified.embedQuery = async ({ query }) => provider.embedQuery(query);
3709
+ const provider = new aiRag.LocalTransformersEmbeddingProvider({
3710
+ modelName: args.embeddingModel,
3711
+ cacheDir: args.embeddingCacheDir
3712
+ });
3713
+ unified.embeddingInfo = {
3714
+ provider: "local",
3715
+ model: args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2",
3716
+ maxTokens: 512
3717
+ };
3718
+ unified.embedQuery = async ({ query }) => {
3719
+ const embedding = await provider.embedQuery(query);
3720
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
3721
+ return embedding;
3722
+ };
3723
+ unified.embedDocuments = async ({ texts }) => {
3724
+ const embeddings = await provider.embedDocuments(texts);
3725
+ if (unified.embeddingInfo && embeddings[0]) {
3726
+ unified.embeddingInfo.dimensions = embeddings[0].length;
3727
+ }
3728
+ return embeddings;
3729
+ };
3131
3730
  log(
3132
3731
  `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
3133
3732
  );
@@ -3143,6 +3742,7 @@ async function buildUnified(args) {
3143
3742
  }
3144
3743
  const model = args.embeddingModel ?? "text-embedding-3-small";
3145
3744
  const baseURL = "https://openrouter.ai/api/v1";
3745
+ unified.embeddingInfo = { provider: "openrouter", model };
3146
3746
  unified.embedQuery = async ({ query }) => {
3147
3747
  const res = await fetch(`${baseURL}/embeddings`, {
3148
3748
  method: "POST",
@@ -3160,49 +3760,150 @@ async function buildUnified(args) {
3160
3760
  const body = await res.json();
3161
3761
  const embedding = body.data?.[0]?.embedding;
3162
3762
  if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
3763
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
3163
3764
  return embedding;
3164
3765
  };
3165
3766
  log(`embedQuery wired via openrouter (model=${model})`);
3166
3767
  }
3167
- if (args.memoryBackend === "sqlite-vec") {
3168
- const store = createRawMessageStore({ env: void 0 });
3169
- const manager2 = await store.getManager();
3170
- if (typeof manager2.searchMessagesSemantically !== "function") {
3768
+ if (args.rerankerProvider === "local") {
3769
+ if (!aiRag) throw new Error("Local reranker modules were not loaded");
3770
+ const reranker = new aiRag.LocalTransformersReranker({
3771
+ modelName: args.rerankerModel,
3772
+ cacheDir: args.rerankerCacheDir,
3773
+ batchSize: args.rerankerBatchSize,
3774
+ maxTokens: args.rerankerMaxTokens
3775
+ });
3776
+ await reranker.warmup();
3777
+ unified.reranker = reranker;
3778
+ unified.rerankerInfo = {
3779
+ provider: "local",
3780
+ model: reranker.getModelName(),
3781
+ maxTokens: reranker.getMaxTokens(),
3782
+ ready: true
3783
+ };
3784
+ log(`reranker wired via local Transformers.js (model=${reranker.getModelName()})`);
3785
+ } else {
3786
+ unified.rerankerInfo = { provider: "none", ready: false };
3787
+ }
3788
+ if (args.memoryBackend !== "none") {
3789
+ if (args.memoryBackend === "chroma" && !args.chromaUrl) {
3790
+ throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3791
+ }
3792
+ if (args.memoryBackend === "lancedb" && !args.lancedbUri) {
3793
+ throw new Error("--memory-backend=lancedb requires --lancedb-uri <uri> (or LANCEDB_URI env)");
3794
+ }
3795
+ if (args.memoryBackend === "milvus" && !args.milvusAddress) {
3796
+ throw new Error("--memory-backend=milvus requires --milvus-address <address> (or MILVUS_ADDRESS env)");
3797
+ }
3798
+ if (args.memoryBackend === "lancedb") {
3799
+ try {
3800
+ await import('@lancedb/lancedb');
3801
+ } catch (error) {
3802
+ throw new Error(
3803
+ `LanceDB requires the optional dependency @lancedb/lancedb (${error.message})`
3804
+ );
3805
+ }
3806
+ }
3807
+ if (args.memoryBackend === "milvus") {
3808
+ try {
3809
+ await import('@zilliz/milvus2-sdk-node');
3810
+ } catch (error) {
3811
+ throw new Error(
3812
+ `Milvus requires the optional dependency @zilliz/milvus2-sdk-node (${error.message})`
3813
+ );
3814
+ }
3815
+ }
3816
+ const rawStore = createRawMessageStore({ env: void 0 });
3817
+ const manager2 = await rawStore.getManager();
3818
+ if (typeof manager2.getRawMessageSearchChunks !== "function" || typeof manager2.getRawMessageSearchIndexStats !== "function") {
3171
3819
  throw new Error(
3172
- "the active raw-message manager does not implement searchMessagesSemantically; sqlite-vec ANN for the memory source is unavailable in this build"
3820
+ `${args.memoryBackend} child retrieval requires the SQLite raw-message catalog; the Postgres manager remains on the legacy parent fallback in this release`
3173
3821
  );
3174
3822
  }
3175
- unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
3176
- const rows = await manager2.searchMessagesSemantically({
3177
- userId,
3178
- queryEmbedding,
3179
- limit,
3180
- threshold,
3181
- botId
3823
+ if (typeof manager2.lexicalSearchMessages === "function") {
3824
+ unified.searchRawMessagesLexical = async (input) => {
3825
+ const rows = await manager2.lexicalSearchMessages(input);
3826
+ return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
3827
+ };
3828
+ }
3829
+ let externalStore;
3830
+ if (args.memoryBackend === "chroma") {
3831
+ externalStore = new ChromaVectorStore({
3832
+ url: args.chromaUrl,
3833
+ collectionName: "opencontext_raw_message_chunks"
3834
+ });
3835
+ } else if (args.memoryBackend === "lancedb") {
3836
+ const { LanceDBStore } = await import('@melandlabs/rag/lancedb-store');
3837
+ externalStore = new LanceDBStore({
3838
+ uri: args.lancedbUri,
3839
+ tableName: args.lancedbTable ?? "opencontext_raw_message_chunks",
3840
+ defaultFusion: "rrf",
3841
+ candidateMultiplier: 4
3842
+ });
3843
+ } else if (args.memoryBackend === "milvus") {
3844
+ const { MilvusStore } = await import('@melandlabs/rag/milvus-store');
3845
+ externalStore = new MilvusStore({
3846
+ address: args.milvusAddress,
3847
+ token: args.milvusToken,
3848
+ database: args.milvusDatabase,
3849
+ collectionName: args.milvusCollection ?? "opencontext_raw_message_chunks",
3850
+ dimension: args.milvusDimension,
3851
+ defaultFusion: "rrf",
3852
+ candidateMultiplier: 4
3182
3853
  });
3183
- return rows.map((r) => ({
3184
- id: r.id,
3185
- content: r.content,
3186
- similarity: r.similarity,
3187
- metadata: r.metadata ?? {}
3188
- }));
3189
- };
3190
- log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
3191
- } else if (args.memoryBackend === "chroma") {
3192
- if (!args.chromaUrl)
3193
- throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3194
- if (!aiRag) {
3195
- throw new Error("ai-rag modules not loaded");
3196
3854
  }
3197
- const store = new aiRag.ChromaVectorStore({
3198
- url: args.chromaUrl,
3199
- collectionName: "opencontext_raw_messages"
3200
- });
3201
- unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
3202
- const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
3203
- return results.filter((r) => r.score >= threshold).map((r) => ({ id: r.id, content: r.content, similarity: r.score, metadata: r.metadata ?? {} }));
3855
+ if (args.memoryBackend === "sqlite-vec") {
3856
+ if (typeof manager2.searchMessagesSemantically !== "function") {
3857
+ throw new Error("sqlite-vec child search is unavailable in the active raw-message manager");
3858
+ }
3859
+ unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
3860
+ const rows = await manager2.searchMessagesSemantically({
3861
+ userId,
3862
+ queryEmbedding,
3863
+ limit,
3864
+ threshold,
3865
+ botId
3866
+ });
3867
+ return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
3868
+ };
3869
+ } else if (externalStore) {
3870
+ const childIndex = new RawMessageChildVectorIndex({
3871
+ backend: args.memoryBackend,
3872
+ store: externalStore,
3873
+ catalog: {
3874
+ getMessageById: (messageId) => manager2.getMessageById(messageId),
3875
+ // biome-ignore lint/style/noNonNullAssertion: guarded above
3876
+ getRawMessageSearchChunks: (input) => manager2.getRawMessageSearchChunks(input)
3877
+ }
3878
+ });
3879
+ unified.rawMessageChildIndex = childIndex;
3880
+ unified.searchRawMessagesAnn = (input) => childIndex.search(input);
3881
+ if (args.memoryBackend !== "chroma") {
3882
+ unified.searchRawMessagesHybrid = (input) => childIndex.search({ ...input, hybrid: true });
3883
+ }
3884
+ }
3885
+ unified.getRawMessageRetrievalStatus = async () => {
3886
+ const stats = await manager2.getRawMessageSearchIndexStats();
3887
+ const embeddingDimensions = unified.embeddingInfo?.dimensions;
3888
+ const dimensionMismatch = embeddingDimensions !== void 0 && stats.embeddingDimensions.length > 0 && !stats.embeddingDimensions.includes(embeddingDimensions);
3889
+ const semanticReady = Boolean(unified.embedQuery) && (args.memoryBackend === "sqlite-vec" ? stats.semanticReady : stats.embeddedChunkCount > 0) && !dimensionMismatch;
3890
+ return {
3891
+ backend: args.memoryBackend,
3892
+ embeddingProvider: unified.embeddingInfo?.provider,
3893
+ embeddingModel: unified.embeddingInfo?.model,
3894
+ embeddingDimensions,
3895
+ childCount: stats.chunkCount,
3896
+ embeddedChildCount: stats.embeddedChunkCount,
3897
+ indexedDimensions: stats.embeddingDimensions,
3898
+ semanticReady,
3899
+ lexicalReady: stats.lexicalReady,
3900
+ semanticDegradedReason: dimensionMismatch ? "semantic_dimension_mismatch" : !unified.embedQuery ? "semantic_unavailable" : stats.embeddedChunkCount === 0 ? "semantic_child_vectors_empty" : void 0,
3901
+ rerankerProvider: unified.rerankerInfo?.provider,
3902
+ rerankerModel: unified.rerankerInfo?.model,
3903
+ rerankerReady: unified.rerankerInfo?.ready ?? false
3904
+ };
3204
3905
  };
3205
- log("memory backend wired via chroma (collection=opencontext_raw_messages)");
3906
+ log(`memory backend wired via ${args.memoryBackend} with SQLite child catalog and lexical search`);
3206
3907
  }
3207
3908
  if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
3208
3909
  if (!unified.embedQuery) {
@@ -3329,20 +4030,41 @@ function printUnifiedHelp() {
3329
4030
  --embedding-model <name> Model name
3330
4031
  (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
3331
4032
  openrouter \u2192 text-embedding-3-small)
4033
+ --embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
3332
4034
  Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
3333
4035
 
4036
+ Reranking (after RRF/source fusion, before final Top-K):
4037
+ --reranker-provider <name> local | none
4038
+ (env: RERANKER_PROVIDER, default: none)
4039
+ --reranker-model <name> Local sequence-classification model
4040
+ (env: LOCAL_RERANKER_MODEL,
4041
+ default: Xenova/ms-marco-MiniLM-L-6-v2)
4042
+ --reranker-cache-dir <path> Persistent model cache (env: LOCAL_RERANKER_CACHE_DIR)
4043
+ --reranker-batch-size <int> Pair scoring batch size (env: LOCAL_RERANKER_BATCH_SIZE,
4044
+ default: 8)
4045
+ --reranker-max-tokens <int> Query/document pair token limit
4046
+ (env: LOCAL_RERANKER_MAX_TOKENS, default: 512)
4047
+
3334
4048
  Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
3335
4049
  --chroma-url <url> Chroma server URL
3336
4050
  (env: CHROMA_URL; required when any *-backend=chroma)
3337
- --memory-backend <name> sqlite-vec | chroma | none
4051
+ --memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
3338
4052
  (env: MEMORY_BACKEND, default: none)
4053
+ --lancedb-uri <uri> LanceDB directory or URI (env: LANCEDB_URI)
4054
+ --lancedb-table <name> Optional table name (env: LANCEDB_TABLE)
4055
+ --milvus-address <address> Milvus endpoint (env: MILVUS_ADDRESS)
4056
+ --milvus-token <token> Optional Milvus token (env: MILVUS_TOKEN)
4057
+ --milvus-database <name> Optional database (env: MILVUS_DATABASE)
4058
+ --milvus-collection <name> Optional collection (env: MILVUS_COLLECTION)
4059
+ --milvus-dimension <int> Optional vector dimension (env: MILVUS_DIMENSION)
3339
4060
  --insights-backend <name> sqlite-vec | chroma | none
3340
4061
  (env: INSIGHTS_BACKEND, default: none)
3341
4062
  --insights-collection <name> Chroma collection (default: opencontext_insights)
3342
4063
  --knowledge-backend <name> chroma | none
3343
4064
  (env: KNOWLEDGE_BACKEND, default: none)
3344
4065
  --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
3345
- Note: the chroma backends dynamically import @melandlabs/ai-rag.
4066
+ Notes: memory Chroma uses @melandlabs/rag; insight/knowledge Chroma use
4067
+ @melandlabs/ai-rag. LanceDB and Milvus require their optional peer dependencies.
3346
4068
 
3347
4069
  Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
3348
4070
  --reasoning Enable the LLM reasoning layer so /v1/search and