@melandlabs/memory-store 0.3.1 → 1.0.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,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import { vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
2
3
  import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
3
4
  import { ChromaVectorStore } from '@melandlabs/rag';
4
5
  import { mkdirSync } from 'fs';
5
6
  import { dirname } from 'path';
6
7
  import { getOpenContextPath } from '@melandlabs/env-config';
7
- import { SQLiteRawMessageManager } from '@melandlabs/sqlite';
8
+ import { getSQLiteVsaStore, SQLiteRawMessageManager } from '@melandlabs/sqlite';
8
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
11
  import { z } from 'zod';
12
+ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidation';
13
+ import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
11
14
 
12
15
  var __defProp = Object.defineProperty;
13
16
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -18,8 +21,6 @@ var __export = (target, all) => {
18
21
  for (var name in all)
19
22
  __defProp(target, name, { get: all[name], enumerable: true });
20
23
  };
21
-
22
- // src/contracts.ts
23
24
  function normalizeTimestampToMs(value) {
24
25
  if (!Number.isFinite(value)) {
25
26
  return 0;
@@ -326,6 +327,41 @@ var init_raw_message_store = __esm({
326
327
  init_chroma_memory_index();
327
328
  init_raw_message_store();
328
329
 
330
+ // src/search/reranker.ts
331
+ async function applyReranker(reranker, query, results) {
332
+ if (!reranker || results.length === 0) {
333
+ return results;
334
+ }
335
+ const scores = await reranker.rerank({
336
+ query,
337
+ candidates: results.map((result) => ({
338
+ id: result.id,
339
+ content: result.content,
340
+ metadata: result.metadata
341
+ }))
342
+ });
343
+ if (scores.length === 0) {
344
+ return results;
345
+ }
346
+ const byId = new Map(results.map((result) => [result.id, result]));
347
+ const reordered = [];
348
+ const seen = /* @__PURE__ */ new Set();
349
+ for (const { id, score } of scores) {
350
+ const hit = byId.get(id);
351
+ if (!hit || seen.has(id)) {
352
+ continue;
353
+ }
354
+ seen.add(id);
355
+ reordered.push({ ...hit, metadata: { ...hit.metadata, rerankerScore: score } });
356
+ }
357
+ for (const hit of results) {
358
+ if (!seen.has(hit.id)) {
359
+ reordered.push(hit);
360
+ }
361
+ }
362
+ return reordered;
363
+ }
364
+
329
365
  // src/search/utilities.ts
330
366
  var DEFAULT_LIMIT = 10;
331
367
  var DEFAULT_THRESHOLD = 0.7;
@@ -447,6 +483,264 @@ function isRawMemorySemanticResult(result) {
447
483
  const item = result;
448
484
  return typeof item.id === "string" && typeof item.content === "string" && typeof item.similarity === "number" && Boolean(item.metadata) && typeof item.metadata === "object";
449
485
  }
486
+ async function resolveScopePeer(input) {
487
+ const peers = input.peerFilter ?? [];
488
+ if (peers.length === 0) {
489
+ return { peers: [], warnings: [] };
490
+ }
491
+ if (typeof input.scopeCheck !== "function") {
492
+ return { peers, warnings: [] };
493
+ }
494
+ const retained = [];
495
+ const dropped = [];
496
+ for (const peer of peers) {
497
+ const ok = await input.scopeCheck({ userId: input.userId, peers: [peer] });
498
+ if (ok) {
499
+ retained.push(peer);
500
+ } else {
501
+ dropped.push(peer);
502
+ }
503
+ }
504
+ if (dropped.length === 0) {
505
+ return { peers, warnings: [] };
506
+ }
507
+ if (retained.length === 0) {
508
+ return {
509
+ peers: [],
510
+ warnings: [
511
+ {
512
+ source: "memory",
513
+ code: "peer_filter_outside_user_scope",
514
+ message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored; falling back to userId scope`
515
+ }
516
+ ]
517
+ };
518
+ }
519
+ return {
520
+ peers: retained,
521
+ warnings: [
522
+ {
523
+ source: "memory",
524
+ code: "peer_filter_outside_user_scope",
525
+ message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored because they fall outside userId ${input.userId}`
526
+ }
527
+ ]
528
+ };
529
+ }
530
+
531
+ // src/search/gather-evidence.ts
532
+ var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
533
+ var MAX_EVIDENCE_CHARS = 600;
534
+ function toSummaryResult(row) {
535
+ const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
536
+ const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
537
+ return {
538
+ type: "memory",
539
+ id: row.summaryId,
540
+ content: row.summaryText,
541
+ similarity: 1,
542
+ metadata: {
543
+ tier: "summary",
544
+ summaryTier: row.summaryTier,
545
+ keywords: row.keywords,
546
+ startTimestamp: start,
547
+ endTimestamp: end
548
+ }
549
+ };
550
+ }
551
+ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
552
+ if (typeof deps.searchSummaries !== "function") {
553
+ return { hits: [], warnings: [] };
554
+ }
555
+ const keywords = input.query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
556
+ try {
557
+ const rows = await deps.searchSummaries({
558
+ userId: input.userId,
559
+ query: input.query,
560
+ keywords,
561
+ limit,
562
+ threshold,
563
+ authToken: input.authToken,
564
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
565
+ });
566
+ const hits = rows.map(toSummaryResult);
567
+ return { hits, warnings: [] };
568
+ } catch (error) {
569
+ logger.warn?.("[memory-store] gather-evidence: summary search failed:", error);
570
+ return {
571
+ hits: [],
572
+ warnings: [
573
+ {
574
+ source: "memory",
575
+ code: "reflect_summaries_failed",
576
+ message: error.message ?? "reflect_summaries_failed"
577
+ }
578
+ ]
579
+ };
580
+ }
581
+ }
582
+ function truncate(input, max = MAX_EVIDENCE_CHARS) {
583
+ if (input.length <= max) {
584
+ return input;
585
+ }
586
+ return `${input.slice(0, max - 1)}\u2026`;
587
+ }
588
+ function buildSynthesisPrompt(input) {
589
+ const grouped = /* @__PURE__ */ new Map();
590
+ for (const item of input.evidence) {
591
+ const bucket = grouped.get(item.source) ?? [];
592
+ bucket.push(item);
593
+ grouped.set(item.source, bucket);
594
+ }
595
+ const sections = [];
596
+ for (const tier of DEFAULT_TIERS) {
597
+ const items = grouped.get(tier) ?? [];
598
+ if (items.length === 0) {
599
+ continue;
600
+ }
601
+ const lines = items.map(
602
+ (item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
603
+ );
604
+ sections.push(`## ${tier}
605
+ ${lines.join("\n")}`);
606
+ }
607
+ const schemaDirective = input.responseSchema ? `
608
+ Respond with a JSON object that conforms to the following schema:
609
+ ${JSON.stringify(input.responseSchema, null, 2)}
610
+ 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.";
611
+ if (sections.length === 0) {
612
+ return `You are a reflection assistant.
613
+
614
+ Question: ${input.query}
615
+
616
+ No evidence was found across any tier. State that explicitly.${schemaDirective}`;
617
+ }
618
+ return `You are a reflection assistant. Synthesise the user's question using only the evidence below. Do not invent facts.
619
+
620
+ Question: ${input.query}
621
+
622
+ Evidence (grouped by tier):
623
+ ${sections.join("\n\n")}${schemaDirective}`;
624
+ }
625
+ function extractJsonPayload(text) {
626
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
627
+ if (fenced?.[1]) {
628
+ return fenced[1];
629
+ }
630
+ const start = text.indexOf("{");
631
+ if (start === -1) {
632
+ return void 0;
633
+ }
634
+ let depth = 0;
635
+ let inString = false;
636
+ let escaped = false;
637
+ for (let i = start; i < text.length; i += 1) {
638
+ const ch = text[i];
639
+ if (escaped) {
640
+ escaped = false;
641
+ continue;
642
+ }
643
+ if (inString) {
644
+ if (ch === "\\") {
645
+ escaped = true;
646
+ } else if (ch === '"') {
647
+ inString = false;
648
+ }
649
+ continue;
650
+ }
651
+ if (ch === '"') {
652
+ inString = true;
653
+ } else if (ch === "{") {
654
+ depth += 1;
655
+ } else if (ch === "}") {
656
+ depth -= 1;
657
+ if (depth === 0) {
658
+ return text.slice(start, i + 1);
659
+ }
660
+ }
661
+ }
662
+ return void 0;
663
+ }
664
+ function coerceAnswer(text, schema) {
665
+ if (!schema) {
666
+ return text;
667
+ }
668
+ const payload = extractJsonPayload(text);
669
+ if (!payload) {
670
+ return text;
671
+ }
672
+ try {
673
+ const parsed = JSON.parse(payload);
674
+ if (parsed && typeof parsed === "object") {
675
+ const answer = parsed.answer;
676
+ if (typeof answer === "string") {
677
+ return answer;
678
+ }
679
+ }
680
+ } catch {
681
+ }
682
+ return text;
683
+ }
684
+ async function synthesizeAnswer(input) {
685
+ const { query, hits, responseSchema, deps, logger } = input;
686
+ const evidence = hits.map((hit) => ({
687
+ id: hit.id,
688
+ source: mapHitToTier(hit),
689
+ snippet: truncate(hit.content),
690
+ score: hit.similarity
691
+ }));
692
+ const warnings = [];
693
+ const complete = deps.reasoning?.complete;
694
+ if (!complete) {
695
+ warnings.push({
696
+ source: "memory",
697
+ code: "reflect_llm_not_configured",
698
+ message: "No `reasoning.complete` callback is configured; search() returned evidence without an LLM synthesis."
699
+ });
700
+ return { answer: "", warnings };
701
+ }
702
+ const prompt = buildSynthesisPrompt({ query, evidence, responseSchema });
703
+ try {
704
+ const raw = await complete(prompt);
705
+ const answer = coerceAnswer(raw, responseSchema);
706
+ logger.log?.("[memory-store] search synthesis completed", {
707
+ evidenceCount: evidence.length,
708
+ answerChars: answer.length
709
+ });
710
+ return { answer, warnings };
711
+ } catch (error) {
712
+ logger.warn?.("[memory-store] search synthesis failed:", error);
713
+ warnings.push({
714
+ source: "memory",
715
+ code: "reflect_llm_failed",
716
+ message: error.message ?? "reflect_llm_failed"
717
+ });
718
+ return { answer: "", warnings };
719
+ }
720
+ }
721
+ function mapHitToTier(hit) {
722
+ const tierMarker = hit.metadata?.tier;
723
+ if (tierMarker === "summary") {
724
+ return "summary";
725
+ }
726
+ if (hit.type === "memory") {
727
+ return "raw";
728
+ }
729
+ if (hit.type === "insight") {
730
+ return "insight";
731
+ }
732
+ if (hit.type === "knowledge") {
733
+ return "knowledge";
734
+ }
735
+ return hit.type;
736
+ }
737
+ async function resolveSearchScopePeers(input) {
738
+ return resolveScopePeer({
739
+ userId: input.userId,
740
+ peerFilter: input.peerFilter,
741
+ scopeCheck: input.deps.peerScopeCheck
742
+ });
743
+ }
450
744
 
451
745
  // src/search/unified-search.ts
452
746
  function deriveLexicalKeywords(query) {
@@ -518,7 +812,7 @@ async function embedQueryVariant(embedQuery, input, query) {
518
812
  authToken: input.authToken
519
813
  });
520
814
  }
521
- async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger) {
815
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
522
816
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
523
817
  let semantic = [];
524
818
  if (isRawMessageChromaEnabled()) {
@@ -548,6 +842,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
548
842
  }
549
843
  } else if (typeof deps.searchRawMessagesAnn === "function") {
550
844
  const searchRawMessagesAnn = deps.searchRawMessagesAnn;
845
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
551
846
  semantic = (await Promise.all(
552
847
  filters.map(
553
848
  (filter) => searchRawMessagesAnn({
@@ -555,7 +850,9 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
555
850
  queryEmbedding,
556
851
  limit,
557
852
  threshold,
558
- botId: "botId" in filter ? filter.botId : void 0
853
+ botId: "botId" in filter ? filter.botId : void 0,
854
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
855
+ ...factTypes ? { factTypes } : {}
559
856
  })
560
857
  )
561
858
  )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
@@ -565,11 +862,14 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
565
862
  const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
566
863
  const manager2 = await getRawMessageManager2();
567
864
  if (typeof manager2.searchMessagesSemantically === "function") {
865
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
568
866
  const results = await manager2.searchMessagesSemantically({
569
867
  userId: input.userId,
570
868
  queryEmbedding,
571
869
  limit,
572
- threshold
870
+ threshold,
871
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
872
+ ...factTypes ? { factTypes } : {}
573
873
  });
574
874
  semantic = results.map((r) => ({
575
875
  type: "memory",
@@ -605,7 +905,7 @@ function toIterativeRecallCandidate(hit) {
605
905
  metadata: hit.metadata
606
906
  };
607
907
  }
608
- async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger) {
908
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
609
909
  if (keywords.length === 0) {
610
910
  return [];
611
911
  }
@@ -613,13 +913,16 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
613
913
  try {
614
914
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
615
915
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
916
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
616
917
  return (await Promise.all(
617
918
  filters.map(
618
919
  (botId) => searchRawMessagesLexical({
619
920
  userId: input.userId,
620
921
  keywords,
621
922
  limit: Math.ceil(limit / filters.length),
622
- botId
923
+ botId,
924
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
925
+ ...factTypes ? { factTypes } : {}
623
926
  })
624
927
  )
625
928
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -633,13 +936,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
633
936
  try {
634
937
  const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
635
938
  const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
939
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
636
940
  return (await Promise.all(
637
941
  filters.map(
638
942
  (botId) => lexicalSearchRawMessages2({
639
943
  userId: input.userId,
640
944
  keywords,
641
945
  limit: Math.ceil(limit / filters.length),
642
- botId
946
+ botId,
947
+ ...factTypes ? { factTypes } : {}
643
948
  })
644
949
  )
645
950
  )).flat().filter(Boolean).map(
@@ -652,6 +957,42 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
652
957
  }
653
958
  return [];
654
959
  }
960
+ function searchInputToUnified(input) {
961
+ return {
962
+ userId: input.userId,
963
+ query: input.query,
964
+ sources: input.sources ? [...input.sources] : void 0,
965
+ limit: input.limit,
966
+ threshold: input.threshold,
967
+ authToken: input.authToken,
968
+ includeArchivedInsights: input.includeArchivedInsights,
969
+ botIds: input.botIds,
970
+ documentIds: input.documentIds,
971
+ asOf: input.asOf,
972
+ dateFrom: input.dateFrom,
973
+ dateTo: input.dateTo,
974
+ mergeStrategy: input.mergeStrategy,
975
+ peerFilter: input.peerFilter,
976
+ reasoningStrategy: input.reasoningStrategy,
977
+ factTypes: input.factTypes
978
+ };
979
+ }
980
+ function mapHitToEvidenceSource(hit) {
981
+ const tierMarker = hit.metadata?.tier;
982
+ if (tierMarker === "summary") {
983
+ return "summary";
984
+ }
985
+ if (hit.type === "memory") {
986
+ return "raw";
987
+ }
988
+ if (hit.type === "insight") {
989
+ return "insight";
990
+ }
991
+ if (hit.type === "knowledge") {
992
+ return "knowledge";
993
+ }
994
+ return hit.type;
995
+ }
655
996
  function createUnifiedSearch(deps = {}) {
656
997
  const logger = deps.logger ?? console;
657
998
  function resolveReasoningStrategy(input) {
@@ -661,130 +1002,7 @@ function createUnifiedSearch(deps = {}) {
661
1002
  }
662
1003
  return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
663
1004
  }
664
- async function searchUnifiedMemory(input) {
665
- const query = input.query.trim();
666
- const sources = normalizeUnifiedMemorySearchSources(input.sources);
667
- const limit = clampUnifiedMemorySearchLimit(input.limit);
668
- const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
669
- const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy);
670
- const reasoningStrategy = resolveReasoningStrategy(input);
671
- const warnings = [];
672
- if (!query) {
673
- return {
674
- query,
675
- sources,
676
- results: [],
677
- count: 0,
678
- warnings
679
- };
680
- }
681
- let memorySubs;
682
- const reasoningInfo = { strategy: reasoningStrategy };
683
- if (input.dateFrom || input.dateTo) {
684
- reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
685
- }
686
- if (sources.includes("memory")) {
687
- if (isRawMessageStorageAvailable()) {
688
- try {
689
- memorySubs = await runMemorySource(
690
- input,
691
- limit,
692
- threshold,
693
- warnings,
694
- reasoningStrategy,
695
- reasoningInfo
696
- );
697
- } catch (error) {
698
- logger.warn?.("[memory-store] memory source failed:", error);
699
- warnings.push({
700
- source: "memory",
701
- code: "memory_search_failed",
702
- message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
703
- });
704
- }
705
- } else {
706
- warnings.push({
707
- source: "memory",
708
- code: "raw_message_storage_unavailable",
709
- message: "Raw memory storage is not available in this environment."
710
- });
711
- }
712
- }
713
- const insightHits = [];
714
- if (sources.includes("insights") && typeof deps.searchInsights === "function") {
715
- try {
716
- const hits = await deps.searchInsights({
717
- userId: input.userId,
718
- query: input.query,
719
- limit,
720
- threshold,
721
- botIds: input.botIds,
722
- includeArchived: input.includeArchivedInsights,
723
- authToken: input.authToken
724
- });
725
- for (const hit of hits) {
726
- insightHits.push({
727
- type: "insight",
728
- id: hit.id,
729
- content: hit.content,
730
- similarity: hit.similarity,
731
- metadata: hit.metadata
732
- });
733
- }
734
- } catch (error) {
735
- logger.warn?.("[memory-store] insights source failed:", error);
736
- warnings.push({
737
- source: "insights",
738
- code: "insights_search_failed",
739
- message: error.message ?? "insights_search_failed"
740
- });
741
- }
742
- } else if (sources.includes("insights")) ;
743
- const knowledgeHits = [];
744
- if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
745
- try {
746
- const hits = await deps.searchKnowledge({
747
- userId: input.userId,
748
- query: input.query,
749
- options: {
750
- limit,
751
- threshold,
752
- documentIds: input.documentIds
753
- },
754
- authToken: input.authToken
755
- });
756
- for (const hit of hits) {
757
- knowledgeHits.push(toKnowledgeResult(hit));
758
- }
759
- } catch (error) {
760
- logger.warn?.("[memory-store] knowledge source failed:", error);
761
- warnings.push({
762
- source: "knowledge",
763
- code: "knowledge_search_failed",
764
- message: error.message ?? "knowledge_search_failed"
765
- });
766
- }
767
- } else if (sources.includes("knowledge")) ;
768
- const merged = mergeAcrossSources({
769
- memorySubs,
770
- insightHits,
771
- knowledgeHits,
772
- limit,
773
- strategy: mergeStrategy
774
- });
775
- const output = {
776
- query: input.query,
777
- sources,
778
- results: merged,
779
- count: merged.length,
780
- warnings
781
- };
782
- if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
783
- output.reasoning = reasoningInfo;
784
- }
785
- return output;
786
- }
787
- async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo) {
1005
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
788
1006
  if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
789
1007
  warnings.push({
790
1008
  source: "memory",
@@ -810,7 +1028,14 @@ function createUnifiedSearch(deps = {}) {
810
1028
  executor: {
811
1029
  search: async (request) => {
812
1030
  const keywords2 = request.keywords.length > 0 ? request.keywords : deriveLexicalKeywords(input.query);
813
- const lexicalHits = await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger);
1031
+ const lexicalHits = await runLexicalSearchForKeywords(
1032
+ deps,
1033
+ input,
1034
+ keywords2,
1035
+ limit,
1036
+ logger,
1037
+ peerPeers
1038
+ );
814
1039
  let semanticHits = [];
815
1040
  if (typeof deps.embedQuery === "function") {
816
1041
  try {
@@ -822,7 +1047,8 @@ function createUnifiedSearch(deps = {}) {
822
1047
  queryEmbedding,
823
1048
  limit,
824
1049
  threshold,
825
- logger
1050
+ logger,
1051
+ peerPeers
826
1052
  );
827
1053
  } catch (error) {
828
1054
  logger.warn?.(
@@ -867,7 +1093,7 @@ function createUnifiedSearch(deps = {}) {
867
1093
  });
868
1094
  const keywords2 = deriveLexicalKeywords(input.query);
869
1095
  const lexical2 = filterByDateRange(
870
- await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger),
1096
+ await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
871
1097
  input.dateFrom,
872
1098
  input.dateTo
873
1099
  );
@@ -893,7 +1119,7 @@ function createUnifiedSearch(deps = {}) {
893
1119
  );
894
1120
  const lists = await Promise.all(
895
1121
  embeddings.map(
896
- (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger)
1122
+ (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
897
1123
  )
898
1124
  );
899
1125
  semantic = mergeByMaxScore(lists);
@@ -909,7 +1135,15 @@ function createUnifiedSearch(deps = {}) {
909
1135
  }
910
1136
  if (semantic.length === 0) {
911
1137
  const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
912
- semantic = await runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger);
1138
+ semantic = await runSemanticSearchForEmbedding(
1139
+ deps,
1140
+ input,
1141
+ queryEmbedding,
1142
+ limit,
1143
+ threshold,
1144
+ logger,
1145
+ peerPeers
1146
+ );
913
1147
  }
914
1148
  let lexical = [];
915
1149
  const keywords = deriveLexicalKeywords(input.query);
@@ -918,13 +1152,16 @@ function createUnifiedSearch(deps = {}) {
918
1152
  try {
919
1153
  const lexFilters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
920
1154
  const searchRawMessagesLexical = deps.searchRawMessagesLexical;
1155
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
921
1156
  lexical = (await Promise.all(
922
1157
  lexFilters.map(
923
1158
  (botId) => searchRawMessagesLexical({
924
1159
  userId: input.userId,
925
1160
  keywords,
926
1161
  limit,
927
- botId
1162
+ botId,
1163
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {},
1164
+ ...factTypes ? { factTypes } : {}
928
1165
  })
929
1166
  )
930
1167
  )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
@@ -952,32 +1189,284 @@ function createUnifiedSearch(deps = {}) {
952
1189
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
953
1190
  };
954
1191
  }
955
- async function searchRawMemorySemantically(input) {
1192
+ async function searchUnifiedMemory(input) {
1193
+ const query = input.query.trim();
1194
+ const sources = normalizeUnifiedMemorySearchSources(input.sources);
956
1195
  const limit = clampUnifiedMemorySearchLimit(input.limit);
957
1196
  const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1197
+ const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1198
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1199
+ );
958
1200
  const reasoningStrategy = resolveReasoningStrategy(input);
959
- const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
960
- return mergeAcrossSources({
961
- memorySubs: sub,
962
- insightHits: [],
963
- knowledgeHits: [],
964
- limit,
965
- strategy: normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy)
966
- });
967
- }
968
- return { searchUnifiedMemory, searchRawMemorySemantically };
969
- }
970
- function mergeAcrossSources(input) {
971
- const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
972
- const all = [
973
- ...memorySubs.semantic,
974
- ...memorySubs.lexical,
975
- ...input.insightHits,
976
- ...input.knowledgeHits
977
- ];
978
- if (input.strategy !== "rrf") {
979
- return mergeUnifiedMemorySearchResults(all, input.limit);
980
- }
1201
+ const warnings = [];
1202
+ const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
1203
+ userId: input.userId,
1204
+ peerFilter: input.peerFilter,
1205
+ scopeCheck: deps.peerScopeCheck
1206
+ }) : { peers: [], warnings: [] };
1207
+ warnings.push(...peerScope.warnings);
1208
+ const peerPeers = peerScope.peers;
1209
+ if (!query) {
1210
+ return {
1211
+ query,
1212
+ sources,
1213
+ results: [],
1214
+ count: 0,
1215
+ warnings
1216
+ };
1217
+ }
1218
+ let memorySubs;
1219
+ const reasoningInfo = { strategy: reasoningStrategy };
1220
+ if (input.dateFrom || input.dateTo) {
1221
+ reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
1222
+ }
1223
+ if (sources.includes("memory")) {
1224
+ const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
1225
+ if (isRawMessageStorageAvailable() || hasRawProviders) {
1226
+ try {
1227
+ memorySubs = await runMemorySource(
1228
+ input,
1229
+ limit,
1230
+ threshold,
1231
+ warnings,
1232
+ reasoningStrategy,
1233
+ reasoningInfo,
1234
+ peerPeers
1235
+ );
1236
+ } catch (error) {
1237
+ logger.warn?.("[memory-store] memory source failed:", error);
1238
+ warnings.push({
1239
+ source: "memory",
1240
+ code: "memory_search_failed",
1241
+ message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
1242
+ });
1243
+ }
1244
+ } else {
1245
+ warnings.push({
1246
+ source: "memory",
1247
+ code: "raw_message_storage_unavailable",
1248
+ message: "Raw memory storage is not available in this environment."
1249
+ });
1250
+ }
1251
+ }
1252
+ const insightHits = [];
1253
+ if (sources.includes("insights") && typeof deps.searchInsights === "function") {
1254
+ try {
1255
+ const hits = await deps.searchInsights({
1256
+ userId: input.userId,
1257
+ query: input.query,
1258
+ limit,
1259
+ threshold,
1260
+ botIds: input.botIds,
1261
+ includeArchived: input.includeArchivedInsights,
1262
+ authToken: input.authToken,
1263
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1264
+ });
1265
+ for (const hit of hits) {
1266
+ insightHits.push({
1267
+ type: "insight",
1268
+ id: hit.id,
1269
+ content: hit.content,
1270
+ similarity: hit.similarity,
1271
+ metadata: hit.metadata
1272
+ });
1273
+ }
1274
+ } catch (error) {
1275
+ logger.warn?.("[memory-store] insights source failed:", error);
1276
+ warnings.push({
1277
+ source: "insights",
1278
+ code: "insights_search_failed",
1279
+ message: error.message ?? "insights_search_failed"
1280
+ });
1281
+ }
1282
+ } else if (sources.includes("insights")) ;
1283
+ const knowledgeHits = [];
1284
+ if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
1285
+ try {
1286
+ const hits = await deps.searchKnowledge({
1287
+ userId: input.userId,
1288
+ query: input.query,
1289
+ options: {
1290
+ limit,
1291
+ threshold,
1292
+ documentIds: input.documentIds
1293
+ },
1294
+ authToken: input.authToken,
1295
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
1296
+ });
1297
+ for (const hit of hits) {
1298
+ knowledgeHits.push(toKnowledgeResult(hit));
1299
+ }
1300
+ } catch (error) {
1301
+ logger.warn?.("[memory-store] knowledge source failed:", error);
1302
+ warnings.push({
1303
+ source: "knowledge",
1304
+ code: "knowledge_search_failed",
1305
+ message: error.message ?? "knowledge_search_failed"
1306
+ });
1307
+ }
1308
+ } else if (sources.includes("knowledge")) ;
1309
+ const merged = mergeAcrossSources({
1310
+ memorySubs,
1311
+ insightHits,
1312
+ knowledgeHits,
1313
+ limit,
1314
+ strategy: mergeStrategy
1315
+ });
1316
+ const ranked = await applyReranker(deps.reranker, input.query, merged);
1317
+ const output = {
1318
+ query: input.query,
1319
+ sources,
1320
+ results: ranked,
1321
+ count: ranked.length,
1322
+ warnings
1323
+ };
1324
+ if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
1325
+ output.reasoning = reasoningInfo;
1326
+ }
1327
+ return output;
1328
+ }
1329
+ async function searchRawMemorySemantically(input) {
1330
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
1331
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1332
+ const reasoningStrategy = resolveReasoningStrategy(input);
1333
+ const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
1334
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1335
+ );
1336
+ const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
1337
+ return mergeAcrossSources({
1338
+ memorySubs: sub,
1339
+ insightHits: [],
1340
+ knowledgeHits: [],
1341
+ limit,
1342
+ strategy: mergeStrategy
1343
+ });
1344
+ }
1345
+ async function search(input) {
1346
+ const query = input.query.trim();
1347
+ const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
1348
+ const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
1349
+ const peerScope = await resolveSearchScopePeers({
1350
+ deps,
1351
+ userId: input.userId,
1352
+ peerFilter: input.peerFilter
1353
+ });
1354
+ if (!query) {
1355
+ const empty = {
1356
+ query,
1357
+ sources: input.sources ? [...input.sources] : [],
1358
+ results: [],
1359
+ evidence: [],
1360
+ count: 0,
1361
+ warnings: peerScope.warnings,
1362
+ answer: ""
1363
+ };
1364
+ return empty;
1365
+ }
1366
+ const tiers = input.tiers ? [...input.tiers] : wantsSynthesis ? ["summary", "raw", "insight", "knowledge"] : ["raw", "insight", "knowledge"];
1367
+ const wantsSummaryTier = tiers.includes("summary");
1368
+ const wantsRawTier = tiers.includes("raw");
1369
+ const wantsInsightTier = tiers.includes("insight");
1370
+ const wantsKnowledgeTier = tiers.includes("knowledge");
1371
+ const warnings = [...peerScope.warnings];
1372
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
1373
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1374
+ let unifiedHits = [];
1375
+ let sources = input.sources ? [...input.sources] : [];
1376
+ let unifiedReasoning;
1377
+ if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
1378
+ const inferredSources = sources.length > 0 ? sources : [
1379
+ ...wantsRawTier ? ["memory"] : [],
1380
+ ...wantsInsightTier ? ["insights"] : [],
1381
+ ...wantsKnowledgeTier ? ["knowledge"] : []
1382
+ ];
1383
+ const unified = await searchUnifiedMemory({
1384
+ ...searchInputToUnified(input),
1385
+ sources: inferredSources.length > 0 ? inferredSources : void 0
1386
+ });
1387
+ unifiedHits = unified.results;
1388
+ sources = unified.sources;
1389
+ unifiedReasoning = unified.reasoning;
1390
+ warnings.push(...unified.warnings);
1391
+ }
1392
+ let summaryHits = [];
1393
+ if (wantsSummaryTier && typeof deps.searchSummaries === "function") {
1394
+ const summaryBucket = await gatherSummaries(
1395
+ deps,
1396
+ { userId: input.userId, query: input.query, authToken: input.authToken },
1397
+ limit,
1398
+ threshold,
1399
+ logger,
1400
+ peerScope.peers
1401
+ );
1402
+ warnings.push(...summaryBucket.warnings);
1403
+ summaryHits = summaryBucket.hits;
1404
+ }
1405
+ const tierLists = [];
1406
+ if (unifiedHits.length > 0) {
1407
+ tierLists.push({ name: "memory", hits: unifiedHits });
1408
+ }
1409
+ if (summaryHits.length > 0) {
1410
+ tierLists.push({ name: "summary", hits: summaryHits });
1411
+ }
1412
+ let hits;
1413
+ if (tierLists.length === 0) {
1414
+ hits = [];
1415
+ } else if (tierLists.length === 1) {
1416
+ hits = tierLists[0].hits.slice(0, limit);
1417
+ } else {
1418
+ hits = mergeUnifiedMemorySearchResultsRrf(tierLists, limit);
1419
+ }
1420
+ const evidence = hits.map((hit) => ({
1421
+ id: hit.id,
1422
+ source: mapHitToEvidenceSource(hit),
1423
+ snippet: hit.content,
1424
+ score: hit.similarity,
1425
+ timestamp: getCandidateTimestamp(hit.metadata)
1426
+ }));
1427
+ const base = {
1428
+ query,
1429
+ sources,
1430
+ results: hits,
1431
+ evidence,
1432
+ count: hits.length,
1433
+ warnings
1434
+ };
1435
+ if (unifiedReasoning) {
1436
+ base.reasoning = unifiedReasoning;
1437
+ }
1438
+ if (!wantsSynthesis) {
1439
+ return base;
1440
+ }
1441
+ const { answer, warnings: synthWarnings } = await synthesizeAnswer({
1442
+ query,
1443
+ hits,
1444
+ responseSchema: synthesisSchema,
1445
+ deps,
1446
+ logger
1447
+ });
1448
+ base.answer = answer;
1449
+ base.warnings = [...base.warnings, ...synthWarnings];
1450
+ return base;
1451
+ }
1452
+ return {
1453
+ search,
1454
+ searchUnifiedMemory,
1455
+ searchRawMemorySemantically,
1456
+ consolidate: (input) => applyReflectedPlan(deps, {}, input)
1457
+ };
1458
+ }
1459
+ function mergeAcrossSources(input) {
1460
+ const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
1461
+ const all = [
1462
+ ...memorySubs.semantic,
1463
+ ...memorySubs.lexical,
1464
+ ...input.insightHits,
1465
+ ...input.knowledgeHits
1466
+ ];
1467
+ if (input.strategy !== "rrf") {
1468
+ return mergeUnifiedMemorySearchResults(all, input.limit);
1469
+ }
981
1470
  const lists = [];
982
1471
  if (memorySubs.semantic.length > 0) {
983
1472
  lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
@@ -997,9 +1486,283 @@ function mergeAcrossSources(input) {
997
1486
  return mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
998
1487
  }
999
1488
 
1489
+ // src/search/apply-reflect.ts
1490
+ var DEFAULT_TIERS2 = ["summary", "raw", "insight", "knowledge"];
1491
+ function reflectTierToEvidenceTier(source) {
1492
+ switch (source) {
1493
+ case "raw":
1494
+ return "short";
1495
+ case "summary":
1496
+ return "long";
1497
+ default:
1498
+ return "mid";
1499
+ }
1500
+ }
1501
+ function toMemoryEvidenceRecord(input, fallbackUserId) {
1502
+ const tier = reflectTierToEvidenceTier(input.source);
1503
+ const timestamp = input.timestamp ?? Date.now();
1504
+ return {
1505
+ id: input.id,
1506
+ userId: fallbackUserId,
1507
+ timestamp,
1508
+ tier,
1509
+ text: input.snippet,
1510
+ metadata: {
1511
+ source: input.source,
1512
+ score: input.score,
1513
+ ...input.timestamp !== void 0 ? { originalTimestamp: input.timestamp } : {}
1514
+ }
1515
+ };
1516
+ }
1517
+ async function applyReflectedPlan(deps, store, input, _logger) {
1518
+ const query = input.query.trim();
1519
+ const warnings = [];
1520
+ const limit = clampUnifiedMemorySearchLimit(input.limit);
1521
+ const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
1522
+ const tiers = input.tiers && input.tiers.length > 0 ? input.tiers : DEFAULT_TIERS2;
1523
+ const searchInput = {
1524
+ userId: input.userId,
1525
+ query,
1526
+ botIds: input.botIds,
1527
+ dateFrom: input.dateFrom,
1528
+ dateTo: input.dateTo,
1529
+ tiers,
1530
+ limit,
1531
+ threshold,
1532
+ authToken: input.authToken,
1533
+ peerFilter: input.peerFilter,
1534
+ synthesize: true
1535
+ };
1536
+ const search = createUnifiedSearch(deps);
1537
+ const result = await search.search(searchInput);
1538
+ warnings.push(...result.warnings);
1539
+ const evidence = result.evidence.map((item) => ({
1540
+ id: item.id,
1541
+ source: item.source ?? "raw",
1542
+ snippet: item.snippet,
1543
+ score: item.score,
1544
+ timestamp: item.timestamp
1545
+ }));
1546
+ const records = evidence.map((item) => toMemoryEvidenceRecord(item, input.userId));
1547
+ const applyInput = {
1548
+ userId: input.userId,
1549
+ ownerScope: input.ownerScope,
1550
+ records,
1551
+ plan: input.plan,
1552
+ dryRun: input.dryRun,
1553
+ expectedVersion: input.expectedVersion,
1554
+ getClusterKey: input.getClusterKey ?? defaultClusterKey,
1555
+ llmReview: deps.reasoning?.complete ? {
1556
+ complete: deps.reasoning.complete,
1557
+ maxTokens: input.llmPlanReview?.maxTokens
1558
+ } : void 0,
1559
+ graphStore: input.graphStore ?? store.graphStore,
1560
+ storage: input.storage ?? store.storage
1561
+ };
1562
+ const applyOutput = await applyReflectedConsolidationPlan(applyInput);
1563
+ for (const warning of applyOutput.warnings) {
1564
+ warnings.push({
1565
+ source: "memory",
1566
+ code: warning.code,
1567
+ message: warning.message
1568
+ });
1569
+ }
1570
+ const summaryAnswer = makePlanSummary(applyOutput.plan);
1571
+ return {
1572
+ answer: result.answer ? `${result.answer}
1573
+
1574
+ Plan summary:
1575
+ ${summaryAnswer}` : summaryAnswer,
1576
+ plan: applyOutput.plan,
1577
+ persistenceResult: applyOutput.persistenceResult,
1578
+ deprecationCounts: applyOutput.deprecationCounts,
1579
+ warnings,
1580
+ applied: applyOutput.applied,
1581
+ evidence
1582
+ };
1583
+ }
1584
+ function defaultClusterKey(record) {
1585
+ return record.text ?? record.id;
1586
+ }
1587
+ function makePlanSummary(plan) {
1588
+ const lines = [];
1589
+ const counts = {
1590
+ preserve: plan.actions.preserve.length,
1591
+ observe: plan.actions.observe.length,
1592
+ decay: plan.actions.decay.length,
1593
+ deprecate: plan.actions.deprecate.length
1594
+ };
1595
+ lines.push(
1596
+ `- preserve=${counts.preserve} observe=${counts.observe} decay=${counts.decay} deprecate=${counts.deprecate}`
1597
+ );
1598
+ for (const entry of plan.actions.deprecate.slice(0, 5)) {
1599
+ lines.push(
1600
+ ` deprecate ${entry.recordIds.length} record(s) from cluster ${entry.clusterKey} -> ${entry.supersededBySummaryId ?? entry.winningClusterKey}`
1601
+ );
1602
+ }
1603
+ return lines.join("\n");
1604
+ }
1605
+ var DEFAULT_MAX_FACTS = 1e3;
1606
+ var LOW_CONFIDENCE_THRESHOLD = 0.1;
1607
+ function toHRR(data) {
1608
+ const arr = data instanceof Float32Array ? data : new Float32Array(data);
1609
+ return { dim: arr.length, data: arr };
1610
+ }
1611
+ function generateFactId() {
1612
+ const stamp = Date.now().toString(36);
1613
+ const rand = Math.random().toString(36).slice(2, 10);
1614
+ return `vsa-${stamp}-${rand}`;
1615
+ }
1616
+ function createVsaRecall(storage) {
1617
+ return {
1618
+ async storeFact(input) {
1619
+ const roleVector = vsaNormalizeVector(input.roleVector, "roleVector");
1620
+ const fillerVector = vsaNormalizeVector(input.fillerVector, "fillerVector");
1621
+ vsaAssertSameDim(roleVector, fillerVector, "storeFact");
1622
+ const dim = input.dim ?? roleVector.length;
1623
+ if (roleVector.length !== dim) {
1624
+ throw new Error(`storeFact: roleVector.length (${roleVector.length}) must equal dim (${dim})`);
1625
+ }
1626
+ const now = Date.now();
1627
+ const fact = {
1628
+ factId: input.factId ?? generateFactId(),
1629
+ userId: input.userId,
1630
+ roleLabel: input.roleLabel,
1631
+ fillerLabel: input.fillerLabel,
1632
+ roleVector,
1633
+ fillerVector,
1634
+ dim,
1635
+ scopeTag: input.scopeTag ?? "default",
1636
+ botId: input.botId,
1637
+ createdAt: now
1638
+ };
1639
+ await storage.storeFact(fact);
1640
+ return { factId: fact.factId, createdAt: now };
1641
+ },
1642
+ async recall(input) {
1643
+ const roleVector = vsaNormalizeVector(input.roleVector, "roleVector");
1644
+ const scopeTag = input.scopeTag ?? "default";
1645
+ const maxFacts = input.maxFacts ?? DEFAULT_MAX_FACTS;
1646
+ if (input.vocabulary.length === 0) {
1647
+ throw new Error("vsaRecall: vocabulary must contain at least one entry");
1648
+ }
1649
+ const facts = await storage.queryFacts({
1650
+ userId: input.userId,
1651
+ scopeTag,
1652
+ botId: input.botId,
1653
+ includeDeprecated: false,
1654
+ limit: maxFacts
1655
+ });
1656
+ const warnings = [];
1657
+ if (facts.length === 0) {
1658
+ warnings.push({
1659
+ code: "vsa_no_facts",
1660
+ message: `No VSA facts for userId=${input.userId} scopeTag=${scopeTag}`
1661
+ });
1662
+ return {
1663
+ fillerLabel: "",
1664
+ score: 0,
1665
+ allScores: [],
1666
+ factCount: 0,
1667
+ warnings
1668
+ };
1669
+ }
1670
+ const refDim = facts[0].dim;
1671
+ const dimMatchedFacts = facts.filter((fact) => {
1672
+ if (fact.dim === refDim) return true;
1673
+ warnings.push({
1674
+ code: "vsa_dim_mismatch",
1675
+ message: `Fact ${fact.factId} has dim=${fact.dim}, expected ${refDim}; skipping`
1676
+ });
1677
+ return false;
1678
+ });
1679
+ if (roleVector.length !== refDim) {
1680
+ throw new Error(
1681
+ `vsaRecall: roleVector dim (${roleVector.length}) must match stored fact dim (${refDim})`
1682
+ );
1683
+ }
1684
+ if (dimMatchedFacts.length === 0) {
1685
+ warnings.push({
1686
+ code: "vsa_no_facts",
1687
+ message: `All stored facts for userId=${input.userId} scopeTag=${scopeTag} had a non-matching dim`
1688
+ });
1689
+ return {
1690
+ fillerLabel: "",
1691
+ score: 0,
1692
+ allScores: [],
1693
+ factCount: 0,
1694
+ warnings
1695
+ };
1696
+ }
1697
+ const memoryVec = superpose(
1698
+ dimMatchedFacts.map((fact) => bind(toHRR(fact.roleVector), toHRR(fact.fillerVector)))
1699
+ );
1700
+ const candidate = unbind(memoryVec, toHRR(roleVector));
1701
+ const vocabVecs = input.vocabulary.map(
1702
+ (entry) => toHRR(vsaNormalizeVector(entry.vector, `vocabulary[${entry.label}].vector`))
1703
+ );
1704
+ const bestVec = cleanup(candidate, vocabVecs);
1705
+ const bestIdx = vocabVecs.indexOf(bestVec);
1706
+ const allScores = input.vocabulary.map((entry, idx) => ({
1707
+ label: entry.label,
1708
+ score: cosineSimilarity(candidate, vocabVecs[idx])
1709
+ }));
1710
+ allScores.sort((a, b) => b.score - a.score);
1711
+ const fillerLabel = bestIdx >= 0 ? input.vocabulary[bestIdx].label : "";
1712
+ const score = allScores[0]?.score ?? 0;
1713
+ if (score < LOW_CONFIDENCE_THRESHOLD) {
1714
+ warnings.push({
1715
+ code: "vsa_low_confidence",
1716
+ message: `Best match "${fillerLabel}" score ${score.toFixed(3)} below ${LOW_CONFIDENCE_THRESHOLD}`
1717
+ });
1718
+ }
1719
+ if (facts.length >= maxFacts) {
1720
+ warnings.push({
1721
+ code: "vsa_fact_limit_reached",
1722
+ message: `Loaded ${facts.length} facts (cap=${maxFacts}); recall may degrade with more`
1723
+ });
1724
+ }
1725
+ return {
1726
+ fillerLabel,
1727
+ score,
1728
+ allScores,
1729
+ factCount: facts.length,
1730
+ warnings
1731
+ };
1732
+ },
1733
+ async listFacts(input) {
1734
+ const facts = await storage.queryFacts({
1735
+ userId: input.userId,
1736
+ scopeTag: input.scopeTag,
1737
+ botId: input.botId,
1738
+ includeDeprecated: input.includeDeprecated
1739
+ });
1740
+ return facts.map((fact) => ({
1741
+ factId: fact.factId,
1742
+ userId: fact.userId,
1743
+ roleLabel: fact.roleLabel,
1744
+ fillerLabel: fact.fillerLabel,
1745
+ scopeTag: fact.scopeTag,
1746
+ botId: fact.botId,
1747
+ createdAt: fact.createdAt,
1748
+ deprecatedAt: fact.deprecatedAt
1749
+ }));
1750
+ },
1751
+ async forget(input) {
1752
+ const result = await storage.deprecateFacts({
1753
+ userId: input.userId,
1754
+ factIds: input.factIds,
1755
+ reason: input.reason
1756
+ });
1757
+ return { deprecatedCount: result.deprecatedCount };
1758
+ }
1759
+ };
1760
+ }
1761
+
1000
1762
  // src/mcp.ts
1001
1763
  init_chroma_memory_index();
1002
1764
  init_raw_message_store();
1765
+ init_sqlite_raw_message_store();
1003
1766
  var DEFAULT_NAME = "@melandlabs/memory-store";
1004
1767
  var DEFAULT_VERSION = "0.1.0";
1005
1768
  async function embedMissingMessages(messages, deps) {
@@ -1039,6 +1802,9 @@ async function startMcpServer(options = {}) {
1039
1802
  env: options.env
1040
1803
  });
1041
1804
  const search = createUnifiedSearch(options.unified);
1805
+ const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
1806
+ const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
1807
+ const vsa = createVsaRecall(vsaStorage);
1042
1808
  server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
1043
1809
  content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
1044
1810
  }));
@@ -1046,6 +1812,15 @@ async function startMcpServer(options = {}) {
1046
1812
  userId: z.string(),
1047
1813
  query: z.string().min(1),
1048
1814
  sources: z.array(z.enum(["memory", "insights", "knowledge"])).optional().describe("Default: ['memory','insights','knowledge']"),
1815
+ tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
1816
+ "Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
1817
+ ),
1818
+ synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
1819
+ "Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
1820
+ ),
1821
+ // Top-level pass-through kept for backward-shaped callers; the SDK
1822
+ // coerces it into `synthesize.responseSchema` when present.
1823
+ responseSchema: z.record(z.string(), z.unknown()).optional(),
1049
1824
  limit: z.number().int().min(1).max(50).default(10),
1050
1825
  threshold: z.number().min(-1).max(1).default(0.7),
1051
1826
  botIds: z.array(z.string()).optional(),
@@ -1078,17 +1853,51 @@ async function startMcpServer(options = {}) {
1078
1853
  userId: z.string(),
1079
1854
  messageId: z.string()
1080
1855
  };
1856
+ const reflectApplySchema = {
1857
+ userId: z.string(),
1858
+ query: z.string().min(1),
1859
+ ownerScopeUserId: z.string().describe("Required: who owns the writes."),
1860
+ botIds: z.array(z.string()).optional(),
1861
+ dateFrom: z.string().optional(),
1862
+ dateTo: z.string().optional(),
1863
+ tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe("Default: ['summary','raw','insight','knowledge']"),
1864
+ limit: z.number().int().min(1).max(200).default(20),
1865
+ threshold: z.number().min(-1).max(1).default(0.7),
1866
+ dryRun: z.boolean().default(false),
1867
+ expectedVersion: z.string().optional(),
1868
+ llmPlanReviewMaxTokens: z.number().int().min(64).max(8192).optional(),
1869
+ authToken: z.string().optional()
1870
+ };
1081
1871
  server.registerTool(
1082
- "memory.searchUnified",
1872
+ "memory.search",
1083
1873
  {
1084
- title: "Search Unified Memory",
1085
- description: "Semantic search across raw messages (always), and optionally insights and uploaded knowledge.",
1874
+ title: "Search Memory",
1875
+ description: "Cross-source semantic search across raw messages, insights, and uploaded knowledge. Set `synthesize: true` to opt into LLM synthesis over the gathered evidence. With synthesis enabled, the response carries `answer`, `evidence`, `results`, and `warnings`; otherwise just `results`, `evidence`, and `count`.",
1086
1876
  // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
1087
1877
  inputSchema: searchSchema
1088
1878
  },
1089
1879
  async (args) => {
1090
1880
  const a = args;
1091
- const result = await search.searchUnifiedMemory(a);
1881
+ const wantsSynthesis = typeof a.synthesize === "boolean" ? a.synthesize : typeof a.synthesize === "object" && a.synthesize !== null;
1882
+ const responseSchema = typeof a.synthesize === "object" && a.synthesize !== null ? a.synthesize.responseSchema ?? a.responseSchema : a.responseSchema;
1883
+ const input = {
1884
+ userId: a.userId,
1885
+ query: a.query,
1886
+ sources: a.sources,
1887
+ limit: a.limit,
1888
+ threshold: a.threshold,
1889
+ botIds: a.botIds,
1890
+ documentIds: a.documentIds,
1891
+ authToken: a.authToken,
1892
+ includeArchivedInsights: a.includeArchivedInsights,
1893
+ ...a.tiers ? { tiers: a.tiers } : {},
1894
+ ...wantsSynthesis ? {
1895
+ synthesize: {
1896
+ ...responseSchema ? { responseSchema } : {}
1897
+ }
1898
+ } : {}
1899
+ };
1900
+ const result = await search.search(input);
1092
1901
  return {
1093
1902
  content: [{ type: "text", text: JSON.stringify(result) }]
1094
1903
  };
@@ -1156,6 +1965,212 @@ async function startMcpServer(options = {}) {
1156
1965
  };
1157
1966
  }
1158
1967
  );
1968
+ server.registerTool(
1969
+ "memory.consolidate",
1970
+ {
1971
+ title: "Consolidate",
1972
+ description: "Agentic write-back: gathers evidence like `memory.search({ synthesize: true })`, builds a memory-consolidation plan, optionally asks the LLM to veto unsafe entries, then persists via the attached graph store + soft-deprecates via the storage adapter. Set `dryRun: true` to inspect the plan without writing.",
1973
+ // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
1974
+ inputSchema: reflectApplySchema
1975
+ },
1976
+ async (args) => {
1977
+ const a = args;
1978
+ const input = {
1979
+ userId: a.userId,
1980
+ query: a.query,
1981
+ ownerScope: { userId: a.ownerScopeUserId },
1982
+ botIds: a.botIds,
1983
+ dateFrom: a.dateFrom,
1984
+ dateTo: a.dateTo,
1985
+ tiers: a.tiers,
1986
+ limit: a.limit,
1987
+ threshold: a.threshold,
1988
+ dryRun: a.dryRun ?? false,
1989
+ expectedVersion: a.expectedVersion,
1990
+ authToken: a.authToken,
1991
+ llmPlanReview: a.llmPlanReviewMaxTokens !== void 0 ? { maxTokens: a.llmPlanReviewMaxTokens } : void 0
1992
+ };
1993
+ const result = await applyReflectedPlan(
1994
+ { ...options.unified ?? {} },
1995
+ { graphStore: options.graphStore, storage: options.storage },
1996
+ input);
1997
+ return {
1998
+ content: [{ type: "text", text: JSON.stringify(result) }]
1999
+ };
2000
+ }
2001
+ );
2002
+ const vsaVocabularyEntry = {
2003
+ label: z.string().min(1),
2004
+ vector: z.array(z.number()).describe("Float32 vector for the vocabulary entry.")
2005
+ };
2006
+ const vsaStoreSchema = {
2007
+ userId: z.string(),
2008
+ roleLabel: z.string().min(1),
2009
+ fillerLabel: z.string().min(1),
2010
+ roleVector: z.array(z.number()),
2011
+ fillerVector: z.array(z.number()),
2012
+ scopeTag: z.string().optional(),
2013
+ botId: z.string().optional(),
2014
+ factId: z.string().optional()
2015
+ };
2016
+ const vsaRecallSchema = {
2017
+ userId: z.string(),
2018
+ roleLabel: z.string().min(1),
2019
+ roleVector: z.array(z.number()),
2020
+ vocabulary: z.array(z.object(vsaVocabularyEntry).passthrough()),
2021
+ scopeTag: z.string().optional(),
2022
+ botId: z.string().optional(),
2023
+ maxFacts: z.number().int().min(1).max(1e4).optional()
2024
+ };
2025
+ const vsaListSchema = {
2026
+ userId: z.string(),
2027
+ scopeTag: z.string().optional(),
2028
+ botId: z.string().optional(),
2029
+ includeDeprecated: z.boolean().default(false)
2030
+ };
2031
+ const vsaForgetSchema = {
2032
+ userId: z.string(),
2033
+ factIds: z.array(z.string()).min(1),
2034
+ reason: z.string().optional()
2035
+ };
2036
+ server.registerTool(
2037
+ "memory.vsaStore",
2038
+ {
2039
+ title: "VSA Store Fact",
2040
+ description: "Persist a (role, filler) binding into the user's VSA memory. The vectors are stored as Float32 BLOBs alongside the labels; later `memory.vsaRecall` rebuilds the memory vector and picks the best vocabulary match.",
2041
+ // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
2042
+ inputSchema: vsaStoreSchema
2043
+ },
2044
+ async (args) => {
2045
+ const a = args;
2046
+ try {
2047
+ const result = await vsa.storeFact({
2048
+ userId: a.userId,
2049
+ roleLabel: a.roleLabel,
2050
+ fillerLabel: a.fillerLabel,
2051
+ roleVector: a.roleVector,
2052
+ fillerVector: a.fillerVector,
2053
+ scopeTag: a.scopeTag,
2054
+ botId: a.botId,
2055
+ factId: a.factId
2056
+ });
2057
+ return {
2058
+ content: [{ type: "text", text: JSON.stringify(result) }]
2059
+ };
2060
+ } catch (error) {
2061
+ return {
2062
+ content: [
2063
+ {
2064
+ type: "text",
2065
+ text: JSON.stringify({ error: error.message ?? "vsa.store failed" })
2066
+ }
2067
+ ],
2068
+ isError: true
2069
+ };
2070
+ }
2071
+ }
2072
+ );
2073
+ server.registerTool(
2074
+ "memory.vsaRecall",
2075
+ {
2076
+ title: "VSA Recall",
2077
+ description: "Rebuild the user's VSA memory vector from stored (role, filler) facts, unbind by the requested role, and cleanup against the supplied vocabulary. Returns the best-match label plus a sorted score list and warnings (e.g. low confidence, dim mismatch).",
2078
+ // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
2079
+ inputSchema: vsaRecallSchema
2080
+ },
2081
+ async (args) => {
2082
+ const a = args;
2083
+ try {
2084
+ const result = await vsa.recall({
2085
+ userId: a.userId,
2086
+ roleLabel: a.roleLabel,
2087
+ roleVector: a.roleVector,
2088
+ vocabulary: a.vocabulary,
2089
+ scopeTag: a.scopeTag,
2090
+ botId: a.botId,
2091
+ maxFacts: a.maxFacts
2092
+ });
2093
+ return {
2094
+ content: [{ type: "text", text: JSON.stringify(result) }]
2095
+ };
2096
+ } catch (error) {
2097
+ return {
2098
+ content: [
2099
+ {
2100
+ type: "text",
2101
+ text: JSON.stringify({ error: error.message ?? "vsa.recall failed" })
2102
+ }
2103
+ ],
2104
+ isError: true
2105
+ };
2106
+ }
2107
+ }
2108
+ );
2109
+ server.registerTool(
2110
+ "memory.vsaList",
2111
+ {
2112
+ title: "VSA List Facts",
2113
+ description: "List the (role, filler) facts stored for a user. Vectors are stripped from the response \u2014 for the underlying storage layer, query SQLite directly.",
2114
+ // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
2115
+ inputSchema: vsaListSchema
2116
+ },
2117
+ async (args) => {
2118
+ const a = args;
2119
+ try {
2120
+ const facts = await vsa.listFacts({
2121
+ userId: a.userId,
2122
+ scopeTag: a.scopeTag,
2123
+ botId: a.botId,
2124
+ includeDeprecated: a.includeDeprecated
2125
+ });
2126
+ return {
2127
+ content: [{ type: "text", text: JSON.stringify({ facts }) }]
2128
+ };
2129
+ } catch (error) {
2130
+ return {
2131
+ content: [
2132
+ {
2133
+ type: "text",
2134
+ text: JSON.stringify({ error: error.message ?? "vsa.list failed" })
2135
+ }
2136
+ ],
2137
+ isError: true
2138
+ };
2139
+ }
2140
+ }
2141
+ );
2142
+ server.registerTool(
2143
+ "memory.vsaForget",
2144
+ {
2145
+ title: "VSA Forget Facts",
2146
+ description: "Soft-delete one or more VSA facts by id. Idempotent \u2014 facts already deprecated return `deprecatedCount: 0`.",
2147
+ // biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
2148
+ inputSchema: vsaForgetSchema
2149
+ },
2150
+ async (args) => {
2151
+ const a = args;
2152
+ try {
2153
+ const result = await vsa.forget({
2154
+ userId: a.userId,
2155
+ factIds: a.factIds,
2156
+ reason: a.reason
2157
+ });
2158
+ return {
2159
+ content: [{ type: "text", text: JSON.stringify(result) }]
2160
+ };
2161
+ } catch (error) {
2162
+ return {
2163
+ content: [
2164
+ {
2165
+ type: "text",
2166
+ text: JSON.stringify({ error: error.message ?? "vsa.forget failed" })
2167
+ }
2168
+ ],
2169
+ isError: true
2170
+ };
2171
+ }
2172
+ }
2173
+ );
1159
2174
  const transport = new StdioServerTransport();
1160
2175
  await server.connect(transport);
1161
2176
  return server;