@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.
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isFactType, vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
2
- import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
2
+ import '@melandlabs/ai/memory';
3
3
  import { ChromaVectorStore } from '@melandlabs/rag';
4
4
  import { mkdirSync } from 'fs';
5
5
  import { dirname } from 'path';
@@ -12,6 +12,7 @@ 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';
15
16
 
16
17
  var __defProp = Object.defineProperty;
17
18
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -22,27 +23,6 @@ var __export = (target, all) => {
22
23
  for (var name in all)
23
24
  __defProp(target, name, { get: all[name], enumerable: true });
24
25
  };
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
26
  var init_contracts = __esm({
47
27
  "src/contracts.ts"() {
48
28
  }
@@ -68,61 +48,9 @@ function getChromaStore(collectionName) {
68
48
  collectionName
69
49
  });
70
50
  }
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
51
  function toBooleanValue(value) {
81
52
  return value === true || value === "true" || value === 1 || value === "1";
82
53
  }
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
54
  async function searchRawMessagesWithChroma(input) {
127
55
  if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
128
56
  return [];
@@ -324,59 +252,73 @@ var init_raw_message_store = __esm({
324
252
  }
325
253
  });
326
254
 
327
- // src/embed-on-insert.ts
328
- async function embedMissingMessages(messages, deps) {
329
- if (typeof deps.embedQuery !== "function") {
330
- return messages;
255
+ // src/search/unified-search.ts
256
+ init_chroma_memory_index();
257
+ init_raw_message_store();
258
+
259
+ // src/search/applicability.ts
260
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
261
+ "global",
262
+ "task",
263
+ "conversation",
264
+ "channel",
265
+ "project",
266
+ "custom"
267
+ ]);
268
+ function isRecord(value) {
269
+ return typeof value === "object" && value !== null && !Array.isArray(value);
270
+ }
271
+ function invalidRuntimeContext(message) {
272
+ throw new TypeError(`memory-store search runtime context: ${message}`);
273
+ }
274
+ function validateApplicabilityContext(value, index) {
275
+ const path = `applicabilityContexts[${index}]`;
276
+ if (!isRecord(value)) {
277
+ invalidRuntimeContext(`${path} must be an object`);
331
278
  }
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;
279
+ const scope = value.scope;
280
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
281
+ invalidRuntimeContext(`${path}.scope is not supported`);
282
+ }
283
+ if (value.key !== void 0 && typeof value.key !== "string") {
284
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
285
+ }
286
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
287
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
288
+ }
289
+ for (const field of ["validFrom", "validUntil"]) {
290
+ const boundary = value[field];
291
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
292
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
341
293
  }
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
294
  }
354
- return out;
355
295
  }
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
- };
296
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
297
+ if (runtimeContext === void 0) {
298
+ return void 0;
372
299
  }
373
- return { messages, warnings: [] };
300
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
301
+ invalidRuntimeContext("applicabilityContexts must be an array");
302
+ }
303
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
304
+ validateApplicabilityContext(context, index);
305
+ }
306
+ let applicabilityAt;
307
+ if (input.asOf === void 0) {
308
+ applicabilityAt = now();
309
+ } else {
310
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
311
+ if (!Number.isFinite(parsed)) {
312
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
313
+ }
314
+ applicabilityAt = parsed;
315
+ }
316
+ return {
317
+ applicabilityContexts: runtimeContext.applicabilityContexts,
318
+ applicabilityAt
319
+ };
374
320
  }
375
321
 
376
- // src/search/unified-search.ts
377
- init_chroma_memory_index();
378
- init_raw_message_store();
379
-
380
322
  // src/search/reranker.ts
381
323
  async function applyReranker(reranker, query, results) {
382
324
  if (!reranker || results.length === 0) {
@@ -570,6 +512,7 @@ function materializeSignals(lists, hit) {
570
512
  function listNameToChannel(name) {
571
513
  if (name === "memory-semantic") return "semantic";
572
514
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
515
+ if (name === "memory-hybrid") return "hybrid";
573
516
  if (name === "memory-entity") return "entity";
574
517
  return void 0;
575
518
  }
@@ -627,7 +570,6 @@ async function resolveScopePeer(input) {
627
570
 
628
571
  // src/search/gather-evidence.ts
629
572
  var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
630
- var MAX_EVIDENCE_CHARS = 600;
631
573
  function toSummaryResult(row) {
632
574
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
633
575
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -645,7 +587,7 @@ function toSummaryResult(row) {
645
587
  }
646
588
  };
647
589
  }
648
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
590
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
649
591
  if (typeof deps.searchSummaries !== "function") {
650
592
  return { hits: [], warnings: [] };
651
593
  }
@@ -658,7 +600,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
658
600
  limit,
659
601
  threshold,
660
602
  authToken: input.authToken,
661
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
603
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
604
+ ...runtimeContext ?? {}
662
605
  });
663
606
  const hits = rows.map(toSummaryResult);
664
607
  return { hits, warnings: [] };
@@ -676,12 +619,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
676
619
  };
677
620
  }
