@jungjaehoon/mama-core 1.4.2 → 1.5.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.
Files changed (118) hide show
  1. package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
  2. package/db/migrations/027-create-entity-review-support.sql +44 -0
  3. package/db/migrations/028-create-entity-audit-tables.sql +45 -0
  4. package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
  5. package/db/migrations/030-case-first-memory-substrate.sql +901 -0
  6. package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
  7. package/dist/canonicalize.d.ts +38 -0
  8. package/dist/canonicalize.js +106 -0
  9. package/dist/cases/case-links.d.ts +116 -0
  10. package/dist/cases/case-links.js +794 -0
  11. package/dist/cases/composition-overrides.d.ts +90 -0
  12. package/dist/cases/composition-overrides.js +724 -0
  13. package/dist/cases/corrections.d.ts +156 -0
  14. package/dist/cases/corrections.js +1045 -0
  15. package/dist/cases/freshness.d.ts +57 -0
  16. package/dist/cases/freshness.js +283 -0
  17. package/dist/cases/live-state.d.ts +48 -0
  18. package/dist/cases/live-state.js +256 -0
  19. package/dist/cases/membership-explain.d.ts +56 -0
  20. package/dist/cases/membership-explain.js +140 -0
  21. package/dist/cases/membership-matcher.d.ts +23 -0
  22. package/dist/cases/membership-matcher.js +193 -0
  23. package/dist/cases/merge-split.d.ts +61 -0
  24. package/dist/cases/merge-split.js +368 -0
  25. package/dist/cases/role-inference.d.ts +10 -0
  26. package/dist/cases/role-inference.js +85 -0
  27. package/dist/cases/search-rollup.d.ts +23 -0
  28. package/dist/cases/search-rollup.js +225 -0
  29. package/dist/cases/sqlite-transaction.d.ts +10 -0
  30. package/dist/cases/sqlite-transaction.js +62 -0
  31. package/dist/cases/store.d.ts +74 -0
  32. package/dist/cases/store.js +816 -0
  33. package/dist/cases/target-ref.d.ts +25 -0
  34. package/dist/cases/target-ref.js +22 -0
  35. package/dist/cases/timeline-range.d.ts +6 -0
  36. package/dist/cases/timeline-range.js +322 -0
  37. package/dist/cases/tombstone-sweeper.d.ts +13 -0
  38. package/dist/cases/tombstone-sweeper.js +128 -0
  39. package/dist/cases/types.d.ts +244 -0
  40. package/dist/cases/types.js +47 -0
  41. package/dist/cases/wiki-page-index.d.ts +50 -0
  42. package/dist/cases/wiki-page-index.js +350 -0
  43. package/dist/connectors/event-index.d.ts +44 -0
  44. package/dist/connectors/event-index.js +366 -0
  45. package/dist/connectors/types.d.ts +69 -0
  46. package/dist/connectors/types.js +3 -0
  47. package/dist/db-manager.d.ts +4 -0
  48. package/dist/db-manager.js +10 -3
  49. package/dist/decision-formatter.d.ts +2 -0
  50. package/dist/decision-formatter.js +11 -8
  51. package/dist/embedding-server/mobile/daemon.d.ts +1 -0
  52. package/dist/embedding-server/mobile/daemon.js +11 -1
  53. package/dist/entities/audit-metrics.d.ts +73 -0
  54. package/dist/entities/audit-metrics.js +128 -0
  55. package/dist/entities/candidate-generator.d.ts +9 -0
  56. package/dist/entities/candidate-generator.js +436 -0
  57. package/dist/entities/entity-impact.d.ts +41 -0
  58. package/dist/entities/entity-impact.js +140 -0
  59. package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
  60. package/dist/entities/entity-linked-decision-counts.js +29 -0
  61. package/dist/entities/entity-list.d.ts +23 -0
  62. package/dist/entities/entity-list.js +99 -0
  63. package/dist/entities/entity-orphan-list.d.ts +26 -0
  64. package/dist/entities/entity-orphan-list.js +91 -0
  65. package/dist/entities/entity-search.d.ts +21 -0
  66. package/dist/entities/entity-search.js +150 -0
  67. package/dist/entities/errors.d.ts +65 -0
  68. package/dist/entities/errors.js +124 -0
  69. package/dist/entities/exact-merge-backfill.d.ts +11 -0
  70. package/dist/entities/exact-merge-backfill.js +161 -0
  71. package/dist/entities/lineage-backfill.d.ts +12 -0
  72. package/dist/entities/lineage-backfill.js +101 -0
  73. package/dist/entities/lineage-store.d.ts +51 -0
  74. package/dist/entities/lineage-store.js +308 -0
  75. package/dist/entities/normalization.d.ts +14 -0
  76. package/dist/entities/normalization.js +63 -0
  77. package/dist/entities/policy-store.d.ts +17 -0
  78. package/dist/entities/policy-store.js +263 -0
  79. package/dist/entities/policy-types.d.ts +72 -0
  80. package/dist/entities/policy-types.js +59 -0
  81. package/dist/entities/projection.d.ts +8 -0
  82. package/dist/entities/projection.js +82 -0
  83. package/dist/entities/provenance-query.d.ts +33 -0
  84. package/dist/entities/provenance-query.js +167 -0
  85. package/dist/entities/read-identity.d.ts +31 -0
  86. package/dist/entities/read-identity.js +264 -0
  87. package/dist/entities/recall-bridge.d.ts +5 -0
  88. package/dist/entities/recall-bridge.js +171 -0
  89. package/dist/entities/resolution-engine.d.ts +15 -0
  90. package/dist/entities/resolution-engine.js +69 -0
  91. package/dist/entities/rollback-preview.d.ts +39 -0
  92. package/dist/entities/rollback-preview.js +235 -0
  93. package/dist/entities/source-locator.d.ts +3 -0
  94. package/dist/entities/source-locator.js +32 -0
  95. package/dist/entities/store.d.ts +75 -0
  96. package/dist/entities/store.js +465 -0
  97. package/dist/entities/types.d.ts +162 -0
  98. package/dist/entities/types.js +30 -0
  99. package/dist/index.d.ts +47 -2
  100. package/dist/index.js +66 -25
  101. package/dist/mama-api.d.ts +22 -1
  102. package/dist/mama-api.js +272 -64
  103. package/dist/memory/api.d.ts +28 -1
  104. package/dist/memory/api.js +293 -31
  105. package/dist/memory/types.d.ts +11 -2
  106. package/dist/search/feedback-store.d.ts +59 -0
  107. package/dist/search/feedback-store.js +211 -0
  108. package/dist/search/question-type.d.ts +5 -0
  109. package/dist/search/question-type.js +41 -0
  110. package/dist/search/ranker-features.d.ts +23 -0
  111. package/dist/search/ranker-features.js +104 -0
  112. package/dist/search/ranker-rescore.d.ts +31 -0
  113. package/dist/search/ranker-rescore.js +226 -0
  114. package/dist/search/ranker-trainer.d.ts +94 -0
  115. package/dist/search/ranker-trainer.js +860 -0
  116. package/dist/test-utils.d.ts +14 -0
  117. package/dist/test-utils.js +52 -0
  118. package/package.json +21 -3
