@melandlabs/memory-store 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.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';
@@ -11,6 +11,7 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
11
11
  import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
12
12
  import { isEntityKind } from '@melandlabs/contracts/entity-edge';
13
13
  import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
14
+ import { createHash } from 'crypto';
14
15
 
15
16
  var __defProp = Object.defineProperty;
16
17
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -21,27 +22,6 @@ var __export = (target, all) => {
21
22
  for (var name in all)
22
23
  __defProp(target, name, { get: all[name], enumerable: true });
23
24
  };
24
- function normalizeTimestampToMs(value) {
25
- if (!Number.isFinite(value)) {
26
- return 0;
27
- }
28
- if (value < 1e11) {
29
- return Math.floor(value * 1e3);
30
- }
31
- return Math.floor(value);
32
- }
33
- function rawMessageToMemoryRecord(message) {
34
- return {
35
- id: message.messageId,
36
- userId: message.userId,
37
- timestamp: normalizeTimestampToMs(message.timestamp),
38
- text: message.archivedAt ? void 0 : message.content,
39
- mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
40
- embedding: message.embedding,
41
- embeddingModel: message.embeddingModel,
42
- embeddingContentHash: message.embeddingContentHash
43
- };
44
- }
45
25
  var init_contracts = __esm({
46
26
  "src/contracts.ts"() {
47
27
  }
@@ -67,61 +47,9 @@ function getChromaStore(collectionName) {
67
47
  collectionName
68
48
  });
69
49
  }
70
- function normalizeTimestampToMs2(value) {
71
- if (!Number.isFinite(value)) {
72
- return 0;
73
- }
74
- if (value < 1e11) {
75
- return Math.floor(value * 1e3);
76
- }
77
- return Math.floor(value);
78
- }
79
50
  function toBooleanValue(value) {
80
51
  return value === true || value === "true" || value === 1 || value === "1";
81
52
  }
82
- function rawMessageToChunk(message) {
83
- if (!message.embedding || message.embedding.length === 0) {
84
- return null;
85
- }
86
- const record = rawMessageToMemoryRecord(message);
87
- const document = buildMemoryRecordEmbeddingDocument(record);
88
- if (!document.content) {
89
- return null;
90
- }
91
- return {
92
- id: message.messageId,
93
- documentId: message.messageId,
94
- content: document.content,
95
- embedding: message.embedding,
96
- metadata: {
97
- sourceType: "raw_message",
98
- userId: message.userId,
99
- platform: message.platform,
100
- botId: message.botId,
101
- channel: message.channel,
102
- person: message.person,
103
- timestamp: normalizeTimestampToMs2(message.timestamp),
104
- memoryStage: message.memoryStage,
105
- embeddingModel: message.embeddingModel,
106
- embeddingDimensions: message.embeddingDimensions,
107
- contentHash: message.embeddingContentHash,
108
- archived: Boolean(message.archivedAt),
109
- role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
110
- }
111
- };
112
- }
113
- async function upsertRawMessagesToChroma(messages) {
114
- if (!isRawMessageChromaEnabled() || messages.length === 0) {
115
- return 0;
116
- }
117
- const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
118
- if (chunks.length === 0) {
119
- return 0;
120
- }
121
- const store = getChromaStore(getRawMessagesCollectionName());
122
- await store.addChunks(chunks);
123
- return chunks.length;
124
- }
125
53
  async function searchRawMessagesWithChroma(input) {
126
54
  if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
127
55
  return [];
@@ -323,59 +251,73 @@ var init_raw_message_store = __esm({
323
251
  }
324
252
  });
325
253
 
326
- // src/embed-on-insert.ts
327
- async function embedMissingMessages(messages, deps) {
328
- if (typeof deps.embedQuery !== "function") {
329
- return messages;
254
+ // src/search/unified-search.ts
255
+ init_chroma_memory_index();
256
+ init_raw_message_store();
257
+
258
+ // src/search/applicability.ts
259
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
260
+ "global",
261
+ "task",
262
+ "conversation",
263
+ "channel",
264
+ "project",
265
+ "custom"
266
+ ]);
267
+ function isRecord(value) {
268
+ return typeof value === "object" && value !== null && !Array.isArray(value);
269
+ }
270
+ function invalidRuntimeContext(message) {
271
+ throw new TypeError(`memory-store search runtime context: ${message}`);
272
+ }
273
+ function validateApplicabilityContext(value, index) {
274
+ const path = `applicabilityContexts[${index}]`;
275
+ if (!isRecord(value)) {
276
+ invalidRuntimeContext(`${path} must be an object`);
330
277
  }
331
- const out = [];
332
- for (const message of messages) {
333
- if (Array.isArray(message.embedding) && message.embedding.length > 0) {
334
- out.push(message);
335
- continue;
336
- }
337
- if (typeof message.content !== "string" || message.content.length === 0) {
338
- out.push(message);
339
- continue;
278
+ const scope = value.scope;
279
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
280
+ invalidRuntimeContext(`${path}.scope is not supported`);
281
+ }
282
+ if (value.key !== void 0 && typeof value.key !== "string") {
283
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
284
+ }
285
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
286
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
287
+ }
288
+ for (const field of ["validFrom", "validUntil"]) {
289
+ const boundary = value[field];
290
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
291
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
340
292
  }
341
- const vector = await deps.embedQuery({
342
- userId: message.userId,
343
- query: message.content
344
- });
345
- out.push({
346
- ...message,
347
- embedding: vector,
348
- embeddingModel: message.embeddingModel ?? "server",
349
- embeddingDimensions: vector.length,
350
- embeddingUpdatedAt: Date.now()
351
- });
352
293
  }
353
- return out;
354
294
  }
355
- var AUTO_APPLIED_WARNING = {
356
- code: "embed_on_insert_auto_applied",
357
- 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."
358
- };
359
- async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
360
- const messages = incoming;
361
- const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
362
- const embedderWired = typeof unified?.embedQuery === "function";
363
- if (embedOnInsert === true && anyMissing) {
364
- return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
365
- }
366
- if (embedOnInsert !== true && anyMissing && embedderWired) {
367
- return {
368
- messages: await embedMissingMessages(messages, unified ?? {}),
369
- warnings: [AUTO_APPLIED_WARNING]
370
- };
295
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
296
+ if (runtimeContext === void 0) {
297
+ return void 0;
298
+ }
299
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
300
+ invalidRuntimeContext("applicabilityContexts must be an array");
371
301
  }
