@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/index.js CHANGED
@@ -260,6 +260,69 @@ var init_raw_message_store = __esm({
260
260
  init_chroma_memory_index();
261
261
  init_raw_message_store();
262
262
 
263
+ // src/search/applicability.ts
264
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
265
+ "global",
266
+ "task",
267
+ "conversation",
268
+ "channel",
269
+ "project",
270
+ "custom"
271
+ ]);
272
+ function isRecord(value) {
273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
274
+ }
275
+ function invalidRuntimeContext(message) {
276
+ throw new TypeError(`memory-store search runtime context: ${message}`);
277
+ }
278
+ function validateApplicabilityContext(value, index) {
279
+ const path = `applicabilityContexts[${index}]`;
280
+ if (!isRecord(value)) {
281
+ invalidRuntimeContext(`${path} must be an object`);
282
+ }
283
+ const scope = value.scope;
284
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
285
+ invalidRuntimeContext(`${path}.scope is not supported`);
286
+ }
287
+ if (value.key !== void 0 && typeof value.key !== "string") {
288
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
289
+ }
290
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
291
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
292
+ }
293
+ for (const field of ["validFrom", "validUntil"]) {
294
+ const boundary = value[field];
295
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
296
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
297
+ }
298
+ }
299
+ }
300
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
301
+ if (runtimeContext === void 0) {
302
+ return void 0;
303
+ }
304
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
305
+ invalidRuntimeContext("applicabilityContexts must be an array");
306
+ }
307
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
308
+ validateApplicabilityContext(context, index);
309
+ }
310
+ let applicabilityAt;
311
+ if (input.asOf === void 0) {
312
+ applicabilityAt = now();
313
+ } else {
314
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
315
+ if (!Number.isFinite(parsed)) {
316
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
317
+ }
318
+ applicabilityAt = parsed;
319
+ }
320
+ return {
321
+ applicabilityContexts: runtimeContext.applicabilityContexts,
322
+ applicabilityAt
323
+ };
324
+ }
325
+
263
326
  // src/search/utilities.ts
264
327
  function deriveLexicalKeywords(query) {
265
328
  return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
@@ -418,6 +481,7 @@ function materializeSignals(lists, hit) {
418
481
  function listNameToChannel(name) {
419
482
  if (name === "memory-semantic") return "semantic";
420
483
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
484
+ if (name === "memory-hybrid") return "hybrid";
421
485
  if (name === "memory-entity") return "entity";
422
486
  return void 0;
423
487
  }
@@ -627,7 +691,6 @@ async function applyReranker(reranker, query, results) {
627
691
 
628
692
  // src/search/gather-evidence.ts
629
693
  var DEFAULT_TIERS2 = ["summary", "raw", "insight", "knowledge"];
630
- var MAX_EVIDENCE_CHARS = 600;
631
694
  function toSummaryResult(row) {
632
695
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
633
696
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -645,7 +708,7 @@ function toSummaryResult(row) {
645
708
  }
646
709
  };
647
710
  }
648
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
711
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
649
712
  if (typeof deps.searchSummaries !== "function") {
650
713
  return { hits: [], warnings: [] };
651
714
  }
@@ -658,7 +721,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
658
721
  limit,
659
722
  threshold,
660
723
  authToken: input.authToken,
661
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
724
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
725
+ ...runtimeContext ?? {}
662
726
  });
663
727
  const hits = rows.map(toSummaryResult);
664
728
  return { hits, warnings: [] };
@@ -676,12 +740,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
676
740
  };
677
741
  }
