@melandlabs/memory-store 1.2.5 → 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;
372
300
  }
373
- return { messages, warnings: [] };
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;
316
+ }
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) {
@@ -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,7 +1129,7 @@ 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 = []) {
1132
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1160
1133
  if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1161
1134
  warnings.push({
1162
1135
  source: "memory",
@@ -1189,7 +1162,9 @@ function createUnifiedSearch(deps = {}) {
1189
1162
  keywords2,
1190
1163
  limit,
1191
1164
  logger,
1192
- peerPeers
1165
+ peerPeers,
1166
+ warnings,
1167
+ runtimeContext
1193
1168
  );
1194
1169
  let semanticHits = [];
1195
1170
  if (typeof deps.embedQuery === "function") {
@@ -1203,7 +1178,9 @@ function createUnifiedSearch(deps = {}) {
1203
1178
  limit,
1204
1179
  threshold,
1205
1180
  logger,
1206
- peerPeers
1181
+ peerPeers,
1182
+ warnings,
1183
+ runtimeContext
1207
1184
  );
1208
1185
  } catch (error) {
1209
1186
  logger.warn?.(
@@ -1244,6 +1221,11 @@ function createUnifiedSearch(deps = {}) {
1244
1221
  unionEvidence = evidence;
1245
1222
  }
1246
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
+ });
1247
1229
  warnings.push({
1248
1230
  source: "memory",
1249
1231
  code: "memory_lexical_search_fallback",
@@ -1251,7 +1233,16 @@ function createUnifiedSearch(deps = {}) {
1251
1233
  });
1252
1234
  const keywords2 = deriveLexicalKeywords(input.query);
1253
1235
  const lexical2 = filterByDateRange(
1254
- 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
+ ),
1255
1246
  input.dateFrom,
1256
1247
  input.dateTo
1257
1248
  );
@@ -1259,6 +1250,26 @@ function createUnifiedSearch(deps = {}) {
1259
1250
  }
1260
1251
  let semantic = [];
1261
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
+ }
1262
1273
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1263
1274
  try {
1264
1275
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1277,7 +1288,17 @@ function createUnifiedSearch(deps = {}) {
1277
1288
  );
1278
1289
  const lists = await Promise.all(
1279
1290
  embeddings.map(
1280
- (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
+ )
1281
1302
  )
1282
1303
  );
1283
1304
  semantic = mergeByMaxScore(lists);
@@ -1300,7 +1321,9 @@ function createUnifiedSearch(deps = {}) {
1300
1321
  limit,
1301
1322
  threshold,
1302
1323
  logger,
1303
- peerPeers
1324
+ peerPeers,
1325
+ warnings,
1326
+ runtimeContext
1304
1327
  );
1305
1328
  }
1306
1329
  let lexical = [];
@@ -1319,7 +1342,8 @@ function createUnifiedSearch(deps = {}) {
1319
1342
  limit,
1320
1343
  botId,
1321
1344
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1322
- ...factTypes ? { factTypes } : {}
1345
+ ...factTypes ? { factTypes } : {},
1346
+ ...runtimeContext ?? {}
1323
1347
  })
1324
1348
  )