678
621
  }
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
622
  function buildSynthesisPrompt(input) {
686
623
  const grouped = /* @__PURE__ */ new Map();
687
624
  for (const item of input.evidence) {
@@ -695,9 +632,10 @@ function buildSynthesisPrompt(input) {
695
632
  if (items.length === 0) {
696
633
  continue;
697
634
  }
698
- const lines = items.map(
699
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
700
- );
635
+ const lines = items.map((item, index) => {
636
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
637
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
638
+ });
701
639
  sections.push(`## ${tier}
702
640
  ${lines.join("\n")}`);
703
641
  }
@@ -779,12 +717,15 @@ function coerceAnswer(text, schema) {
779
717
  return text;
780
718
  }
781
719
  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
720
+ const { query, responseSchema, deps, logger } = input;
721
+ const evidence = input.evidence.map((item) => ({
722
+ id: item.id,
723
+ source: mapEvidenceSourceToTier(item.source),
724
+ snippet: item.snippet,
725
+ score: item.score,
726
+ // Forward per-fact provenance (e.g. `metadata.source` from
727
+ // `opencontext add --source`) so the synthesis prompt can cite it.
728
+ metadata: item.metadata
788
729
  }));
789
730
  const warnings = [];
790
731
  const complete = deps.reasoning?.complete;
@@ -815,21 +756,10 @@ async function synthesizeAnswer(input) {
815
756
  return { answer: "", warnings };
816
757
  }
817
758
  }
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;
759
+ function mapEvidenceSourceToTier(source) {
760
+ if (source === "memory") return "raw";
761
+ if (source === "insights") return "insight";
762
+ return source;
833
763
  }
834
764
  async function resolveSearchScopePeers(input) {
835
765
  return resolveScopePeer({
@@ -853,7 +783,7 @@ function deriveEntityKeywords(query) {
853
783
  }
854
784
  return out.slice(0, 16);
855
785
  }
856
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
786
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
857
787
  if (typeof deps.entitySearch !== "function") {
858
788
  return [];
859
789
  }
@@ -871,7 +801,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
871
801
  keywords,
872
802
  limit: Math.ceil(limit / filters.length),
873
803
  botId,
874
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
804
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
805
+ ...runtimeContext ?? {}
875
806
  })
876
807
  )
877
808
  )).flat();
@@ -959,6 +890,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
959
890
  return true;
960
891
  });
961
892
  }