678
742
  }
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
743
  function buildSynthesisPrompt(input) {
686
744
  const grouped = /* @__PURE__ */ new Map();
687
745
  for (const item of input.evidence) {
@@ -695,9 +753,10 @@ function buildSynthesisPrompt(input) {
695
753
  if (items.length === 0) {
696
754
  continue;
697
755
  }
698
- const lines = items.map(
699
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
700
- );
756
+ const lines = items.map((item, index) => {
757
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
758
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
759
+ });
701
760
  sections.push(`## ${tier}
702
761
  ${lines.join("\n")}`);
703
762
  }
@@ -779,12 +838,15 @@ function coerceAnswer(text, schema) {
779
838
  return text;
780
839
  }
781
840
  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
841
+ const { query, responseSchema, deps, logger } = input;
842
+ const evidence = input.evidence.map((item) => ({
843
+ id: item.id,
844
+ source: mapEvidenceSourceToTier(item.source),
845
+ snippet: item.snippet,
846
+ score: item.score,
847
+ // Forward per-fact provenance (e.g. `metadata.source` from
848
+ // `opencontext add --source`) so the synthesis prompt can cite it.
849
+ metadata: item.metadata
788
850
  }));
789
851
  const warnings = [];
790
852
  const complete = deps.reasoning?.complete;
@@ -815,21 +877,10 @@ async function synthesizeAnswer(input) {
815
877
  return { answer: "", warnings };
816
878
  }
817
879
  }
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;
880
+ function mapEvidenceSourceToTier(source) {
881
+ if (source === "memory") return "raw";
882
+ if (source === "insights") return "insight";
883
+ return source;
833
884
  }
834
885
  async function resolveSearchScopePeers(input) {
835
886
  return resolveScopePeer({
@@ -853,7 +904,7 @@ function deriveEntityKeywords(query) {
853
904
  }
854
905
  return out.slice(0, 16);
855
906
  }
856
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
907
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
857
908
  if (typeof deps.entitySearch !== "function") {
858
909
  return [];
859
910
  }
@@ -871,7 +922,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
871
922
  keywords,
872
923
  limit: Math.ceil(limit / filters.length),
873
924
  botId,
874
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
925
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
926
+ ...runtimeContext ?? {}
875
927
  })
876
928
  )
877
929
  )).flat();
@@ -959,6 +1011,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
959
1011
  return true;
960
1012
  });
961
1013
  }
1014
+ function warnApplicabilityNotEnforced(warnings) {
1015
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
1016
+ return;
1017
+ }
1018
+ warnings.push({
1019
+ source: "memory",
1020
+ code: "memory_applicability_not_enforced",
1021
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
1022
+ });
1023
+ }
962
1024
  async function embedQueryVariant(embedQuery, input, query) {
963
1025
  return embedQuery({
964
1026
  userId: input.userId,
@@ -966,10 +1028,29 @@ async function embedQueryVariant(embedQuery, input, query) {
966
1028
  authToken: input.authToken
967
1029
  });
968
1030
  }
969
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
1031
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
970
1032
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
971
1033
  let semantic = [];
972
- if (isRawMessageChromaEnabled()) {
1034
+ if (typeof deps.searchRawMessagesAnn === "function") {
1035
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
1036
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1037
+ const includeDeprecated = input.includeDeprecated === true;
1038
+ semantic = (await Promise.all(
1039
+ filters.map(
1040
+ (filter) => searchRawMessagesAnn({
1041
+ userId: input.userId,
1042
+ queryEmbedding,
1043
+ limit,
1044
+ threshold,
1045
+ botId: "botId" in filter ? filter.botId : void 0,
1046
+ includeDeprecated,
1047
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1048
+ ...factTypes ? { factTypes } : {},
1049
+ ...runtimeContext ?? {}
1050
+ })
1051
+ )
1052
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1053
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
973
1054
  try {
974
1055
  semantic = (await Promise.all(
975
1056
  filters.map((filter) => {
@@ -994,24 +1075,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
994
1075
  error
995
1076
  );
996
1077
  }
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
1078
  }
1014
- if (semantic.length === 0) {
1079
+ if (semantic.length === 0 && runtimeContext !== void 0) {
1080
+ warnApplicabilityNotEnforced(warnings);
1081
+ return [];
1082
+ }
1083
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1015
1084
  try {
1016
1085
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1017
1086
  const manager2 = await getRawMessageManager2();
@@ -1022,6 +1091,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1022
1091
  queryEmbedding,
1023
1092
  limit,
1024
1093
  threshold,
1094
+ includeDeprecated: input.includeDeprecated === true,
1025
1095
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1026
1096
  ...factTypes ? { factTypes } : {}
1027
1097
  });
@@ -1051,6 +1121,18 @@ function mergeByMaxScore(lists) {
1051
1121
  }
1052
1122
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1053
1123
  }
1124
+ function dedupeChannelByParent(hits) {
1125
+ const strongest = /* @__PURE__ */ new Map();
1126
+ for (const hit of hits) {
1127
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1128
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1129
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1130
+ const key = `${normalized.type}:${parentId}`;
1131
+ const current = strongest.get(key);
1132
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1133
+ }
1134
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1135
+ }
1054
1136
  function toIterativeRecallCandidate(hit) {
1055
1137
  return {
1056
1138
  id: hit.id,
@@ -1059,7 +1141,7 @@ function toIterativeRecallCandidate(hit) {
1059
1141
  metadata: hit.metadata
1060
1142
  };
1061
1143
  }
1062
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1144
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1063
1145
  if (keywords.length === 0) {
1064
1146
  return [];
1065
1147
  }
@@ -1068,6 +1150,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1068
1150
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1069
1151
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1070
1152
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1153
+ const includeDeprecated = input.includeDeprecated === true;
1071
1154
  return (await Promise.all(
1072
1155
  filters.map(
1073
1156
  (botId) => searchRawMessagesLexical({
@@ -1075,8 +1158,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1075
1158
  keywords,
1076
1159
  limit: Math.ceil(limit / filters.length),
1077
1160
  botId,
1161
+ includeDeprecated,
1078
1162
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1079
- ...factTypes ? { factTypes } : {}
1163
+ ...factTypes ? { factTypes } : {},
1164
+ ...runtimeContext ?? {}
1080
1165
  })
1081
1166
  )
1082
1167
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1087,10 +1172,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1087
1172
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1088
1173
  }
1089
1174
  }
1175
+ if (runtimeContext !== void 0) {
1176
+ warnApplicabilityNotEnforced(warnings);
1177
+ return [];
1178
+ }
1090
1179
  try {
1091
1180
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1092
1181
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1093
1182
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1183
+ const includeDeprecated = input.includeDeprecated === true;
1094
1184
  return (await Promise.all(
1095
1185
  filters.map(
1096
1186
  (botId) => lexicalSearchRawMessages2({
@@ -1098,6 +1188,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1098
1188
  keywords,
1099
1189
  limit: Math.ceil(limit / filters.length),
1100
1190
  botId,
1191
+ includeDeprecated,
1101
1192
  ...factTypes ? { factTypes } : {}
1102
1193
  })
1103
1194
  )
@@ -1128,7 +1219,9 @@ function searchInputToUnified(input) {
1128
1219
  mergeStrategy: input.mergeStrategy,
1129
1220
  peerFilter: input.peerFilter,
1130
1221
  reasoningStrategy: input.reasoningStrategy,
1131
- factTypes: input.factTypes
1222
+ factTypes: input.factTypes,
1223
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1224
+ includeDeprecated: input.includeDeprecated
1132
1225
  };
1133
1226
  }
1134
1227
  function mapHitToEvidenceSource(hit) {
@@ -1156,7 +1249,7 @@ function createUnifiedSearch(deps = {}) {
1156
1249
  }
1157
1250
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1158
1251
  }
1159
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1252
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1160
1253
  if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1161
1254
  warnings.push({
1162
1255
  source: "memory",
@@ -1189,7 +1282,9 @@ function createUnifiedSearch(deps = {}) {
1189
1282
  keywords2,
1190
1283
  limit,
1191
1284
  logger,
1192
- peerPeers
1285
+ peerPeers,
1286
+ warnings,
1287
+ runtimeContext
1193
1288
  );
1194
1289
  let semanticHits = [];
1195
1290
  if (typeof deps.embedQuery === "function") {
@@ -1203,7 +1298,9 @@ function createUnifiedSearch(deps = {}) {
1203
1298
  limit,
1204
1299
  threshold,
1205
1300
  logger,
1206
- peerPeers
1301
+ peerPeers,
1302
+ warnings,
1303
+ runtimeContext
1207
1304
  );
1208
1305
  } catch (error) {
1209
1306
  logger.warn?.(
@@ -1244,6 +1341,11 @@ function createUnifiedSearch(deps = {}) {
1244
1341
  unionEvidence = evidence;
1245
1342
  }
1246
1343
  if (typeof deps.embedQuery !== "function") {
1344
+ warnings.push({
1345
+ source: "memory",
1346
+ code: "semantic_unavailable",
1347
+ message: "Semantic search is unavailable because no embedding provider is configured."
1348
+ });
1247
1349
  warnings.push({
1248
1350
  source: "memory",
1249
1351
  code: "memory_lexical_search_fallback",
@@ -1251,7 +1353,16 @@ function createUnifiedSearch(deps = {}) {
1251
1353
  });
1252
1354
  const keywords2 = deriveLexicalKeywords(input.query);
1253
1355
  const lexical2 = filterByDateRange(
1254
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1356
+ await runLexicalSearchForKeywords(
1357
+ deps,
1358
+ input,
1359
+ keywords2,
1360
+ limit,
1361
+ logger,
1362
+ peerPeers,
1363
+ warnings,
1364
+ runtimeContext
1365
+ ),
1255
1366
  input.dateFrom,
1256
1367
  input.dateTo
1257
1368
  );
@@ -1259,6 +1370,26 @@ function createUnifiedSearch(deps = {}) {
1259
1370
  }
1260
1371
  let semantic = [];
1261
1372
  const embedQuery = deps.embedQuery;
1373
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1374
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1375
+ );
1376
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1377
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1378
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1379
+ const hybrid = (await Promise.all(
1380
+ filters.map(
1381
+ (botId) => deps.searchRawMessagesHybrid?.({
1382
+ userId: input.userId,
1383
+ query: input.query,
1384
+ queryEmbedding,
1385
+ limit: Math.ceil(limit / filters.length),
1386
+ botId,
1387
+ ...runtimeContext ?? {}
1388
+ })
1389
+ )
1390
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1391
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1392
+ }
1262
1393
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1263
1394
  try {
1264
1395
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1277,7 +1408,17 @@ function createUnifiedSearch(deps = {}) {
1277
1408
  );
1278
1409
  const lists = await Promise.all(
1279
1410
  embeddings.map(
1280
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1411
+ (embedding) => runSemanticSearchForEmbedding(
1412
+ deps,
1413
+ input,
1414
+ embedding,
1415
+ limit,
1416
+ threshold,
1417
+ logger,
1418
+ peerPeers,
1419
+ warnings,
1420
+ runtimeContext
1421
+ )
1281
1422
  )
1282
1423
  );
1283
1424
  semantic = mergeByMaxScore(lists);
@@ -1300,7 +1441,9 @@ function createUnifiedSearch(deps = {}) {
1300
1441
  limit,
1301
1442
  threshold,
1302
1443
  logger,
1303
- peerPeers
1444
+ peerPeers,
1445
+ warnings,
1446
+ runtimeContext
1304
1447
  );
1305
1448
  }
1306
1449
  let lexical = [];
@@ -1319,7 +1462,8 @@ function createUnifiedSearch(deps = {}) {
1319
1462
  limit,
1320
1463
  botId,
1321
1464
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1322
- ...factTypes ? { factTypes } : {}
1465
+ ...factTypes ? { factTypes } : {},
1466
+ ...runtimeContext ?? {}
1323
1467
  })
1324
1468
  )
1325
1469
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1345,7 +1489,7 @@ function createUnifiedSearch(deps = {}) {
1345
1489
  let entity;
1346
1490
  if (typeof deps.entitySearch === "function") {
1347
1491
  entity = filterByDateRange(
1348
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1492
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1349
1493
  input.dateFrom,
1350
1494
  input.dateTo
1351
1495
  );
@@ -1407,14 +1551,16 @@ function createUnifiedSearch(deps = {}) {
1407
1551
  }
1408
1552
  return out;
1409
1553
  }
1410
- async function searchUnifiedMemory(input) {
1554
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1411
1555
  const query = input.query.trim();
1412
1556
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1413
1557
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1414
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1415
1558
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1416
1559
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1417
1560
  );
1561
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1562
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1563
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1418
1564
  const reasoningStrategy = resolveReasoningStrategy(input);
1419
1565
  const warnings = [];
1420
1566
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1434,23 +1580,46 @@ function createUnifiedSearch(deps = {}) {
1434
1580
  };
1435
1581
  }
1436
1582
  let memorySubs;
1583
+ let retrievalStatus;
1437
1584
  const reasoningInfo = { strategy: reasoningStrategy };
1438
1585
  if (input.dateFrom || input.dateTo) {
1439
1586
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1440
1587
  }
1441
1588
  if (sources.includes("memory")) {
1442
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1589
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1443
1590
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1444
1591
  try {
1445
1592
  memorySubs = await runMemorySource(
1446
1593
  input,
1447
- limit,
1448
- threshold,
1594
+ candidateLimit,
1595
+ memoryThreshold,
1449
1596
  warnings,
1450
1597
  reasoningStrategy,
1451
1598
  reasoningInfo,
1452
- peerPeers
1599
+ peerPeers,
1600
+ runtimeContext
1453
1601
  );
1602
+ memorySubs = {
1603
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1604
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1605
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1606
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1607
+ };
1608
+ if (deps.getRawMessageRetrievalStatus) {
1609
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1610
+ }
1611
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1612
+ let code = "semantic_candidates_empty";
1613
+ let message = "Semantic search was available but returned no candidates.";
1614
+ if (retrievalStatus?.embeddedChildCount === 0) {
1615
+ code = "semantic_unavailable";
1616
+ message = "No embedded RawMessage children are available for semantic search.";
1617
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1618
+ code = "semantic_dimension_mismatch";
1619
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1620
+ }
1621
+ warnings.push({ source: "memory", code, message });
1622
+ }
1454
1623
  } catch (error) {
1455
1624
  logger.warn?.("[memory-store] memory source failed:", error);
1456
1625
  warnings.push({
@@ -1473,12 +1642,13 @@ function createUnifiedSearch(deps = {}) {
1473
1642
  const hits = await deps.searchInsights({
1474
1643
  userId: input.userId,
1475
1644
  query: input.query,
1476
- limit,
1645
+ limit: candidateLimit,
1477
1646
  threshold,
1478
1647
  botIds: input.botIds,
1479
1648
  includeArchived: input.includeArchivedInsights,
1480
1649
  authToken: input.authToken,
1481
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1650
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1651
+ ...runtimeContext ?? {}
1482
1652
  });
1483
1653
  for (const hit of hits) {
1484
1654
  insightHits.push({
@@ -1505,12 +1675,13 @@ function createUnifiedSearch(deps = {}) {
1505
1675
  userId: input.userId,
1506
1676
  query: input.query,
1507
1677
  options: {
1508
- limit,
1678
+ limit: candidateLimit,
1509
1679
  threshold,
1510
1680
  documentIds: input.documentIds
1511
1681
  },
1512
1682
  authToken: input.authToken,
1513
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1683
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1684
+ ...runtimeContext ?? {}
1514
1685
  });
1515
1686
  for (const hit of hits) {
1516
1687
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1528,10 +1699,14 @@ function createUnifiedSearch(deps = {}) {
1528
1699
  memorySubs,
1529
1700
  insightHits,
1530
1701
  knowledgeHits,
1531
- limit,
1702
+ limit: candidateLimit,
1532
1703
  strategy: mergeStrategy
1533
1704
  });
1534
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1705
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1706
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1707
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1708
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1709
+ const ranked = reranked.slice(0, limit);
1535
1710
  const output = {
1536
1711
  query: input.query,
1537
1712
  sources,
@@ -1539,28 +1714,51 @@ function createUnifiedSearch(deps = {}) {
1539
1714
  count: ranked.length,
1540
1715
  warnings
1541
1716
  };
1717
+ if (input.includeRetrievalDiagnostics) {
1718
+ output.retrievalDiagnostics = {
1719
+ mergeStrategy,
1720
+ candidateLimit,
1721
+ backend: retrievalStatus?.backend,
1722
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1723
+ candidateCounts: {
1724
+ semantic: memorySubs?.semantic.length ?? 0,
1725
+ lexical: memorySubs?.lexical.length ?? 0,
1726
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1727
+ entity: memorySubs?.entity?.length ?? 0,
1728
+ fused: merged.length,
1729
+ final: ranked.length
1730
+ },
1731
+ channels: {
1732
+ semantic: memorySubs?.semantic ?? [],
1733
+ lexical: memorySubs?.lexical ?? [],
1734
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1735
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1736
+ },
1737
+ fusedBeforeRerank: merged,
1738
+ reranker: {
1739
+ enabled: Boolean(deps.reranker),
1740
+ provider: deps.rerankerInfo?.provider,
1741
+ model: deps.rerankerInfo?.model,
1742
+ inputCount: merged.length,
1743
+ outputCount: reranked.length,
1744
+ latencyMs: rerankerLatencyMs,
1745
+ orderChanged: rerankerOrderChanged
1746
+ },
1747
+ final: ranked
1748
+ };
1749
+ }
1542
1750
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1543
1751
  output.reasoning = reasoningInfo;
1544
1752
  }
1545
1753
  return output;
1546
1754
  }
1755
+ async function searchUnifiedMemory(input) {
1756
+ return searchUnifiedMemoryWithRuntime(input);
1757
+ }
1547
1758
  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
- });
1759
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1562
1760
  }
1563
- async function search(input) {
1761
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1564
1762
  const query = input.query.trim();
1565
1763
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1566
1764
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1592,19 +1790,24 @@ function createUnifiedSearch(deps = {}) {
1592
1790
  let unifiedHits = [];
1593
1791
  let sources = input.sources ? [...input.sources] : [];
1594
1792
  let unifiedReasoning;
1793
+ let retrievalDiagnostics;
1595
1794
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1596
1795
  const inferredSources = sources.length > 0 ? sources : [
1597
1796
  ...wantsRawTier ? ["memory"] : [],
1598
1797
  ...wantsInsightTier ? ["insights"] : [],
1599
1798
  ...wantsKnowledgeTier ? ["knowledge"] : []
1600
1799
  ];
1601
- const unified = await searchUnifiedMemory({
1602
- ...searchInputToUnified(input),
1603
- sources: inferredSources.length > 0 ? inferredSources : void 0
1604
- });
1800
+ const unified = await searchUnifiedMemoryWithRuntime(
1801
+ {
1802
+ ...searchInputToUnified(input),
1803
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1804
+ },
1805
+ runtimeContext
1806
+ );
1605
1807
  unifiedHits = unified.results;
1606
1808
  sources = unified.sources;
1607
1809
  unifiedReasoning = unified.reasoning;
1810
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1608
1811
  warnings.push(...unified.warnings);
1609
1812
  }
1610
1813
  let summaryHits = [];
@@ -1615,7 +1818,8 @@ function createUnifiedSearch(deps = {}) {
1615
1818
  limit,
1616
1819
  threshold,
1617
1820
  logger,
1618
- peerScope.peers
1821
+ peerScope.peers,
1822
+ runtimeContext
1619
1823
  );
1620
1824
  warnings.push(...summaryBucket.warnings);
1621
1825
  summaryHits = summaryBucket.hits;
@@ -1640,7 +1844,8 @@ function createUnifiedSearch(deps = {}) {
1640
1844
  source: mapHitToEvidenceSource(hit),
1641
1845
  snippet: hit.content,
1642
1846
  score: hit.similarity,
1643
- timestamp: getCandidateTimestamp(hit.metadata)
1847
+ timestamp: getCandidateTimestamp(hit.metadata),
1848
+ metadata: hit.metadata
1644
1849
  }));
1645
1850
  const base = {
1646
1851
  query,
@@ -1653,12 +1858,15 @@ function createUnifiedSearch(deps = {}) {
1653
1858
  if (unifiedReasoning) {
1654
1859
  base.reasoning = unifiedReasoning;
1655
1860
  }
1861
+ if (retrievalDiagnostics) {
1862
+ base.retrievalDiagnostics = retrievalDiagnostics;
1863
+ }
1656
1864
  if (!wantsSynthesis) {
1657
1865
  return base;
1658
1866
  }
1659
1867
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1660
1868
  query,
1661
- hits,
1869
+ evidence,
1662
1870
  responseSchema: synthesisSchema,
1663
1871
  deps,
1664
1872
  logger
@@ -1667,6 +1875,10 @@ function createUnifiedSearch(deps = {}) {
1667
1875
  base.warnings = [...base.warnings, ...synthWarnings];
1668
1876
  return base;
1669
1877
  }
1878
+ async function search(input, runtimeContext) {
1879
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1880
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1881
+ }
1670
1882
  return {
1671
1883
  search,
1672
1884
  searchUnifiedMemory,
@@ -1682,6 +1894,7 @@ function mergeAcrossSources(input) {
1682
1894
  for (const hit of [
1683
1895
  ...memorySubs.semantic,
1684
1896
  ...memorySubs.lexical,
1897
+ ...memorySubs.hybrid ?? [],
1685
1898
  ...input.insightHits,
1686
1899
  ...input.knowledgeHits
1687
1900
  ]) {
@@ -1697,6 +1910,7 @@ function mergeAcrossSources(input) {
1697
1910
  const all = [
1698
1911
  ...memorySubs.semantic,
1699
1912
  ...memorySubs.lexical,
1913
+ ...memorySubs.hybrid ?? [],
1700
1914
  ...memorySubs.entity ?? [],
1701
1915
  ...input.insightHits,
1702
1916
  ...input.knowledgeHits
@@ -1708,6 +1922,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1708
1922
  const lists = [];
1709
1923
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1710
1924
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1925
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1926
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1927
+ }
1711
1928
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1712
1929
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1713
1930
  }