372
- return { messages, warnings: [] };
302
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
303
+ validateApplicabilityContext(context, index);
304
+ }
305
+ let applicabilityAt;
306
+ if (input.asOf === void 0) {
307
+ applicabilityAt = now();
308
+ } else {
309
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
310
+ if (!Number.isFinite(parsed)) {
311
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
312
+ }
313
+ applicabilityAt = parsed;
314
+ }
315
+ return {
316
+ applicabilityContexts: runtimeContext.applicabilityContexts,
317
+ applicabilityAt
318
+ };
373
319
  }
374
320
 
375
- // src/search/unified-search.ts
376
- init_chroma_memory_index();
377
- init_raw_message_store();
378
-
379
321
  // src/search/reranker.ts
380
322
  async function applyReranker(reranker, query, results) {
381
323
  if (!reranker || results.length === 0) {
@@ -462,7 +404,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
462
404
  return value === "rrf" ? "rrf" : "similarity";
463
405
  }
464
406
  function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
465
- if (value === "rewrite" || value === "iterative") {
407
+ if (value === "rewrite" || value === "iterative" || value === "union") {
466
408
  return value;
467
409
  }
468
410
  return defaultStrategy;
@@ -569,6 +511,7 @@ function materializeSignals(lists, hit) {
569
511
  function listNameToChannel(name) {
570
512
  if (name === "memory-semantic") return "semantic";
571
513
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
514
+ if (name === "memory-hybrid") return "hybrid";
572
515
  if (name === "memory-entity") return "entity";
573
516
  return void 0;
574
517
  }
@@ -626,7 +569,6 @@ async function resolveScopePeer(input) {
626
569
 
627
570
  // src/search/gather-evidence.ts
628
571
  var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
629
- var MAX_EVIDENCE_CHARS = 600;
630
572
  function toSummaryResult(row) {
631
573
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
632
574
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -644,7 +586,7 @@ function toSummaryResult(row) {
644
586
  }
645
587
  };
646
588
  }
647
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
589
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
648
590
  if (typeof deps.searchSummaries !== "function") {
649
591
  return { hits: [], warnings: [] };
650
592
  }
@@ -657,7 +599,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
657
599
  limit,
658
600
  threshold,
659
601
  authToken: input.authToken,
660
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
602
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
603
+ ...runtimeContext ?? {}
661
604
  });
662
605
  const hits = rows.map(toSummaryResult);
663
606
  return { hits, warnings: [] };
@@ -675,12 +618,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
675
618
  };
676
619
  }
677
620
  }
678
- function truncate(input, max = MAX_EVIDENCE_CHARS) {
679
- if (input.length <= max) {
680
- return input;
681
- }
682
- return `${input.slice(0, max - 1)}\u2026`;
683
- }
684
621
  function buildSynthesisPrompt(input) {
685
622
  const grouped = /* @__PURE__ */ new Map();
686
623
  for (const item of input.evidence) {
@@ -694,9 +631,10 @@ function buildSynthesisPrompt(input) {
694
631
  if (items.length === 0) {
695
632
  continue;
696
633
  }
697
- const lines = items.map(
698
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
699
- );
634
+ const lines = items.map((item, index) => {
635
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
636
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
637
+ });
700
638
  sections.push(`## ${tier}
701
639
  ${lines.join("\n")}`);
702
640
  }
@@ -778,12 +716,15 @@ function coerceAnswer(text, schema) {
778
716
  return text;
779
717
  }
780
718
  async function synthesizeAnswer(input) {
781
- const { query, hits, responseSchema, deps, logger } = input;
782
- const evidence = hits.map((hit) => ({
783
- id: hit.id,
784
- source: mapHitToTier(hit),
785
- snippet: truncate(hit.content),
786
- score: hit.similarity
719
+ const { query, responseSchema, deps, logger } = input;
720
+ const evidence = input.evidence.map((item) => ({
721
+ id: item.id,
722
+ source: mapEvidenceSourceToTier(item.source),
723
+ snippet: item.snippet,
724
+ score: item.score,
725
+ // Forward per-fact provenance (e.g. `metadata.source` from
726
+ // `opencontext add --source`) so the synthesis prompt can cite it.
727
+ metadata: item.metadata
787
728
  }));
788
729
  const warnings = [];
789
730
  const complete = deps.reasoning?.complete;
@@ -814,21 +755,10 @@ async function synthesizeAnswer(input) {
814
755
  return { answer: "", warnings };
815
756
  }
816
757
  }
817
- function mapHitToTier(hit) {
818
- const tierMarker = hit.metadata?.tier;
819
- if (tierMarker === "summary") {
820
- return "summary";
821
- }
822
- if (hit.type === "memory") {
823
- return "raw";
824
- }
825
- if (hit.type === "insight") {
826
- return "insight";
827
- }
828
- if (hit.type === "knowledge") {
829
- return "knowledge";
830
- }
831
- return hit.type;
758
+ function mapEvidenceSourceToTier(source) {
759
+ if (source === "memory") return "raw";
760
+ if (source === "insights") return "insight";
761
+ return source;
832
762
  }
833
763
  async function resolveSearchScopePeers(input) {
834
764
  return resolveScopePeer({
@@ -852,7 +782,7 @@ function deriveEntityKeywords(query) {
852
782
  }
853
783
  return out.slice(0, 16);
854
784
  }
855
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
785
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
856
786
  if (typeof deps.entitySearch !== "function") {
857
787
  return [];
858
788
  }
@@ -870,7 +800,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
870
800
  keywords,
871
801
  limit: Math.ceil(limit / filters.length),
872
802
  botId,
873
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
803
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
804
+ ...runtimeContext ?? {}
874
805
  })
875
806
  )
876
807
  )).flat();
@@ -958,6 +889,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
958
889
  return true;
959
890
  });
960
891
  }