package/dist/mama-api.js CHANGED
@@ -80,6 +80,9 @@ const finding_store_js_1 = require("./memory/finding-store.js");
80
80
  Object.defineProperty(exports, "listOpenAuditFindings", { enumerable: true, get: function () { return finding_store_js_1.listOpenAuditFindings; } });
81
81
  const event_store_js_1 = require("./memory/event-store.js");
82
82
  Object.defineProperty(exports, "listRecentMemoryEvents", { enumerable: true, get: function () { return event_store_js_1.listRecentMemoryEvents; } });
83
+ const search_rollup_js_1 = require("./cases/search-rollup.js");
84
+ const ranker_rescore_js_1 = require("./search/ranker-rescore.js");
85
+ const ranker_features_js_1 = require("./search/ranker-features.js");
83
86
  // Session-level warning cooldown cache (Story 1.1, 1.2)
84
87
  // Prevents spam by tracking warned topics per session
85
88
  const warnedTopicsCache = new Map();
@@ -111,7 +114,7 @@ const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
111
114
  * outcome: 'success'
112
115
  * });
113
116
  */
114
- async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context: _trust_context = null, is_static, scopes: inputScopes, event_date, }) {
117
+ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context: _trust_context = null, is_static, scopes: inputScopes, event_date, timelineEvent, }) {
115
118
  // Validate required fields
116
119
  if (!topic || typeof topic !== 'string') {
117
120
  throw new Error('mama.save() requires topic (string)');
@@ -161,7 +164,7 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
161
164
  // Future: Will use decision_type column for proper distinction
162
165
  const _userInvolvement = type === 'user_decision' ? 'approved' : null;
163
166
  (0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
164
- const { id: decisionId } = await (0, api_js_1.saveMemory)({
167
+ const { id: decisionId, timeline_event_id: timelineEventId, timeline_event_ids: timelineEventIds, } = await (0, api_js_1.saveMemory)({
165
168
  topic,
166
169
  kind: is_static === 1 ? 'preference' : 'decision',
167
170
  summary: decision,
@@ -173,6 +176,7 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
173
176
  source_type: 'legacy_save',
174
177
  },
175
178
  eventDate: event_date ?? undefined,
179
+ timelineEvent,
176
180
  });
177
181
  (0, progress_indicator_js_1.logComplete)(`Decision saved: ${decisionId.substring(0, 20)}...`);
178
182
  // Update user_involvement, outcome, failure_reason, limitation
@@ -255,13 +259,17 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
255
259
  if (searchResults && typeof searchResults === 'object' && 'results' in searchResults) {
256
260
  // Filter out the decision we just saved
257
261
  similar_decisions = searchResults.results
262
+ .filter((d) => d.source_type === 'decision')
258
263
  .filter((d) => d.id !== decisionId)
259
264
  .map((d) => ({
260
265
  id: d.id,
261
266
  topic: d.topic,
262
267
  decision: d.decision,
263
268
  similarity: d.similarity,
269
+ retrieval_score: d.retrieval_score ?? null,
264
270
  created_at: d.created_at,
271
+ event_date: d.event_date ?? null,
272
+ event_datetime: d.event_datetime ?? null,
265
273
  }));
266
274
  if (similar_decisions.length > 0) {
267
275
  (0, progress_indicator_js_1.logComplete)(`Found ${similar_decisions.length} related decision(s)`);
@@ -308,6 +316,9 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
308
316
  return {
309
317
  success: true,
310
318
  id: decisionId,
319
+ saved_decision_id: decisionId,
320
+ timeline_event_id: timelineEventId ?? null,
321
+ timeline_event_ids: timelineEventIds ?? [],
311
322
  ...(similar_decisions.length > 0 && { similar_decisions }),
312
323
  ...(warning && { warning }),
313
324
  ...(collaboration_hint && { collaboration_hint }),
@@ -698,60 +709,260 @@ function applyRecencyBoost(results, options = {}) {
698
709
  })
699
710
  .sort((a, b) => (b.final_score ?? 0) - (a.final_score ?? 0));
700
711
  }
712
+ /** Phase 3 Task 33: learned-ranker meta attached to mama.suggest response. */
713
+ function buildRankerMeta(applied, modelId, skippedReason) {
714
+ const meta = {
715
+ model_id: modelId,
716
+ feature_set_version: ranker_features_js_1.SEARCH_RANKER_FEATURE_SET_VERSION,
717
+ applied,
718
+ mode: 'offline',
719
+ };
720
+ if (skippedReason) {
721
+ meta.skipped_reason = skippedReason;
722
+ }
723
+ return meta;
724
+ }
725
+ function resultRecord(result) {
726
+ if (typeof result.record === 'object' &&
727
+ result.record !== null &&
728
+ !Array.isArray(result.record)) {
729
+ return result.record;
730
+ }
731
+ return {};
732
+ }
733
+ function stringOrNull(value) {
734
+ if (value === null || value === undefined) {
735
+ return null;
736
+ }
737
+ return String(value);
738
+ }
739
+ function numberOrNull(value) {
740
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
741
+ return null;
742
+ }
743
+ return value;
744
+ }
745
+ function confidenceValue(record, fallback) {
746
+ const numeric = numberOrNull(record.confidence);
747
+ if (numeric !== null) {
748
+ return numeric;
749
+ }
750
+ switch (record.confidence) {
751
+ case 'high':
752
+ return 0.9;
753
+ case 'medium':
754
+ return 0.6;
755
+ case 'low':
756
+ return 0.3;
757
+ default:
758
+ return fallback;
759
+ }
760
+ }
761
+ function mapRolledUpResult(result) {
762
+ const record = resultRecord(result);
763
+ const topic = stringOrNull(record.topic ?? record.title) ?? result.source_id;
764
+ // For wiki_page leaves, prefer the markdown body (`content`) in `decision` so
765
+ // downstream consumers see the meaningful body rather than the short title.
766
+ // Decision/checkpoint records use their own fields (summary/decision).
767
+ const isWikiPageLeaf = result.source_type === 'wiki_page';
768
+ const decision = isWikiPageLeaf
769
+ ? (stringOrNull(record.content ?? record.summary ?? record.decision ?? record.title) ??
770
+ result.source_id)
771
+ : (stringOrNull(record.summary ?? record.decision ?? record.title ?? record.content) ??
772
+ result.source_id);
773
+ const reasoning = stringOrNull(record.details ?? record.reasoning ?? record.status_reason ?? record.content) ??
774
+ '';
775
+ return {
776
+ id: result.source_id,
777
+ topic,
778
+ decision,
779
+ reasoning,
780
+ confidence: confidenceValue(record, result.score),
781
+ // Back-compat with pre-rollup consumers (swarm-mama-adapter tests,
782
+ // older callers): similarity mirrors the retrieval score when we don't
783
+ // have a separate similarity measure. retrieval_score remains the
784
+ // authoritative field for Phase 3 code paths.
785
+ similarity: result.score,
786
+ retrieval_score: result.score,
787
+ created_at: record.created_at ?? null,
788
+ event_date: record.event_date ?? null,
789
+ event_datetime: record.event_datetime ?? null,
790
+ graph_source: 'primary',
791
+ graph_rank: 1,
792
+ related_to: null,
793
+ edge_reason: null,
794
+ case_id: result.case_id,
795
+ source_type: result.source_type,
796
+ contributing_leaves: result.contributing_leaves ?? null,
797
+ };
798
+ }
701
799
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
702
800
  async function suggest(userQuestion, options = {}) {
703
801
  if (!userQuestion || typeof userQuestion !== 'string') {
704
802
  throw new Error('mama.suggest() requires userQuestion (string)');
705
803
  }
706
- const { format = 'json', limit = 5, threshold, useReranking = false,
804
+ const { format = 'json', limit = 5, threshold, useReranking = false, rerankWithLearned = false,
707
805
  // Recency boosting parameters (Gaussian Decay - Elasticsearch style)
708
806
  recencyWeight = 0.3, // 0-1: How much to weight recency (0.3 = 70% semantic, 30% recency)
709
807
  recencyScale = 7, // Days until recency score drops to 50%
710
808
  recencyDecay = 0.5, // Score at scale point (0.5 = 50%)
711
809
  disableRecency = false, // Set true to disable recency boosting entirely
712
810
  } = options;
811
+ const rerankPoolLimit = rerankWithLearned ? Math.max(limit * 4, limit + 5) : limit;
713
812
  try {
714
813
  const bundle = await (0, api_js_1.recallMemory)(userQuestion, {
715
814
  includeProfile: false,
716
815
  topicPrefix: options.topicPrefix,
816
+ limit: rerankPoolLimit,
717
817
  ...(options.scopes && { scopes: options.scopes }),
718
818
  });
819
+ const fusedHits = bundle.fused_hits ?? [];
820
+ const rolledUp = fusedHits.length > 0 ? (0, search_rollup_js_1.rollUpSearchHits)({ fusedHits, adapter: (0, memory_store_js_1.getAdapter)() }) : [];
821
+ // Phase 3 Task 33: compute base ranker meta once so every return path
822
+ // (rolledUp, memories fallback, vector-search fallback) can attach it.
823
+ // rerankWithLearned-driven rescoring still only applies to result arrays
824
+ // that match the ranker's expected shape (id + source_type + case_id).
825
+ const baseRankerMeta = useReranking
826
+ ? buildRankerMeta(false, null, 'llm_reranking_requested')
827
+ : rerankWithLearned
828
+ ? null // marker: rescoring requested, actual meta set per-path after rescore
829
+ : buildRankerMeta(false, null, 'feature_disabled');
830
+ const applyLearnedRanker = (results) => {
831
+ if (baseRankerMeta !== null) {
832
+ return { results, meta: baseRankerMeta };
833
+ }
834
+ // Phase 3 Task 33: the caller opted in via rerankWithLearned, but the
835
+ // runtime `search_ranker_enabled` gate still has final say. This lets
836
+ // operators disable the learned ranker globally during rollback without
837
+ // touching any caller code.
838
+ let runtimeEnabled = true;
839
+ try {
840
+ runtimeEnabled = (0, ranker_rescore_js_1.isSearchRankerEnabled)((0, memory_store_js_1.getAdapter)());
841
+ }
842
+ catch (err) {
843
+ (0, debug_logger_js_1.warn)(`[mama.suggest] isSearchRankerEnabled check failed: ${String(err)}`);
844
+ }
845
+ if (!runtimeEnabled) {
846
+ return { results, meta: buildRankerMeta(false, null, 'feature_disabled') };
847
+ }
848
+ try {
849
+ const rescored = (0, ranker_rescore_js_1.rescoreSearchResults)((0, memory_store_js_1.getAdapter)(), {
850
+ query: userQuestion,
851
+ results,
852
+ });
853
+ return {
854
+ results: rescored.results,
855
+ meta: buildRankerMeta(rescored.skipped_reason === undefined, rescored.model_id, rescored.skipped_reason),
856
+ };
857
+ }
858
+ catch (err) {
859
+ (0, debug_logger_js_1.warn)(`[mama.suggest] learned-ranker rescore failed: ${String(err)}`);
860
+ return { results, meta: buildRankerMeta(false, null, 'rescore_error') };
861
+ }
862
+ };
863
+ const summarizeGraphExpansion = (rows) => {
864
+ const sources = {
865
+ primary: 0,
866
+ supersedes_chain: 0,
867
+ refines: 0,
868
+ refined_by: 0,
869
+ contradicts: 0,
870
+ };
871
+ let expandedCount = 0;
872
+ for (const row of rows) {
873
+ const graphSource = row.graph_source ?? 'primary';
874
+ if (graphSource === 'primary') {
875
+ sources.primary += 1;
876
+ continue;
877
+ }
878
+ expandedCount += 1;
879
+ if (graphSource in sources) {
880
+ const key = graphSource;
881
+ sources[key] += 1;
882
+ }
883
+ }
884
+ return {
885
+ total_results: rows.length,
886
+ primary_count: sources.primary,
887
+ expanded_count: expandedCount,
888
+ sources,
889
+ };
890
+ };
891
+ if (rolledUp.length > 0) {
892
+ const filteredResults = rolledUp.slice(0, rerankPoolLimit);
893
+ const { results: mappedResults, meta: rankerMeta } = applyLearnedRanker(filteredResults.map(mapRolledUpResult));
894
+ const limitedResults = mappedResults.slice(0, limit);
895
+ if (format === 'markdown') {
896
+ const context = limitedResults
897
+ .map((result, index) => `${index + 1}. [${result.topic}] ${result.decision}\n ${result.reasoning}`)
898
+ .join('\n');
899
+ return `🔍 Search method: memory_v2\n${context}`;
900
+ }
901
+ return {
902
+ query: userQuestion,
903
+ results: limitedResults,
904
+ meta: {
905
+ count: limitedResults.length,
906
+ search_method: 'memory_v2',
907
+ threshold: threshold || 'adaptive',
908
+ recency_boost: disableRecency
909
+ ? null
910
+ : {
911
+ weight: recencyWeight,
912
+ scale: recencyScale,
913
+ decay: recencyDecay,
914
+ },
915
+ graph_expansion: summarizeGraphExpansion(limitedResults),
916
+ ranker: rankerMeta,
917
+ },
918
+ };
919
+ }
719
920
  if (bundle.memories.length > 0) {
720
921
  // recallMemory uses RRF fusion — confidence is overwritten with the normalized
721
922
  // retrieval score (0-1 range, where 1.0 = best match in this result set).
722
923
  // The original stored confidence is lost after RRF normalization.
723
924
  // We capture the retrieval score separately so `similarity` reflects search
724
925
  // relevance while `confidence` is passed through as-is from the bundle.
725
- let filteredMemories = bundle.memories;
726
- // Apply limit
727
- filteredMemories = filteredMemories.slice(0, limit);
926
+ const filteredMemories = bundle.memories.slice(0, rerankPoolLimit);
927
+ const baseRows = filteredMemories.map((memory) => ({
928
+ id: memory.id,
929
+ topic: memory.topic,
930
+ decision: memory.summary,
931
+ reasoning: memory.details,
932
+ confidence: memory.confidence,
933
+ // recallMemory currently normalizes fused retrieval rank into `confidence`.
934
+ // Keep that value visible as retrieval_score, but do not pretend it is
935
+ // semantic similarity; save-time warning logic keys off `similarity`.
936
+ similarity: null,
937
+ retrieval_score: memory.confidence ?? null,
938
+ final_score: memory.confidence ?? null,
939
+ created_at: memory.created_at,
940
+ event_date: memory.event_date ?? null,
941
+ event_datetime: memory.event_datetime ?? null,
942
+ graph_source: 'primary',
943
+ graph_rank: 1,
944
+ related_to: null,
945
+ edge_reason: null,
946
+ case_id: null,
947
+ source_type: memory.kind ??
948
+ memory.source?.source_type ??
949
+ memory.source_type ??
950
+ memory.type ??
951
+ 'decision',
952
+ }));
953
+ const { results: rankedRows, meta: rankerMeta } = applyLearnedRanker(baseRows);
954
+ const limitedRows = rankedRows.slice(0, limit);
728
955
  if (format === 'markdown') {
729
- const context = filteredMemories
730
- .map((memory, index) => `${index + 1}. [${memory.topic}] ${memory.summary}\n ${memory.details}`)
956
+ const context = limitedRows
957
+ .map((row, index) => `${index + 1}. [${row.topic}] ${row.decision}\n ${row.reasoning}`)
731
958
  .join('\n');
732
959
  return `🔍 Search method: memory_v2\n${context}`;
733
960
  }
734
961
  return {
735
962
  query: userQuestion,
736
- results: filteredMemories.map((memory, idx) => ({
737
- id: memory.id,
738
- topic: memory.topic,
739
- decision: memory.summary,
740
- reasoning: memory.details,
741
- confidence: memory.confidence,
742
- // memory.confidence is the normalized RRF retrieval score (0-1) after
743
- // recallMemory. Use it as similarity so the field reflects search relevance
744
- // rather than the original stored trust score.
745
- similarity: memory.confidence ?? 1 - idx * 0.01,
746
- created_at: memory.created_at,
747
- event_date: memory.event_date ?? null,
748
- graph_source: 'primary',
749
- graph_rank: 1,
750
- related_to: null,
751
- edge_reason: null,
752
- })),
963
+ results: limitedRows,
753
964
  meta: {
754
- count: filteredMemories.length,
965
+ count: limitedRows.length,
755
966
  search_method: 'memory_v2',
756
967
  threshold: threshold || 'adaptive',
757
968
  recency_boost: disableRecency
@@ -761,18 +972,8 @@ async function suggest(userQuestion, options = {}) {
761
972
  scale: recencyScale,
762
973
  decay: recencyDecay,
763
974
  },
764
- graph_expansion: {
765
- total_results: filteredMemories.length,
766
- primary_count: bundle.graph_context.primary.length,
767
- expanded_count: bundle.graph_context.expanded.length,
768
- sources: {
769
- primary: bundle.graph_context.primary.length,
770
- supersedes_chain: 0,
771
- refines: 0,
772
- refined_by: 0,
773
- contradicts: 0,
774
- },
775
- },
975
+ graph_expansion: summarizeGraphExpansion(limitedRows),
976
+ ranker: rankerMeta,
776
977
  },
777
978
  };
778
979
  }
@@ -791,7 +992,7 @@ async function suggest(userQuestion, options = {}) {
791
992
  const wordCount = userQuestion.split(/\s+/).length;
792
993
  const adaptiveThreshold = threshold !== undefined ? threshold : wordCount < 3 ? 0.7 : 0.6;
793
994
  // Vector search
794
- results = await (0, memory_store_js_1.vectorSearch)(queryEmbedding, limit * 2, 0.5); // Get more candidates
995
+ results = await (0, memory_store_js_1.vectorSearch)(queryEmbedding, rerankPoolLimit * 2, 0.5); // Get more candidates
795
996
  // Filter by adaptive threshold
796
997
  results = results.filter((r) => r.similarity >= adaptiveThreshold);
797
998
  // Stage 1.4: Temporal boost — detect time-related queries and boost matching results
@@ -839,7 +1040,7 @@ async function suggest(userQuestion, options = {}) {
839
1040
  // Stage 1.7: FTS5 hybrid merge (Haiku Memory Layer)
840
1041
  {
841
1042
  try {
842
- const ftsResults = await (0, db_manager_js_1.fts5Search)(userQuestion, limit * 2);
1043
+ const ftsResults = await (0, db_manager_js_1.fts5Search)(userQuestion, rerankPoolLimit * 2);
843
1044
  if (ftsResults.length > 0) {
844
1045
  // Normalize FTS5 ranks (BM25 returns negative values, closer to 0 = better)
845
1046
  const maxRank = Math.max(...ftsResults.map((r) => Math.abs(r.rank)));
@@ -924,7 +1125,7 @@ async function suggest(userQuestion, options = {}) {
924
1125
  ORDER BY created_at DESC
925
1126
  LIMIT ?
926
1127
  `);
927
- const rows = (await stmt.all(...likeParams, limit));
1128
+ const rows = (await stmt.all(...likeParams, rerankPoolLimit));
928
1129
  results = rows.map((row) => ({
929
1130
  ...row,
930
1131
  similarity: 0.75, // Assign moderate similarity for keyword matches
@@ -949,8 +1150,32 @@ async function suggest(userQuestion, options = {}) {
949
1150
  if (useReranking) {
950
1151
  results = await rerankWithLLM(userQuestion, results);
951
1152
  }
952
- // Slice to limit
953
- const finalResults = results.slice(0, limit);
1153
+ const rerankCandidateResults = results.slice(0, rerankPoolLimit);
1154
+ const vectorRows = rerankCandidateResults.map((r) => ({
1155
+ id: r.id,
1156
+ topic: r.topic,
1157
+ decision: r.decision,
1158
+ reasoning: r.reasoning,
1159
+ confidence: r.confidence,
1160
+ similarity: r.similarity,
1161
+ created_at: r.created_at,
1162
+ event_date: r.event_date ?? null,
1163
+ event_datetime: r.event_datetime ?? null,
1164
+ // Recency metadata (NEW - Gaussian Decay)
1165
+ recency_score: r.recency_score,
1166
+ recency_age_days: r.recency_age_days,
1167
+ final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
1168
+ retrieval_score: r.similarity ?? null,
1169
+ // Graph metadata (NEW - Phase 1)
1170
+ graph_source: r.graph_source || 'primary',
1171
+ graph_rank: r.graph_rank || 1.0,
1172
+ related_to: r.related_to || null,
1173
+ edge_reason: r.edge_reason || null,
1174
+ case_id: null,
1175
+ source_type: 'decision',
1176
+ }));
1177
+ const { results: rankedVectorRows, meta: rankerMeta } = applyLearnedRanker(vectorRows);
1178
+ const finalResults = rankedVectorRows.slice(0, limit);
954
1179
  // Markdown format (for human display)
955
1180
  if (format === 'markdown') {
956
1181
  const context = (0, decision_formatter_js_1.formatContext)(finalResults, { maxTokens: 500 });
@@ -976,27 +1201,9 @@ async function suggest(userQuestion, options = {}) {
976
1201
  contradicts: finalResults.filter((r) => r.graph_source === 'contradicts').length,
977
1202
  },
978
1203
  };
979
- // JSON format (default - LLM-first)
980
1204
  return {
981
1205
  query: userQuestion,
982
- results: finalResults.map((r) => ({
983
- id: r.id,
984
- topic: r.topic,
985
- decision: r.decision,
986
- reasoning: r.reasoning,
987
- confidence: r.confidence,
988
- similarity: r.similarity,
989
- created_at: r.created_at,
990
- // Recency metadata (NEW - Gaussian Decay)
991
- recency_score: r.recency_score,
992
- recency_age_days: r.recency_age_days,
993
- final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
994
- // Graph metadata (NEW - Phase 1)
995
- graph_source: r.graph_source || 'primary',
996
- graph_rank: r.graph_rank || 1.0,
997
- related_to: r.related_to || null,
998
- edge_reason: r.edge_reason || null,
999
- })),
1206
+ results: finalResults,
1000
1207
  meta: {
1001
1208
  count: finalResults.length,
1002
1209
  search_method: searchMethod,
@@ -1011,6 +1218,7 @@ async function suggest(userQuestion, options = {}) {
1011
1218
  },
1012
1219
  // Graph expansion stats (NEW - Phase 1)
1013
1220
  graph_expansion: searchMethod.includes('graph') ? graphStats : null,
1221
+ ranker: rankerMeta,
1014
1222
  },
1015
1223
  };
1016
1224
  }
@@ -1068,7 +1276,7 @@ async function listDecisions(options = {}) {
1068
1276
  JOIN memory_scope_bindings msb ON msb.memory_id = d.id
1069
1277
  WHERE msb.scope_id IN (${placeholders})
1070
1278
  AND d.superseded_by IS NULL
1071
- ORDER BY d.created_at DESC
1279
+ ORDER BY COALESCE(d.event_datetime, d.created_at) DESC, d.created_at DESC
1072
1280
  LIMIT ?
1073
1281
  `);
1074
1282
  decisions = await stmt.all(...scopeIds, limit);
@@ -1077,7 +1285,7 @@ async function listDecisions(options = {}) {
1077
1285
  const stmt = adapter.prepare(`
1078
1286
  SELECT * FROM decisions
1079
1287
  WHERE superseded_by IS NULL
1080
- ORDER BY created_at DESC
1288
+ ORDER BY COALESCE(event_datetime, created_at) DESC, created_at DESC
1081
1289
  LIMIT ?
1082
1290
  `);
1083
1291
  decisions = await stmt.all(limit);
@@ -1,6 +1,7 @@
1
+ import { type WikiPageIndexRecord } from '../cases/wiki-page-index.js';
1
2
  import { resolveMemoryEvolution } from './evolution-engine.js';
2
3
  import { getChannelSummary, upsertChannelSummary } from './channel-summary-store.js';
3
- import type { MemoryKind, MemoryAgentBootstrap, MemoryAuditAck, MemoryEdge, MemoryScopeRef, MemoryStatus, ProfileSnapshot, RecallBundle, IngestConversationInput, ExtractedMemoryUnit, IngestConversationResult } from './types.js';
4
+ import type { MemoryKind, MemoryAgentBootstrap, MemoryAuditAck, MemoryEdge, MemoryRecord, MemoryScopeRef, MemoryStatus, ProfileSnapshot, RecallBundle, IngestConversationInput, ExtractedMemoryUnit, IngestConversationResult } from './types.js';
4
5
  interface SaveMemoryInput {
5
6
  topic: string;
6
7
  kind: MemoryKind;
@@ -23,6 +24,21 @@ interface SaveMemoryInput {
23
24
  * Stored in the event_date column. Defaults to created_at if omitted.
24
25
  */
25
26
  eventDate?: string;
27
+ /** Source event timestamp in milliseconds when known. */
28
+ eventDateTime?: number;
29
+ entityObservationIds?: string[];
30
+ timelineEvent?: {
31
+ id?: string;
32
+ entity_id?: string;
33
+ event_type: string;
34
+ role?: string | null;
35
+ valid_from?: number | null;
36
+ valid_to?: number | null;
37
+ observed_at?: number | null;
38
+ source_ref?: string | null;
39
+ summary: string;
40
+ details?: string | null;
41
+ };
26
42
  }
27
43
  interface RecallMemoryOptions {
28
44
  scopes?: MemoryScopeRef[];
@@ -30,12 +46,20 @@ interface RecallMemoryOptions {
30
46
  includeHistory?: boolean;
31
47
  skipGraphExpansion?: boolean;
32
48
  topicPrefix?: string;
49
+ limit?: number;
33
50
  }
34
51
  interface IngestMemoryInput {
35
52
  content: string;
36
53
  scopes?: MemoryScopeRef[];
37
54
  source: SaveMemoryInput['source'];
38
55
  eventDate?: string;
56
+ eventDateTime?: number;
57
+ }
58
+ export interface FusedHit {
59
+ source_type: 'decision' | 'wiki_page';
60
+ source_id: string;
61
+ record: MemoryRecord | WikiPageIndexRecord;
62
+ fused_rank_score: number;
39
63
  }
40
64
  export declare const LEXICAL_STOPWORDS: Set<string>;
41
65
  export declare function getLexicalQueryTokens(query: string): string[];
@@ -43,6 +67,9 @@ export declare function loadEdgesForIds(ids: string[]): Promise<MemoryEdge[]>;
43
67
  export declare function saveMemory(input: SaveMemoryInput): Promise<{
44
68
  success: boolean;
45
69
  id: string;
70
+ saved_decision_id?: string;
71
+ timeline_event_id?: string | null;
72
+ timeline_event_ids?: string[];
46
73
  }>;
47
74
  export declare function buildProfile(scopes: MemoryScopeRef[]): Promise<ProfileSnapshot>;
48
75
  export declare function recallMemory(query: string, options?: RecallMemoryOptions): Promise<RecallBundle>;