@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.
@@ -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);
@@ -300,7 +363,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
300
363
  return value === "rrf" ? "rrf" : "similarity";
301
364
  }
302
365
  function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
303
- if (value === "rewrite" || value === "iterative") {
366
+ if (value === "rewrite" || value === "iterative" || value === "union") {
304
367
  return value;
305
368
  }
306
369
  return defaultStrategy;
@@ -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,8 +1238,8 @@ 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 = []) {
1149
- if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
1241
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
1242
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
1150
1243
  warnings.push({
1151
1244
  source: "memory",
1152
1245
  code: "memory_iterative_planner_not_configured",
@@ -1162,7 +1255,8 @@ function createUnifiedSearch(deps = {}) {
1162
1255
  });
1163
1256
  if (reasoningInfo) reasoningInfo.degraded = true;
1164
1257
  }
1165
- if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
1258
+ let unionEvidence = null;
1259
+ if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
1166
1260
  const planner = deps.reasoning.iterativePlanner;
1167
1261
  const result = await planner.plan({
1168
1262
  query: input.query,
@@ -1177,7 +1271,9 @@ function createUnifiedSearch(deps = {}) {
1177
1271
  keywords2,
1178
1272
  limit,
1179
1273
  logger,
1180
- peerPeers
1274
+ peerPeers,
1275
+ warnings,
1276
+ runtimeContext
1181
1277
  );
1182
1278
  let semanticHits = [];
1183
1279
  if (typeof deps.embedQuery === "function") {
@@ -1191,7 +1287,9 @@ function createUnifiedSearch(deps = {}) {
1191
1287
  limit,
1192
1288
  threshold,
1193
1289
  logger,
1194
- peerPeers
1290
+ peerPeers,
1291
+ warnings,
1292
+ runtimeContext
1195
1293
  );
1196
1294
  } catch (error) {
1197
1295
  logger.warn?.(
@@ -1219,16 +1317,24 @@ function createUnifiedSearch(deps = {}) {
1219
1317
  reasoningInfo.degraded = true;
1220
1318
  }
1221
1319
  }
1222
- const semantic2 = result.evidence.map((candidate) => ({
1320
+ const evidence = result.evidence.map((candidate) => ({
1223
1321
  type: "memory",
1224
1322
  id: candidate.id,
1225
1323
  content: candidate.content,
1226
1324
  similarity: candidate.similarity,
1227
- metadata: { ...candidate.metadata, reasoning: "iterative" }
1325
+ metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
1228
1326
  }));
1229
- return { semantic: semantic2, lexical: [] };
1327
+ if (reasoningStrategy === "iterative") {
1328
+ return { semantic: evidence, lexical: [] };
1329
+ }
1330
+ unionEvidence = evidence;
1230
1331
  }
1231
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
+ });
1232
1338
  warnings.push({
1233
1339
  source: "memory",
1234
1340
  code: "memory_lexical_search_fallback",
@@ -1236,7 +1342,16 @@ function createUnifiedSearch(deps = {}) {
1236
1342
  });
1237
1343
  const keywords2 = deriveLexicalKeywords(input.query);
1238
1344
  const lexical2 = filterByDateRange(
1239
- 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
+ ),
1240
1355
  input.dateFrom,
1241
1356
  input.dateTo
1242
1357
  );
@@ -1244,6 +1359,26 @@ function createUnifiedSearch(deps = {}) {
1244
1359
  }
1245
1360
  let semantic = [];
1246
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
+ }
1247
1382
  if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
1248
1383
  try {
1249
1384
  const variants = await deps.reasoning.queryRewriter.rewrite({
@@ -1262,7 +1397,17 @@ function createUnifiedSearch(deps = {}) {
1262
1397
  );
1263
1398
  const lists = await Promise.all(
1264
1399
  embeddings.map(
1265
- (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
+ )
1266
1411
  )
1267
1412
  );
1268
1413
  semantic = mergeByMaxScore(lists);
@@ -1285,7 +1430,9 @@ function createUnifiedSearch(deps = {}) {
1285
1430
  limit,
1286
1431
  threshold,
1287
1432
  logger,
1288
- peerPeers
1433
+ peerPeers,
1434
+ warnings,
1435
+ runtimeContext
1289
1436
  );
1290
1437
  }
1291
1438
  let lexical = [];
@@ -1304,7 +1451,8 @@ function createUnifiedSearch(deps = {}) {
1304
1451
  limit,
1305
1452
  botId,
1306
1453
  ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1307
- ...factTypes ? { factTypes } : {}
1454
+ ...factTypes ? { factTypes } : {},
1455
+ ...runtimeContext ?? {}
1308
1456
  })
1309
1457
  )
1310
1458
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -1330,7 +1478,7 @@ function createUnifiedSearch(deps = {}) {
1330
1478
  let entity;
1331
1479
  if (typeof deps.entitySearch === "function") {
1332
1480
  entity = filterByDateRange(
1333
- await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1481
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
1334
1482
  input.dateFrom,
1335
1483
  input.dateTo
1336
1484
  );
@@ -1352,6 +1500,37 @@ function createUnifiedSearch(deps = {}) {
1352
1500
  message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1353
1501
  });
1354
1502
  }
1503
+ if (unionEvidence) {
1504
+ const plannerHits = unionEvidence;
1505
+ const base = mergeByMaxScore([
1506
+ filterByDateRange(semantic, input.dateFrom, input.dateTo),
1507
+ filterByDateRange(lexical, input.dateFrom, input.dateTo)
1508
+ ]);
1509
+ const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
1510
+ const seen = /* @__PURE__ */ new Set();
1511
+ const merged = [];
1512
+ plannerHits.forEach((hit, i) => {
1513
+ if (seen.has(hit.id)) {
1514
+ return;
1515
+ }
1516
+ seen.add(hit.id);
1517
+ merged.push({
1518
+ ...hit,
1519
+ similarity: maxBase + (plannerHits.length - i) * 1e-6
1520
+ });
1521
+ });
1522
+ for (const hit of base) {
1523
+ if (merged.length >= limit) {
1524
+ break;
1525
+ }
1526
+ if (seen.has(hit.id)) {
1527
+ continue;
1528
+ }
1529
+ seen.add(hit.id);
1530
+ merged.push(hit);
1531
+ }
1532
+ return { semantic: merged.slice(0, limit), lexical: [] };
1533
+ }
1355
1534
  const out = {
1356
1535
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1357
1536
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
@@ -1361,14 +1540,16 @@ function createUnifiedSearch(deps = {}) {
1361
1540
  }
1362
1541
  return out;
1363
1542
  }
1364
- async function searchUnifiedMemory(input) {
1543
+ async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
1365
1544
  const query = input.query.trim();
1366
1545
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
1367
1546
  const limit = clampUnifiedMemorySearchLimit(input.limit);
1368
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1369
1547
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1370
1548
  input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1371
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;
1372
1553
  const reasoningStrategy = resolveReasoningStrategy(input);
1373
1554
  const warnings = [];
1374
1555
  const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
@@ -1388,23 +1569,46 @@ function createUnifiedSearch(deps = {}) {
1388
1569
  };
1389
1570
  }
1390
1571
  let memorySubs;
1572
+ let retrievalStatus;
1391
1573
  const reasoningInfo = { strategy: reasoningStrategy };
1392
1574
  if (input.dateFrom || input.dateTo) {
1393
1575
  reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1394
1576
  }
1395
1577
  if (sources.includes("memory")) {
1396
- 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";
1397
1579
  if (isRawMessageStorageAvailable() || hasRawProviders) {
1398
1580
  try {
1399
1581
  memorySubs = await runMemorySource(
1400
1582
  input,
1401
- limit,
1402
- threshold,
1583
+ candidateLimit,
1584
+ memoryThreshold,
1403
1585
  warnings,
1404
1586
  reasoningStrategy,
1405
1587
  reasoningInfo,
1406
- peerPeers
1588
+ peerPeers,
1589
+ runtimeContext
1407
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
+ }
1408
1612
  } catch (error) {
1409
1613
  logger.warn?.("[memory-store] memory source failed:", error);
1410
1614
  warnings.push({
@@ -1427,12 +1631,13 @@ function createUnifiedSearch(deps = {}) {
1427
1631
  const hits = await deps.searchInsights({
1428
1632
  userId: input.userId,
1429
1633
  query: input.query,
1430
- limit,
1634
+ limit: candidateLimit,
1431
1635
  threshold,
1432
1636
  botIds: input.botIds,
1433
1637
  includeArchived: input.includeArchivedInsights,
1434
1638
  authToken: input.authToken,
1435
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1639
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1640
+ ...runtimeContext ?? {}
1436
1641
  });
1437
1642
  for (const hit of hits) {
1438
1643
  insightHits.push({
@@ -1459,12 +1664,13 @@ function createUnifiedSearch(deps = {}) {
1459
1664
  userId: input.userId,
1460
1665
  query: input.query,
1461
1666
  options: {
1462
- limit,
1667
+ limit: candidateLimit,
1463
1668
  threshold,
1464
1669
  documentIds: input.documentIds
1465
1670
  },
1466
1671
  authToken: input.authToken,
1467
- ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1672
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1673
+ ...runtimeContext ?? {}
1468
1674
  });
1469
1675
  for (const hit of hits) {
1470
1676
  knowledgeHits.push(toKnowledgeResult(hit));
@@ -1482,10 +1688,14 @@ function createUnifiedSearch(deps = {}) {
1482
1688
  memorySubs,
1483
1689
  insightHits,
1484
1690
  knowledgeHits,
1485
- limit,
1691
+ limit: candidateLimit,
1486
1692
  strategy: mergeStrategy
1487
1693
  });
1488
- 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);
1489
1699
  const output = {
1490
1700
  query: input.query,
1491
1701
  sources,
@@ -1493,28 +1703,51 @@ function createUnifiedSearch(deps = {}) {
1493
1703
  count: ranked.length,
1494
1704
  warnings
1495
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
+ }
1496
1739
  if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1497
1740
  output.reasoning = reasoningInfo;
1498
1741
  }
1499
1742
  return output;
1500
1743
  }
1744
+ async function searchUnifiedMemory(input) {
1745
+ return searchUnifiedMemoryWithRuntime(input);
1746
+ }
1501
1747
  async function searchRawMemorySemantically(input) {
1502
- const limit = clampUnifiedMemorySearchLimit(input.limit);
1503
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1504
- const reasoningStrategy = resolveReasoningStrategy(input);
1505
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1506
- input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1507
- );
1508
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1509
- return mergeAcrossSources({
1510
- memorySubs: sub,
1511
- insightHits: [],
1512
- knowledgeHits: [],
1513
- limit,
1514
- strategy: mergeStrategy
1515
- });
1748
+ return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
1516
1749
  }
1517
- async function search(input) {
1750
+ async function searchWithResolvedRuntime(input, runtimeContext) {
1518
1751
  const query = input.query.trim();
1519
1752
  const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1520
1753
  const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
@@ -1546,19 +1779,24 @@ function createUnifiedSearch(deps = {}) {
1546
1779
  let unifiedHits = [];
1547
1780
  let sources = input.sources ? [...input.sources] : [];
1548
1781
  let unifiedReasoning;
1782
+ let retrievalDiagnostics;
1549
1783
  if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1550
1784
  const inferredSources = sources.length > 0 ? sources : [
1551
1785
  ...wantsRawTier ? ["memory"] : [],
1552
1786
  ...wantsInsightTier ? ["insights"] : [],
1553
1787
  ...wantsKnowledgeTier ? ["knowledge"] : []
1554
1788
  ];
1555
- const unified = await searchUnifiedMemory({
1556
- ...searchInputToUnified(input),
1557
- sources: inferredSources.length > 0 ? inferredSources : void 0
1558
- });
1789
+ const unified = await searchUnifiedMemoryWithRuntime(
1790
+ {
1791
+ ...searchInputToUnified(input),
1792
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1793
+ },
1794
+ runtimeContext
1795
+ );
1559
1796
  unifiedHits = unified.results;
1560
1797
  sources = unified.sources;
1561
1798
  unifiedReasoning = unified.reasoning;
1799
+ retrievalDiagnostics = unified.retrievalDiagnostics;
1562
1800
  warnings.push(...unified.warnings);
1563
1801
  }
1564
1802
  let summaryHits = [];
@@ -1569,7 +1807,8 @@ function createUnifiedSearch(deps = {}) {
1569
1807
  limit,
1570
1808
  threshold,
1571
1809
  logger,
1572
- peerScope.peers
1810
+ peerScope.peers,
1811
+ runtimeContext
1573
1812
  );
1574
1813
  warnings.push(...summaryBucket.warnings);
1575
1814
  summaryHits = summaryBucket.hits;
@@ -1594,7 +1833,8 @@ function createUnifiedSearch(deps = {}) {
1594
1833
  source: mapHitToEvidenceSource(hit),
1595
1834
  snippet: hit.content,
1596
1835
  score: hit.similarity,
1597
- timestamp: getCandidateTimestamp(hit.metadata)
1836
+ timestamp: getCandidateTimestamp(hit.metadata),
1837
+ metadata: hit.metadata
1598
1838
  }));
1599
1839
  const base = {
1600
1840
  query,
@@ -1607,12 +1847,15 @@ function createUnifiedSearch(deps = {}) {
1607
1847
  if (unifiedReasoning) {
1608
1848
  base.reasoning = unifiedReasoning;
1609
1849
  }
1850
+ if (retrievalDiagnostics) {
1851
+ base.retrievalDiagnostics = retrievalDiagnostics;
1852
+ }
1610
1853
  if (!wantsSynthesis) {
1611
1854
  return base;
1612
1855
  }
1613
1856
  const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1614
1857
  query,
1615
- hits,
1858
+ evidence,
1616
1859
  responseSchema: synthesisSchema,
1617
1860
  deps,
1618
1861
  logger
@@ -1621,6 +1864,10 @@ function createUnifiedSearch(deps = {}) {
1621
1864
  base.warnings = [...base.warnings, ...synthWarnings];
1622
1865
  return base;
1623
1866
  }
1867
+ async function search(input, runtimeContext) {
1868
+ const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
1869
+ return searchWithResolvedRuntime(input, resolvedRuntime);
1870
+ }
1624
1871
  return {
1625
1872
  search,
1626
1873
  searchUnifiedMemory,
@@ -1636,6 +1883,7 @@ function mergeAcrossSources(input) {
1636
1883
  for (const hit of [
1637
1884
  ...memorySubs.semantic,
1638
1885
  ...memorySubs.lexical,
1886
+ ...memorySubs.hybrid ?? [],
1639
1887
  ...input.insightHits,
1640
1888
  ...input.knowledgeHits
1641
1889
  ]) {
@@ -1651,6 +1899,7 @@ function mergeAcrossSources(input) {
1651
1899
  const all = [
1652
1900
  ...memorySubs.semantic,
1653
1901
  ...memorySubs.lexical,
1902
+ ...memorySubs.hybrid ?? [],
1654
1903
  ...memorySubs.entity ?? [],
1655
1904
  ...input.insightHits,
1656
1905
  ...input.knowledgeHits
@@ -1662,6 +1911,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1662
1911
  const lists = [];
1663
1912
  if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1664
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
+ }
1665
1917
  if (memorySubs.entity && memorySubs.entity.length > 0) {
1666
1918
  lists.push({ name: "memory-entity", hits: memorySubs.entity });
1667
1919
  }