893
+ function warnApplicabilityNotEnforced(warnings) {
894
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
895
+ return;
896
+ }
897
+ warnings.push({
898
+ source: "memory",
899
+ code: "memory_applicability_not_enforced",
900
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
901
+ });
902
+ }
962
903
  async function embedQueryVariant(embedQuery, input, query) {
963
904
  return embedQuery({
964
905
  userId: input.userId,
@@ -966,10 +907,29 @@ async function embedQueryVariant(embedQuery, input, query) {
966
907
  authToken: input.authToken
967
908
  });
968
909
  }
969
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
910
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
970
911
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
971
912
  let semantic = [];
972
- if (isRawMessageChromaEnabled()) {
913
+ if (typeof deps.searchRawMessagesAnn === "function") {
914
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
915
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
916
+ const includeDeprecated = input.includeDeprecated === true;
917
+ semantic = (await Promise.all(
918
+ filters.map(
919
+ (filter) => searchRawMessagesAnn({
920
+ userId: input.userId,
921
+ queryEmbedding,
922
+ limit,
923
+ threshold,
924
+ botId: "botId" in filter ? filter.botId : void 0,
925
+ includeDeprecated,
926
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
927
+ ...factTypes ? { factTypes } : {},
928
+ ...runtimeContext ?? {}
929
+ })
930
+ )
931
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
932
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
973
933
  try {
974
934
  semantic = (await Promise.all(
975
935
  filters.map((filter) => {
@@ -994,24 +954,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
994
954
  error
995
955
  );
996
956
  }
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
957
  }
1014
- if (semantic.length === 0) {
958
+ if (semantic.length === 0 && runtimeContext !== void 0) {
959
+ warnApplicabilityNotEnforced(warnings);
960
+ return [];
961
+ }
962
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1015
963
  try {
1016
964
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1017
965
  const manager2 = await getRawMessageManager2();
@@ -1022,6 +970,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1022
970
  queryEmbedding,
1023
971
  limit,
1024
972
  threshold,
973
+ includeDeprecated: input.includeDeprecated === true,
1025
974
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1026
975
  ...factTypes ? { factTypes } : {}
1027
976
  });
@@ -1051,6 +1000,18 @@ function mergeByMaxScore(lists) {
1051
1000
  }
1052
1001
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1053
1002
  }
1003
+ function dedupeChannelByParent(hits) {
1004
+ const strongest = /* @__PURE__ */ new Map();
1005
+ for (const hit of hits) {
1006
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1007
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1008
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1009
+ const key = `${normalized.type}:${parentId}`;
1010
+ const current = strongest.get(key);
1011
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1012
+ }
1013
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1014
+ }
1054
1015
  function toIterativeRecallCandidate(hit) {
1055
1016
  return {
1056
1017
  id: hit.id,
@@ -1059,7 +1020,7 @@ function toIterativeRecallCandidate(hit) {
1059
1020
  metadata: hit.metadata
1060
1021
  };
1061
1022
  }
1062
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1023
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1063
1024
  if (keywords.length === 0) {
1064
1025
  return [];
1065
1026
  }
@@ -1068,6 +1029,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1068
1029
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1069
1030
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1070
1031
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1032
+ const includeDeprecated = input.includeDeprecated === true;
1071
1033
  return (await Promise.all(
1072
1034
  filters.map(
1073
1035
  (botId) => searchRawMessagesLexical({
@@ -1075,8 +1037,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1075
1037
  keywords,
1076
1038
  limit: Math.ceil(limit / filters.length),
1077
1039
  botId,
1040
+ includeDeprecated,
1078
1041
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1079
- ...factTypes ? { factTypes } : {}
1042
+ ...factTypes ? { factTypes } : {},
1043
+ ...runtimeContext ?? {}
1080
1044
  })
1081
1045
  )
1082
1046
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1087,10 +1051,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1087
1051
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1088
1052
  }
1089
1053
  }
1054
+ if (runtimeContext !== void 0) {
1055
+ warnApplicabilityNotEnforced(warnings);
1056
+ return [];
1057
+ }
1090
1058
  try {
1091
1059
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1092
1060
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1093
1061
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1062
+ const includeDeprecated = input.includeDeprecated === true;
1094
1063
  return (await Promise.all(
1095
1064
  filters.map(
1096
1065
  (botId) => lexicalSearchRawMessages2({
@@ -1098,6 +1067,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1098
1067
  keywords,
1099
1068
  limit: Math.ceil(limit / filters.length),
1100
1069
  botId,
1070
+ includeDeprecated,
1101
1071
  ...factTypes ? { factTypes } : {}
1102
1072
  })
1103
1073
  )
@@ -1128,7 +1098,9 @@ function searchInputToUnified(input) {
1128
1098
  mergeStrategy: input.mergeStrategy,
1129
1099
  peerFilter: input.peerFilter,
1130
1100
  reasoningStrategy: input.reasoningStrategy,
1131
- factTypes: input.factTypes
1101
+ factTypes: input.factTypes,
1102
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1103
+ includeDeprecated: input.includeDeprecated
1132
1104
  };
1133
1105
  }
1134
1106
  function mapHitToEvidenceSource(hit) {
@@ -1156,7 +1128,7 @@ function createUnifiedSearch(deps = {}) {
1156
1128
  }
1157
1129
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1158
1130
  }
1159
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1131
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1160
1132
  if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1161
1133
  warnings.push({
1162
1134
  source: "memory",
@@ -1189,7 +1161,9 @@ function createUnifiedSearch(deps = {}) {
1189
1161
  keywords2,
1190
1162
  limit,
1191
1163
  logger,
1192
- peerPeers
1164
+ peerPeers,
1165
+ warnings,
1166
+ runtimeContext
1193
1167
  );
1194
1168
  let semanticHits = [];
1195
1169
  if (typeof deps.embedQuery === "function") {
@@ -1203,7 +1177,9 @@ function createUnifiedSearch(deps = {}) {
1203
1177
  limit,
1204
1178
  threshold,
1205
1179
  logger,
1206
- peerPeers
1180
+ peerPeers,
1181
+ warnings,
1182
+ runtimeContext
1207
1183
  );
1208
1184
  } catch (error) {
1209
1185
  logger.warn?.(
@@ -1244,6 +1220,11 @@ function createUnifiedSearch(deps = {}) {
1244
1220
  unionEvidence = evidence;
1245
1221
  }
1246
1222
  if (typeof deps.embedQuery !== "function") {
1223
+ warnings.push({
1224
+ source: "memory",
1225
+ code: "semantic_unavailable",
1226
+ message: "Semantic search is unavailable because no embedding provider is configured."
1227
+ });
1247
1228
  warnings.push({
1248
1229
  source: "memory",
1249
1230
  code: "memory_lexical_search_fallback",
@@ -1251,7 +1232,16 @@ function createUnifiedSearch(deps = {}) {
1251
1232
  });
1252
1233
  const keywords2 = deriveLexicalKeywords(input.query);
1253
1234
  const lexical2 = filterByDateRange(
1254
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1235
+ await runLexicalSearchForKeywords(
1236
+ deps,
1237
+ input,
1238
+ keywords2,
1239
+ limit,
1240
+ logger,
1241
+ peerPeers,
1242
+ warnings,
1243
+ runtimeContext
1244
+ ),
1255
1245
  input.dateFrom,
1256
1246
  input.dateTo
1257
1247
  );
@@ -1259,6 +1249,26 @@ function createUnifiedSearch(deps = {}) {
1259
1249
  }
1260
1250
  let semantic = [];
1261
1251
  const embedQuery = deps.embedQuery;
1252
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1253
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1254
+ );
1255
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1256
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1257
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1258
+ const hybrid = (await Promise.all(
1259
+ filters.map(
1260
+ (botId) => deps.searchRawMessagesHybrid?.({
1261
+ userId: input.userId,
1262
+ query: input.query,
1263
+ queryEmbedding,
1264
+ limit: Math.ceil(limit / filters.length),
1265
+ botId,
1266
+ ...runtimeContext ?? {}
1267
+ })
1268
+ )
1269
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1270
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1271
+ }
1262
1272
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1263
1273
  try {
1264
1274
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1277,7 +1287,17 @@ function createUnifiedSearch(deps = {}) {
1277
1287
  );
1278
1288
  const lists = await Promise.all(
1279
1289
  embeddings.map(
1280
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1290
+ (embedding) => runSemanticSearchForEmbedding(
1291
+ deps,
1292
+ input,
1293
+ embedding,
1294
+ limit,
1295
+ threshold,
1296
+ logger,
1297
+ peerPeers,
1298
+ warnings,
1299
+ runtimeContext
1300
+ )
1281
1301
  )
1282
1302
  );
1283
1303
  semantic = mergeByMaxScore(lists);
@@ -1300,7 +1320,9 @@ function createUnifiedSearch(deps = {}) {
1300
1320
  limit,
1301
1321
  threshold,
1302
1322
  logger,
1303
- peerPeers
1323
+ peerPeers,
1324
+ warnings,
1325
+ runtimeContext
1304
1326
  );
1305
1327
  }
1306
1328
  let lexical = [];
@@ -1319,7 +1341,8 @@ function createUnifiedSearch(deps = {}) {
1319
1341
  limit,
1320
1342
  botId,
1321
1343
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1322
- ...factTypes ? { factTypes } : {}
1344
+ ...factTypes ? { factTypes } : {},
1345
+ ...runtimeContext ?? {}
1323
1346
  })
1324
1347
  )
1325
1348
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1345,7 +1368,7 @@ function createUnifiedSearch(deps = {}) {
1345
1368
  let entity;
1346
1369
  if (typeof deps.entitySearch === "function") {
1347
1370
  entity = filterByDateRange(
1348
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1371
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1349
1372
  input.dateFrom,
1350
1373
  input.dateTo
1351
1374
  );
@@ -1407,14 +1430,16 @@ function createUnifiedSearch(deps = {}) {
1407
1430
  }
1408
1431
  return out;
1409
1432
  }
1410
- async function searchUnifiedMemory(input) {
1433
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1411
1434
  const query = input.query.trim();
1412
1435
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1413
1436
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1414
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1415
1437
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1416
1438
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1417
1439
  );
1440
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1441
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1442
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1418
1443
  const reasoningStrategy = resolveReasoningStrategy(input);
1419
1444
  const warnings = [];
1420
1445
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1434,23 +1459,46 @@ function createUnifiedSearch(deps = {}) {
1434
1459
  };
1435
1460
  }
1436
1461
  let memorySubs;
1462
+ let retrievalStatus;
1437
1463
  const reasoningInfo = { strategy: reasoningStrategy };
1438
1464
  if (input.dateFrom || input.dateTo) {
1439
1465
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1440
1466
  }
1441
1467
  if (sources.includes("memory")) {
1442
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1468
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1443
1469
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1444
1470
  try {
1445
1471
  memorySubs = await runMemorySource(
1446
1472
  input,
1447
- limit,
1448
- threshold,
1473
+ candidateLimit,
1474
+ memoryThreshold,
1449
1475
  warnings,
1450
1476
  reasoningStrategy,
1451
1477
  reasoningInfo,
1452
- peerPeers
1478
+ peerPeers,
1479
+ runtimeContext
1453
1480
  );
1481
+ memorySubs = {
1482
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1483
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1484
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1485
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1486
+ };
1487
+ if (deps.getRawMessageRetrievalStatus) {
1488
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1489
+ }
1490
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1491
+ let code = "semantic_candidates_empty";
1492
+ let message = "Semantic search was available but returned no candidates.";
1493
+ if (retrievalStatus?.embeddedChildCount === 0) {
1494
+ code = "semantic_unavailable";
1495
+ message = "No embedded RawMessage children are available for semantic search.";
1496
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1497
+ code = "semantic_dimension_mismatch";
1498
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1499
+ }
1500
+ warnings.push({ source: "memory", code, message });
1501
+ }
1454
1502
  } catch (error) {
1455
1503
  logger.warn?.("[memory-store] memory source failed:", error);
1456
1504
  warnings.push({
@@ -1473,12 +1521,13 @@ function createUnifiedSearch(deps = {}) {
1473
1521
  const hits = await deps.searchInsights({
1474
1522
  userId: input.userId,
1475
1523
  query: input.query,
1476
- limit,
1524
+ limit: candidateLimit,
1477
1525
  threshold,
1478
1526
  botIds: input.botIds,
1479
1527
  includeArchived: input.includeArchivedInsights,
1480
1528
  authToken: input.authToken,
1481
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1529
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1530
+ ...runtimeContext ?? {}
1482
1531
  });
1483
1532
  for (const hit of hits) {
1484
1533
  insightHits.push({
@@ -1505,12 +1554,13 @@ function createUnifiedSearch(deps = {}) {
1505
1554
  userId: input.userId,
1506
1555
  query: input.query,
1507
1556
  options: {
1508
- limit,
1557
+ limit: candidateLimit,
1509
1558
  threshold,
1510
1559
  documentIds: input.documentIds
1511
1560
  },
1512
1561
  authToken: input.authToken,
1513
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1562
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1563
+ ...runtimeContext ?? {}
1514
1564
  });
1515
1565
  for (const hit of hits) {
1516
1566
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1528,10 +1578,14 @@ function createUnifiedSearch(deps = {}) {
1528
1578
  memorySubs,
1529
1579
  insightHits,
1530
1580
  knowledgeHits,
1531
- limit,
1581
+ limit: candidateLimit,
1532
1582
  strategy: mergeStrategy
1533
1583
  });
1534
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1584
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1585
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1586
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1587
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1588
+ const ranked = reranked.slice(0, limit);
1535
1589
  const output = {
1536
1590
  query: input.query,
1537
1591
  sources,
@@ -1539,28 +1593,51 @@ function createUnifiedSearch(deps = {}) {
1539
1593
  count: ranked.length,
1540
1594
  warnings
1541
1595
  };
1596
+ if (input.includeRetrievalDiagnostics) {
1597
+ output.retrievalDiagnostics = {
1598
+ mergeStrategy,
1599
+ candidateLimit,
1600
+ backend: retrievalStatus?.backend,
1601
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1602
+ candidateCounts: {
1603
+ semantic: memorySubs?.semantic.length ?? 0,
1604
+ lexical: memorySubs?.lexical.length ?? 0,
1605
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1606
+ entity: memorySubs?.entity?.length ?? 0,
1607
+ fused: merged.length,
1608
+ final: ranked.length
1609
+ },
1610
+ channels: {
1611
+ semantic: memorySubs?.semantic ?? [],
1612
+ lexical: memorySubs?.lexical ?? [],
1613
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1614
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1615
+ },
1616
+ fusedBeforeRerank: merged,
1617
+ reranker: {
1618
+ enabled: Boolean(deps.reranker),
1619
+ provider: deps.rerankerInfo?.provider,
1620
+ model: deps.rerankerInfo?.model,
1621
+ inputCount: merged.length,
1622
+ outputCount: reranked.length,
1623
+ latencyMs: rerankerLatencyMs,
1624
+ orderChanged: rerankerOrderChanged
1625
+ },
1626
+ final: ranked
1627
+ };
1628
+ }
1542
1629
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1543
1630
  output.reasoning = reasoningInfo;
1544
1631
  }
1545
1632
  return output;
1546
1633
  }
1634
+ async function searchUnifiedMemory(input) {
1635
+ return searchUnifiedMemoryWithRuntime(input);
1636
+ }
1547
1637
  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
- });
1638
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1562
1639
  }
1563
- async function search(input) {
1640
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1564
1641
  const query = input.query.trim();
1565
1642
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1566
1643
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1592,19 +1669,24 @@ function createUnifiedSearch(deps = {}) {
1592
1669
  let unifiedHits = [];
1593
1670
  let sources = input.sources ? [...input.sources] : [];
1594
1671
  let unifiedReasoning;
1672
+ let retrievalDiagnostics;
1595
1673
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1596
1674
  const inferredSources = sources.length > 0 ? sources : [
1597
1675
  ...wantsRawTier ? ["memory"] : [],
1598
1676
  ...wantsInsightTier ? ["insights"] : [],
1599
1677
  ...wantsKnowledgeTier ? ["knowledge"] : []
1600
1678
  ];
1601
- const unified = await searchUnifiedMemory({
1602
- ...searchInputToUnified(input),
1603
- sources: inferredSources.length > 0 ? inferredSources : void 0
1604
- });
1679
+ const unified = await searchUnifiedMemoryWithRuntime(
1680
+ {
1681
+ ...searchInputToUnified(input),
1682
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1683
+ },
1684
+ runtimeContext
1685
+ );
1605
1686
  unifiedHits = unified.results;
1606
1687
  sources = unified.sources;
1607
1688
  unifiedReasoning = unified.reasoning;
1689
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1608
1690
  warnings.push(...unified.warnings);
1609
1691
  }
1610
1692
  let summaryHits = [];
@@ -1615,7 +1697,8 @@ function createUnifiedSearch(deps = {}) {
1615
1697
  limit,
1616
1698
  threshold,
1617
1699
  logger,
1618
- peerScope.peers
1700
+ peerScope.peers,
1701
+ runtimeContext
1619
1702
  );
1620
1703
  warnings.push(...summaryBucket.warnings);
1621
1704
  summaryHits = summaryBucket.hits;
@@ -1640,7 +1723,8 @@ function createUnifiedSearch(deps = {}) {
1640
1723
  source: mapHitToEvidenceSource(hit),
1641
1724
  snippet: hit.content,
1642
1725
  score: hit.similarity,
1643
- timestamp: getCandidateTimestamp(hit.metadata)
1726
+ timestamp: getCandidateTimestamp(hit.metadata),
1727
+ metadata: hit.metadata
1644
1728
  }));
1645
1729
  const base = {
1646
1730
  query,
@@ -1653,12 +1737,15 @@ function createUnifiedSearch(deps = {}) {
1653
1737
  if (unifiedReasoning) {
1654
1738
  base.reasoning = unifiedReasoning;
1655
1739
  }
1740
+ if (retrievalDiagnostics) {
1741
+ base.retrievalDiagnostics = retrievalDiagnostics;
1742
+ }
1656
1743
  if (!wantsSynthesis) {
1657
1744
  return base;
1658
1745
  }
1659
1746
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1660
1747
  query,
1661
- hits,
1748
+ evidence,
1662
1749
  responseSchema: synthesisSchema,
1663
1750
  deps,
1664
1751
  logger
@@ -1667,6 +1754,10 @@ function createUnifiedSearch(deps = {}) {
1667
1754
  base.warnings = [...base.warnings, ...synthWarnings];
1668
1755
  return base;
1669
1756
  }
1757
+ async function search(input, runtimeContext) {
1758
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1759
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1760
+ }
1670
1761
  return {
1671
1762
  search,
1672
1763
  searchUnifiedMemory,
@@ -1682,6 +1773,7 @@ function mergeAcrossSources(input) {
1682
1773
  for (const hit of [
1683
1774
  ...memorySubs.semantic,
1684
1775
  ...memorySubs.lexical,
1776
+ ...memorySubs.hybrid ?? [],
1685
1777
  ...input.insightHits,
1686
1778
  ...input.knowledgeHits
1687
1779
  ]) {
@@ -1697,6 +1789,7 @@ function mergeAcrossSources(input) {
1697
1789
  const all = [
1698
1790
  ...memorySubs.semantic,
1699
1791
  ...memorySubs.lexical,
1792
+ ...memorySubs.hybrid ?? [],
1700
1793
  ...memorySubs.entity ?? [],
1701
1794
  ...input.insightHits,
1702
1795
  ...input.knowledgeHits
@@ -1708,6 +1801,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1708
1801
  const lists = [];
1709
1802
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1710
1803
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1804
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1805
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1806
+ }
1711
1807
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1712
1808
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1713
1809
  }
@@ -2172,8 +2268,250 @@ function createVsaRecall(storage) {
2172
2268
  };
2173
2269
  }
2174
2270
 
2271
+ // ../shared/src/tokens.ts
2272
+ var estimateTokens = (text) => {
2273
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
2274
+ const otherChars = text.length - chineseChars;
2275
+ const wordCount = Math.ceil(otherChars / 5);
2276
+ return chineseChars + wordCount;
2277
+ };
2278
+
2279
+ // ../shared/src/text-chunking.ts
2280
+ var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
2281
+ var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
2282
+ var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
2283
+ var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
2284
+ function largestEndWithinTokenBudget(text, start, maxTokens) {
2285
+ let low = start + 1;
2286
+ let high = text.length;
2287
+ let best = low;
2288
+ while (low <= high) {
2289
+ const middle = Math.floor((low + high) / 2);
2290
+ if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
2291
+ best = middle;
2292
+ low = middle + 1;
2293
+ } else {
2294
+ high = middle - 1;
2295
+ }
2296
+ }
2297
+ return best;
2298
+ }
2299
+ function preferNaturalEnd(text, start, hardEnd) {
2300
+ if (hardEnd >= text.length) return text.length;
2301
+ const searchStart = start + Math.floor((hardEnd - start) * 0.6);
2302
+ const candidate = text.slice(searchStart, hardEnd);
2303
+ for (const boundary of PARAGRAPH_BOUNDARIES) {
2304
+ const index = candidate.lastIndexOf(boundary);
2305
+ if (index >= 0) return searchStart + index + boundary.length;
2306
+ }
2307
+ for (let offset = candidate.length; offset > 0; offset -= 1) {
2308
+ const prefix = candidate.slice(0, offset);
2309
+ if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
2310
+ }
2311
+ for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
2312
+ if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
2313
+ }
2314
+ return hardEnd;
2315
+ }
2316
+ function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
2317
+ if (overlapTokens <= 0) return chunkEnd;
2318
+ let low = chunkStart + 1;
2319
+ let high = chunkEnd;
2320
+ let best = chunkEnd;
2321
+ while (low <= high) {
2322
+ const middle = Math.floor((low + high) / 2);
2323
+ if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
2324
+ best = middle;
2325
+ high = middle - 1;
2326
+ } else {
2327
+ low = middle + 1;
2328
+ }
2329
+ }
2330
+ return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
2331
+ }
2332
+ function chunkTextByEstimatedTokens(text, options = {}) {
2333
+ if (text.length === 0) return [];
2334
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
2335
+ const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
2336
+ if (overlapTokens >= maxTokens) {
2337
+ throw new Error("overlapTokens must be smaller than maxTokens");
2338
+ }
2339
+ const chunks = [];
2340
+ let start = 0;
2341
+ while (start < text.length) {
2342
+ const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
2343
+ const end = preferNaturalEnd(text, start, hardEnd);
2344
+ chunks.push({
2345
+ chunkIndex: chunks.length,
2346
+ startPosition: start,
2347
+ endPosition: end,
2348
+ content: text.slice(start, end)
2349
+ });
2350
+ if (end >= text.length) break;
2351
+ start = overlapStart(text, start, end, overlapTokens);
2352
+ }
2353
+ return chunks;
2354
+ }
2355
+
2356
+ // src/embed-on-insert.ts
2357
+ var AUTO_APPLIED_WARNING = {
2358
+ code: "embed_on_insert_auto_applied",
2359
+ message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
2360
+ };
2361
+ var SEMANTIC_UNAVAILABLE_WARNING = {
2362
+ code: "semantic_index_unavailable",
2363
+ message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
2364
+ };
2365
+ function sha256(value) {
2366
+ return createHash("sha256").update(value).digest("hex");
2367
+ }
2368
+ function isUsableEmbedding(embedding) {
2369
+ return Boolean(
2370
+ embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
2371
+ );
2372
+ }
2373
+ async function embedText(text, userId, deps) {
2374
+ if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
2375
+ const embedding = await deps.embedQuery({ userId, query: text });
2376
+ return isUsableEmbedding(embedding) ? embedding : void 0;
2377
+ }
2378
+ async function embedMissingMessages(messages, deps) {
2379
+ if (typeof deps.embedQuery !== "function") return messages;
2380
+ const out = [];
2381
+ for (const message of messages) {
2382
+ if (isUsableEmbedding(message.embedding) || !message.content) {
2383
+ out.push(message);
2384
+ continue;
2385
+ }
2386
+ const embedding = await embedText(message.content, message.userId, deps);
2387
+ out.push(
2388
+ embedding ? {
2389
+ ...message,
2390
+ embedding,
2391
+ embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
2392
+ embeddingContentHash: sha256(message.content),
2393
+ embeddingDimensions: embedding.length,
2394
+ embeddingUpdatedAt: Date.now()
2395
+ } : message
2396
+ );
2397
+ }
2398
+ return out;
2399
+ }
2400
+ async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
2401
+ const deps = unified ?? {};
2402
+ const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
2403
+ const shouldEmbed = embedOnInsert === true || embedderAvailable;
2404
+ const messages = [];
2405
+ const chunks = [];
2406
+ let missingSemanticEmbedding = false;
2407
+ let generatedEmbedding = false;
2408
+ const plans = incoming.map((message) => ({
2409
+ message,
2410
+ pieces: chunkTextByEstimatedTokens(message.content)
2411
+ }));
2412
+ const generatedEmbeddings = /* @__PURE__ */ new Map();
2413
+ const pendingByUser = /* @__PURE__ */ new Map();
2414
+ for (const [messageIndex, plan] of plans.entries()) {
2415
+ for (const piece of plan.pieces) {
2416
+ const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
2417
+ if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
2418
+ const pending = pendingByUser.get(plan.message.userId) ?? [];
2419
+ pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
2420
+ pendingByUser.set(plan.message.userId, pending);
2421
+ }
2422
+ }
2423
+ for (const [userId, pending] of pendingByUser) {
2424
+ let embeddings;
2425
+ if (typeof deps.embedDocuments === "function") {
2426
+ const batch = await deps.embedDocuments({
2427
+ userId,
2428
+ texts: pending.map((item) => item.text)
2429
+ });
2430
+ if (batch.length !== pending.length) {
2431
+ throw new Error(
2432
+ `Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
2433
+ );
2434
+ }
2435
+ embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
2436
+ } else {
2437
+ embeddings = [];
2438
+ for (const item of pending) {
2439
+ embeddings.push(await embedText(item.text, userId, deps));
2440
+ }
2441
+ }
2442
+ for (const [index, item] of pending.entries()) {
2443
+ const embedding = embeddings[index];
2444
+ if (embedding) generatedEmbeddings.set(item.key, embedding);
2445
+ }
2446
+ }
2447
+ for (const [messageIndex, plan] of plans.entries()) {
2448
+ const incomingMessage = plan.message;
2449
+ const pieces = plan.pieces;
2450
+ let message = incomingMessage;
2451
+ for (const piece of pieces) {
2452
+ const contentHash = sha256(piece.content);
2453
+ const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
2454
+ const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
2455
+ generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
2456
+ if (!embedding) missingSemanticEmbedding = true;
2457
+ const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
2458
+ chunks.push({
2459
+ chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
2460
+ messageId: incomingMessage.messageId,
2461
+ userId: incomingMessage.userId,
2462
+ chunkIndex: piece.chunkIndex,
2463
+ chunkCount: pieces.length,
2464
+ startPosition: piece.startPosition,
2465
+ endPosition: piece.endPosition,
2466
+ content: piece.content,
2467
+ contentHash,
2468
+ embedding,
2469
+ embeddingModel,
2470
+ embeddingDimensions: embedding?.length,
2471
+ embeddingUpdatedAt: embedding ? Date.now() : void 0
2472
+ });
2473
+ if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
2474
+ message = {
2475
+ ...incomingMessage,
2476
+ embedding,
2477
+ embeddingModel,
2478
+ embeddingContentHash: contentHash,
2479
+ embeddingDimensions: embedding.length,
2480
+ embeddingUpdatedAt: Date.now()
2481
+ };
2482
+ }
2483
+ }
2484
+ messages.push(message);
2485
+ }
2486
+ const warnings = [];
2487
+ if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
2488
+ if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
2489
+ return { messages, chunks, warnings };
2490
+ }
2491
+
2492
+ // src/storage/raw-message-ingest.ts
2493
+ async function persistRawMessages(input) {
2494
+ const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
2495
+ let ids;
2496
+ if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
2497
+ ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
2498
+ } else {
2499
+ const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
2500
+ if (typeof input.manager.upsertRawMessages === "function") {
2501
+ await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
2502
+ } else if (typeof input.manager.storeMessages === "function") {
2503
+ ids = await input.manager.storeMessages(legacyMessages);
2504
+ } else {
2505
+ throw new Error("active raw-message manager exposes no supported write method");
2506
+ }
2507
+ }
2508
+ if (input.externalIndex) {
2509
+ await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
2510
+ }
2511
+ return { count: prepared.messages.length, ids, warnings: prepared.warnings };
2512
+ }
2513
+
2175
2514
  // src/mcp.ts
2176
- init_chroma_memory_index();
2177
2515
  init_raw_message_store();
2178
2516
  init_sqlite_raw_message_store();
2179
2517
  var DEFAULT_NAME = "@melandlabs/memory-store";
@@ -2186,13 +2524,37 @@ async function startMcpServer(options = {}) {
2186
2524
  const rawStore = createRawMessageStore({
2187
2525
  env: options.env
2188
2526
  });
2189
- const search = createUnifiedSearch(options.unified);
2527
+ const rawMessageManager = await rawStore.getManager();
2528
+ const search = createUnifiedSearch({
2529
+ ...options.unified,
2530
+ searchRawMessagesLexical: async (input) => {
2531
+ if (typeof rawMessageManager.lexicalSearchMessages !== "function") return [];
2532
+ const results = await rawMessageManager.lexicalSearchMessages(input);
2533
+ return results.filter(Boolean).map((result) => ({
2534
+ id: result.id,
2535
+ content: result.content,
2536
+ similarity: result.similarity,
2537
+ metadata: result.metadata ?? {}
2538
+ }));
2539
+ }
2540
+ });
2190
2541
  const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
2191
2542
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2192
2543
  const vsa = createVsaRecall(vsaStorage);
2193
- server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
2194
- content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
2195
- }));
2544
+ server.tool("memory.health", "Returns the health status of the memory store.", async () => {
2545
+ const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
2546
+ backend: "unknown",
2547
+ childCount: 0,
2548
+ embeddedChildCount: 0,
2549
+ indexedDimensions: [],
2550
+ semanticReady: false,
2551
+ lexicalReady: false,
2552
+ semanticDegradedReason: "health_check_failed"
2553
+ })) : void 0;
2554
+ return {
2555
+ content: [{ type: "text", text: JSON.stringify({ ok: true, retrieval }) }]
2556
+ };
2557
+ });
2196
2558
  const searchSchema = {
2197
2559
  userId: z.string(),
2198
2560
  query: z.string().min(1),
@@ -2210,7 +2572,11 @@ async function startMcpServer(options = {}) {
2210
2572
  // coerces it into `synthesize.responseSchema` when present.
2211
2573
  responseSchema: z.record(z.string(), z.unknown()).optional(),
2212
2574
  limit: z.number().int().min(1).max(50).default(10),
2213
- threshold: z.number().min(-1).max(1).default(0.7),
2575
+ threshold: z.number().min(-1).max(1).optional().describe(
2576
+ "Optional similarity floor. Omit it to let the default RRF pipeline fuse the full candidate window."
2577
+ ),
2578
+ mergeStrategy: z.enum(["rrf", "similarity"]).optional(),
2579
+ includeRetrievalDiagnostics: z.boolean().optional(),
2214
2580
  botIds: z.array(z.string()).optional(),
2215
2581
  documentIds: z.array(z.string()).optional(),
2216
2582
  includeArchivedInsights: z.boolean().default(false),
@@ -2274,6 +2640,8 @@ async function startMcpServer(options = {}) {
2274
2640
  sources: a.sources,
2275
2641
  limit: a.limit,
2276
2642
  threshold: a.threshold,
2643
+ mergeStrategy: a.mergeStrategy,
2644
+ includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
2277
2645
  botIds: a.botIds,
2278
2646
  documentIds: a.documentIds,
2279
2647
  authToken: a.authToken,
@@ -2304,37 +2672,24 @@ async function startMcpServer(options = {}) {
2304
2672
  const a = args;
2305
2673
  const manager2 = await rawStore.getManager();
2306
2674
  const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
2307
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2308
- incoming,
2309
- a.embedOnInsert,
2310
- options.unified
2311
- );
2312
- let result;
2313
- if (typeof manager2.upsertRawMessages === "function") {
2314
- result = await manager2.upsertRawMessages({
2315
- userId: a.userId,
2316
- messages
2317
- });
2318
- } else if (typeof manager2.storeMessages === "function") {
2319
- const ids = await manager2.storeMessages(messages);
2320
- result = { inserted: ids.length, ids };
2321
- } else {
2322
- throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
2323
- }
2324
- try {
2325
- await upsertRawMessagesToChroma(messages);
2326
- } catch (error) {
2327
- console.warn("[memory-store/mcp] chroma upsert failed:", error);
2328
- }
2675
+ const persisted = await persistRawMessages({
2676
+ manager: manager2,
2677
+ userId: a.userId,
2678
+ messages: incoming,
2679
+ embedOnInsert: a.embedOnInsert,
2680
+ unified: options.unified,
2681
+ externalIndex: options.unified?.rawMessageChildIndex
2682
+ });
2683
+ const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
2329
2684
  return {
2330
2685
  content: [
2331
2686
  {
2332
2687
  type: "text",
2333
2688
  text: JSON.stringify({
2334
2689
  ok: true,
2335
- count: messages.length,
2690
+ count: persisted.count,
2336
2691
  result,
2337
- ...warnings.length > 0 ? { warnings } : {}
2692
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2338
2693
  })
2339
2694
  }
2340
2695
  ]
@@ -2674,7 +3029,17 @@ async function startMcpServer(options = {}) {
2674
3029
  );
2675
3030
  const okfMcpSpecifier = "@melandlabs/okf/mcp";
2676
3031
  const { registerOkfTools } = await import(okfMcpSpecifier);
2677
- registerOkfTools(server, rawStore);
3032
+ registerOkfTools(server, rawStore, {
3033
+ writeMessages: async (userId, messages) => {
3034
+ await persistRawMessages({
3035
+ manager: rawMessageManager,
3036
+ userId,
3037
+ messages,
3038
+ unified: options.unified,
3039
+ externalIndex: options.unified?.rawMessageChildIndex
3040
+ });
3041
+ }
3042
+ });
2678
3043
  const transport = new StdioServerTransport();
2679
3044
  await server.connect(transport);
2680
3045
  return server;