@melandlabs/memory-store 1.2.5 → 1.3.1

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