@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/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);
@@ -311,7 +374,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
311
374
  return value === "rrf" ? "rrf" : "similarity";
312
375
  }
313
376
  function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
314
- if (value === "rewrite" || value === "iterative") {
377
+ if (value === "rewrite" || value === "iterative" || value === "union") {
315
378
  return value;
316
379
  }
317
380
  return defaultStrategy;
@@ -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,8 +1249,8 @@ 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 = []) {
1160
- if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
1252
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1253
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1161
1254
  warnings.push({
1162
1255
  source: "memory",
1163
1256
  code: "memory_iterative_planner_not_configured",
@@ -1173,7 +1266,8 @@ function createUnifiedSearch(deps = {}) {
1173
1266
  });
1174
1267
  if (reasoningInfo) reasoningInfo.degraded = true;
1175
1268
  }
1176
- if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
1269
+ let unionEvidence = null;
1270
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
1177
1271
  const planner = deps.reasoning.iterativePlanner;
1178
1272
  const result = await planner.plan({
1179
1273
  query: input.query,
@@ -1188,7 +1282,9 @@ function createUnifiedSearch(deps = {}) {
1188
1282
  keywords2,
1189
1283
  limit,
1190
1284
  logger,
1191
- peerPeers
1285
+ peerPeers,
1286
+ warnings,
1287
+ runtimeContext
1192
1288
  );
1193
1289
  let semanticHits = [];
1194
1290
  if (typeof deps.embedQuery === "function") {
@@ -1202,7 +1298,9 @@ function createUnifiedSearch(deps = {}) {
1202
1298
  limit,
1203
1299
  threshold,
1204
1300
  logger,
1205
- peerPeers
1301
+ peerPeers,
1302
+ warnings,
1303
+ runtimeContext
1206
1304
  );
1207
1305
  } catch (error) {
1208
1306
  logger.warn?.(
@@ -1230,16 +1328,24 @@ function createUnifiedSearch(deps = {}) {
1230
1328
  reasoningInfo.degraded = true;
1231
1329
  }
1232
1330
  }
1233
- const semantic2 = result.evidence.map((candidate) => ({
1331
+ const evidence = result.evidence.map((candidate) => ({
1234
1332
  type: "memory",
1235
1333
  id: candidate.id,
1236
1334
  content: candidate.content,
1237
1335
  similarity: candidate.similarity,
1238
- metadata: { ...candidate.metadata, reasoning: "iterative" }
1336
+ metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
1239
1337
  }));
1240
- return { semantic: semantic2, lexical: [] };
1338
+ if (reasoningStrategy === "iterative") {
1339
+ return { semantic: evidence, lexical: [] };
1340
+ }
1341
+ unionEvidence = evidence;
1241
1342
  }
1242
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
+ });
1243
1349
  warnings.push({
1244
1350
  source: "memory",
1245
1351
  code: "memory_lexical_search_fallback",
@@ -1247,7 +1353,16 @@ function createUnifiedSearch(deps = {}) {
1247
1353
  });
1248
1354
  const keywords2 = deriveLexicalKeywords(input.query);
1249
1355
  const lexical2 = filterByDateRange(
1250
- 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
+ ),
1251
1366
  input.dateFrom,
1252
1367
  input.dateTo
1253
1368
  );
@@ -1255,6 +1370,26 @@ function createUnifiedSearch(deps = {}) {
1255
1370
  }
1256
1371
  let semantic = [];
1257
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
+ }
1258
1393
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1259
1394
  try {
1260
1395
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1273,7 +1408,17 @@ function createUnifiedSearch(deps = {}) {
1273
1408
  );
1274
1409
  const lists = await Promise.all(
1275
1410
  embeddings.map(
1276
- (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
+ )
1277
1422
  )
1278
1423
  );
1279
1424
  semantic = mergeByMaxScore(lists);
@@ -1296,7 +1441,9 @@ function createUnifiedSearch(deps = {}) {
1296
1441
  limit,
1297
1442
  threshold,
1298
1443
  logger,
1299
- peerPeers
1444
+ peerPeers,
1445
+ warnings,
1446
+ runtimeContext
1300
1447
  );
1301
1448
  }
1302
1449
  let lexical = [];
@@ -1315,7 +1462,8 @@ function createUnifiedSearch(deps = {}) {
1315
1462
  limit,
1316
1463
  botId,
1317
1464
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1318
- ...factTypes ? { factTypes } : {}
1465
+ ...factTypes ? { factTypes } : {},
1466
+ ...runtimeContext ?? {}
1319
1467
  })
1320
1468
  )
1321
1469
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1341,7 +1489,7 @@ function createUnifiedSearch(deps = {}) {
1341
1489
  let entity;
1342
1490
  if (typeof deps.entitySearch === "function") {
1343
1491
  entity = filterByDateRange(
1344
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1492
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1345
1493
  input.dateFrom,
1346
1494
  input.dateTo
1347
1495
  );
@@ -1363,6 +1511,37 @@ function createUnifiedSearch(deps = {}) {
1363
1511
  message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1364
1512
  });
1365
1513
  }
1514
+ if (unionEvidence) {
1515
+ const plannerHits = unionEvidence;
1516
+ const base = mergeByMaxScore([
1517
+ filterByDateRange(semantic, input.dateFrom, input.dateTo),
1518
+ filterByDateRange(lexical, input.dateFrom, input.dateTo)
1519
+ ]);
1520
+ const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
1521
+ const seen = /* @__PURE__ */ new Set();
1522
+ const merged = [];
1523
+ plannerHits.forEach((hit, i) => {
1524
+ if (seen.has(hit.id)) {
1525
+ return;
1526
+ }
1527
+ seen.add(hit.id);
1528
+ merged.push({
1529
+ ...hit,
1530
+ similarity: maxBase + (plannerHits.length - i) * 1e-6
1531
+ });
1532
+ });
1533
+ for (const hit of base) {
1534
+ if (merged.length >= limit) {
1535
+ break;
1536
+ }
1537
+ if (seen.has(hit.id)) {
1538
+ continue;
1539
+ }
1540
+ seen.add(hit.id);
1541
+ merged.push(hit);
1542
+ }
1543
+ return { semantic: merged.slice(0, limit), lexical: [] };
1544
+ }
1366
1545
  const out = {
1367
1546
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1368
1547
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
@@ -1372,14 +1551,16 @@ function createUnifiedSearch(deps = {}) {
1372
1551
  }
1373
1552
  return out;
1374
1553
  }
1375
- async function searchUnifiedMemory(input) {
1554
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1376
1555
  const query = input.query.trim();
1377
1556
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1378
1557
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1379
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1380
1558
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1381
1559
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1382
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;
1383
1564
  const reasoningStrategy = resolveReasoningStrategy(input);
1384
1565
  const warnings = [];
1385
1566
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1399,23 +1580,46 @@ function createUnifiedSearch(deps = {}) {
1399
1580
  };
1400
1581
  }
1401
1582
  let memorySubs;
1583
+ let retrievalStatus;
1402
1584
  const reasoningInfo = { strategy: reasoningStrategy };
1403
1585
  if (input.dateFrom || input.dateTo) {
1404
1586
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1405
1587
  }
1406
1588
  if (sources.includes("memory")) {
1407
- 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";
1408
1590
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1409
1591
  try {
1410
1592
  memorySubs = await runMemorySource(
1411
1593
  input,
1412
- limit,
1413
- threshold,
1594
+ candidateLimit,
1595
+ memoryThreshold,
1414
1596
  warnings,
1415
1597
  reasoningStrategy,
1416
1598
  reasoningInfo,
1417
- peerPeers
1599
+ peerPeers,
1600
+ runtimeContext
1418
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
+ }
1419
1623
  } catch (error) {
1420
1624
  logger.warn?.("[memory-store] memory source failed:", error);
1421
1625
  warnings.push({
@@ -1438,12 +1642,13 @@ function createUnifiedSearch(deps = {}) {
1438
1642
  const hits = await deps.searchInsights({
1439
1643
  userId: input.userId,
1440
1644
  query: input.query,
1441
- limit,
1645
+ limit: candidateLimit,
1442
1646
  threshold,
1443
1647
  botIds: input.botIds,
1444
1648
  includeArchived: input.includeArchivedInsights,
1445
1649
  authToken: input.authToken,
1446
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1650
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1651
+ ...runtimeContext ?? {}
1447
1652
  });
1448
1653
  for (const hit of hits) {
1449
1654
  insightHits.push({
@@ -1470,12 +1675,13 @@ function createUnifiedSearch(deps = {}) {
1470
1675
  userId: input.userId,
1471
1676
  query: input.query,
1472
1677
  options: {
1473
- limit,
1678
+ limit: candidateLimit,
1474
1679
  threshold,
1475
1680
  documentIds: input.documentIds
1476
1681
  },
1477
1682
  authToken: input.authToken,
1478
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1683
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1684
+ ...runtimeContext ?? {}
1479
1685
  });
1480
1686
  for (const hit of hits) {
1481
1687
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1493,10 +1699,14 @@ function createUnifiedSearch(deps = {}) {
1493
1699
  memorySubs,
1494
1700
  insightHits,
1495
1701
  knowledgeHits,
1496
- limit,
1702
+ limit: candidateLimit,
1497
1703
  strategy: mergeStrategy
1498
1704
  });
1499
- 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);
1500
1710
  const output = {
1501
1711
  query: input.query,
1502
1712
  sources,
@@ -1504,28 +1714,51 @@ function createUnifiedSearch(deps = {}) {
1504
1714
  count: ranked.length,
1505
1715
  warnings
1506
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
+ }
1507
1750
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1508
1751
  output.reasoning = reasoningInfo;
1509
1752
  }
1510
1753
  return output;
1511
1754
  }
1755
+ async function searchUnifiedMemory(input) {
1756
+ return searchUnifiedMemoryWithRuntime(input);
1757
+ }
1512
1758
  async function searchRawMemorySemantically(input) {
1513
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1514
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1515
- const reasoningStrategy = resolveReasoningStrategy(input);
1516
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1517
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1518
- );
1519
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1520
- return mergeAcrossSources({
1521
- memorySubs: sub,
1522
- insightHits: [],
1523
- knowledgeHits: [],
1524
- limit,
1525
- strategy: mergeStrategy
1526
- });
1759
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1527
1760
  }
1528
- async function search(input) {
1761
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1529
1762
  const query = input.query.trim();
1530
1763
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1531
1764
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1557,19 +1790,24 @@ function createUnifiedSearch(deps = {}) {
1557
1790
  let unifiedHits = [];
1558
1791
  let sources = input.sources ? [...input.sources] : [];
1559
1792
  let unifiedReasoning;
1793
+ let retrievalDiagnostics;
1560
1794
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1561
1795
  const inferredSources = sources.length > 0 ? sources : [
1562
1796
  ...wantsRawTier ? ["memory"] : [],
1563
1797
  ...wantsInsightTier ? ["insights"] : [],
1564
1798
  ...wantsKnowledgeTier ? ["knowledge"] : []
1565
1799
  ];
1566
- const unified = await searchUnifiedMemory({
1567
- ...searchInputToUnified(input),
1568
- sources: inferredSources.length > 0 ? inferredSources : void 0
1569
- });
1800
+ const unified = await searchUnifiedMemoryWithRuntime(
1801
+ {
1802
+ ...searchInputToUnified(input),
1803
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1804
+ },
1805
+ runtimeContext
1806
+ );
1570
1807
  unifiedHits = unified.results;
1571
1808
  sources = unified.sources;
1572
1809
  unifiedReasoning = unified.reasoning;
1810
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1573
1811
  warnings.push(...unified.warnings);
1574
1812
  }
1575
1813
  let summaryHits = [];
@@ -1580,7 +1818,8 @@ function createUnifiedSearch(deps = {}) {
1580
1818
  limit,
1581
1819
  threshold,
1582
1820
  logger,
1583
- peerScope.peers
1821
+ peerScope.peers,
1822
+ runtimeContext
1584
1823
  );
1585
1824
  warnings.push(...summaryBucket.warnings);
1586
1825
  summaryHits = summaryBucket.hits;
@@ -1605,7 +1844,8 @@ function createUnifiedSearch(deps = {}) {
1605
1844
  source: mapHitToEvidenceSource(hit),
1606
1845
  snippet: hit.content,
1607
1846
  score: hit.similarity,
1608
- timestamp: getCandidateTimestamp(hit.metadata)
1847
+ timestamp: getCandidateTimestamp(hit.metadata),
1848
+ metadata: hit.metadata
1609
1849
  }));
1610
1850
  const base = {
1611
1851
  query,
@@ -1618,12 +1858,15 @@ function createUnifiedSearch(deps = {}) {
1618
1858
  if (unifiedReasoning) {
1619
1859
  base.reasoning = unifiedReasoning;
1620
1860
  }
1861
+ if (retrievalDiagnostics) {
1862
+ base.retrievalDiagnostics = retrievalDiagnostics;
1863
+ }
1621
1864
  if (!wantsSynthesis) {
1622
1865
  return base;
1623
1866
  }
1624
1867
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1625
1868
  query,
1626
- hits,
1869
+ evidence,
1627
1870
  responseSchema: synthesisSchema,
1628
1871
  deps,
1629
1872
  logger
@@ -1632,6 +1875,10 @@ function createUnifiedSearch(deps = {}) {
1632
1875
  base.warnings = [...base.warnings, ...synthWarnings];
1633
1876
  return base;
1634
1877
  }
1878
+ async function search(input, runtimeContext) {
1879
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1880
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1881
+ }
1635
1882
  return {
1636
1883
  search,
1637
1884
  searchUnifiedMemory,
@@ -1647,6 +1894,7 @@ function mergeAcrossSources(input) {
1647
1894
  for (const hit of [
1648
1895
  ...memorySubs.semantic,
1649
1896
  ...memorySubs.lexical,
1897
+ ...memorySubs.hybrid ?? [],
1650
1898
  ...input.insightHits,
1651
1899
  ...input.knowledgeHits
1652
1900
  ]) {
@@ -1662,6 +1910,7 @@ function mergeAcrossSources(input) {
1662
1910
  const all = [
1663
1911
  ...memorySubs.semantic,
1664
1912
  ...memorySubs.lexical,
1913
+ ...memorySubs.hybrid ?? [],
1665
1914
  ...memorySubs.entity ?? [],
1666
1915
  ...input.insightHits,
1667
1916
  ...input.knowledgeHits
@@ -1673,6 +1922,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1673
1922
  const lists = [];
1674
1923
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1675
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
+ }
1676
1928
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1677
1929
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1678
1930
  }