@melandlabs/memory-store 1.2.5 → 1.3.1

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;
301
+ }
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;
373
317
  }
374
- return { messages, warnings: [] };
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) {
@@ -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,7 +1130,7 @@ 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 = []) {
1133
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1161
1134
  if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1162
1135
  warnings.push({
1163
1136
  source: "memory",
@@ -1190,7 +1163,9 @@ function createUnifiedSearch(deps = {}) {
1190
1163
  keywords2,
1191
1164
  limit,
1192
1165
  logger,
1193
- peerPeers
1166
+ peerPeers,
1167
+ warnings,
1168
+ runtimeContext
1194
1169
  );
1195
1170
  let semanticHits = [];
1196
1171
  if (typeof deps.embedQuery === "function") {
@@ -1204,7 +1179,9 @@ function createUnifiedSearch(deps = {}) {
1204
1179
  limit,
1205
1180
  threshold,
1206
1181
  logger,
1207
- peerPeers
1182
+ peerPeers,
1183
+ warnings,
1184
+ runtimeContext
1208
1185
  );
1209
1186
  } catch (error) {
1210
1187
  logger.warn?.(
@@ -1245,6 +1222,11 @@ function createUnifiedSearch(deps = {}) {
1245
1222
  unionEvidence = evidence;
1246
1223
  }
1247
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
+ });
1248
1230
  warnings.push({
1249
1231
  source: "memory",
1250
1232
  code: "memory_lexical_search_fallback",
@@ -1252,7 +1234,16 @@ function createUnifiedSearch(deps = {}) {
1252
1234
  });
1253
1235
  const keywords2 = deriveLexicalKeywords(input.query);
1254
1236
  const lexical2 = filterByDateRange(
1255
- 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
+ ),
1256
1247
  input.dateFrom,
1257
1248
  input.dateTo
1258
1249
  );
@@ -1260,6 +1251,26 @@ function createUnifiedSearch(deps = {}) {
1260
1251
  }
1261
1252
  let semantic = [];
1262
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
+ }
1263
1274
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1264
1275
  try {
1265
1276
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1278,7 +1289,17 @@ function createUnifiedSearch(deps = {}) {
1278
1289
  );
1279
1290
  const lists = await Promise.all(
1280
1291
  embeddings.map(
1281
- (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
+ )
1282
1303
  )
1283
1304
  );
1284
1305
  semantic = mergeByMaxScore(lists);
@@ -1301,7 +1322,9 @@ function createUnifiedSearch(deps = {}) {
1301
1322
  limit,
1302
1323
  threshold,
1303
1324
  logger,
1304
- peerPeers
1325
+ peerPeers,
1326
+ warnings,
1327
+ runtimeContext
1305
1328
  );
1306
1329
  }
1307
1330
  let lexical = [];
@@ -1320,7 +1343,8 @@ function createUnifiedSearch(deps = {}) {
1320
1343
  limit,
1321
1344
  botId,
1322
1345
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1323
- ...factTypes ? { factTypes } : {}
1346
+ ...factTypes ? { factTypes } : {},
1347
+ ...runtimeContext ?? {}
1324
1348
  })
1325
1349
  )
