@melandlabs/memory-store 0.3.1 → 1.1.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.
@@ -1,9 +1,11 @@
1
+ import '@melandlabs/contracts';
1
2
  import '@melandlabs/ai/memory';
2
3
  import { ChromaVectorStore } from '@melandlabs/rag';
3
4
  import { mkdirSync } from 'fs';
4
5
  import { dirname } from 'path';
5
6
  import { getOpenContextPath } from '@melandlabs/env-config';
6
7
  import { SQLiteRawMessageManager } from '@melandlabs/sqlite';
8
+ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidation';
7
9
 
8
10
  var __defProp = Object.defineProperty;
9
11
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -14,8 +16,6 @@ var __export = (target, all) => {
14
16
  for (var name in all)
15
17
  __defProp(target, name, { get: all[name], enumerable: true });
16
18
  };
17
-
18
- // src/contracts.ts
19
19
  var init_contracts = __esm({
20
20
  "src/contracts.ts"() {
21
21
  }
@@ -370,6 +370,416 @@ function isRawMemorySemanticResult(result) {
370
370
  const item = result;
371
371
  return typeof item.id === "string" && typeof item.content === "string" && typeof item.similarity === "number" && Boolean(item.metadata) && typeof item.metadata === "object";
372
372
  }
373
+ async function resolveScopePeer(input) {
374
+ const peers = input.peerFilter ?? [];
375
+ if (peers.length === 0) {
376
+ return { peers: [], warnings: [] };
377
+ }
378
+ if (typeof input.scopeCheck !== "function") {
379
+ return { peers, warnings: [] };
380
+ }
381
+ const retained = [];
382
+ const dropped = [];
383
+ for (const peer of peers) {
384
+ const ok = await input.scopeCheck({ userId: input.userId, peers: [peer] });
385
+ if (ok) {
386
+ retained.push(peer);
387
+ } else {
388
+ dropped.push(peer);
389
+ }
390
+ }
391
+ if (dropped.length === 0) {
392
+ return { peers, warnings: [] };
393
+ }
394
+ if (retained.length === 0) {
395
+ return {
396
+ peers: [],
397
+ warnings: [
398
+ {
399
+ source: "memory",
400
+ code: "peer_filter_outside_user_scope",
401
+ message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored; falling back to userId scope`
402
+ }
403
+ ]
404
+ };
405
+ }
406
+ return {
407
+ peers: retained,
408
+ warnings: [
409
+ {
410
+ source: "memory",
411
+ code: "peer_filter_outside_user_scope",
412
+ message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored because they fall outside userId ${input.userId}`
413
+ }
414
+ ]
415
+ };
416
+ }
417
+
418
+ // src/search/apply-reflect.ts
419
+ var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
420
+ function reflectTierToEvidenceTier(source) {
421
+ switch (source) {
422
+ case "raw":
423
+ return "short";
424
+ case "summary":
425
+ return "long";
426
+ default:
427
+ return "mid";
428
+ }
429
+ }
430
+ function toMemoryEvidenceRecord(input, fallbackUserId) {
431
+ const tier = reflectTierToEvidenceTier(input.source);
432
+ const timestamp = input.timestamp ?? Date.now();
433
+ return {
434
+ id: input.id,
435
+ userId: fallbackUserId,
436
+ timestamp,
437
+ tier,
438
+ text: input.snippet,
439
+ metadata: {
440
+ source: input.source,
441
+ score: input.score,
442
+ ...input.timestamp !== void 0 ? { originalTimestamp: input.timestamp } : {}
443
+ }
444
+ };
445
+ }
446
+ async function applyReflectedPlan(deps, store, input, _logger) {
447
+ const query = input.query.trim();
448
+ const warnings = [];
449
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
450
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
451
+ const tiers = input.tiers && input.tiers.length > 0 ? input.tiers : DEFAULT_TIERS;
452
+ const searchInput = {
453
+ userId: input.userId,
454
+ query,
455
+ botIds: input.botIds,
456
+ dateFrom: input.dateFrom,
457
+ dateTo: input.dateTo,
458
+ tiers,
459
+ limit,
460
+ threshold,
461
+ authToken: input.authToken,
462
+ peerFilter: input.peerFilter,
463
+ synthesize: true
464
+ };
465
+ const search = createUnifiedSearch(deps);
466
+ const result = await search.search(searchInput);
467
+ warnings.push(...result.warnings);
468
+ const evidence = result.evidence.map((item) => ({
469
+ id: item.id,
470
+ source: item.source ?? "raw",
471
+ snippet: item.snippet,
472
+ score: item.score,
473
+ timestamp: item.timestamp
474
+ }));
475
+ const records = evidence.map((item) => toMemoryEvidenceRecord(item, input.userId));
476
+ const applyInput = {
477
+ userId: input.userId,
478
+ ownerScope: input.ownerScope,
479
+ records,
480
+ plan: input.plan,
481
+ dryRun: input.dryRun,
482
+ expectedVersion: input.expectedVersion,
483
+ getClusterKey: input.getClusterKey ?? defaultClusterKey,
484
+ llmReview: deps.reasoning?.complete ? {
485
+ complete: deps.reasoning.complete,
486
+ maxTokens: input.llmPlanReview?.maxTokens
487
+ } : void 0,
488
+ graphStore: input.graphStore ?? store.graphStore,
489
+ storage: input.storage ?? store.storage
490
+ };
491
+ const applyOutput = await applyReflectedConsolidationPlan(applyInput);
492
+ for (const warning of applyOutput.warnings) {
493
+ warnings.push({
494
+ source: "memory",
495
+ code: warning.code,
496
+ message: warning.message
497
+ });
498
+ }
499
+ const summaryAnswer = makePlanSummary(applyOutput.plan);
500
+ return {
501
+ answer: result.answer ? `${result.answer}
502
+
503
+ Plan summary:
504
+ ${summaryAnswer}` : summaryAnswer,
505
+ plan: applyOutput.plan,
506
+ persistenceResult: applyOutput.persistenceResult,
507
+ deprecationCounts: applyOutput.deprecationCounts,
508
+ warnings,
509
+ applied: applyOutput.applied,
510
+ evidence
511
+ };
512
+ }
513
+ function defaultClusterKey(record) {
514
+ return record.text ?? record.id;
515
+ }
516
+ function makePlanSummary(plan) {
517
+ const lines = [];
518
+ const counts = {
519
+ preserve: plan.actions.preserve.length,
520
+ observe: plan.actions.observe.length,
521
+ decay: plan.actions.decay.length,
522
+ deprecate: plan.actions.deprecate.length
523
+ };
524
+ lines.push(
525
+ `- preserve=${counts.preserve} observe=${counts.observe} decay=${counts.decay} deprecate=${counts.deprecate}`
526
+ );
527
+ for (const entry of plan.actions.deprecate.slice(0, 5)) {
528
+ lines.push(
529
+ ` deprecate ${entry.recordIds.length} record(s) from cluster ${entry.clusterKey} -> ${entry.supersededBySummaryId ?? entry.winningClusterKey}`
530
+ );
531
+ }
532
+ return lines.join("\n");
533
+ }
534
+
535
+ // src/search/reranker.ts
536
+ async function applyReranker(reranker, query, results) {
537
+ if (!reranker || results.length === 0) {
538
+ return results;
539
+ }
540
+ const scores = await reranker.rerank({
541
+ query,
542
+ candidates: results.map((result) => ({
543
+ id: result.id,
544
+ content: result.content,
545
+ metadata: result.metadata
546
+ }))
547
+ });
548
+ if (scores.length === 0) {
549
+ return results;
550
+ }
551
+ const byId = new Map(results.map((result) => [result.id, result]));
552
+ const reordered = [];
553
+ const seen = /* @__PURE__ */ new Set();
554
+ for (const { id, score } of scores) {
555
+ const hit = byId.get(id);
556
+ if (!hit || seen.has(id)) {
557
+ continue;
558
+ }
559
+ seen.add(id);
560
+ reordered.push({ ...hit, metadata: { ...hit.metadata, rerankerScore: score } });
561
+ }
562
+ for (const hit of results) {
563
+ if (!seen.has(hit.id)) {
564
+ reordered.push(hit);
565
+ }
566
+ }
567
+ return reordered;
568
+ }
569
+
570
+ // src/search/gather-evidence.ts
571
+ var DEFAULT_TIERS2 = ["summary", "raw", "insight", "knowledge"];
572
+ var MAX_EVIDENCE_CHARS = 600;
573
+ function toSummaryResult(row) {
574
+ const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
575
+ const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
576
+ return {
577
+ type: "memory",
578
+ id: row.summaryId,
579
+ content: row.summaryText,
580
+ similarity: 1,
581
+ metadata: {
582
+ tier: "summary",
583
+ summaryTier: row.summaryTier,
584
+ keywords: row.keywords,
585
+ startTimestamp: start,
586
+ endTimestamp: end
587
+ }
588
+ };
589
+ }
590
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
591
+ if (typeof deps.searchSummaries !== "function") {
592
+ return { hits: [], warnings: [] };
593
+ }
594
+ const keywords = input.query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
595
+ try {
596
+ const rows = await deps.searchSummaries({
597
+ userId: input.userId,
598
+ query: input.query,
599
+ keywords,
600
+ limit,
601
+ threshold,
602
+ authToken: input.authToken,
603
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
604
+ });
605
+ const hits = rows.map(toSummaryResult);
606
+ return { hits, warnings: [] };
607
+ } catch (error) {
608
+ logger.warn?.("[memory-store] gather-evidence: summary search failed:", error);
609
+ return {
610
+ hits: [],
611
+ warnings: [
612
+ {
613
+ source: "memory",
614
+ code: "reflect_summaries_failed",
615
+ message: error.message ?? "reflect_summaries_failed"
616
+ }
617
+ ]
618
+ };
619
+ }
620
+ }
621
+ function truncate(input, max = MAX_EVIDENCE_CHARS) {
622
+ if (input.length <= max) {
623
+ return input;
624
+ }
625
+ return `${input.slice(0, max - 1)}\u2026`;
626
+ }
627
+ function buildSynthesisPrompt(input) {
628
+ const grouped = /* @__PURE__ */ new Map();
629
+ for (const item of input.evidence) {
630
+ const bucket = grouped.get(item.source) ?? [];
631
+ bucket.push(item);
632
+ grouped.set(item.source, bucket);
633
+ }
634
+ const sections = [];
635
+ for (const tier of DEFAULT_TIERS2) {
636
+ const items = grouped.get(tier) ?? [];
637
+ if (items.length === 0) {
638
+ continue;
639
+ }
640
+ const lines = items.map(
641
+ (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
642
+ );
643
+ sections.push(`## ${tier}
644
+ ${lines.join("\n")}`);
645
+ }
646
+ const schemaDirective = input.responseSchema ? `
647
+ Respond with a JSON object that conforms to the following schema:
648
+ ${JSON.stringify(input.responseSchema, null, 2)}
649
+ Wrap your JSON in a single fenced \`\`\`json code block; do not add any prose outside the code block.` : "\nRespond in plain prose. Be concise; cite evidence by its [n] number.";
650
+ if (sections.length === 0) {
651
+ return `You are a reflection assistant.
652
+
653
+ Question: ${input.query}
654
+
655
+ No evidence was found across any tier. State that explicitly.${schemaDirective}`;
656
+ }
657
+ return `You are a reflection assistant. Synthesise the user's question using only the evidence below. Do not invent facts.
658
+
659
+ Question: ${input.query}
660
+
661
+ Evidence (grouped by tier):
662
+ ${sections.join("\n\n")}${schemaDirective}`;
663
+ }
664
+ function extractJsonPayload(text) {
665
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
666
+ if (fenced?.[1]) {
667
+ return fenced[1];
668
+ }
669
+ const start = text.indexOf("{");
670
+ if (start === -1) {
671
+ return void 0;
672
+ }
673
+ let depth = 0;
674
+ let inString = false;
675
+ let escaped = false;
676
+ for (let i = start; i < text.length; i += 1) {
677
+ const ch = text[i];
678
+ if (escaped) {
679
+ escaped = false;
680
+ continue;
681
+ }
682
+ if (inString) {
683
+ if (ch === "\\") {
684
+ escaped = true;
685
+ } else if (ch === '"') {
686
+ inString = false;
687
+ }
688
+ continue;
689
+ }
690
+ if (ch === '"') {
691
+ inString = true;
692
+ } else if (ch === "{") {
693
+ depth += 1;
694
+ } else if (ch === "}") {
695
+ depth -= 1;
696
+ if (depth === 0) {
697
+ return text.slice(start, i + 1);
698
+ }
699
+ }
700
+ }
701
+ return void 0;
702
+ }
703
+ function coerceAnswer(text, schema) {
704
+ if (!schema) {
705
+ return text;
706
+ }
707
+ const payload = extractJsonPayload(text);
708
+ if (!payload) {
709
+ return text;
710
+ }
711
+ try {
712
+ const parsed = JSON.parse(payload);
713
+ if (parsed && typeof parsed === "object") {
714
+ const answer = parsed.answer;
715
+ if (typeof answer === "string") {
716
+ return answer;
717
+ }
718
+ }
719
+ } catch {
720
+ }
721
+ return text;
722
+ }
723
+ async function synthesizeAnswer(input) {
724
+ const { query, hits, responseSchema, deps, logger } = input;
725
+ const evidence = hits.map((hit) => ({
726
+ id: hit.id,
727
+ source: mapHitToTier(hit),
728
+ snippet: truncate(hit.content),
729
+ score: hit.similarity
730
+ }));
731
+ const warnings = [];
732
+ const complete = deps.reasoning?.complete;
733
+ if (!complete) {
734
+ warnings.push({
735
+ source: "memory",
736
+ code: "reflect_llm_not_configured",
737
+ message: "No `reasoning.complete` callback is configured; search() returned evidence without an LLM synthesis."
738
+ });
739
+ return { answer: "", warnings };
740
+ }
741
+ const prompt = buildSynthesisPrompt({ query, evidence, responseSchema });
742
+ try {
743
+ const raw = await complete(prompt);
744
+ const answer = coerceAnswer(raw, responseSchema);
745
+ logger.log?.("[memory-store] search synthesis completed", {
746
+ evidenceCount: evidence.length,
747
+ answerChars: answer.length
748
+ });
749
+ return { answer, warnings };
750
+ } catch (error) {
751
+ logger.warn?.("[memory-store] search synthesis failed:", error);
752
+ warnings.push({
753
+ source: "memory",
754
+ code: "reflect_llm_failed",
755
+ message: error.message ?? "reflect_llm_failed"
756
+ });
757
+ return { answer: "", warnings };
758
+ }
759
+ }
760
+ function mapHitToTier(hit) {
761
+ const tierMarker = hit.metadata?.tier;
762
+ if (tierMarker === "summary") {
763
+ return "summary";
764
+ }
765
+ if (hit.type === "memory") {
766
+ return "raw";
767
+ }
768
+ if (hit.type === "insight") {
769
+ return "insight";
770
+ }
771
+ if (hit.type === "knowledge") {
772
+ return "knowledge";
773
+ }
774
+ return hit.type;
775
+ }
776
+ async function resolveSearchScopePeers(input) {
777
+ return resolveScopePeer({
778
+ userId: input.userId,
779
+ peerFilter: input.peerFilter,
780
+ scopeCheck: input.deps.peerScopeCheck
781
+ });
782
+ }
373
783
 
374
784
  // src/search/unified-search.ts
375
785
  function deriveLexicalKeywords(query) {
@@ -441,7 +851,7 @@ async function embedQueryVariant(embedQuery, input, query) {
441
851
  authToken: input.authToken
442
852
  });
443
853
  }
444
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger) {
854
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
445
855
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
446
856
  let semantic = [];
447
857
  if (isRawMessageChromaEnabled()) {
@@ -471,6 +881,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
471
881
  }
472
882
  } else if (typeof deps.searchRawMessagesAnn === "function") {
473
883
  const searchRawMessagesAnn = deps.searchRawMessagesAnn;
884
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
474
885
  semantic = (await Promise.all(
475
886
  filters.map(
476
887
  (filter) => searchRawMessagesAnn({
@@ -478,7 +889,9 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
478
889
  queryEmbedding,
479
890
  limit,
480
891
  threshold,
481
- botId: "botId" in filter ? filter.botId : void 0
892
+ botId: "botId" in filter ? filter.botId : void 0,
893
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
894
+ ...factTypes ? { factTypes } : {}
482
895
  })
483
896
  )
484
897
  )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
@@ -488,11 +901,14 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
488
901
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
489
902
  const manager2 = await getRawMessageManager2();
490
903
  if (typeof manager2.searchMessagesSemantically === "function") {
904
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
491
905
  const results = await manager2.searchMessagesSemantically({
492
906
  userId: input.userId,
493
907
  queryEmbedding,
494
908
  limit,
495
- threshold
909
+ threshold,
910
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
911
+ ...factTypes ? { factTypes } : {}
496
912
  });
497
913
  semantic = results.map((r) => ({
498
914
  type: "memory",
@@ -528,7 +944,7 @@ function toIterativeRecallCandidate(hit) {
528
944
  metadata: hit.metadata
529
945
  };
530
946
  }
531
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger) {
947
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
532
948
  if (keywords.length === 0) {
533
949
  return [];
534
950
  }
@@ -536,13 +952,16 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
536
952
  try {
537
953
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
538
954
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
955
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
539
956
  return (await Promise.all(
540
957
  filters.map(
541
958
  (botId) => searchRawMessagesLexical({
542
959
  userId: input.userId,
543
960
  keywords,
544
961
  limit: Math.ceil(limit / filters.length),
545
- botId
962
+ botId,
963
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
964
+ ...factTypes ? { factTypes } : {}
546
965
  })
547
966
  )
548
967
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -556,13 +975,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
556
975
  try {
557
976
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
558
977
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
978
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
559
979
  return (await Promise.all(
560
980
  filters.map(
561
981
  (botId) => lexicalSearchRawMessages2({
562
982
  userId: input.userId,
563
983
  keywords,
564
984
  limit: Math.ceil(limit / filters.length),
565
- botId
985
+ botId,
986
+ ...factTypes ? { factTypes } : {}
566
987
  })
567
988
  )
568
989
  )).flat().filter(Boolean).map(
@@ -575,6 +996,42 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
575
996
  }
576
997
  return [];
577
998
  }
999
+ function searchInputToUnified(input) {
1000
+ return {
1001
+ userId: input.userId,
1002
+ query: input.query,
1003
+ sources: input.sources ? [...input.sources] : void 0,
1004
+ limit: input.limit,
1005
+ threshold: input.threshold,
1006
+ authToken: input.authToken,
1007
+ includeArchivedInsights: input.includeArchivedInsights,
1008
+ botIds: input.botIds,
1009
+ documentIds: input.documentIds,
1010
+ asOf: input.asOf,
1011
+ dateFrom: input.dateFrom,
1012
+ dateTo: input.dateTo,
1013
+ mergeStrategy: input.mergeStrategy,
1014
+ peerFilter: input.peerFilter,
1015
+ reasoningStrategy: input.reasoningStrategy,
1016
+ factTypes: input.factTypes
1017
+ };
1018
+ }
1019
+ function mapHitToEvidenceSource(hit) {
1020
+ const tierMarker = hit.metadata?.tier;
1021
+ if (tierMarker === "summary") {
1022
+ return "summary";
1023
+ }
1024
+ if (hit.type === "memory") {
1025
+ return "raw";
1026
+ }
1027
+ if (hit.type === "insight") {
1028
+ return "insight";
1029
+ }
1030
+ if (hit.type === "knowledge") {
1031
+ return "knowledge";
1032
+ }
1033
+ return hit.type;
1034
+ }
578
1035
  function createUnifiedSearch(deps = {}) {
579
1036
  const logger = deps.logger ?? console;
580
1037
  function resolveReasoningStrategy(input) {
@@ -584,130 +1041,7 @@ function createUnifiedSearch(deps = {}) {
584
1041
  }
585
1042
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
586
1043
  }
587
- async function searchUnifiedMemory(input) {
588
- const query = input.query.trim();
589
- const sources = normalizeUnifiedMemorySearchSources(input.sources);
590
- const limit = clampUnifiedMemorySearchLimit(input.limit);
591
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
592
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy);
593
- const reasoningStrategy = resolveReasoningStrategy(input);
594
- const warnings = [];
595
- if (!query) {
596
- return {
597
- query,
598
- sources,
599
- results: [],
600
- count: 0,
601
- warnings
602
- };
603
- }
604
- let memorySubs;
605
- const reasoningInfo = { strategy: reasoningStrategy };
606
- if (input.dateFrom || input.dateTo) {
607
- reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
608
- }
609
- if (sources.includes("memory")) {
610
- if (isRawMessageStorageAvailable()) {
611
- try {
612
- memorySubs = await runMemorySource(
613
- input,
614
- limit,
615
- threshold,
616
- warnings,
617
- reasoningStrategy,
618
- reasoningInfo
619
- );
620
- } catch (error) {
621
- logger.warn?.("[memory-store] memory source failed:", error);
622
- warnings.push({
623
- source: "memory",
624
- code: "memory_search_failed",
625
- message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
626
- });
627
- }
628
- } else {
629
- warnings.push({
630
- source: "memory",
631
- code: "raw_message_storage_unavailable",
632
- message: "Raw memory storage is not available in this environment."
633
- });
634
- }
635
- }
636
- const insightHits = [];
637
- if (sources.includes("insights") && typeof deps.searchInsights === "function") {
638
- try {
639
- const hits = await deps.searchInsights({
640
- userId: input.userId,
641
- query: input.query,
642
- limit,
643
- threshold,
644
- botIds: input.botIds,
645
- includeArchived: input.includeArchivedInsights,
646
- authToken: input.authToken
647
- });
648
- for (const hit of hits) {
649
- insightHits.push({
650
- type: "insight",
651
- id: hit.id,
652
- content: hit.content,
653
- similarity: hit.similarity,
654
- metadata: hit.metadata
655
- });
656
- }
657
- } catch (error) {
658
- logger.warn?.("[memory-store] insights source failed:", error);
659
- warnings.push({
660
- source: "insights",
661
- code: "insights_search_failed",
662
- message: error.message ?? "insights_search_failed"
663
- });
664
- }
665
- } else if (sources.includes("insights")) ;
666
- const knowledgeHits = [];
667
- if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
668
- try {
669
- const hits = await deps.searchKnowledge({
670
- userId: input.userId,
671
- query: input.query,
672
- options: {
673
- limit,
674
- threshold,
675
- documentIds: input.documentIds
676
- },
677
- authToken: input.authToken
678
- });
679
- for (const hit of hits) {
680
- knowledgeHits.push(toKnowledgeResult(hit));
681
- }
682
- } catch (error) {
683
- logger.warn?.("[memory-store] knowledge source failed:", error);
684
- warnings.push({
685
- source: "knowledge",
686
- code: "knowledge_search_failed",
687
- message: error.message ?? "knowledge_search_failed"
688
- });
689
- }
690
- } else if (sources.includes("knowledge")) ;
691
- const merged = mergeAcrossSources({
692
- memorySubs,
693
- insightHits,
694
- knowledgeHits,
695
- limit,
696
- strategy: mergeStrategy
697
- });
698
- const output = {
699
- query: input.query,
700
- sources,
701
- results: merged,
702
- count: merged.length,
703
- warnings
704
- };
705
- if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
706
- output.reasoning = reasoningInfo;
707
- }
708
- return output;
709
- }
710
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo) {
1044
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
711
1045
  if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
712
1046
  warnings.push({
713
1047
  source: "memory",
@@ -733,7 +1067,14 @@ function createUnifiedSearch(deps = {}) {
733
1067
  executor: {
734
1068
  search: async (request) => {
735
1069
  const keywords2 = request.keywords.length > 0 ? request.keywords : deriveLexicalKeywords(input.query);
736
- const lexicalHits = await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger);
1070
+ const lexicalHits = await runLexicalSearchForKeywords(
1071
+ deps,
1072
+ input,
1073
+ keywords2,
1074
+ limit,
1075
+ logger,
1076
+ peerPeers
1077
+ );
737
1078
  let semanticHits = [];
738
1079
  if (typeof deps.embedQuery === "function") {
739
1080
  try {
@@ -745,7 +1086,8 @@ function createUnifiedSearch(deps = {}) {
745
1086
  queryEmbedding,
746
1087
  limit,
747
1088
  threshold,
748
- logger
1089
+ logger,
1090
+ peerPeers
749
1091
  );
750
1092
  } catch (error) {
751
1093
  logger.warn?.(
@@ -790,7 +1132,7 @@ function createUnifiedSearch(deps = {}) {
790
1132
  });
791
1133
  const keywords2 = deriveLexicalKeywords(input.query);
792
1134
  const lexical2 = filterByDateRange(
793
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger),
1135
+ await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
794
1136
  input.dateFrom,
795
1137
  input.dateTo
796
1138
  );
@@ -816,7 +1158,7 @@ function createUnifiedSearch(deps = {}) {
816
1158
  );
817
1159
  const lists = await Promise.all(
818
1160
  embeddings.map(
819
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger)
1161
+ (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
820
1162
  )
821
1163
  );
822
1164
  semantic = mergeByMaxScore(lists);
@@ -832,7 +1174,15 @@ function createUnifiedSearch(deps = {}) {
832
1174
  }
833
1175
  if (semantic.length === 0) {
834
1176
  const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
835
- semantic = await runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger);
1177
+ semantic = await runSemanticSearchForEmbedding(
1178
+ deps,
1179
+ input,
1180
+ queryEmbedding,
1181
+ limit,
1182
+ threshold,
1183
+ logger,
1184
+ peerPeers
1185
+ );
836
1186
  }
837
1187
  let lexical = [];
838
1188
  const keywords = deriveLexicalKeywords(input.query);
@@ -841,13 +1191,16 @@ function createUnifiedSearch(deps = {}) {
841
1191
  try {
842
1192
  const lexFilters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
843
1193
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1194
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
844
1195
  lexical = (await Promise.all(
845
1196
  lexFilters.map(
846
1197
  (botId) => searchRawMessagesLexical({
847
1198
  userId: input.userId,
848
1199
  keywords,
849
1200
  limit,
850
- botId
1201
+ botId,
1202
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1203
+ ...factTypes ? { factTypes } : {}
851
1204
  })
852
1205
  )
853
1206
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -875,20 +1228,272 @@ function createUnifiedSearch(deps = {}) {
875
1228
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
876
1229
  };
877
1230
  }
1231
+ async function searchUnifiedMemory(input) {
1232
+ const query = input.query.trim();
1233
+ const sources = normalizeUnifiedMemorySearchSources(input.sources);
1234
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
1235
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1236
+ const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1237
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1238
+ );
1239
+ const reasoningStrategy = resolveReasoningStrategy(input);
1240
+ const warnings = [];
1241
+ const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
1242
+ userId: input.userId,
1243
+ peerFilter: input.peerFilter,
1244
+ scopeCheck: deps.peerScopeCheck
1245
+ }) : { peers: [], warnings: [] };
1246
+ warnings.push(...peerScope.warnings);
1247
+ const peerPeers = peerScope.peers;
1248
+ if (!query) {
1249
+ return {
1250
+ query,
1251
+ sources,
1252
+ results: [],
1253
+ count: 0,
1254
+ warnings
1255
+ };
1256
+ }
1257
+ let memorySubs;
1258
+ const reasoningInfo = { strategy: reasoningStrategy };
1259
+ if (input.dateFrom || input.dateTo) {
1260
+ reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1261
+ }
1262
+ if (sources.includes("memory")) {
1263
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1264
+ if (isRawMessageStorageAvailable() || hasRawProviders) {
1265
+ try {
1266
+ memorySubs = await runMemorySource(
1267
+ input,
1268
+ limit,
1269
+ threshold,
1270
+ warnings,
1271
+ reasoningStrategy,
1272
+ reasoningInfo,
1273
+ peerPeers
1274
+ );
1275
+ } catch (error) {
1276
+ logger.warn?.("[memory-store] memory source failed:", error);
1277
+ warnings.push({
1278
+ source: "memory",
1279
+ code: "memory_search_failed",
1280
+ message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
1281
+ });
1282
+ }
1283
+ } else {
1284
+ warnings.push({
1285
+ source: "memory",
1286
+ code: "raw_message_storage_unavailable",
1287
+ message: "Raw memory storage is not available in this environment."
1288
+ });
1289
+ }
1290
+ }
1291
+ const insightHits = [];
1292
+ if (sources.includes("insights") && typeof deps.searchInsights === "function") {
1293
+ try {
1294
+ const hits = await deps.searchInsights({
1295
+ userId: input.userId,
1296
+ query: input.query,
1297
+ limit,
1298
+ threshold,
1299
+ botIds: input.botIds,
1300
+ includeArchived: input.includeArchivedInsights,
1301
+ authToken: input.authToken,
1302
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1303
+ });
1304
+ for (const hit of hits) {
1305
+ insightHits.push({
1306
+ type: "insight",
1307
+ id: hit.id,
1308
+ content: hit.content,
1309
+ similarity: hit.similarity,
1310
+ metadata: hit.metadata
1311
+ });
1312
+ }
1313
+ } catch (error) {
1314
+ logger.warn?.("[memory-store] insights source failed:", error);
1315
+ warnings.push({
1316
+ source: "insights",
1317
+ code: "insights_search_failed",
1318
+ message: error.message ?? "insights_search_failed"
1319
+ });
1320
+ }
1321
+ } else if (sources.includes("insights")) ;
1322
+ const knowledgeHits = [];
1323
+ if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
1324
+ try {
1325
+ const hits = await deps.searchKnowledge({
1326
+ userId: input.userId,
1327
+ query: input.query,
1328
+ options: {
1329
+ limit,
1330
+ threshold,
1331
+ documentIds: input.documentIds
1332
+ },
1333
+ authToken: input.authToken,
1334
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1335
+ });
1336
+ for (const hit of hits) {
1337
+ knowledgeHits.push(toKnowledgeResult(hit));
1338
+ }
1339
+ } catch (error) {
1340
+ logger.warn?.("[memory-store] knowledge source failed:", error);
1341
+ warnings.push({
1342
+ source: "knowledge",
1343
+ code: "knowledge_search_failed",
1344
+ message: error.message ?? "knowledge_search_failed"
1345
+ });
1346
+ }
1347
+ } else if (sources.includes("knowledge")) ;
1348
+ const merged = mergeAcrossSources({
1349
+ memorySubs,
1350
+ insightHits,
1351
+ knowledgeHits,
1352
+ limit,
1353
+ strategy: mergeStrategy
1354
+ });
1355
+ const ranked = await applyReranker(deps.reranker, input.query, merged);
1356
+ const output = {
1357
+ query: input.query,
1358
+ sources,
1359
+ results: ranked,
1360
+ count: ranked.length,
1361
+ warnings
1362
+ };
1363
+ if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1364
+ output.reasoning = reasoningInfo;
1365
+ }
1366
+ return output;
1367
+ }
878
1368
  async function searchRawMemorySemantically(input) {
879
1369
  const limit = clampUnifiedMemorySearchLimit(input.limit);
880
1370
  const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
881
1371
  const reasoningStrategy = resolveReasoningStrategy(input);
1372
+ const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1373
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1374
+ );
882
1375
  const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
883
1376
  return mergeAcrossSources({
884
1377
  memorySubs: sub,
885
1378
  insightHits: [],
886
1379
  knowledgeHits: [],
887
1380
  limit,
888
- strategy: normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy)
1381
+ strategy: mergeStrategy
889
1382
  });
890
1383
  }
891
- return { searchUnifiedMemory, searchRawMemorySemantically };
1384
+ async function search(input) {
1385
+ const query = input.query.trim();
1386
+ const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1387
+ const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
1388
+ const peerScope = await resolveSearchScopePeers({
1389
+ deps,
1390
+ userId: input.userId,
1391
+ peerFilter: input.peerFilter
1392
+ });
1393
+ if (!query) {
1394
+ const empty = {
1395
+ query,
1396
+ sources: input.sources ? [...input.sources] : [],
1397
+ results: [],
1398
+ evidence: [],
1399
+ count: 0,
1400
+ warnings: peerScope.warnings,
1401
+ answer: ""
1402
+ };
1403
+ return empty;
1404
+ }
1405
+ const tiers = input.tiers ? [...input.tiers] : wantsSynthesis ? ["summary", "raw", "insight", "knowledge"] : ["raw", "insight", "knowledge"];
1406
+ const wantsSummaryTier = tiers.includes("summary");
1407
+ const wantsRawTier = tiers.includes("raw");
1408
+ const wantsInsightTier = tiers.includes("insight");
1409
+ const wantsKnowledgeTier = tiers.includes("knowledge");
1410
+ const warnings = [...peerScope.warnings];
1411
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
1412
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1413
+ let unifiedHits = [];
1414
+ let sources = input.sources ? [...input.sources] : [];
1415
+ let unifiedReasoning;
1416
+ if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1417
+ const inferredSources = sources.length > 0 ? sources : [
1418
+ ...wantsRawTier ? ["memory"] : [],
1419
+ ...wantsInsightTier ? ["insights"] : [],
1420
+ ...wantsKnowledgeTier ? ["knowledge"] : []
1421
+ ];
1422
+ const unified = await searchUnifiedMemory({
1423
+ ...searchInputToUnified(input),
1424
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1425
+ });
1426
+ unifiedHits = unified.results;
1427
+ sources = unified.sources;
1428
+ unifiedReasoning = unified.reasoning;
1429
+ warnings.push(...unified.warnings);
1430
+ }
1431
+ let summaryHits = [];
1432
+ if (wantsSummaryTier && typeof deps.searchSummaries === "function") {
1433
+ const summaryBucket = await gatherSummaries(
1434
+ deps,
1435
+ { userId: input.userId, query: input.query, authToken: input.authToken },
1436
+ limit,
1437
+ threshold,
1438
+ logger,
1439
+ peerScope.peers
1440
+ );
1441
+ warnings.push(...summaryBucket.warnings);
1442
+ summaryHits = summaryBucket.hits;
1443
+ }
1444
+ const tierLists = [];
1445
+ if (unifiedHits.length > 0) {
1446
+ tierLists.push({ name: "memory", hits: unifiedHits });
1447
+ }
1448
+ if (summaryHits.length > 0) {
1449
+ tierLists.push({ name: "summary", hits: summaryHits });
1450
+ }
1451
+ let hits;
1452
+ if (tierLists.length === 0) {
1453
+ hits = [];
1454
+ } else if (tierLists.length === 1) {
1455
+ hits = tierLists[0].hits.slice(0, limit);
1456
+ } else {
1457
+ hits = mergeUnifiedMemorySearchResultsRrf(tierLists, limit);
1458
+ }
1459
+ const evidence = hits.map((hit) => ({
1460
+ id: hit.id,
1461
+ source: mapHitToEvidenceSource(hit),
1462
+ snippet: hit.content,
1463
+ score: hit.similarity,
1464
+ timestamp: getCandidateTimestamp(hit.metadata)
1465
+ }));
1466
+ const base = {
1467
+ query,
1468
+ sources,
1469
+ results: hits,
1470
+ evidence,
1471
+ count: hits.length,
1472
+ warnings
1473
+ };
1474
+ if (unifiedReasoning) {
1475
+ base.reasoning = unifiedReasoning;
1476
+ }
1477
+ if (!wantsSynthesis) {
1478
+ return base;
1479
+ }
1480
+ const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1481
+ query,
1482
+ hits,
1483
+ responseSchema: synthesisSchema,
1484
+ deps,
1485
+ logger
1486
+ });
1487
+ base.answer = answer;
1488
+ base.warnings = [...base.warnings, ...synthWarnings];
1489
+ return base;
1490
+ }
1491
+ return {
1492
+ search,
1493
+ searchUnifiedMemory,
1494
+ searchRawMemorySemantically,
1495
+ consolidate: (input) => applyReflectedPlan(deps, {}, input)
1496
+ };
892
1497
  }
893
1498
  function mergeAcrossSources(input) {
894
1499
  const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };