@melandlabs/memory-store 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -249,6 +249,69 @@ var init_raw_message_store = __esm({
249
249
  init_chroma_memory_index();
250
250
  init_raw_message_store();
251
251
 
252
+ // src/search/applicability.ts
253
+ var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
254
+ "global",
255
+ "task",
256
+ "conversation",
257
+ "channel",
258
+ "project",
259
+ "custom"
260
+ ]);
261
+ function isRecord(value) {
262
+ return typeof value === "object" && value !== null && !Array.isArray(value);
263
+ }
264
+ function invalidRuntimeContext(message) {
265
+ throw new TypeError(`memory-store search runtime context: ${message}`);
266
+ }
267
+ function validateApplicabilityContext(value, index) {
268
+ const path = `applicabilityContexts[${index}]`;
269
+ if (!isRecord(value)) {
270
+ invalidRuntimeContext(`${path} must be an object`);
271
+ }
272
+ const scope = value.scope;
273
+ if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
274
+ invalidRuntimeContext(`${path}.scope is not supported`);
275
+ }
276
+ if (value.key !== void 0 && typeof value.key !== "string") {
277
+ invalidRuntimeContext(`${path}.key must be a string when provided`);
278
+ }
279
+ if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
280
+ invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
281
+ }
282
+ for (const field of ["validFrom", "validUntil"]) {
283
+ const boundary = value[field];
284
+ if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
285
+ invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
286
+ }
287
+ }
288
+ }
289
+ function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
290
+ if (runtimeContext === void 0) {
291
+ return void 0;
292
+ }
293
+ if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
294
+ invalidRuntimeContext("applicabilityContexts must be an array");
295
+ }
296
+ for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
297
+ validateApplicabilityContext(context, index);
298
+ }
299
+ let applicabilityAt;
300
+ if (input.asOf === void 0) {
301
+ applicabilityAt = now();
302
+ } else {
303
+ const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
304
+ if (!Number.isFinite(parsed)) {
305
+ invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
306
+ }
307
+ applicabilityAt = parsed;
308
+ }
309
+ return {
310
+ applicabilityContexts: runtimeContext.applicabilityContexts,
311
+ applicabilityAt
312
+ };
313
+ }
314
+
252
315
  // src/search/utilities.ts
253
316
  function deriveLexicalKeywords(query) {
254
317
  return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
@@ -407,6 +470,7 @@ function materializeSignals(lists, hit) {
407
470
  function listNameToChannel(name) {
408
471
  if (name === "memory-semantic") return "semantic";
409
472
  if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
473
+ if (name === "memory-hybrid") return "hybrid";
410
474
  if (name === "memory-entity") return "entity";
411
475
  return void 0;
412
476
  }
@@ -616,7 +680,6 @@ async function applyReranker(reranker, query, results) {
616
680
 
617
681
  // src/search/gather-evidence.ts
618
682
  var DEFAULT_TIERS2 = ["summary", "raw", "insight", "knowledge"];
619
- var MAX_EVIDENCE_CHARS = 600;
620
683
  function toSummaryResult(row) {
621
684
  const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
622
685
  const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
@@ -634,7 +697,7 @@ function toSummaryResult(row) {
634
697
  }
635
698
  };
636
699
  }
637
- async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
700
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
638
701
  if (typeof deps.searchSummaries !== "function") {
639
702
  return { hits: [], warnings: [] };
640
703
  }
@@ -647,7 +710,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
647
710
  limit,
648
711
  threshold,
649
712
  authToken: input.authToken,
650
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
713
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
714
+ ...runtimeContext ?? {}
651
715
  });
652
716
  const hits = rows.map(toSummaryResult);
653
717
  return { hits, warnings: [] };
@@ -665,12 +729,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
665
729
  };
666
730
  }
667
731
  }
668
- function truncate(input, max = MAX_EVIDENCE_CHARS) {
669
- if (input.length <= max) {
670
- return input;
671
- }
672
- return `${input.slice(0, max - 1)}\u2026`;
673
- }
674
732
  function buildSynthesisPrompt(input) {
675
733
  const grouped = /* @__PURE__ */ new Map();
676
734
  for (const item of input.evidence) {
@@ -684,9 +742,10 @@ function buildSynthesisPrompt(input) {
684
742
  if (items.length === 0) {
685
743
  continue;
686
744
  }
687
- const lines = items.map(
688
- (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
689
- );
745
+ const lines = items.map((item, index) => {
746
+ const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
747
+ return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
748
+ });
690
749
  sections.push(`## ${tier}
691
750
  ${lines.join("\n")}`);
692
751
  }
@@ -768,12 +827,15 @@ function coerceAnswer(text, schema) {
768
827
  return text;
769
828
  }
770
829
  async function synthesizeAnswer(input) {
771
- const { query, hits, responseSchema, deps, logger } = input;
772
- const evidence = hits.map((hit) => ({
773
- id: hit.id,
774
- source: mapHitToTier(hit),
775
- snippet: truncate(hit.content),
776
- score: hit.similarity
830
+ const { query, responseSchema, deps, logger } = input;
831
+ const evidence = input.evidence.map((item) => ({
832
+ id: item.id,
833
+ source: mapEvidenceSourceToTier(item.source),
834
+ snippet: item.snippet,
835
+ score: item.score,
836
+ // Forward per-fact provenance (e.g. `metadata.source` from
837
+ // `opencontext add --source`) so the synthesis prompt can cite it.
838
+ metadata: item.metadata
777
839
  }));
778
840
  const warnings = [];
779
841
  const complete = deps.reasoning?.complete;
@@ -804,21 +866,10 @@ async function synthesizeAnswer(input) {
804
866
  return { answer: "", warnings };
805
867
  }
806
868
  }
807
- function mapHitToTier(hit) {
808
- const tierMarker = hit.metadata?.tier;
809
- if (tierMarker === "summary") {
810
- return "summary";
811
- }
812
- if (hit.type === "memory") {
813
- return "raw";
814
- }
815
- if (hit.type === "insight") {
816
- return "insight";
817
- }
818
- if (hit.type === "knowledge") {
819
- return "knowledge";
820
- }
821
- return hit.type;
869
+ function mapEvidenceSourceToTier(source) {
870
+ if (source === "memory") return "raw";
871
+ if (source === "insights") return "insight";
872
+ return source;
822
873
  }
823
874
  async function resolveSearchScopePeers(input) {
824
875
  return resolveScopePeer({
@@ -842,7 +893,7 @@ function deriveEntityKeywords(query) {
842
893
  }
843
894
  return out.slice(0, 16);
844
895
  }
845
- async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
896
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
846
897
  if (typeof deps.entitySearch !== "function") {
847
898
  return [];
848
899
  }
@@ -860,7 +911,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
860
911
  keywords,
861
912
  limit: Math.ceil(limit / filters.length),
862
913
  botId,
863
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
914
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
915
+ ...runtimeContext ?? {}
864
916
  })
865
917
  )
866
918
  )).flat();
@@ -948,6 +1000,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
948
1000
  return true;
949
1001
  });
950
1002
  }
1003
+ function warnApplicabilityNotEnforced(warnings) {
1004
+ if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
1005
+ return;
1006
+ }
1007
+ warnings.push({
1008
+ source: "memory",
1009
+ code: "memory_applicability_not_enforced",
1010
+ message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
1011
+ });
1012
+ }
951
1013
  async function embedQueryVariant(embedQuery, input, query) {
952
1014
  return embedQuery({
953
1015
  userId: input.userId,
@@ -955,10 +1017,29 @@ async function embedQueryVariant(embedQuery, input, query) {
955
1017
  authToken: input.authToken
956
1018
  });
957
1019
  }
958
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
1020
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
959
1021
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
960
1022
  let semantic = [];
961
- if (isRawMessageChromaEnabled()) {
1023
+ if (typeof deps.searchRawMessagesAnn === "function") {
1024
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
1025
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1026
+ const includeDeprecated = input.includeDeprecated === true;
1027
+ semantic = (await Promise.all(
1028
+ filters.map(
1029
+ (filter) => searchRawMessagesAnn({
1030
+ userId: input.userId,
1031
+ queryEmbedding,
1032
+ limit,
1033
+ threshold,
1034
+ botId: "botId" in filter ? filter.botId : void 0,
1035
+ includeDeprecated,
1036
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1037
+ ...factTypes ? { factTypes } : {},
1038
+ ...runtimeContext ?? {}
1039
+ })
1040
+ )
1041
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1042
+ } else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
962
1043
  try {
963
1044
  semantic = (await Promise.all(
964
1045
  filters.map((filter) => {
@@ -983,24 +1064,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
983
1064
  error
984
1065
  );
985
1066
  }
986
- } else if (typeof deps.searchRawMessagesAnn === "function") {
987
- const searchRawMessagesAnn = deps.searchRawMessagesAnn;
988
- const factTypes = input.factTypes?.length ? input.factTypes : void 0;
989
- semantic = (await Promise.all(
990
- filters.map(
991
- (filter) => searchRawMessagesAnn({
992
- userId: input.userId,
993
- queryEmbedding,
994
- limit,
995
- threshold,
996
- botId: "botId" in filter ? filter.botId : void 0,
997
- ...peerPeers.length > 0 ? { peers: peerPeers } : {},
998
- ...factTypes ? { factTypes } : {}
999
- })
1000
- )
1001
- )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
1002
1067
  }
1003
- if (semantic.length === 0) {
1068
+ if (semantic.length === 0 && runtimeContext !== void 0) {
1069
+ warnApplicabilityNotEnforced(warnings);
1070
+ return [];
1071
+ }
1072
+ if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
1004
1073
  try {
1005
1074
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
1006
1075
  const manager2 = await getRawMessageManager2();
@@ -1011,6 +1080,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
1011
1080
  queryEmbedding,
1012
1081
  limit,
1013
1082
  threshold,
1083
+ includeDeprecated: input.includeDeprecated === true,
1014
1084
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1015
1085
  ...factTypes ? { factTypes } : {}
1016
1086
  });
@@ -1040,6 +1110,18 @@ function mergeByMaxScore(lists) {
1040
1110
  }
1041
1111
  return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
1042
1112
  }
1113
+ function dedupeChannelByParent(hits) {
1114
+ const strongest = /* @__PURE__ */ new Map();
1115
+ for (const hit of hits) {
1116
+ const sourceMessageId = hit.metadata?.sourceMessageId;
1117
+ const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
1118
+ const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
1119
+ const key = `${normalized.type}:${parentId}`;
1120
+ const current = strongest.get(key);
1121
+ if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
1122
+ }
1123
+ return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
1124
+ }
1043
1125
  function toIterativeRecallCandidate(hit) {
1044
1126
  return {
1045
1127
  id: hit.id,
@@ -1048,7 +1130,7 @@ function toIterativeRecallCandidate(hit) {
1048
1130
  metadata: hit.metadata
1049
1131
  };
1050
1132
  }
1051
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
1133
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
1052
1134
  if (keywords.length === 0) {
1053
1135
  return [];
1054
1136
  }
@@ -1057,6 +1139,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1057
1139
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1058
1140
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1059
1141
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1142
+ const includeDeprecated = input.includeDeprecated === true;
1060
1143
  return (await Promise.all(
1061
1144
  filters.map(
1062
1145
  (botId) => searchRawMessagesLexical({
@@ -1064,8 +1147,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1064
1147
  keywords,
1065
1148
  limit: Math.ceil(limit / filters.length),
1066
1149
  botId,
1150
+ includeDeprecated,
1067
1151
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1068
- ...factTypes ? { factTypes } : {}
1152
+ ...factTypes ? { factTypes } : {},
1153
+ ...runtimeContext ?? {}
1069
1154
  })
1070
1155
  )
1071
1156
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1076,10 +1161,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1076
1161
  logger.warn?.("[memory-store] lexical memory search failed:", error);
1077
1162
  }
1078
1163
  }
1164
+ if (runtimeContext !== void 0) {
1165
+ warnApplicabilityNotEnforced(warnings);
1166
+ return [];
1167
+ }
1079
1168
  try {
1080
1169
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
1081
1170
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1082
1171
  const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1172
+ const includeDeprecated = input.includeDeprecated === true;
1083
1173
  return (await Promise.all(
1084
1174
  filters.map(
1085
1175
  (botId) => lexicalSearchRawMessages2({
@@ -1087,6 +1177,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
1087
1177
  keywords,
1088
1178
  limit: Math.ceil(limit / filters.length),
1089
1179
  botId,
1180
+ includeDeprecated,
1090
1181
  ...factTypes ? { factTypes } : {}
1091
1182
  })
1092
1183
  )
@@ -1117,7 +1208,9 @@ function searchInputToUnified(input) {
1117
1208
  mergeStrategy: input.mergeStrategy,
1118
1209
  peerFilter: input.peerFilter,
1119
1210
  reasoningStrategy: input.reasoningStrategy,
1120
- factTypes: input.factTypes
1211
+ factTypes: input.factTypes,
1212
+ includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
1213
+ includeDeprecated: input.includeDeprecated
1121
1214
  };
1122
1215
  }
1123
1216
  function mapHitToEvidenceSource(hit) {
@@ -1145,7 +1238,7 @@ function createUnifiedSearch(deps = {}) {
1145
1238
  }
1146
1239
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
1147
1240
  }
1148
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
1241
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1149
1242
  if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1150
1243
  warnings.push({
1151
1244
  source: "memory",
@@ -1178,7 +1271,9 @@ function createUnifiedSearch(deps = {}) {
1178
1271
  keywords2,
1179
1272
  limit,
1180
1273
  logger,
1181
- peerPeers
1274
+ peerPeers,
1275
+ warnings,
1276
+ runtimeContext
1182
1277
  );
1183
1278
  let semanticHits = [];
1184
1279
  if (typeof deps.embedQuery === "function") {
@@ -1192,7 +1287,9 @@ function createUnifiedSearch(deps = {}) {
1192
1287
  limit,
1193
1288
  threshold,
1194
1289
  logger,
1195
- peerPeers
1290
+ peerPeers,
1291
+ warnings,
1292
+ runtimeContext
1196
1293
  );
1197
1294
  } catch (error) {
1198
1295
  logger.warn?.(
@@ -1233,6 +1330,11 @@ function createUnifiedSearch(deps = {}) {
1233
1330
  unionEvidence = evidence;
1234
1331
  }
1235
1332
  if (typeof deps.embedQuery !== "function") {
1333
+ warnings.push({
1334
+ source: "memory",
1335
+ code: "semantic_unavailable",
1336
+ message: "Semantic search is unavailable because no embedding provider is configured."
1337
+ });
1236
1338
  warnings.push({
1237
1339
  source: "memory",
1238
1340
  code: "memory_lexical_search_fallback",
@@ -1240,7 +1342,16 @@ function createUnifiedSearch(deps = {}) {
1240
1342
  });
1241
1343
  const keywords2 = deriveLexicalKeywords(input.query);
1242
1344
  const lexical2 = filterByDateRange(
1243
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
1345
+ await runLexicalSearchForKeywords(
1346
+ deps,
1347
+ input,
1348
+ keywords2,
1349
+ limit,
1350
+ logger,
1351
+ peerPeers,
1352
+ warnings,
1353
+ runtimeContext
1354
+ ),
1244
1355
  input.dateFrom,
1245
1356
  input.dateTo
1246
1357
  );
@@ -1248,6 +1359,26 @@ function createUnifiedSearch(deps = {}) {
1248
1359
  }
1249
1360
  let semantic = [];
1250
1361
  const embedQuery = deps.embedQuery;
1362
+ const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1363
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1364
+ );
1365
+ if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
1366
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
1367
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1368
+ const hybrid = (await Promise.all(
1369
+ filters.map(
1370
+ (botId) => deps.searchRawMessagesHybrid?.({
1371
+ userId: input.userId,
1372
+ query: input.query,
1373
+ queryEmbedding,
1374
+ limit: Math.ceil(limit / filters.length),
1375
+ botId,
1376
+ ...runtimeContext ?? {}
1377
+ })
1378
+ )
1379
+ )).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
1380
+ return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
1381
+ }
1251
1382
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1252
1383
  try {
1253
1384
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1266,7 +1397,17 @@ function createUnifiedSearch(deps = {}) {
1266
1397
  );
1267
1398
  const lists = await Promise.all(
1268
1399
  embeddings.map(
1269
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
1400
+ (embedding) => runSemanticSearchForEmbedding(
1401
+ deps,
1402
+ input,
1403
+ embedding,
1404
+ limit,
1405
+ threshold,
1406
+ logger,
1407
+ peerPeers,
1408
+ warnings,
1409
+ runtimeContext
1410
+ )
1270
1411
  )
1271
1412
  );
1272
1413
  semantic = mergeByMaxScore(lists);
@@ -1289,7 +1430,9 @@ function createUnifiedSearch(deps = {}) {
1289
1430
  limit,
1290
1431
  threshold,
1291
1432
  logger,
1292
- peerPeers
1433
+ peerPeers,
1434
+ warnings,
1435
+ runtimeContext
1293
1436
  );
1294
1437
  }
1295
1438
  let lexical = [];
@@ -1308,7 +1451,8 @@ function createUnifiedSearch(deps = {}) {
1308
1451
  limit,
1309
1452
  botId,
1310
1453
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1311
- ...factTypes ? { factTypes } : {}
1454
+ ...factTypes ? { factTypes } : {},
1455
+ ...runtimeContext ?? {}
1312
1456
  })
1313
1457
  )
1314
1458
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1334,7 +1478,7 @@ function createUnifiedSearch(deps = {}) {
1334
1478
  let entity;
1335
1479
  if (typeof deps.entitySearch === "function") {
1336
1480
  entity = filterByDateRange(
1337
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1481
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1338
1482
  input.dateFrom,
1339
1483
  input.dateTo
1340
1484
  );
@@ -1396,14 +1540,16 @@ function createUnifiedSearch(deps = {}) {
1396
1540
  }
1397
1541
  return out;
1398
1542
  }
1399
- async function searchUnifiedMemory(input) {
1543
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1400
1544
  const query = input.query.trim();
1401
1545
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1402
1546
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1403
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1404
1547
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1405
1548
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1406
1549
  );
1550
+ const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
1551
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1552
+ const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
1407
1553
  const reasoningStrategy = resolveReasoningStrategy(input);
1408
1554
  const warnings = [];
1409
1555
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1423,23 +1569,46 @@ function createUnifiedSearch(deps = {}) {
1423
1569
  };
1424
1570
  }
1425
1571
  let memorySubs;
1572
+ let retrievalStatus;
1426
1573
  const reasoningInfo = { strategy: reasoningStrategy };
1427
1574
  if (input.dateFrom || input.dateTo) {
1428
1575
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1429
1576
  }
1430
1577
  if (sources.includes("memory")) {
1431
- const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1578
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
1432
1579
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1433
1580
  try {
1434
1581
  memorySubs = await runMemorySource(
1435
1582
  input,
1436
- limit,
1437
- threshold,
1583
+ candidateLimit,
1584
+ memoryThreshold,
1438
1585
  warnings,
1439
1586
  reasoningStrategy,
1440
1587
  reasoningInfo,
1441
- peerPeers
1588
+ peerPeers,
1589
+ runtimeContext
1442
1590
  );
1591
+ memorySubs = {
1592
+ semantic: dedupeChannelByParent(memorySubs.semantic),
1593
+ lexical: dedupeChannelByParent(memorySubs.lexical),
1594
+ ...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
1595
+ ...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
1596
+ };
1597
+ if (deps.getRawMessageRetrievalStatus) {
1598
+ retrievalStatus = await deps.getRawMessageRetrievalStatus();
1599
+ }
1600
+ if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
1601
+ let code = "semantic_candidates_empty";
1602
+ let message = "Semantic search was available but returned no candidates.";
1603
+ if (retrievalStatus?.embeddedChildCount === 0) {
1604
+ code = "semantic_unavailable";
1605
+ message = "No embedded RawMessage children are available for semantic search.";
1606
+ } else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
1607
+ code = "semantic_dimension_mismatch";
1608
+ message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
1609
+ }
1610
+ warnings.push({ source: "memory", code, message });
1611
+ }
1443
1612
  } catch (error) {
1444
1613
  logger.warn?.("[memory-store] memory source failed:", error);
1445
1614
  warnings.push({
@@ -1462,12 +1631,13 @@ function createUnifiedSearch(deps = {}) {
1462
1631
  const hits = await deps.searchInsights({
1463
1632
  userId: input.userId,
1464
1633
  query: input.query,
1465
- limit,
1634
+ limit: candidateLimit,
1466
1635
  threshold,
1467
1636
  botIds: input.botIds,
1468
1637
  includeArchived: input.includeArchivedInsights,
1469
1638
  authToken: input.authToken,
1470
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1639
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1640
+ ...runtimeContext ?? {}
1471
1641
  });
1472
1642
  for (const hit of hits) {
1473
1643
  insightHits.push({
@@ -1494,12 +1664,13 @@ function createUnifiedSearch(deps = {}) {
1494
1664
  userId: input.userId,
1495
1665
  query: input.query,
1496
1666
  options: {
1497
- limit,
1667
+ limit: candidateLimit,
1498
1668
  threshold,
1499
1669
  documentIds: input.documentIds
1500
1670
  },
1501
1671
  authToken: input.authToken,
1502
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1672
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1673
+ ...runtimeContext ?? {}
1503
1674
  });
1504
1675
  for (const hit of hits) {
1505
1676
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1517,10 +1688,14 @@ function createUnifiedSearch(deps = {}) {
1517
1688
  memorySubs,
1518
1689
  insightHits,
1519
1690
  knowledgeHits,
1520
- limit,
1691
+ limit: candidateLimit,
1521
1692
  strategy: mergeStrategy
1522
1693
  });
1523
- const ranked = await applyReranker(deps.reranker, input.query, merged);
1694
+ const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
1695
+ const reranked = await applyReranker(deps.reranker, input.query, merged);
1696
+ const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
1697
+ const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
1698
+ const ranked = reranked.slice(0, limit);
1524
1699
  const output = {
1525
1700
  query: input.query,
1526
1701
  sources,
@@ -1528,28 +1703,51 @@ function createUnifiedSearch(deps = {}) {
1528
1703
  count: ranked.length,
1529
1704
  warnings
1530
1705
  };
1706
+ if (input.includeRetrievalDiagnostics) {
1707
+ output.retrievalDiagnostics = {
1708
+ mergeStrategy,
1709
+ candidateLimit,
1710
+ backend: retrievalStatus?.backend,
1711
+ semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
1712
+ candidateCounts: {
1713
+ semantic: memorySubs?.semantic.length ?? 0,
1714
+ lexical: memorySubs?.lexical.length ?? 0,
1715
+ hybrid: memorySubs?.hybrid?.length ?? 0,
1716
+ entity: memorySubs?.entity?.length ?? 0,
1717
+ fused: merged.length,
1718
+ final: ranked.length
1719
+ },
1720
+ channels: {
1721
+ semantic: memorySubs?.semantic ?? [],
1722
+ lexical: memorySubs?.lexical ?? [],
1723
+ ...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
1724
+ ...memorySubs?.entity ? { entity: memorySubs.entity } : {}
1725
+ },
1726
+ fusedBeforeRerank: merged,
1727
+ reranker: {
1728
+ enabled: Boolean(deps.reranker),
1729
+ provider: deps.rerankerInfo?.provider,
1730
+ model: deps.rerankerInfo?.model,
1731
+ inputCount: merged.length,
1732
+ outputCount: reranked.length,
1733
+ latencyMs: rerankerLatencyMs,
1734
+ orderChanged: rerankerOrderChanged
1735
+ },
1736
+ final: ranked
1737
+ };
1738
+ }
1531
1739
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1532
1740
  output.reasoning = reasoningInfo;
1533
1741
  }
1534
1742
  return output;
1535
1743
  }
1744
+ async function searchUnifiedMemory(input) {
1745
+ return searchUnifiedMemoryWithRuntime(input);
1746
+ }
1536
1747
  async function searchRawMemorySemantically(input) {
1537
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1538
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1539
- const reasoningStrategy = resolveReasoningStrategy(input);
1540
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1541
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1542
- );
1543
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1544
- return mergeAcrossSources({
1545
- memorySubs: sub,
1546
- insightHits: [],
1547
- knowledgeHits: [],
1548
- limit,
1549
- strategy: mergeStrategy
1550
- });
1748
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1551
1749
  }
1552
- async function search(input) {
1750
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1553
1751
  const query = input.query.trim();
1554
1752
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1555
1753
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1581,19 +1779,24 @@ function createUnifiedSearch(deps = {}) {
1581
1779
  let unifiedHits = [];
1582
1780
  let sources = input.sources ? [...input.sources] : [];
1583
1781
  let unifiedReasoning;
1782
+ let retrievalDiagnostics;
1584
1783
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1585
1784
  const inferredSources = sources.length > 0 ? sources : [
1586
1785
  ...wantsRawTier ? ["memory"] : [],
1587
1786
  ...wantsInsightTier ? ["insights"] : [],
1588
1787
  ...wantsKnowledgeTier ? ["knowledge"] : []
1589
1788
  ];
1590
- const unified = await searchUnifiedMemory({
1591
- ...searchInputToUnified(input),
1592
- sources: inferredSources.length > 0 ? inferredSources : void 0
1593
- });
1789
+ const unified = await searchUnifiedMemoryWithRuntime(
1790
+ {
1791
+ ...searchInputToUnified(input),
1792
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1793
+ },
1794
+ runtimeContext
1795
+ );
1594
1796
  unifiedHits = unified.results;
1595
1797
  sources = unified.sources;
1596
1798
  unifiedReasoning = unified.reasoning;
1799
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1597
1800
  warnings.push(...unified.warnings);
1598
1801
  }
1599
1802
  let summaryHits = [];
@@ -1604,7 +1807,8 @@ function createUnifiedSearch(deps = {}) {
1604
1807
  limit,
1605
1808
  threshold,
1606
1809
  logger,
1607
- peerScope.peers
1810
+ peerScope.peers,
1811
+ runtimeContext
1608
1812
  );
1609
1813
  warnings.push(...summaryBucket.warnings);
1610
1814
  summaryHits = summaryBucket.hits;
@@ -1629,7 +1833,8 @@ function createUnifiedSearch(deps = {}) {
1629
1833
  source: mapHitToEvidenceSource(hit),
1630
1834
  snippet: hit.content,
1631
1835
  score: hit.similarity,
1632
- timestamp: getCandidateTimestamp(hit.metadata)
1836
+ timestamp: getCandidateTimestamp(hit.metadata),
1837
+ metadata: hit.metadata
1633
1838
  }));
1634
1839
  const base = {
1635
1840
  query,
@@ -1642,12 +1847,15 @@ function createUnifiedSearch(deps = {}) {
1642
1847
  if (unifiedReasoning) {
1643
1848
  base.reasoning = unifiedReasoning;
1644
1849
  }
1850
+ if (retrievalDiagnostics) {
1851
+ base.retrievalDiagnostics = retrievalDiagnostics;
1852
+ }
1645
1853
  if (!wantsSynthesis) {
1646
1854
  return base;
1647
1855
  }
1648
1856
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1649
1857
  query,
1650
- hits,
1858
+ evidence,
1651
1859
  responseSchema: synthesisSchema,
1652
1860
  deps,
1653
1861
  logger
@@ -1656,6 +1864,10 @@ function createUnifiedSearch(deps = {}) {
1656
1864
  base.warnings = [...base.warnings, ...synthWarnings];
1657
1865
  return base;
1658
1866
  }
1867
+ async function search(input, runtimeContext) {
1868
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1869
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1870
+ }
1659
1871
  return {
1660
1872
  search,
1661
1873
  searchUnifiedMemory,
@@ -1671,6 +1883,7 @@ function mergeAcrossSources(input) {
1671
1883
  for (const hit of [
1672
1884
  ...memorySubs.semantic,
1673
1885
  ...memorySubs.lexical,
1886
+ ...memorySubs.hybrid ?? [],
1674
1887
  ...input.insightHits,
1675
1888
  ...input.knowledgeHits
1676
1889
  ]) {
@@ -1686,6 +1899,7 @@ function mergeAcrossSources(input) {
1686
1899
  const all = [
1687
1900
  ...memorySubs.semantic,
1688
1901
  ...memorySubs.lexical,
1902
+ ...memorySubs.hybrid ?? [],
1689
1903
  ...memorySubs.entity ?? [],
1690
1904
  ...input.insightHits,
1691
1905
  ...input.knowledgeHits
@@ -1697,6 +1911,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1697
1911
  const lists = [];
1698
1912
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1699
1913
  if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1914
+ if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
1915
+ lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
1916
+ }
1700
1917
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1701
1918
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1702
1919
  }