1325
1349
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1345,7 +1369,7 @@ function createUnifiedSearch(deps = {}) {
1345
1369
  let entity;
1346
1370
  if (typeof deps.entitySearch === "function") {
1347
1371
  entity = filterByDateRange(
1348
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1372
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1349
1373
  input.dateFrom,
1350
1374
  input.dateTo
1351
1375
  );
@@ -1407,14 +1431,16 @@ function createUnifiedSearch(deps = {}) {
1407
1431
  }
1408
1432
  return out;
1409
1433
  }
1410
- async function searchUnifiedMemory(input) {
1434
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1411
1435
  const query = input.query.trim();
1412
1436
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1413
1437
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1414
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1415
1438
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1416
1439
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1417
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;
1418
1444
  const reasoningStrategy = resolveReasoningStrategy(input);
1419
1445
  const warnings = [];
1420
1446
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1434,23 +1460,46 @@ function createUnifiedSearch(deps = {}) {
1434
1460
  };
1435
1461
  }
1436
1462
  let memorySubs;
1463
+ let retrievalStatus;
1437
1464
  const reasoningInfo = { strategy: reasoningStrategy };
1438
1465
  if (input.dateFrom || input.dateTo) {
1439
1466
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1440
1467
  }
1441
1468
  if (sources.includes("memory")) {
1442
- 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";
1443
1470
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1444
1471
  try {
1445
1472
  memorySubs = await runMemorySource(
1446
1473
  input,
1447
- limit,
1448
- threshold,
1474
+ candidateLimit,
1475
+ memoryThreshold,
1449
1476
  warnings,
1450
1477
  reasoningStrategy,
1451
1478
  reasoningInfo,
1452
- peerPeers
1479
+ peerPeers,
1480
+ runtimeContext
1453
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
+ }
1454
1503
  } catch (error) {
1455
1504
  logger.warn?.("[memory-store] memory source failed:", error);
1456
1505
  warnings.push({
@@ -1473,12 +1522,13 @@ function createUnifiedSearch(deps = {}) {
1473
1522
  const hits = await deps.searchInsights({
1474
1523
  userId: input.userId,
1475
1524
  query: input.query,
1476
- limit,
1525
+ limit: candidateLimit,
1477
1526
  threshold,
1478
1527
  botIds: input.botIds,
1479
1528
  includeArchived: input.includeArchivedInsights,
1480
1529
  authToken: input.authToken,
1481
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1530
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1531
+ ...runtimeContext ?? {}
1482
1532
  });
1483
1533
  for (const hit of hits) {
1484
1534
  insightHits.push({
@@ -1505,12 +1555,13 @@ function createUnifiedSearch(deps = {}) {
1505
1555
  userId: input.userId,
1506
1556
  query: input.query,
1507
1557
  options: {
1508
- limit,
1558
+ limit: candidateLimit,
1509
1559
  threshold,
1510
1560
  documentIds: input.documentIds
1511
1561
  },
1512
1562
  authToken: input.authToken,
1513
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1563
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1564
+ ...runtimeContext ?? {}
1514
1565
  });
1515
1566
  for (const hit of hits) {
1516
1567
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1528,10 +1579,14 @@ function createUnifiedSearch(deps = {}) {
1528
1579
  memorySubs,
1529
1580
  insightHits,
1530
1581
  knowledgeHits,
1531
- limit,
1582
+ limit: candidateLimit,
1532
1583
  strategy: mergeStrategy
1533
1584
  });
1534
- 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);
1535
1590
  const output = {
1536
1591
  query: input.query,
1537
1592
  sources,
@@ -1539,28 +1594,51 @@ function createUnifiedSearch(deps = {}) {
1539
1594
  count: ranked.length,
1540
1595
  warnings
1541
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
+ }
1542
1630
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1543
1631
  output.reasoning = reasoningInfo;
1544
1632
  }
1545
1633
  return output;
1546
1634
  }
1635
+ async function searchUnifiedMemory(input) {
1636
+ return searchUnifiedMemoryWithRuntime(input);
1637
+ }
1547
1638
  async function searchRawMemorySemantically(input) {
1548
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1549
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1550
- const reasoningStrategy = resolveReasoningStrategy(input);
1551
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1552
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1553
- );
1554
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1555
- return mergeAcrossSources({
1556
- memorySubs: sub,
1557
- insightHits: [],
1558
- knowledgeHits: [],
1559
- limit,
1560
- strategy: mergeStrategy
1561
- });
1639
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1562
1640
  }
1563
- async function search(input) {
1641
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1564
1642
  const query = input.query.trim();
1565
1643
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1566
1644
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1592,19 +1670,24 @@ function createUnifiedSearch(deps = {}) {
1592
1670
  let unifiedHits = [];
1593
1671
  let sources = input.sources ? [...input.sources] : [];
1594
1672
  let unifiedReasoning;
1673
+ let retrievalDiagnostics;
1595
1674
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1596
1675
  const inferredSources = sources.length > 0 ? sources : [
1597
1676
  ...wantsRawTier ? ["memory"] : [],
1598
1677
  ...wantsInsightTier ? ["insights"] : [],
1599
1678
  ...wantsKnowledgeTier ? ["knowledge"] : []
1600
1679
  ];
1601
- const unified = await searchUnifiedMemory({
1602
- ...searchInputToUnified(input),
1603
- sources: inferredSources.length > 0 ? inferredSources : void 0
1604
- });
1680
+ const unified = await searchUnifiedMemoryWithRuntime(
1681
+ {
1682
+ ...searchInputToUnified(input),
1683
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1684
+ },
1685
+ runtimeContext
1686
+ );
1605
1687
  unifiedHits = unified.results;
1606
1688
  sources = unified.sources;
1607
1689
  unifiedReasoning = unified.reasoning;
1690
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1608
1691
  warnings.push(...unified.warnings);
1609
1692
  }
1610
1693
  let summaryHits = [];
@@ -1615,7 +1698,8 @@ function createUnifiedSearch(deps = {}) {
1615
1698
  limit,
1616
1699
  threshold,
1617
1700
  logger,
1618
- peerScope.peers
1701
+ peerScope.peers,
1702
+ runtimeContext
1619
1703
  );
1620
1704
  warnings.push(...summaryBucket.warnings);
1621
1705
  summaryHits = summaryBucket.hits;
@@ -1640,7 +1724,8 @@ function createUnifiedSearch(deps = {}) {
1640
1724
  source: mapHitToEvidenceSource(hit),
1641
1725
  snippet: hit.content,
1642
1726
  score: hit.similarity,
1643
- timestamp: getCandidateTimestamp(hit.metadata)
1727
+ timestamp: getCandidateTimestamp(hit.metadata),
1728
+ metadata: hit.metadata
1644
1729
  }));
1645
1730
  const base = {
1646
1731
  query,
@@ -1653,12 +1738,15 @@ function createUnifiedSearch(deps = {}) {
1653
1738
  if (unifiedReasoning) {
1654
1739
  base.reasoning = unifiedReasoning;
1655
1740
  }
1741
+ if (retrievalDiagnostics) {
1742
+ base.retrievalDiagnostics = retrievalDiagnostics;
1743
+ }
1656
1744
  if (!wantsSynthesis) {
1657
1745
  return base;
1658
1746
  }
1659
1747
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1660
1748
  query,
1661
- hits,
1749
+ evidence,
1662
1750
  responseSchema: synthesisSchema,
1663
1751
  deps,
1664
1752
  logger
@@ -1667,6 +1755,10 @@ function createUnifiedSearch(deps = {}) {
1667
1755
  base.warnings = [...base.warnings, ...synthWarnings];
1668
1756
  return base;
1669
1757
  }
1758
+ async function search(input, runtimeContext) {
1759
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1760
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1761
+ }
1670
1762
  return {
1671
1763
  search,
1672
1764
  searchUnifiedMemory,
@@ -1682,6 +1774,7 @@ function mergeAcrossSources(input) {
1682
1774
  for (const hit of [
1683
1775
  ...memorySubs.semantic,
1684
1776
  ...memorySubs.lexical,
1777
+ ...memorySubs.hybrid ?? [],
1685
1778
  ...input.insightHits,
1686
1779
  ...input.knowledgeHits
1687
1780
  ]) {
@@ -1697,6 +1790,7 @@ function mergeAcrossSources(input) {
1697
1790
  const all = [
1698
1791
  ...memorySubs.semantic,
1699
1792
  ...memorySubs.lexical,
1793
+ ...memorySubs.hybrid ?? [],
1700
1794
  ...memorySubs.entity ?? [],
1701
1795
  ...input.insightHits,
1702
1796
  ...input.knowledgeHits
@@ -1708,6 +1802,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1708
1802
  const lists = [];
1709
1803
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1710
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
+ }
1711
1808
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1712
1809
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1713
1810
  }
@@ -2172,8 +2269,250 @@ function createVsaRecall(storage) {
2172
2269
  };
2173
2270
  }
2174
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
+
2175
2515
  // src/http.ts
2176
- init_chroma_memory_index();
2177
2516
  init_raw_message_store();
2178
2517
  init_sqlite_raw_message_store();
2179
2518
  async function startHttpServer(options = {}) {
@@ -2203,7 +2542,18 @@ async function startHttpServer(options = {}) {
2203
2542
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2204
2543
  const vsa = createVsaRecall(vsaStorage);
2205
2544
  const app = new Hono();
2206
- 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
+ });
2207
2557
  app.post("/v1/search", async (c) => {
2208
2558
  const body = await c.req.json().catch(() => ({}));
2209
2559
  const userId = typeof body.userId === "string" ? body.userId : null;
@@ -2227,6 +2577,8 @@ async function startHttpServer(options = {}) {
2227
2577
  dateFrom: typeof body.dateFrom === "string" ? body.dateFrom : void 0,
2228
2578
  dateTo: typeof body.dateTo === "string" ? body.dateTo : void 0,
2229
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,
2230
2582
  ...wantsSynthesis ? {
2231
2583
  synthesize: {
2232
2584
  ...responseSchema ? { responseSchema } : {}
@@ -2330,57 +2682,20 @@ async function startHttpServer(options = {}) {
2330
2682
  ...m,
2331
2683
  userId: m.userId ?? body.userId
2332
2684
  }));
2333
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2334
- incoming,
2335
- body.embedOnInsert,
2336
- options.unified
2337
- );
2338
- let result;
2339
- if (typeof manager3.upsertRawMessages === "function") {
2340
- result = await manager3.upsertRawMessages({
2341
- userId: body.userId,
2342
- messages
2343
- });
2344
- } else if (typeof manager3.storeMessages === "function") {
2345
- const ids = await manager3.storeMessages(messages);
2346
- result = { inserted: ids.length, ids };
2347
- } else {
2348
- return c.json(
2349
- {
2350
- error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
2351
- },
2352
- 500
2353
- );
2354
- }
2355
- try {
2356
- await upsertRawMessagesToChroma(messages);
2357
- } catch (error) {
2358
- console.warn("[memory-store/http] chroma upsert failed:", error);
2359
- }
2360
- try {
2361
- const messagesWithEmbeddings = messages.filter(
2362
- (m) => Array.isArray(m.embedding) && m.embedding.length > 0
2363
- );
2364
- if (messagesWithEmbeddings.length > 0) {
2365
- for (const message of messagesWithEmbeddings) {
2366
- if (typeof manager3.upsertVectorForMessage === "function") {
2367
- manager3.upsertVectorForMessage(message.messageId, message.embedding);
2368
- }
2369
- }
2370
- console.log(
2371
- "[memory-store/http] Updated sqlite-vec vector table for",
2372
- messagesWithEmbeddings.length,
2373
- "message(s)"
2374
- );
2375
- }
2376
- } catch (error) {
2377
- console.warn("[memory-store/http] sqlite-vec vector update failed:", error);
2378
- }
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 };
2379
2694
  return c.json({
2380
2695
  ok: true,
2381
- count: messages.length,
2696
+ count: persisted.count,
2382
2697
  result,
2383
- ...warnings.length > 0 ? { warnings } : {}
2698
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2384
2699
  });
2385
2700
  });
2386
2701
  app.get("/v1/raw-messages/:id", async (c) => {
@@ -2479,7 +2794,17 @@ async function startHttpServer(options = {}) {
2479
2794
  });
2480
2795
  const okfHttpSpecifier = "@melandlabs/okf/http";
2481
2796
  const { registerOkfRoutes } = await import(okfHttpSpecifier);
2482
- 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
+ });
2483
2808
  const server = serve({ fetch: app.fetch, port, hostname: host });
2484
2809
  return {
2485
2810
  url: `http://${host}:${port}`,
@@ -2831,6 +3156,121 @@ function createUserVoiceRewriter(options) {
2831
3156
  };
2832
3157
  }
2833
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
+
2834
3274
  // src/server/cli-shared.ts
2835
3275
  init_raw_message_store();
2836
3276
 
@@ -2845,7 +3285,7 @@ function isInsightSQLiteVecEnabled(_env) {
2845
3285
  }
2846
3286
  async function getInsightSQLiteVecStore(_env) {
2847
3287
  const dbPath = resolveSQLiteRawMessageDbPath();
2848
- const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
3288
+ const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
2849
3289
  return await getSQLiteVecStore(dbPath, void 0, {
2850
3290
  collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
2851
3291
  });
@@ -2869,17 +3309,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
2869
3309
  // src/server/cli-shared.ts
2870
3310
  async function loadAiRag() {
2871
3311
  try {
2872
- const [local, chroma] = await Promise.all([
3312
+ const [local, reranker, chroma] = await Promise.all([
2873
3313
  import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
3314
+ import('@melandlabs/ai-rag/local-transformers-reranker'),
2874
3315
  import('@melandlabs/ai-rag/chroma-store')
2875
3316
  ]);
2876
3317
  return {
2877
3318
  LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
3319
+ LocalTransformersReranker: reranker.LocalTransformersReranker,
2878
3320
  ChromaVectorStore: chroma.ChromaVectorStore
2879
3321
  };
2880
3322
  } catch (error) {
2881
3323
  throw new Error(
2882
- `--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})`
2883
3325
  );
2884
3326
  }
2885
3327
  }
@@ -2888,8 +3330,21 @@ function parseUnifiedArgs(argv) {
2888
3330
  const args = {
2889
3331
  embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
2890
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,
2891
3339
  chromaUrl: env.CHROMA_URL,
2892
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,
2893
3348
  insightsBackend: env.INSIGHTS_BACKEND ?? "none",
2894
3349
  insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
2895
3350
  knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
@@ -2914,12 +3369,51 @@ function parseUnifiedArgs(argv) {
2914
3369
  case "--embedding-model":
2915
3370
  args.embeddingModel = takeValue();
2916
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;
2917
3390
  case "--chroma-url":
2918
3391
  args.chromaUrl = takeValue();
2919
3392
  break;
2920
3393
  case "--memory-backend":
2921
3394
  args.memoryBackend = takeValue();
2922
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;
2923
3417
  case "--insights-backend":
2924
3418
  args.insightsBackend = takeValue();
2925
3419
  break;
@@ -2949,31 +3443,60 @@ function parseUnifiedArgs(argv) {
2949
3443
  break;
2950
3444
  }
2951
3445
  }
2952
- for (const [name, value] of [
2953
- ["--embedding-provider", args.embeddingProvider],
2954
- ["--memory-backend", args.memoryBackend],
2955
- ["--insights-backend", args.insightsBackend],
2956
- ["--knowledge-backend", args.knowledgeBackend]
2957
- ]) {
2958
- const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
3446
+ const validate = (name, value, allowed) => {
2959
3447
  if (!allowed.includes(value)) {
2960
3448
  throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
2961
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
+ }
2962
3466
  }
2963
3467
  return args;
2964
3468
  }
2965
3469
  async function buildUnified(args) {
2966
3470
  const unified = {};
2967
3471
  const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
2968
- 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";
2969
3473
  const aiRag = wantsAiRag ? await loadAiRag() : null;
2970
3474
  if (args.embeddingProvider === "local") {
2971
3475
  try {
2972
3476
  if (!aiRag) {
2973
3477
  throw new Error("ai-rag modules not loaded");
2974
3478
  }
2975
- const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
2976
- 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
+ };
2977
3500
  log(
2978
3501
  `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
2979
3502
  );
@@ -2989,6 +3512,7 @@ async function buildUnified(args) {
2989
3512
  }
2990
3513
  const model = args.embeddingModel ?? "text-embedding-3-small";
2991
3514
  const baseURL = "https://openrouter.ai/api/v1";
3515
+ unified.embeddingInfo = { provider: "openrouter", model };
2992
3516
  unified.embedQuery = async ({ query }) => {
2993
3517
  const res = await fetch(`${baseURL}/embeddings`, {
2994
3518
  method: "POST",
@@ -3006,49 +3530,150 @@ async function buildUnified(args) {
3006
3530
  const body = await res.json();
3007
3531
  const embedding = body.data?.[0]?.embedding;
3008
3532
  if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
3533
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
3009
3534
  return embedding;
3010
3535
  };
3011
3536
  log(`embedQuery wired via openrouter (model=${model})`);
3012
3537
  }
3013
- if (args.memoryBackend === "sqlite-vec") {
3014
- const store = createRawMessageStore({ env: void 0 });
3015
- const manager2 = await store.getManager();
3016
- 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") {
3017
3589
  throw new Error(
3018
- "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`
3019
3591
  );
3020
3592
  }
3021
- unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
3022
- const rows = await manager2.searchMessagesSemantically({
3023
- userId,
3024
- queryEmbedding,
3025
- limit,
3026
- threshold,
3027
- 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
3028
3623
  });
3029
- return rows.map((r) => ({
3030
- id: r.id,
3031
- content: r.content,
3032
- similarity: r.similarity,
3033
- metadata: r.metadata ?? {}
3034
- }));
3035
- };
3036
- log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
3037
- } else if (args.memoryBackend === "chroma") {
3038
- if (!args.chromaUrl)
3039
- throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3040
- if (!aiRag) {
3041
- throw new Error("ai-rag modules not loaded");
3042
3624
  }
3043
- const store = new aiRag.ChromaVectorStore({
3044
- url: args.chromaUrl,
3045
- collectionName: "opencontext_raw_messages"
3046
- });
3047
- unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
3048
- const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
3049
- 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
+ };
3050
3675
  };
3051
- 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`);
3052
3677
  }
3053
3678
  if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
3054
3679
  if (!unified.embedQuery) {
@@ -3175,20 +3800,41 @@ function printUnifiedHelp() {
3175
3800
  --embedding-model <name> Model name
3176
3801
  (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
3177
3802
  openrouter \u2192 text-embedding-3-small)
3803
+ --embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
3178
3804
  Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
3179
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
+
3180
3818
  Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
3181
3819
  --chroma-url <url> Chroma server URL
3182
3820
  (env: CHROMA_URL; required when any *-backend=chroma)
3183
- --memory-backend <name> sqlite-vec | chroma | none
3821
+ --memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
3184
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)
3185
3830
  --insights-backend <name> sqlite-vec | chroma | none
3186
3831
  (env: INSIGHTS_BACKEND, default: none)
3187
3832
  --insights-collection <name> Chroma collection (default: opencontext_insights)
3188
3833
  --knowledge-backend <name> chroma | none
3189
3834
  (env: KNOWLEDGE_BACKEND, default: none)
3190
3835
  --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
3191
- 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.
3192
3838
 
3193
3839
  Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
3194
3840
  --reasoning Enable the LLM reasoning layer so /v1/search and