1326
1350
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1346,7 +1370,7 @@ function createUnifiedSearch(deps = {}) {
1346
1370
  let entity;
1347
1371
  if (typeof deps.entitySearch === "function") {
1348
1372
  entity = filterByDateRange(
1349
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1373
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1350
1374
  input.dateFrom,
1351
1375
  input.dateTo
1352
1376
  );
@@ -1408,14 +1432,16 @@ function createUnifiedSearch(deps = {}) {
1408
1432
  }
1409
1433
  return out;
1410
1434
  }
1411
- async function searchUnifiedMemory(input) {
1435
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1412
1436
  const query = input.query.trim();
1413
1437
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1414
1438
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1415
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1416
1439
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1417
1440
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1418
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;
1419
1445
  const reasoningStrategy = resolveReasoningStrategy(input);
1420
1446
  const warnings = [];
1421
1447
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1435,23 +1461,46 @@ function createUnifiedSearch(deps = {}) {
1435
1461
  };
1436
1462
  }
1437
1463
  let memorySubs;
1464
+ let retrievalStatus;
1438
1465
  const reasoningInfo = { strategy: reasoningStrategy };
1439
1466
  if (input.dateFrom || input.dateTo) {
1440
1467
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1441
1468
  }
1442
1469
  if (sources.includes("memory")) {
1443
- 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";
1444
1471
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1445
1472
  try {
1446
1473
  memorySubs = await runMemorySource(
1447
1474
  input,
1448
- limit,
1449
- threshold,
1475
+ candidateLimit,
1476
+ memoryThreshold,
1450
1477
  warnings,
1451
1478
  reasoningStrategy,
1452
1479
  reasoningInfo,
1453
- peerPeers
1480
+ peerPeers,
1481
+ runtimeContext
1454
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
+ }
1455
1504
  } catch (error) {
1456
1505
  logger.warn?.("[memory-store] memory source failed:", error);
1457
1506
  warnings.push({
@@ -1474,12 +1523,13 @@ function createUnifiedSearch(deps = {}) {
1474
1523
  const hits = await deps.searchInsights({
1475
1524
  userId: input.userId,
1476
1525
  query: input.query,
1477
- limit,
1526
+ limit: candidateLimit,
1478
1527
  threshold,
1479
1528
  botIds: input.botIds,
1480
1529
  includeArchived: input.includeArchivedInsights,
1481
1530
  authToken: input.authToken,
1482
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1531
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1532
+ ...runtimeContext ?? {}
1483
1533
  });
1484
1534
  for (const hit of hits) {
1485
1535
  insightHits.push({
@@ -1506,12 +1556,13 @@ function createUnifiedSearch(deps = {}) {
1506
1556
  userId: input.userId,
1507
1557
  query: input.query,
1508
1558
  options: {
1509
- limit,
1559
+ limit: candidateLimit,
1510
1560
  threshold,
1511
1561
  documentIds: input.documentIds
1512
1562
  },
1513
1563
  authToken: input.authToken,
1514
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1564
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1565
+ ...runtimeContext ?? {}
1515
1566
  });
1516
1567
  for (const hit of hits) {
1517
1568
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1529,10 +1580,14 @@ function createUnifiedSearch(deps = {}) {
1529
1580
  memorySubs,
1530
1581
  insightHits,
1531
1582
  knowledgeHits,
1532
- limit,
1583
+ limit: candidateLimit,
1533
1584
  strategy: mergeStrategy
1534
1585
  });
1535
- 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);
1536
1591
  const output = {
1537
1592
  query: input.query,
1538
1593
  sources,
@@ -1540,28 +1595,51 @@ function createUnifiedSearch(deps = {}) {
1540
1595
  count: ranked.length,
1541
1596
  warnings
1542
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
+ }
1543
1631
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1544
1632
  output.reasoning = reasoningInfo;
1545
1633
  }
1546
1634
  return output;
1547
1635
  }
1636
+ async function searchUnifiedMemory(input) {
1637
+ return searchUnifiedMemoryWithRuntime(input);
1638
+ }
1548
1639
  async function searchRawMemorySemantically(input) {
1549
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1550
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1551
- const reasoningStrategy = resolveReasoningStrategy(input);
1552
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1553
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1554
- );
1555
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1556
- return mergeAcrossSources({
1557
- memorySubs: sub,
1558
- insightHits: [],
1559
- knowledgeHits: [],
1560
- limit,
1561
- strategy: mergeStrategy
1562
- });
1640
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1563
1641
  }
1564
- async function search(input) {
1642
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1565
1643
  const query = input.query.trim();
1566
1644
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1567
1645
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1593,19 +1671,24 @@ function createUnifiedSearch(deps = {}) {
1593
1671
  let unifiedHits = [];
1594
1672
  let sources = input.sources ? [...input.sources] : [];
1595
1673
  let unifiedReasoning;
1674
+ let retrievalDiagnostics;
1596
1675
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1597
1676
  const inferredSources = sources.length > 0 ? sources : [
1598
1677
  ...wantsRawTier ? ["memory"] : [],
1599
1678
  ...wantsInsightTier ? ["insights"] : [],
1600
1679
  ...wantsKnowledgeTier ? ["knowledge"] : []
1601
1680
  ];
1602
- const unified = await searchUnifiedMemory({
1603
- ...searchInputToUnified(input),
1604
- sources: inferredSources.length > 0 ? inferredSources : void 0
1605
- });
1681
+ const unified = await searchUnifiedMemoryWithRuntime(
1682
+ {
1683
+ ...searchInputToUnified(input),
1684
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1685
+ },
1686
+ runtimeContext
1687
+ );
1606
1688
  unifiedHits = unified.results;
1607
1689
  sources = unified.sources;
1608
1690
  unifiedReasoning = unified.reasoning;
1691
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1609
1692
  warnings.push(...unified.warnings);
1610
1693
  }
1611
1694
  let summaryHits = [];
@@ -1616,7 +1699,8 @@ function createUnifiedSearch(deps = {}) {
1616
1699
  limit,
1617
1700
  threshold,
1618
1701
  logger,
1619
- peerScope.peers
1702
+ peerScope.peers,
1703
+ runtimeContext
1620
1704
  );
1621
1705
  warnings.push(...summaryBucket.warnings);
1622
1706
  summaryHits = summaryBucket.hits;
@@ -1641,7 +1725,8 @@ function createUnifiedSearch(deps = {}) {
1641
1725
  source: mapHitToEvidenceSource(hit),
1642
1726
  snippet: hit.content,
1643
1727
  score: hit.similarity,
1644
- timestamp: getCandidateTimestamp(hit.metadata)
1728
+ timestamp: getCandidateTimestamp(hit.metadata),
1729
+ metadata: hit.metadata
1645
1730
  }));
1646
1731
  const base = {
1647
1732
  query,
@@ -1654,12 +1739,15 @@ function createUnifiedSearch(deps = {}) {
1654
1739
  if (unifiedReasoning) {
1655
1740
  base.reasoning = unifiedReasoning;
1656
1741
  }
1742
+ if (retrievalDiagnostics) {
1743
+ base.retrievalDiagnostics = retrievalDiagnostics;
1744
+ }
1657
1745
  if (!wantsSynthesis) {
1658
1746
  return base;
1659
1747
  }
1660
1748
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1661
1749
  query,
1662
- hits,
1750
+ evidence,
1663
1751
  responseSchema: synthesisSchema,
1664
1752
  deps,
1665
1753
  logger
@@ -1668,6 +1756,10 @@ function createUnifiedSearch(deps = {}) {
1668
1756
  base.warnings = [...base.warnings, ...synthWarnings];
1669
1757
  return base;
1670
1758
  }
1759
+ async function search(input, runtimeContext) {
1760
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1761
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1762
+ }
1671
1763
  return {
1672
1764
  search,
1673
1765
  searchUnifiedMemory,
@@ -1683,6 +1775,7 @@ function mergeAcrossSources(input) {
1683
1775
  for (const hit of [
1684
1776
  ...memorySubs.semantic,
1685
1777
  ...memorySubs.lexical,
1778
+ ...memorySubs.hybrid ?? [],
1686
1779
  ...input.insightHits,
1687
1780
  ...input.knowledgeHits
1688
1781
  ]) {
@@ -1698,6 +1791,7 @@ function mergeAcrossSources(input) {
1698
1791
  const all = [
1699
1792
  ...memorySubs.semantic,
1700
1793
  ...memorySubs.lexical,
1794
+ ...memorySubs.hybrid ?? [],
1701
1795
  ...memorySubs.entity ?? [],
1702
1796
  ...input.insightHits,
1703
1797
  ...input.knowledgeHits
@@ -1709,6 +1803,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1709
1803
  const lists = [];
1710
1804
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1711
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
+ }
1712
1809
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1713
1810
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1714
1811
  }
@@ -2173,8 +2270,250 @@ function createVsaRecall(storage) {
2173
2270
  };
2174
2271
  }
2175
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
+
2176
2516
  // src/mcp.ts
2177
- init_chroma_memory_index();
2178
2517
  init_raw_message_store();
2179
2518
  init_sqlite_raw_message_store();
2180
2519
  var DEFAULT_NAME = "@melandlabs/memory-store";
@@ -2187,13 +2526,37 @@ async function startMcpServer(options = {}) {
2187
2526
  const rawStore = createRawMessageStore({
2188
2527
  env: options.env
2189
2528
  });
2190
- 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
+ });
2191
2543
  const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
2192
2544
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2193
2545
  const vsa = createVsaRecall(vsaStorage);
2194
- server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
2195
- content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
2196
- }));
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
+ });
2197
2560
  const searchSchema = {
2198
2561
  userId: z.string(),
2199
2562
  query: z.string().min(1),
@@ -2211,7 +2574,11 @@ async function startMcpServer(options = {}) {
2211
2574
  // coerces it into `synthesize.responseSchema` when present.
2212
2575
  responseSchema: z.record(z.string(), z.unknown()).optional(),
2213
2576
  limit: z.number().int().min(1).max(50).default(10),
2214
- 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(),
2215
2582
  botIds: z.array(z.string()).optional(),
2216
2583
  documentIds: z.array(z.string()).optional(),
2217
2584
  includeArchivedInsights: z.boolean().default(false),
@@ -2275,6 +2642,8 @@ async function startMcpServer(options = {}) {
2275
2642
  sources: a.sources,
2276
2643
  limit: a.limit,
2277
2644
  threshold: a.threshold,
2645
+ mergeStrategy: a.mergeStrategy,
2646
+ includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
2278
2647
  botIds: a.botIds,
2279
2648
  documentIds: a.documentIds,
2280
2649
  authToken: a.authToken,
@@ -2305,37 +2674,24 @@ async function startMcpServer(options = {}) {
2305
2674
  const a = args;
2306
2675
  const manager2 = await rawStore.getManager();
2307
2676
  const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
2308
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2309
- incoming,
2310
- a.embedOnInsert,
2311
- options.unified
2312
- );
2313
- let result;
2314
- if (typeof manager2.upsertRawMessages === "function") {
2315
- result = await manager2.upsertRawMessages({
2316
- userId: a.userId,
2317
- messages
2318
- });
2319
- } else if (typeof manager2.storeMessages === "function") {
2320
- const ids = await manager2.storeMessages(messages);
2321
- result = { inserted: ids.length, ids };
2322
- } else {
2323
- throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
2324
- }
2325
- try {
2326
- await upsertRawMessagesToChroma(messages);
2327
- } catch (error) {
2328
- console.warn("[memory-store/mcp] chroma upsert failed:", error);
2329
- }
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 };
2330
2686
  return {
2331
2687
  content: [
2332
2688
  {
2333
2689
  type: "text",
2334
2690
  text: JSON.stringify({
2335
2691
  ok: true,
2336
- count: messages.length,
2692
+ count: persisted.count,
2337
2693
  result,
2338
- ...warnings.length > 0 ? { warnings } : {}
2694
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2339
2695
  })
2340
2696
  }
2341
2697
  ]
@@ -2675,7 +3031,17 @@ async function startMcpServer(options = {}) {
2675
3031
  );
2676
3032
  const okfMcpSpecifier = "@melandlabs/okf/mcp";
2677
3033
  const { registerOkfTools } = await import(okfMcpSpecifier);
2678
- 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
+ });
2679
3045
  const transport = new StdioServerTransport();
2680
3046
  await server.connect(transport);
2681
3047
  return server;
@@ -3020,6 +3386,121 @@ function createUserVoiceRewriter(options) {
3020
3386
  };
3021
3387
  }
3022
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
+
3023
3504
  // src/server/cli-shared.ts
3024
3505
  init_raw_message_store();
3025
3506
 
@@ -3034,7 +3515,7 @@ function isInsightSQLiteVecEnabled(_env) {
3034
3515
  }
3035
3516
  async function getInsightSQLiteVecStore(_env) {
3036
3517
  const dbPath = resolveSQLiteRawMessageDbPath();
3037
- const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
3518
+ const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
3038
3519
  return await getSQLiteVecStore(dbPath, void 0, {
3039
3520
  collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
3040
3521
  });
@@ -3058,17 +3539,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
3058
3539
  // src/server/cli-shared.ts
3059
3540
  async function loadAiRag() {
3060
3541
  try {
3061
- const [local, chroma] = await Promise.all([
3542
+ const [local, reranker, chroma] = await Promise.all([
3062
3543
  import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
3544
+ import('@melandlabs/ai-rag/local-transformers-reranker'),
3063
3545
  import('@melandlabs/ai-rag/chroma-store')
3064
3546
  ]);
3065
3547
  return {
3066
3548
  LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
3549
+ LocalTransformersReranker: reranker.LocalTransformersReranker,
3067
3550
  ChromaVectorStore: chroma.ChromaVectorStore
3068
3551
  };
3069
3552
  } catch (error) {
3070
3553
  throw new Error(
3071
- `--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})`
3072
3555
  );
3073
3556
  }
3074
3557
  }
@@ -3077,8 +3560,21 @@ function parseUnifiedArgs(argv) {
3077
3560
  const args = {
3078
3561
  embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
3079
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,
3080
3569
  chromaUrl: env.CHROMA_URL,
3081
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,
3082
3578
  insightsBackend: env.INSIGHTS_BACKEND ?? "none",
3083
3579
  insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
3084
3580
  knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
@@ -3103,12 +3599,51 @@ function parseUnifiedArgs(argv) {
3103
3599
  case "--embedding-model":
3104
3600
  args.embeddingModel = takeValue();
3105
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;
3106
3620
  case "--chroma-url":
3107
3621
  args.chromaUrl = takeValue();
3108
3622
  break;
3109
3623
  case "--memory-backend":
3110
3624
  args.memoryBackend = takeValue();
3111
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;
3112
3647
  case "--insights-backend":
3113
3648
  args.insightsBackend = takeValue();
3114
3649
  break;
@@ -3138,31 +3673,60 @@ function parseUnifiedArgs(argv) {
3138
3673
  break;
3139
3674
  }
3140
3675
  }
3141
- for (const [name, value] of [
3142
- ["--embedding-provider", args.embeddingProvider],
3143
- ["--memory-backend", args.memoryBackend],
3144
- ["--insights-backend", args.insightsBackend],
3145
- ["--knowledge-backend", args.knowledgeBackend]
3146
- ]) {
3147
- const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
3676
+ const validate = (name, value, allowed) => {
3148
3677
  if (!allowed.includes(value)) {
3149
3678
  throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
3150
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
+ }
3151
3696
  }
3152
3697
  return args;
3153
3698
  }
3154
3699
  async function buildUnified(args) {
3155
3700
  const unified = {};
3156
3701
  const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
3157
- 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";
3158
3703
  const aiRag = wantsAiRag ? await loadAiRag() : null;
3159
3704
  if (args.embeddingProvider === "local") {
3160
3705
  try {
3161
3706
  if (!aiRag) {
3162
3707
  throw new Error("ai-rag modules not loaded");
3163
3708
  }
3164
- const provider = new aiRag.LocalTransformersEmbeddingProvider({ modelName: args.embeddingModel });
3165
- 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
+ };
3166
3730
  log(
3167
3731
  `embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
3168
3732
  );
@@ -3178,6 +3742,7 @@ async function buildUnified(args) {
3178
3742
  }
3179
3743
  const model = args.embeddingModel ?? "text-embedding-3-small";
3180
3744
  const baseURL = "https://openrouter.ai/api/v1";
3745
+ unified.embeddingInfo = { provider: "openrouter", model };
3181
3746
  unified.embedQuery = async ({ query }) => {
3182
3747
  const res = await fetch(`${baseURL}/embeddings`, {
3183
3748
  method: "POST",
@@ -3195,49 +3760,150 @@ async function buildUnified(args) {
3195
3760
  const body = await res.json();
3196
3761
  const embedding = body.data?.[0]?.embedding;
3197
3762
  if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
3763
+ if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
3198
3764
  return embedding;
3199
3765
  };
3200
3766
  log(`embedQuery wired via openrouter (model=${model})`);
3201
3767
  }
3202
- if (args.memoryBackend === "sqlite-vec") {
3203
- const store = createRawMessageStore({ env: void 0 });
3204
- const manager2 = await store.getManager();
3205
- 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") {
3206
3819
  throw new Error(
3207
- "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`
3208
3821
  );
3209
3822
  }
3210
- unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
3211
- const rows = await manager2.searchMessagesSemantically({
3212
- userId,
3213
- queryEmbedding,
3214
- limit,
3215
- threshold,
3216
- 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
3217
3853
  });
3218
- return rows.map((r) => ({
3219
- id: r.id,
3220
- content: r.content,
3221
- similarity: r.similarity,
3222
- metadata: r.metadata ?? {}
3223
- }));
3224
- };
3225
- log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
3226
- } else if (args.memoryBackend === "chroma") {
3227
- if (!args.chromaUrl)
3228
- throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
3229
- if (!aiRag) {
3230
- throw new Error("ai-rag modules not loaded");
3231
3854
  }
3232
- const store = new aiRag.ChromaVectorStore({
3233
- url: args.chromaUrl,
3234
- collectionName: "opencontext_raw_messages"
3235
- });
3236
- unified.searchRawMessagesAnn = async ({ queryEmbedding, limit, threshold }) => {
3237
- const results = await store.similaritySearchWithOptions(queryEmbedding, { limit: limit + 1 });
3238
- 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
+ };
3239
3905
  };
3240
- 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`);
3241
3907
  }
3242
3908
  if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
3243
3909
  if (!unified.embedQuery) {
@@ -3364,20 +4030,41 @@ function printUnifiedHelp() {
3364
4030
  --embedding-model <name> Model name
3365
4031
  (env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
3366
4032
  openrouter \u2192 text-embedding-3-small)
4033
+ --embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
3367
4034
  Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
3368
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
+
3369
4048
  Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
3370
4049
  --chroma-url <url> Chroma server URL
3371
4050
  (env: CHROMA_URL; required when any *-backend=chroma)
3372
- --memory-backend <name> sqlite-vec | chroma | none
4051
+ --memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
3373
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)
3374
4060
  --insights-backend <name> sqlite-vec | chroma | none
3375
4061
  (env: INSIGHTS_BACKEND, default: none)
3376
4062
  --insights-collection <name> Chroma collection (default: opencontext_insights)
3377
4063
  --knowledge-backend <name> chroma | none
3378
4064
  (env: KNOWLEDGE_BACKEND, default: none)
3379
4065
  --knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
3380
- 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.
3381
4068
 
3382
4069
  Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
3383
4070
  --reasoning Enable the LLM reasoning layer so /v1/search and