892
+ function warnApplicabilityNotEnforced(warnings) {
893
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
894
+ return;
895
+ }
896
+ warnings.push({
897
+ source: "memory",
898
+ code: "memory_applicability_not_enforced",
899
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
900
+ });
901
+ }
961
902
  async function embedQueryVariant(embedQuery, input, query) {
962
903
  return embedQuery({
963
904
  userId: input.userId,
@@ -965,10 +906,29 @@ async function embedQueryVariant(embedQuery, input, query) {
965
906
  authToken: input.authToken
966
907
  });
967
908
  }
968
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
909
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
969
910
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
970
911
  let semantic = [];
971
- if (isRawMessageChromaEnabled()) {
912
+ if (typeof deps.searchRawMessagesAnn === "function") {
913
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
914
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
915
+ const includeDeprecated = input.includeDeprecated === true;
916
+ semantic = (await Promise.all(
917
+ filters.map(
918
+ (filter) => searchRawMessagesAnn({
919
+ userId: input.userId,
920
+ queryEmbedding,
921
+ limit,
922
+ threshold,
923
+ botId: "botId" in filter ? filter.botId : void 0,
924
+ includeDeprecated,
925
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
926
+ ...factTypes ? { factTypes } : {},
927
+ ...runtimeContext ?? {}
928
+ })
929
+ )
930
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
931
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
972
932
  try {
973
933
  semantic = (await Promise.all(
974
934
  filters.map((filter) => {
@@ -993,24 +953,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
993
953
  error
994
954
  );
995
955
  }
996
- } else if (typeof deps.searchRawMessagesAnn === "function") {
997
- const searchRawMessagesAnn = deps.searchRawMessagesAnn;
998
- const factTypes = input.factTypes?.length ? input.factTypes : void 0;
999
- semantic = (await Promise.all(
1000
- filters.map(
1001
- (filter) => searchRawMessagesAnn({
1002
- userId: input.userId,
1003
- queryEmbedding,
1004
- limit,
1005
- threshold,
1006
- botId: "botId" in filter ? filter.botId : void 0,
1007
- ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1008
- ...factTypes ? { factTypes } : {}
1009
- })
1010
- )
1011
- )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1012
956
  }
1013
- if (semantic.length === 0) {
957
+ if (semantic.length === 0 && runtimeContext !== void 0) {
958
+ warnApplicabilityNotEnforced(warnings);
959
+ return [];
960
+ }
961
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1014
962
  try {
1015
963
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1016
964
  const manager2 = await getRawMessageManager2();
@@ -1021,6 +969,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1021
969
  queryEmbedding,
1022
970
  limit,
1023
971
  threshold,
972
+ includeDeprecated: input.includeDeprecated === true,
1024
973
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1025
974
  ...factTypes ? { factTypes } : {}
1026
975
  });
@@ -1050,6 +999,18 @@ function mergeByMaxScore(lists) {
1050
999
  }
1051
1000
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1052
1001
  }
1002
+ function dedupeChannelByParent(hits) {
1003
+ const strongest = /* @__PURE__ */ new Map();
1004
+ for (const hit of hits) {
1005
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1006
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1007
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1008
+ const key = `${normalized.type}:${parentId}`;
1009
+ const current = strongest.get(key);
1010
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1011
+ }
1012
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1013
+ }
1053
1014
  function toIterativeRecallCandidate(hit) {
1054
1015
  return {
1055
1016
  id: hit.id,
@@ -1058,7 +1019,7 @@ function toIterativeRecallCandidate(hit) {
1058
1019
  metadata: hit.metadata
1059
1020
  };
1060
1021
  }
1061
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1022
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1062
1023
  if (keywords.length === 0) {
1063
1024
  return [];
1064
1025
  }
@@ -1067,6 +1028,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1067
1028
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1068
1029
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1069
1030
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1031
+ const includeDeprecated = input.includeDeprecated === true;
1070
1032
  return (await Promise.all(
1071
1033
  filters.map(
1072
1034
  (botId) => searchRawMessagesLexical({
@@ -1074,8 +1036,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1074
1036
  keywords,
1075
1037
  limit: Math.ceil(limit / filters.length),
1076
1038
  botId,
1039
+ includeDeprecated,
1077
1040
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1078
- ...factTypes ? { factTypes } : {}
1041
+ ...factTypes ? { factTypes } : {},
1042
+ ...runtimeContext ?? {}
1079
1043
  })
1080
1044
  )
1081
1045
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1086,10 +1050,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1086
1050
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1087
1051
  }
1088
1052
  }
1053
+ if (runtimeContext !== void 0) {
1054
+ warnApplicabilityNotEnforced(warnings);
1055
+ return [];
1056
+ }
1089
1057
  try {
1090
1058
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1091
1059
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1092
1060
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1061
+ const includeDeprecated = input.includeDeprecated === true;
1093
1062
  return (await Promise.all(
1094
1063
  filters.map(
1095
1064
  (botId) => lexicalSearchRawMessages2({
@@ -1097,6 +1066,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1097
1066
  keywords,
1098
1067
  limit: Math.ceil(limit / filters.length),
1099
1068
  botId,
1069
+ includeDeprecated,
1100
1070
  ...factTypes ? { factTypes } : {}
1101
1071
  })
1102
1072
  )
@@ -1127,7 +1097,9 @@ function searchInputToUnified(input) {
1127
1097
  mergeStrategy: input.mergeStrategy,
1128
1098
  peerFilter: input.peerFilter,
1129
1099
  reasoningStrategy: input.reasoningStrategy,
1130
- factTypes: input.factTypes
1100
+ factTypes: input.factTypes,
1101
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1102
+ includeDeprecated: input.includeDeprecated
1131
1103
  };
1132
1104
  }
1133
1105
  function mapHitToEvidenceSource(hit) {
@@ -1155,8 +1127,8 @@ function createUnifiedSearch(deps = {}) {
1155
1127
  }
1156
1128
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1157
1129
  }
1158
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1159
- if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
1130
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1131
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1160
1132
  warnings.push({
1161
1133
  source: "memory",
1162
1134
  code: "memory_iterative_planner_not_configured",
@@ -1172,7 +1144,8 @@ function createUnifiedSearch(deps = {}) {
1172
1144
  });
1173
1145
  if (reasoningInfo) reasoningInfo.degraded = true;
1174
1146
  }
1175
- if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
1147
+ let unionEvidence = null;
1148
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
1176
1149
  const planner = deps.reasoning.iterativePlanner;
1177
1150
  const result = await planner.plan({
1178
1151
  query: input.query,
@@ -1187,7 +1160,9 @@ function createUnifiedSearch(deps = {}) {
1187
1160
  keywords2,
1188
1161
  limit,
1189
1162
  logger,
1190
- peerPeers
1163
+ peerPeers,
1164
+ warnings,
1165
+ runtimeContext
1191
1166
  );
1192
1167
  let semanticHits = [];
1193
1168
  if (typeof deps.embedQuery === "function") {
@@ -1201,7 +1176,9 @@ function createUnifiedSearch(deps = {}) {
1201
1176
  limit,
1202
1177
  threshold,
1203
1178
  logger,
1204
- peerPeers
1179
+ peerPeers,
1180
+ warnings,
1181
+ runtimeContext
1205
1182
  );
1206
1183
  } catch (error) {
1207
1184
  logger.warn?.(
@@ -1229,16 +1206,24 @@ function createUnifiedSearch(deps = {}) {
1229
1206
  reasoningInfo.degraded = true;
1230
1207
  }
1231
1208
  }
1232
- const semantic2 = result.evidence.map((candidate) => ({
1209
+ const evidence = result.evidence.map((candidate) => ({
1233
1210
  type: "memory",
1234
1211
  id: candidate.id,
1235
1212
  content: candidate.content,
1236
1213
  similarity: candidate.similarity,
1237
- metadata: { ...candidate.metadata, reasoning: "iterative" }
1214
+ metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
1238
1215
  }));
1239
- return { semantic: semantic2, lexical: [] };
1216
+ if (reasoningStrategy === "iterative") {
1217
+ return { semantic: evidence, lexical: [] };
1218
+ }
1219
+ unionEvidence = evidence;
1240
1220
  }
1241
1221
  if (typeof deps.embedQuery !== "function") {
1222
+ warnings.push({
1223
+ source: "memory",
1224
+ code: "semantic_unavailable",
1225
+ message: "Semantic search is unavailable because no embedding provider is configured."
1226
+ });
1242
1227
  warnings.push({
1243
1228
  source: "memory",
1244
1229
  code: "memory_lexical_search_fallback",
@@ -1246,7 +1231,16 @@ function createUnifiedSearch(deps = {}) {
1246
1231
  });
1247
1232
  const keywords2 = deriveLexicalKeywords(input.query);
1248
1233
  const lexical2 = filterByDateRange(
1249
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1234
+ await runLexicalSearchForKeywords(
1235
+ deps,
1236
+ input,
1237
+ keywords2,
1238
+ limit,
1239
+ logger,
1240
+ peerPeers,
1241
+ warnings,
1242
+ runtimeContext
1243
+ ),
1250
1244
  input.dateFrom,
1251
1245
  input.dateTo
1252
1246
  );
@@ -1254,6 +1248,26 @@ function createUnifiedSearch(deps = {}) {
1254
1248
  }
1255
1249
  let semantic = [];
1256
1250
  const embedQuery = deps.embedQuery;
1251
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1252
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1253
+ );
1254
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1255
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1256
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1257
+ const hybrid = (await Promise.all(
1258
+ filters.map(
1259
+ (botId) => deps.searchRawMessagesHybrid?.({
1260
+ userId: input.userId,
1261
+ query: input.query,
1262
+ queryEmbedding,
1263
+ limit: Math.ceil(limit / filters.length),
1264
+ botId,
1265
+ ...runtimeContext ?? {}
1266
+ })
1267
+ )
1268
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1269
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1270
+ }
1257
1271
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1258
1272
  try {
1259
1273
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1272,7 +1286,17 @@ function createUnifiedSearch(deps = {}) {
1272
1286
  );
1273
1287
  const lists = await Promise.all(
1274
1288
  embeddings.map(
1275
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1289
+ (embedding) => runSemanticSearchForEmbedding(
1290
+ deps,
1291
+ input,
1292
+ embedding,
1293
+ limit,
1294
+ threshold,
1295
+ logger,
1296
+ peerPeers,
1297
+ warnings,
1298
+ runtimeContext
1299
+ )
1276
1300
  )
1277
1301
  );
1278
1302
  semantic = mergeByMaxScore(lists);
@@ -1295,7 +1319,9 @@ function createUnifiedSearch(deps = {}) {
1295
1319
  limit,
1296
1320
  threshold,
1297
1321
  logger,
1298
- peerPeers
1322
+ peerPeers,
1323
+ warnings,
1324
+ runtimeContext
1299
1325
  );
1300
1326
  }
1301
1327
  let lexical = [];
@@ -1314,7 +1340,8 @@ function createUnifiedSearch(deps = {}) {
1314
1340
  limit,
1315
1341
  botId,
1316
1342
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1317
- ...factTypes ? { factTypes } : {}
1343
+ ...factTypes ? { factTypes } : {},
1344
+ ...runtimeContext ?? {}
1318
1345
  })
1319
1346
  )
1320
1347
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1340,7 +1367,7 @@ function createUnifiedSearch(deps = {}) {
1340
1367
  let entity;
1341
1368
  if (typeof deps.entitySearch === "function") {
1342
1369
  entity = filterByDateRange(
1343
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1370
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1344
1371
  input.dateFrom,
1345
1372
  input.dateTo
1346
1373
  );
@@ -1362,6 +1389,37 @@ function createUnifiedSearch(deps = {}) {
1362
1389
  message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1363
1390
  });
1364
1391
  }
1392
+ if (unionEvidence) {
1393
+ const plannerHits = unionEvidence;
1394
+ const base = mergeByMaxScore([
1395
+ filterByDateRange(semantic, input.dateFrom, input.dateTo),
1396
+ filterByDateRange(lexical, input.dateFrom, input.dateTo)
1397
+ ]);
1398
+ const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
1399
+ const seen = /* @__PURE__ */ new Set();
1400
+ const merged = [];
1401
+ plannerHits.forEach((hit, i) => {
1402
+ if (seen.has(hit.id)) {
1403
+ return;
1404
+ }
1405
+ seen.add(hit.id);
1406
+ merged.push({
1407
+ ...hit,
1408
+ similarity: maxBase + (plannerHits.length - i) * 1e-6
1409
+ });
1410
+ });
1411
+ for (const hit of base) {
1412
+ if (merged.length >= limit) {
1413
+ break;
1414
+ }
1415
+ if (seen.has(hit.id)) {
1416
+ continue;
1417
+ }
1418
+ seen.add(hit.id);
1419
+ merged.push(hit);
1420
+ }
1421
+ return { semantic: merged.slice(0, limit), lexical: [] };
1422
+ }
1365
1423
  const out = {
1366
1424
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1367
1425
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
@@ -1371,14 +1429,16 @@ function createUnifiedSearch(deps = {}) {
1371
1429
  }
1372
1430
  return out;
1373
1431
  }
1374
- async function searchUnifiedMemory(input) {
1432
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1375
1433
  const query = input.query.trim();
1376
1434
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1377
1435
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1378
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1379
1436
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1380
1437
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1381
1438
  );
1439
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1440
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1441
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1382
1442
  const reasoningStrategy = resolveReasoningStrategy(input);
1383
1443
  const warnings = [];
1384
1444
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1398,23 +1458,46 @@ function createUnifiedSearch(deps = {}) {
1398
1458
  };
1399
1459
  }
1400
1460
  let memorySubs;
1461
+ let retrievalStatus;
1401
1462
  const reasoningInfo = { strategy: reasoningStrategy };
1402
1463
  if (input.dateFrom || input.dateTo) {
1403
1464
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1404
1465
  }
1405
1466
  if (sources.includes("memory")) {
1406
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1467
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1407
1468
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1408
1469
  try {
1409
1470
  memorySubs = await runMemorySource(
1410
1471
  input,
1411
- limit,
1412
- threshold,
1472
+ candidateLimit,
1473
+ memoryThreshold,
1413
1474
  warnings,
1414
1475
  reasoningStrategy,
1415
1476
  reasoningInfo,
1416
- peerPeers
1477
+ peerPeers,
1478
+ runtimeContext
1417
1479
  );
1480
+ memorySubs = {
1481
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1482
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1483
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1484
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1485
+ };
1486
+ if (deps.getRawMessageRetrievalStatus) {
1487
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1488
+ }
1489
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1490
+ let code = "semantic_candidates_empty";
1491
+ let message = "Semantic search was available but returned no candidates.";
1492
+ if (retrievalStatus?.embeddedChildCount === 0) {
1493
+ code = "semantic_unavailable";
1494
+ message = "No embedded RawMessage children are available for semantic search.";
1495
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1496
+ code = "semantic_dimension_mismatch";
1497
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1498
+ }
1499
+ warnings.push({ source: "memory", code, message });
1500
+ }
1418
1501
  } catch (error) {
1419
1502
  logger.warn?.("[memory-store] memory source failed:", error);
1420
1503
  warnings.push({
@@ -1437,12 +1520,13 @@ function createUnifiedSearch(deps = {}) {
1437
1520
  const hits = await deps.searchInsights({
1438
1521
  userId: input.userId,
1439
1522
  query: input.query,
1440
- limit,
1523
+ limit: candidateLimit,
1441
1524
  threshold,
1442
1525
  botIds: input.botIds,
1443
1526
  includeArchived: input.includeArchivedInsights,
1444
1527
  authToken: input.authToken,
1445
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1528
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1529
+ ...runtimeContext ?? {}
1446
1530
  });
1447
1531
  for (const hit of hits) {
1448
1532
  insightHits.push({
@@ -1469,12 +1553,13 @@ function createUnifiedSearch(deps = {}) {
1469
1553
  userId: input.userId,
1470
1554
  query: input.query,
1471
1555
  options: {
1472
- limit,
1556
+ limit: candidateLimit,
1473
1557
  threshold,
1474
1558
  documentIds: input.documentIds
1475
1559
  },
1476
1560
  authToken: input.authToken,
1477
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1561
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1562
+ ...runtimeContext ?? {}
1478
1563
  });
1479
1564
  for (const hit of hits) {
1480
1565
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1492,10 +1577,14 @@ function createUnifiedSearch(deps = {}) {
1492
1577
  memorySubs,
1493
1578
  insightHits,
1494
1579
  knowledgeHits,
1495
- limit,
1580
+ limit: candidateLimit,
1496
1581
  strategy: mergeStrategy
1497
1582
  });
1498
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1583
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1584
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1585
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1586
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1587
+ const ranked = reranked.slice(0, limit);
1499
1588
  const output = {
1500
1589
  query: input.query,
1501
1590
  sources,
@@ -1503,28 +1592,51 @@ function createUnifiedSearch(deps = {}) {
1503
1592
  count: ranked.length,
1504
1593
  warnings
1505
1594
  };
1595
+ if (input.includeRetrievalDiagnostics) {
1596
+ output.retrievalDiagnostics = {
1597
+ mergeStrategy,
1598
+ candidateLimit,
1599
+ backend: retrievalStatus?.backend,
1600
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1601
+ candidateCounts: {
1602
+ semantic: memorySubs?.semantic.length ?? 0,
1603
+ lexical: memorySubs?.lexical.length ?? 0,
1604
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1605
+ entity: memorySubs?.entity?.length ?? 0,
1606
+ fused: merged.length,
1607
+ final: ranked.length
1608
+ },
1609
+ channels: {
1610
+ semantic: memorySubs?.semantic ?? [],
1611
+ lexical: memorySubs?.lexical ?? [],
1612
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1613
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1614
+ },
1615
+ fusedBeforeRerank: merged,
1616
+ reranker: {
1617
+ enabled: Boolean(deps.reranker),
1618
+ provider: deps.rerankerInfo?.provider,
1619
+ model: deps.rerankerInfo?.model,
1620
+ inputCount: merged.length,
1621
+ outputCount: reranked.length,
1622
+ latencyMs: rerankerLatencyMs,
1623
+ orderChanged: rerankerOrderChanged
1624
+ },
1625
+ final: ranked
1626
+ };
1627
+ }
1506
1628
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1507
1629
  output.reasoning = reasoningInfo;
1508
1630
  }
1509
1631
  return output;
1510
1632
  }
1633
+ async function searchUnifiedMemory(input) {
1634
+ return searchUnifiedMemoryWithRuntime(input);
1635
+ }
1511
1636
  async function searchRawMemorySemantically(input) {
1512
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1513
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1514
- const reasoningStrategy = resolveReasoningStrategy(input);
1515
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1516
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1517
- );
1518
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1519
- return mergeAcrossSources({
1520
- memorySubs: sub,
1521
- insightHits: [],
1522
- knowledgeHits: [],
1523
- limit,
1524
- strategy: mergeStrategy
1525
- });
1637
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1526
1638
  }
1527
- async function search(input) {
1639
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1528
1640
  const query = input.query.trim();
1529
1641
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1530
1642
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1556,19 +1668,24 @@ function createUnifiedSearch(deps = {}) {
1556
1668
  let unifiedHits = [];
1557
1669
  let sources = input.sources ? [...input.sources] : [];
1558
1670
  let unifiedReasoning;
1671
+ let retrievalDiagnostics;
1559
1672
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1560
1673
  const inferredSources = sources.length > 0 ? sources : [
1561
1674
  ...wantsRawTier ? ["memory"] : [],
1562
1675
  ...wantsInsightTier ? ["insights"] : [],
1563
1676
  ...wantsKnowledgeTier ? ["knowledge"] : []
1564
1677
  ];
1565
- const unified = await searchUnifiedMemory({
1566
- ...searchInputToUnified(input),
1567
- sources: inferredSources.length > 0 ? inferredSources : void 0
1568
- });
1678
+ const unified = await searchUnifiedMemoryWithRuntime(
1679
+ {
1680
+ ...searchInputToUnified(input),
1681
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1682
+ },
1683
+ runtimeContext
1684
+ );
1569
1685
  unifiedHits = unified.results;
1570
1686
  sources = unified.sources;
1571
1687
  unifiedReasoning = unified.reasoning;
1688
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1572
1689
  warnings.push(...unified.warnings);
1573
1690
  }
1574
1691
  let summaryHits = [];
@@ -1579,7 +1696,8 @@ function createUnifiedSearch(deps = {}) {
1579
1696
  limit,
1580
1697
  threshold,
1581
1698
  logger,
1582
- peerScope.peers
1699
+ peerScope.peers,
1700
+ runtimeContext
1583
1701
  );
1584
1702
  warnings.push(...summaryBucket.warnings);
1585
1703
  summaryHits = summaryBucket.hits;
@@ -1604,7 +1722,8 @@ function createUnifiedSearch(deps = {}) {
1604
1722
  source: mapHitToEvidenceSource(hit),
1605
1723
  snippet: hit.content,
1606
1724
  score: hit.similarity,
1607
- timestamp: getCandidateTimestamp(hit.metadata)
1725
+ timestamp: getCandidateTimestamp(hit.metadata),
1726
+ metadata: hit.metadata
1608
1727
  }));
1609
1728
  const base = {
1610
1729
  query,
@@ -1617,12 +1736,15 @@ function createUnifiedSearch(deps = {}) {
1617
1736
  if (unifiedReasoning) {
1618
1737
  base.reasoning = unifiedReasoning;
1619
1738
  }
1739
+ if (retrievalDiagnostics) {
1740
+ base.retrievalDiagnostics = retrievalDiagnostics;
1741
+ }
1620
1742
  if (!wantsSynthesis) {
1621
1743
  return base;
1622
1744
  }
1623
1745
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1624
1746
  query,
1625
- hits,
1747
+ evidence,
1626
1748
  responseSchema: synthesisSchema,
1627
1749
  deps,
1628
1750
  logger
@@ -1631,6 +1753,10 @@ function createUnifiedSearch(deps = {}) {
1631
1753
  base.warnings = [...base.warnings, ...synthWarnings];
1632
1754
  return base;
1633
1755
  }
1756
+ async function search(input, runtimeContext) {
1757
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1758
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1759
+ }
1634
1760
  return {
1635
1761
  search,
1636
1762
  searchUnifiedMemory,
@@ -1646,6 +1772,7 @@ function mergeAcrossSources(input) {
1646
1772
  for (const hit of [
1647
1773
  ...memorySubs.semantic,
1648
1774
  ...memorySubs.lexical,
1775
+ ...memorySubs.hybrid ?? [],
1649
1776
  ...input.insightHits,
1650
1777
  ...input.knowledgeHits
1651
1778
  ]) {
@@ -1661,6 +1788,7 @@ function mergeAcrossSources(input) {
1661
1788
  const all = [
1662
1789
  ...memorySubs.semantic,
1663
1790
  ...memorySubs.lexical,
1791
+ ...memorySubs.hybrid ?? [],
1664
1792
  ...memorySubs.entity ?? [],
1665
1793
  ...input.insightHits,
1666
1794
  ...input.knowledgeHits
@@ -1672,6 +1800,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1672
1800
  const lists = [];
1673
1801
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1674
1802
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1803
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1804
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1805
+ }
1675
1806
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1676
1807
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1677
1808
  }
@@ -2136,8 +2267,250 @@ function createVsaRecall(storage) {
2136
2267
  };
2137
2268
  }
2138
2269
 
2270
+ // ../shared/src/tokens.ts
2271
+ var estimateTokens = (text) => {
2272
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
2273
+ const otherChars = text.length - chineseChars;
2274
+ const wordCount = Math.ceil(otherChars / 5);
2275
+ return chineseChars + wordCount;
2276
+ };
2277
+
2278
+ // ../shared/src/text-chunking.ts
2279
+ var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
2280
+ var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
2281
+ var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
2282
+ var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
2283
+ function largestEndWithinTokenBudget(text, start, maxTokens) {
2284
+ let low = start + 1;
2285
+ let high = text.length;
2286
+ let best = low;
2287
+ while (low <= high) {
2288
+ const middle = Math.floor((low + high) / 2);
2289
+ if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
2290
+ best = middle;
2291
+ low = middle + 1;
2292
+ } else {
2293
+ high = middle - 1;
2294
+ }
2295
+ }
2296
+ return best;
2297
+ }
2298
+ function preferNaturalEnd(text, start, hardEnd) {
2299
+ if (hardEnd >= text.length) return text.length;
2300
+ const searchStart = start + Math.floor((hardEnd - start) * 0.6);
2301
+ const candidate = text.slice(searchStart, hardEnd);
2302
+ for (const boundary of PARAGRAPH_BOUNDARIES) {
2303
+ const index = candidate.lastIndexOf(boundary);
2304
+ if (index >= 0) return searchStart + index + boundary.length;
2305
+ }
2306
+ for (let offset = candidate.length; offset > 0; offset -= 1) {
2307
+ const prefix = candidate.slice(0, offset);
2308
+ if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
2309
+ }
2310
+ for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
2311
+ if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
2312
+ }
2313
+ return hardEnd;
2314
+ }
2315
+ function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
2316
+ if (overlapTokens <= 0) return chunkEnd;
2317
+ let low = chunkStart + 1;
2318
+ let high = chunkEnd;
2319
+ let best = chunkEnd;
2320
+ while (low <= high) {
2321
+ const middle = Math.floor((low + high) / 2);
2322
+ if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
2323
+ best = middle;
2324
+ high = middle - 1;
2325
+ } else {
2326
+ low = middle + 1;
2327
+ }
2328
+ }
2329
+ return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
2330
+ }
2331
+ function chunkTextByEstimatedTokens(text, options = {}) {
2332
+ if (text.length === 0) return [];
2333
+ const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
2334
+ const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
2335
+ if (overlapTokens >= maxTokens) {
2336
+ throw new Error("overlapTokens must be smaller than maxTokens");
2337
+ }
2338
+ const chunks = [];
2339
+ let start = 0;
2340
+ while (start < text.length) {
2341
+ const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
2342
+ const end = preferNaturalEnd(text, start, hardEnd);
2343
+ chunks.push({
2344
+ chunkIndex: chunks.length,
2345
+ startPosition: start,
2346
+ endPosition: end,
2347
+ content: text.slice(start, end)
2348
+ });
2349
+ if (end >= text.length) break;
2350
+ start = overlapStart(text, start, end, overlapTokens);
2351
+ }
2352
+ return chunks;
2353
+ }
2354
+
2355
+ // src/embed-on-insert.ts
2356
+ var AUTO_APPLIED_WARNING = {
2357
+ code: "embed_on_insert_auto_applied",
2358
+ message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
2359
+ };
2360
+ var SEMANTIC_UNAVAILABLE_WARNING = {
2361
+ code: "semantic_index_unavailable",
2362
+ message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
2363
+ };
2364
+ function sha256(value) {
2365
+ return createHash("sha256").update(value).digest("hex");
2366
+ }
2367
+ function isUsableEmbedding(embedding) {
2368
+ return Boolean(
2369
+ embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
2370
+ );
2371
+ }
2372
+ async function embedText(text, userId, deps) {
2373
+ if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
2374
+ const embedding = await deps.embedQuery({ userId, query: text });
2375
+ return isUsableEmbedding(embedding) ? embedding : void 0;
2376
+ }
2377
+ async function embedMissingMessages(messages, deps) {
2378
+ if (typeof deps.embedQuery !== "function") return messages;
2379
+ const out = [];
2380
+ for (const message of messages) {
2381
+ if (isUsableEmbedding(message.embedding) || !message.content) {
2382
+ out.push(message);
2383
+ continue;
2384
+ }
2385
+ const embedding = await embedText(message.content, message.userId, deps);
2386
+ out.push(
2387
+ embedding ? {
2388
+ ...message,
2389
+ embedding,
2390
+ embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
2391
+ embeddingContentHash: sha256(message.content),
2392
+ embeddingDimensions: embedding.length,
2393
+ embeddingUpdatedAt: Date.now()
2394
+ } : message
2395
+ );
2396
+ }
2397
+ return out;
2398
+ }
2399
+ async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
2400
+ const deps = unified ?? {};
2401
+ const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
2402
+ const shouldEmbed = embedOnInsert === true || embedderAvailable;
2403
+ const messages = [];
2404
+ const chunks = [];
2405
+ let missingSemanticEmbedding = false;
2406
+ let generatedEmbedding = false;
2407
+ const plans = incoming.map((message) => ({
2408
+ message,
2409
+ pieces: chunkTextByEstimatedTokens(message.content)
2410
+ }));
2411
+ const generatedEmbeddings = /* @__PURE__ */ new Map();
2412
+ const pendingByUser = /* @__PURE__ */ new Map();
2413
+ for (const [messageIndex, plan] of plans.entries()) {
2414
+ for (const piece of plan.pieces) {
2415
+ const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
2416
+ if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
2417
+ const pending = pendingByUser.get(plan.message.userId) ?? [];
2418
+ pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
2419
+ pendingByUser.set(plan.message.userId, pending);
2420
+ }
2421
+ }
2422
+ for (const [userId, pending] of pendingByUser) {
2423
+ let embeddings;
2424
+ if (typeof deps.embedDocuments === "function") {
2425
+ const batch = await deps.embedDocuments({
2426
+ userId,
2427
+ texts: pending.map((item) => item.text)
2428
+ });
2429
+ if (batch.length !== pending.length) {
2430
+ throw new Error(
2431
+ `Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
2432
+ );
2433
+ }
2434
+ embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
2435
+ } else {
2436
+ embeddings = [];
2437
+ for (const item of pending) {
2438
+ embeddings.push(await embedText(item.text, userId, deps));
2439
+ }
2440
+ }
2441
+ for (const [index, item] of pending.entries()) {
2442
+ const embedding = embeddings[index];
2443
+ if (embedding) generatedEmbeddings.set(item.key, embedding);
2444
+ }
2445
+ }
2446
+ for (const [messageIndex, plan] of plans.entries()) {
2447
+ const incomingMessage = plan.message;
2448
+ const pieces = plan.pieces;
2449
+ let message = incomingMessage;
2450
+ for (const piece of pieces) {
2451
+ const contentHash = sha256(piece.content);
2452
+ const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
2453
+ const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
2454
+ generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
2455
+ if (!embedding) missingSemanticEmbedding = true;
2456
+ const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
2457
+ chunks.push({
2458
+ chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
2459
+ messageId: incomingMessage.messageId,
2460
+ userId: incomingMessage.userId,
2461
+ chunkIndex: piece.chunkIndex,
2462
+ chunkCount: pieces.length,
2463
+ startPosition: piece.startPosition,
2464
+ endPosition: piece.endPosition,
2465
+ content: piece.content,
2466
+ contentHash,
2467
+ embedding,
2468
+ embeddingModel,
2469
+ embeddingDimensions: embedding?.length,
2470
+ embeddingUpdatedAt: embedding ? Date.now() : void 0
2471
+ });
2472
+ if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
2473
+ message = {
2474
+ ...incomingMessage,
2475
+ embedding,
2476
+ embeddingModel,
2477
+ embeddingContentHash: contentHash,
2478
+ embeddingDimensions: embedding.length,
2479
+ embeddingUpdatedAt: Date.now()
2480
+ };
2481
+ }
2482
+ }
2483
+ messages.push(message);
2484
+ }
2485
+ const warnings = [];
2486
+ if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
2487
+ if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
2488
+ return { messages, chunks, warnings };
2489
+ }
2490
+
2491
+ // src/storage/raw-message-ingest.ts
2492
+ async function persistRawMessages(input) {
2493
+ const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
2494
+ let ids;
2495
+ if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
2496
+ ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
2497
+ } else {
2498
+ const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
2499
+ if (typeof input.manager.upsertRawMessages === "function") {
2500
+ await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
2501
+ } else if (typeof input.manager.storeMessages === "function") {
2502
+ ids = await input.manager.storeMessages(legacyMessages);
2503
+ } else {
2504
+ throw new Error("active raw-message manager exposes no supported write method");
2505
+ }
2506
+ }
2507
+ if (input.externalIndex) {
2508
+ await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
2509
+ }
2510
+ return { count: prepared.messages.length, ids, warnings: prepared.warnings };
2511
+ }
2512
+
2139
2513
  // src/http.ts
2140
- init_chroma_memory_index();
2141
2514
  init_raw_message_store();
2142
2515
  init_sqlite_raw_message_store();
2143
2516
  async function startHttpServer(options = {}) {
@@ -2167,7 +2540,18 @@ async function startHttpServer(options = {}) {
2167
2540
  const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
2168
2541
  const vsa = createVsaRecall(vsaStorage);
2169
2542
  const app = new Hono();
2170
- app.get("/health", (c) => c.json({ ok: true, store: "memory", ts: Date.now() }));
2543
+ app.get("/health", async (c) => {
2544
+ const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
2545
+ backend: "unknown",
2546
+ childCount: 0,
2547
+ embeddedChildCount: 0,
2548
+ indexedDimensions: [],
2549
+ semanticReady: false,
2550
+ lexicalReady: false,
2551
+ semanticDegradedReason: "health_check_failed"
2552
+ })) : void 0;
2553
+ return c.json({ ok: true, store: "memory", ts: Date.now(), retrieval });
2554
+ });
2171
2555
  app.post("/v1/search", async (c) => {
2172
2556
  const body = await c.req.json().catch(() => ({}));
2173
2557
  const userId = typeof body.userId === "string" ? body.userId : null;
@@ -2190,7 +2574,9 @@ async function startHttpServer(options = {}) {
2190
2574
  authToken: typeof body.authToken === "string" ? body.authToken : void 0,
2191
2575
  dateFrom: typeof body.dateFrom === "string" ? body.dateFrom : void 0,
2192
2576
  dateTo: typeof body.dateTo === "string" ? body.dateTo : void 0,
2193
- reasoningStrategy: body.reasoningStrategy === "rewrite" || body.reasoningStrategy === "iterative" ? body.reasoningStrategy : void 0,
2577
+ reasoningStrategy: body.reasoningStrategy === "rewrite" || body.reasoningStrategy === "iterative" || body.reasoningStrategy === "union" ? body.reasoningStrategy : void 0,
2578
+ mergeStrategy: body.mergeStrategy === "rrf" || body.mergeStrategy === "similarity" ? body.mergeStrategy : void 0,
2579
+ includeRetrievalDiagnostics: body.includeRetrievalDiagnostics === true,
2194
2580
  ...wantsSynthesis ? {
2195
2581
  synthesize: {
2196
2582
  ...responseSchema ? { responseSchema } : {}
@@ -2294,57 +2680,20 @@ async function startHttpServer(options = {}) {
2294
2680
  ...m,
2295
2681
  userId: m.userId ?? body.userId
2296
2682
  }));
2297
- const { messages, warnings } = await applyEmbedOnInsertPolicy(
2298
- incoming,
2299
- body.embedOnInsert,
2300
- options.unified
2301
- );
2302
- let result;
2303
- if (typeof manager3.upsertRawMessages === "function") {
2304
- result = await manager3.upsertRawMessages({
2305
- userId: body.userId,
2306
- messages
2307
- });
2308
- } else if (typeof manager3.storeMessages === "function") {
2309
- const ids = await manager3.storeMessages(messages);
2310
- result = { inserted: ids.length, ids };
2311
- } else {
2312
- return c.json(
2313
- {
2314
- error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
2315
- },
2316
- 500
2317
- );
2318
- }
2319
- try {
2320
- await upsertRawMessagesToChroma(messages);
2321
- } catch (error) {
2322
- console.warn("[memory-store/http] chroma upsert failed:", error);
2323
- }
2324
- try {
2325
- const messagesWithEmbeddings = messages.filter(
2326
- (m) => Array.isArray(m.embedding) && m.embedding.length > 0
2327
- );
2328
- if (messagesWithEmbeddings.length > 0) {
2329
- for (const message of messagesWithEmbeddings) {
2330
- if (typeof manager3.upsertVectorForMessage === "function") {
2331
- manager3.upsertVectorForMessage(message.messageId, message.embedding);
2332
- }
2333
- }
2334
- console.log(
2335
- "[memory-store/http] Updated sqlite-vec vector table for",
2336
- messagesWithEmbeddings.length,
2337
- "message(s)"
2338
- );
2339
- }
2340
- } catch (error) {
2341
- console.warn("[memory-store/http] sqlite-vec vector update failed:", error);
2342
- }
2683
+ const persisted = await persistRawMessages({
2684
+ manager: manager3,
2685
+ userId: body.userId,
2686
+ messages: incoming,
2687
+ embedOnInsert: body.embedOnInsert,
2688
+ unified: options.unified,
2689
+ externalIndex: options.unified?.rawMessageChildIndex
2690
+ });
2691
+ const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
2343
2692
  return c.json({
2344
2693
  ok: true,
2345
- count: messages.length,
2694
+ count: persisted.count,
2346
2695
  result,
2347
- ...warnings.length > 0 ? { warnings } : {}
2696
+ ...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
2348
2697
  });
2349
2698
  });
2350
2699
  app.get("/v1/raw-messages/:id", async (c) => {
@@ -2443,7 +2792,17 @@ async function startHttpServer(options = {}) {
2443
2792
  });
2444
2793
  const okfHttpSpecifier = "@melandlabs/okf/http";
2445
2794
  const { registerOkfRoutes } = await import(okfHttpSpecifier);
2446
- registerOkfRoutes(app, rawStore);
2795
+ registerOkfRoutes(app, rawStore, {
2796
+ writeMessages: async (userId, messages) => {
2797
+ await persistRawMessages({
2798
+ manager: manager2,
2799
+ userId,
2800
+ messages,
2801
+ unified: options.unified,
2802
+ externalIndex: options.unified?.rawMessageChildIndex
2803
+ });
2804
+ }
2805
+ });
2447
2806
  const server = serve({ fetch: app.fetch, port, hostname: host });
2448
2807
  return {
2449
2808
  url: `http://${host}:${port}`,