@jungjaehoon/mama-core 1.3.3 → 1.4.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 (50) hide show
  1. package/README.md +67 -19
  2. package/db/migrations/001-initial-decision-graph.sql +5 -0
  3. package/db/migrations/014-add-is-static-column.sql +5 -0
  4. package/db/migrations/015-add-fts5-search.sql +34 -0
  5. package/db/migrations/016-add-memory-v2-columns.sql +14 -0
  6. package/db/migrations/017-create-memory-scopes.sql +10 -0
  7. package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
  8. package/db/migrations/019-create-memory-events.sql +18 -0
  9. package/db/migrations/020-create-audit-findings.sql +18 -0
  10. package/db/migrations/021-create-memory-truth.sql +20 -0
  11. package/db/migrations/022-create-channel-summaries.sql +9 -0
  12. package/db/migrations/023-create-channel-summary-state.sql +9 -0
  13. package/db/migrations/024-add-event-date.sql +9 -0
  14. package/dist/db-adapter/base-adapter.d.ts +1 -1
  15. package/dist/db-adapter/index.js +2 -8
  16. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
  17. package/dist/db-adapter/node-sqlite-adapter.js +99 -45
  18. package/dist/db-manager.d.ts +30 -2
  19. package/dist/db-manager.js +131 -6
  20. package/dist/decision-formatter.d.ts +2 -0
  21. package/dist/embedding-server/mobile/session-manager.js +22 -7
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.js +31 -3
  24. package/dist/mama-api.d.ts +67 -2
  25. package/dist/mama-api.js +295 -80
  26. package/dist/memory/api.d.ts +81 -0
  27. package/dist/memory/api.js +972 -0
  28. package/dist/memory/bootstrap-builder.d.ts +10 -0
  29. package/dist/memory/bootstrap-builder.js +66 -0
  30. package/dist/memory/channel-summary-state-store.d.ts +19 -0
  31. package/dist/memory/channel-summary-state-store.js +223 -0
  32. package/dist/memory/channel-summary-store.d.ts +8 -0
  33. package/dist/memory/channel-summary-store.js +41 -0
  34. package/dist/memory/event-store.d.ts +5 -0
  35. package/dist/memory/event-store.js +72 -0
  36. package/dist/memory/evolution-engine.d.ts +22 -0
  37. package/dist/memory/evolution-engine.js +133 -0
  38. package/dist/memory/extraction-prompt.d.ts +4 -0
  39. package/dist/memory/extraction-prompt.js +89 -0
  40. package/dist/memory/finding-store.d.ts +5 -0
  41. package/dist/memory/finding-store.js +75 -0
  42. package/dist/memory/profile-builder.d.ts +3 -0
  43. package/dist/memory/profile-builder.js +23 -0
  44. package/dist/memory/scope-store.d.ts +5 -0
  45. package/dist/memory/scope-store.js +13 -0
  46. package/dist/memory/truth-store.d.ts +11 -0
  47. package/dist/memory/truth-store.js +102 -0
  48. package/dist/memory/types.d.ts +267 -0
  49. package/dist/memory/types.js +74 -0
  50. package/package.json +4 -1
package/dist/mama-api.js CHANGED
@@ -22,6 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.listRecentMemoryEvents = exports.listOpenAuditFindings = exports.getChannelSummary = exports.upsertChannelSummary = exports.recordMemoryAudit = exports.createAuditAck = exports.buildMemoryBootstrap = exports.evolveMemory = exports.ingestMemory = exports.buildProfile = exports.recallMemory = exports.saveMemory = void 0;
25
26
  exports.save = save;
26
27
  exports.suggest = suggest;
27
28
  exports.list = listDecisions;
@@ -56,12 +57,29 @@ const os_1 = __importDefault(require("os"));
56
57
  const crypto_1 = __importDefault(require("crypto"));
57
58
  // Internal modules
58
59
  const decision_tracker_js_1 = require("./decision-tracker.js");
60
+ const db_manager_js_1 = require("./db-manager.js");
59
61
  const memory_store_js_1 = require("./memory-store.js");
62
+ const db_manager_js_2 = require("./db-manager.js");
60
63
  const decision_formatter_js_1 = require("./decision-formatter.js");
61
64
  const progress_indicator_js_1 = require("./progress-indicator.js");
62
65
  const embeddings_js_1 = require("./embeddings.js");
63
66
  const ollama_client_js_1 = require("./ollama-client.js");
64
67
  const debug_logger_js_1 = require("./debug-logger.js");
68
+ const api_js_1 = require("./memory/api.js");
69
+ Object.defineProperty(exports, "saveMemory", { enumerable: true, get: function () { return api_js_1.saveMemory; } });
70
+ Object.defineProperty(exports, "recallMemory", { enumerable: true, get: function () { return api_js_1.recallMemory; } });
71
+ Object.defineProperty(exports, "buildProfile", { enumerable: true, get: function () { return api_js_1.buildProfile; } });
72
+ Object.defineProperty(exports, "ingestMemory", { enumerable: true, get: function () { return api_js_1.ingestMemory; } });
73
+ Object.defineProperty(exports, "evolveMemory", { enumerable: true, get: function () { return api_js_1.evolveMemory; } });
74
+ Object.defineProperty(exports, "buildMemoryBootstrap", { enumerable: true, get: function () { return api_js_1.buildMemoryBootstrap; } });
75
+ Object.defineProperty(exports, "createAuditAck", { enumerable: true, get: function () { return api_js_1.createAuditAck; } });
76
+ Object.defineProperty(exports, "recordMemoryAudit", { enumerable: true, get: function () { return api_js_1.recordMemoryAudit; } });
77
+ Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return api_js_1.upsertChannelSummary; } });
78
+ Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return api_js_1.getChannelSummary; } });
79
+ const finding_store_js_1 = require("./memory/finding-store.js");
80
+ Object.defineProperty(exports, "listOpenAuditFindings", { enumerable: true, get: function () { return finding_store_js_1.listOpenAuditFindings; } });
81
+ const event_store_js_1 = require("./memory/event-store.js");
82
+ Object.defineProperty(exports, "listRecentMemoryEvents", { enumerable: true, get: function () { return event_store_js_1.listRecentMemoryEvents; } });
65
83
  // Session-level warning cooldown cache (Story 1.1, 1.2)
66
84
  // Prevents spam by tracking warned topics per session
67
85
  const warnedTopicsCache = new Map();
@@ -93,7 +111,7 @@ const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
93
111
  * outcome: 'success'
94
112
  * });
95
113
  */
96
- async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context = null, }) {
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, }) {
97
115
  // Validate required fields
98
116
  if (!topic || typeof topic !== 'string') {
99
117
  throw new Error('mama.save() requires topic (string)');
@@ -117,42 +135,49 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
117
135
  if (outcome && !validOutcomes.includes(outcome)) {
118
136
  throw new Error(`mama.save() outcome must be one of: ${validOutcomes.join(', ')} (got: ${outcome})`);
119
137
  }
138
+ // Validate scopes shape
139
+ if (inputScopes !== undefined && inputScopes !== null) {
140
+ if (!Array.isArray(inputScopes)) {
141
+ throw new Error('mama.save() scopes must be an array');
142
+ }
143
+ const validKinds = ['global', 'user', 'channel', 'project'];
144
+ for (const scope of inputScopes) {
145
+ if (!scope ||
146
+ typeof scope !== 'object' ||
147
+ typeof scope.kind !== 'string' ||
148
+ typeof scope.id !== 'string') {
149
+ throw new Error('mama.save() each scope must have kind (string) and id (string)');
150
+ }
151
+ if (typeof scope.id === 'string' && scope.id.trim().length === 0) {
152
+ throw new Error('mama.save() each scope.id must be a non-empty string');
153
+ }
154
+ if (!validKinds.includes(scope.kind)) {
155
+ throw new Error(`mama.save() scope kind must be one of: ${validKinds.join(', ')} (got: ${scope.kind})`);
156
+ }
157
+ }
158
+ }
120
159
  // Map type to user_involvement field
121
160
  // Note: Current schema uses user_involvement ('requested', 'approved', 'rejected')
122
161
  // Future: Will use decision_type column for proper distinction
123
162
  const _userInvolvement = type === 'user_decision' ? 'approved' : null;
124
- // Create detection object for learnDecision()
125
- // Convert null to undefined for type compatibility
126
- const detection = {
163
+ (0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
164
+ const { id: decisionId } = await (0, api_js_1.saveMemory)({
127
165
  topic,
128
- decision,
129
- reasoning,
166
+ kind: is_static === 1 ? 'preference' : 'decision',
167
+ summary: decision,
168
+ details: reasoning,
130
169
  confidence,
131
- trust_context: trust_context ?? undefined,
132
- };
133
- // Create tool execution context
134
- // Use current timestamp and generate session ID
135
- const sessionId = `mama_api_${Date.now()}`;
136
- const toolExecution = {
137
- tool_name: 'mama.save',
138
- tool_input: { topic, decision },
139
- exit_code: 0,
140
- session_id: sessionId,
141
- timestamp: Date.now(),
142
- };
143
- // Create session context
144
- const sessionContext = {
145
- session_id: sessionId,
146
- latest_user_message: `Save ${type}: ${topic}`,
147
- recent_exchange: `Claude: ${reasoning.substring(0, 100)}...`,
148
- };
149
- // Call internal learnDecision function
150
- // Note: learnDecision returns { decisionId, notification }
151
- (0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
152
- const { decisionId } = await (0, decision_tracker_js_1.learnDecision)(detection, toolExecution, sessionContext);
170
+ scopes: Array.isArray(inputScopes) && inputScopes.length > 0 ? inputScopes : [],
171
+ source: {
172
+ package: 'mama-core',
173
+ source_type: 'legacy_save',
174
+ },
175
+ eventDate: event_date ?? undefined,
176
+ });
153
177
  (0, progress_indicator_js_1.logComplete)(`Decision saved: ${decisionId.substring(0, 20)}...`);
154
178
  // Update user_involvement, outcome, failure_reason, limitation
155
179
  // Note: learnDecision always sets 'requested', we need to override it
180
+ await (0, db_manager_js_2.initDB)();
156
181
  const adapter = (0, memory_store_js_1.getAdapter)();
157
182
  // Build UPDATE query dynamically based on what fields are provided
158
183
  const updates = [];
@@ -189,6 +214,11 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
189
214
  updates.push('limitation = ?');
190
215
  values.push(limitation);
191
216
  }
217
+ // is_static (user profile marker)
218
+ if (is_static !== undefined) {
219
+ updates.push('is_static = ?');
220
+ values.push(is_static);
221
+ }
192
222
  // Execute UPDATE if we have any fields to update
193
223
  if (updates.length > 0) {
194
224
  values.push(decisionId); // WHERE id = ?
@@ -209,53 +239,58 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
209
239
  let reasoning_graph = null;
210
240
  // Only run auto-search for decisions (not checkpoints) with a topic
211
241
  if (topic) {
212
- try {
213
- // Story 1.1: Auto-search using suggest()
214
- (0, progress_indicator_js_1.logSearching)('Searching for related decisions...');
215
- const searchResults = await suggest(topic, {
216
- limit: 3,
217
- threshold: 0.7,
218
- disableRecency: true, // Pure semantic similarity for comparison
219
- });
220
- // Handle suggest() result which can be string | null | object
221
- if (searchResults && typeof searchResults === 'object' && 'results' in searchResults) {
222
- // Filter out the decision we just saved
223
- similar_decisions = searchResults.results
224
- .filter((d) => d.id !== decisionId)
225
- .map((d) => ({
226
- id: d.id,
227
- topic: d.topic,
228
- decision: d.decision,
229
- similarity: d.similarity,
230
- created_at: d.created_at,
231
- }));
232
- if (similar_decisions.length > 0) {
233
- (0, progress_indicator_js_1.logComplete)(`Found ${similar_decisions.length} related decision(s)`);
234
- }
235
- // Story 1.2: Warning logic (similarity >= 0.85)
236
- const highSimilarity = similar_decisions.find((d) => (d.similarity ?? 0) >= 0.85);
237
- if (highSimilarity && !_isTopicInCooldown(topic)) {
238
- warning = `High similarity (${((highSimilarity.similarity ?? 0) * 100).toFixed(0)}%) with existing decision "${highSimilarity.decision.substring(0, 50)}..."`;
239
- _markTopicWarned(topic);
240
- }
241
- // Story 1.2: Collaboration hint
242
- if (similar_decisions.length > 0) {
243
- collaboration_hint = _generateCollaborationHint(similar_decisions);
242
+ // Skip global similarity search and reasoning graph for scoped saves
243
+ if (!inputScopes || !Array.isArray(inputScopes) || inputScopes.length === 0) {
244
+ try {
245
+ // Story 1.1: Auto-search using suggest()
246
+ // NOTE: suggest() searches globally and does not yet support scoped similarity search.
247
+ // Cross-scope suggestions are possible here. Track as a follow-up.
248
+ (0, progress_indicator_js_1.logSearching)('Searching for related decisions...');
249
+ const searchResults = await suggest(topic, {
250
+ limit: 3,
251
+ threshold: 0.7,
252
+ disableRecency: true, // Pure semantic similarity for comparison
253
+ });
254
+ // Handle suggest() result which can be string | null | object
255
+ if (searchResults && typeof searchResults === 'object' && 'results' in searchResults) {
256
+ // Filter out the decision we just saved
257
+ similar_decisions = searchResults.results
258
+ .filter((d) => d.id !== decisionId)
259
+ .map((d) => ({
260
+ id: d.id,
261
+ topic: d.topic,
262
+ decision: d.decision,
263
+ similarity: d.similarity,
264
+ created_at: d.created_at,
265
+ }));
266
+ if (similar_decisions.length > 0) {
267
+ (0, progress_indicator_js_1.logComplete)(`Found ${similar_decisions.length} related decision(s)`);
268
+ }
269
+ // Story 1.2: Warning logic (similarity >= 0.85)
270
+ const highSimilarity = similar_decisions.find((d) => (d.similarity ?? 0) >= 0.85);
271
+ if (highSimilarity && !_isTopicInCooldown(topic)) {
272
+ warning = `High similarity (${((highSimilarity.similarity ?? 0) * 100).toFixed(0)}%) with existing decision "${highSimilarity.decision.substring(0, 50)}..."`;
273
+ _markTopicWarned(topic);
274
+ }
275
+ // Story 1.2: Collaboration hint
276
+ if (similar_decisions.length > 0) {
277
+ collaboration_hint = _generateCollaborationHint(similar_decisions);
278
+ }
244
279
  }
245
280
  }
246
- }
247
- catch (error) {
248
- // Story 1.1 AC3: Best-effort - save succeeds even if auto-search fails
249
- const errMsg = error instanceof Error ? error.message : String(error);
250
- (0, debug_logger_js_1.error)('Auto-search failed:', errMsg);
251
- }
252
- // Story 1.2: Reasoning graph info
253
- try {
254
- reasoning_graph = await _getReasoningGraphInfo(topic, decisionId);
255
- }
256
- catch (error) {
257
- const errMsg = error instanceof Error ? error.message : String(error);
258
- (0, debug_logger_js_1.error)('Reasoning graph query failed:', errMsg);
281
+ catch (error) {
282
+ // Story 1.1 AC3: Best-effort - save succeeds even if auto-search fails
283
+ const errMsg = error instanceof Error ? error.message : String(error);
284
+ (0, debug_logger_js_1.error)('Auto-search failed:', errMsg);
285
+ }
286
+ // Story 1.2: Reasoning graph info
287
+ try {
288
+ reasoning_graph = await _getReasoningGraphInfo(topic, decisionId);
289
+ }
290
+ catch (error) {
291
+ const errMsg = error instanceof Error ? error.message : String(error);
292
+ (0, debug_logger_js_1.error)('Reasoning graph query failed:', errMsg);
293
+ }
259
294
  }
260
295
  // Story 2.2: Parse reasoning for relationship edges (builds_on, debates, synthesizes)
261
296
  if (reasoning) {
@@ -676,6 +711,71 @@ async function suggest(userQuestion, options = {}) {
676
711
  disableRecency = false, // Set true to disable recency boosting entirely
677
712
  } = options;
678
713
  try {
714
+ const bundle = await (0, api_js_1.recallMemory)(userQuestion, {
715
+ includeProfile: false,
716
+ topicPrefix: options.topicPrefix,
717
+ ...(options.scopes && { scopes: options.scopes }),
718
+ });
719
+ if (bundle.memories.length > 0) {
720
+ // recallMemory uses RRF fusion — confidence is overwritten with the normalized
721
+ // retrieval score (0-1 range, where 1.0 = best match in this result set).
722
+ // The original stored confidence is lost after RRF normalization.
723
+ // We capture the retrieval score separately so `similarity` reflects search
724
+ // 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);
728
+ if (format === 'markdown') {
729
+ const context = filteredMemories
730
+ .map((memory, index) => `${index + 1}. [${memory.topic}] ${memory.summary}\n ${memory.details}`)
731
+ .join('\n');
732
+ return `🔍 Search method: memory_v2\n${context}`;
733
+ }
734
+ return {
735
+ 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
+ })),
753
+ meta: {
754
+ count: filteredMemories.length,
755
+ search_method: 'memory_v2',
756
+ threshold: threshold || 'adaptive',
757
+ recency_boost: disableRecency
758
+ ? null
759
+ : {
760
+ weight: recencyWeight,
761
+ scale: recencyScale,
762
+ decay: recencyDecay,
763
+ },
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
+ },
776
+ },
777
+ };
778
+ }
679
779
  // 1. Try vector search first (if sqlite-vss is available)
680
780
  // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-explicit-any
681
781
  let results = [];
@@ -694,6 +794,37 @@ async function suggest(userQuestion, options = {}) {
694
794
  results = await (0, memory_store_js_1.vectorSearch)(queryEmbedding, limit * 2, 0.5); // Get more candidates
695
795
  // Filter by adaptive threshold
696
796
  results = results.filter((r) => r.similarity >= adaptiveThreshold);
797
+ // Stage 1.4: Temporal boost — detect time-related queries and boost matching results
798
+ {
799
+ const temporalPatterns = [
800
+ // English
801
+ /\b(yesterday|today|last\s+(?:week|month|year)|(\d+)\s+(?:days?|weeks?|months?)\s+ago)\b/i,
802
+ /\b(before|after|since|until|during)\s+\w+/i,
803
+ /\b(how\s+long|when\s+did|what\s+date|what\s+day)\b/i,
804
+ // Korean
805
+ /(?:어제|오늘|그제|지난\s*(?:주|달|해)|(\d+)\s*(?:일|주|달|개월)\s*(?:전|후|뒤))/,
806
+ /(?:언제|얼마나|며칠|몇\s*(?:일|주|달|개월))/,
807
+ ];
808
+ const isTemporalQuery = temporalPatterns.some((p) => p.test(userQuestion));
809
+ if (isTemporalQuery && results.length > 0) {
810
+ // Boost results that contain date/time references in their content
811
+ const datePatterns = [
812
+ /\d{4}[-/]\d{1,2}[-/]\d{1,2}/,
813
+ /(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d/i,
814
+ /\d+\s*(?:일|월|년|주|시간|분)/,
815
+ /(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i,
816
+ /(?:월요일|화요일|수요일|목요일|금요일|토요일|일요일)/,
817
+ ];
818
+ for (const result of results) {
819
+ const content = `${result.decision || ''} ${result.reasoning || ''}`;
820
+ const hasDateRef = datePatterns.some((p) => p.test(content));
821
+ if (hasDateRef) {
822
+ result.similarity = Math.min(1.0, (result.similarity || 0) + 0.1);
823
+ }
824
+ }
825
+ results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
826
+ }
827
+ }
697
828
  // Stage 1.5: Apply recency boosting (Gaussian Decay)
698
829
  // Allows Claude to adjust search strategy (recent vs historical)
699
830
  if (results.length > 0 && !disableRecency) {
@@ -705,6 +836,52 @@ async function suggest(userQuestion, options = {}) {
705
836
  });
706
837
  searchMethod = 'vector+recency';
707
838
  }
839
+ // Stage 1.7: FTS5 hybrid merge (Haiku Memory Layer)
840
+ {
841
+ try {
842
+ const ftsResults = await (0, db_manager_js_1.fts5Search)(userQuestion, limit * 2);
843
+ if (ftsResults.length > 0) {
844
+ // Normalize FTS5 ranks (BM25 returns negative values, closer to 0 = better)
845
+ const maxRank = Math.max(...ftsResults.map((r) => Math.abs(r.rank)));
846
+ const ftsMap = new Map(ftsResults.map((r) => [r.id, maxRank > 0 ? 1 - Math.abs(r.rank) / maxRank : 0.5]));
847
+ // Tunable hybrid weights (env: MAMA_VECTOR_WEIGHT, MAMA_FTS5_WEIGHT)
848
+ const vectorWeight = parseFloat(process.env.MAMA_VECTOR_WEIGHT || '0.6');
849
+ const fts5Weight = parseFloat(process.env.MAMA_FTS5_WEIGHT || '0.4');
850
+ // Merge: boost existing results that also matched FTS5
851
+ for (const result of results) {
852
+ const ftsScore = ftsMap.get(result.id);
853
+ if (ftsScore !== undefined) {
854
+ result.similarity = vectorWeight * result.similarity + fts5Weight * ftsScore;
855
+ ftsMap.delete(result.id);
856
+ }
857
+ }
858
+ // Add FTS5-only results (not in embedding results)
859
+ for (const [id, ftsScore] of ftsMap) {
860
+ const ftsResult = ftsResults.find((r) => r.id === id);
861
+ if (ftsResult) {
862
+ // Need to get full decision record
863
+ const adapter = (0, memory_store_js_1.getAdapter)();
864
+ const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ? AND superseded_by IS NULL');
865
+ const decision = stmt.get(id);
866
+ if (decision) {
867
+ results.push({
868
+ ...decision,
869
+ similarity: fts5Weight * ftsScore, // Only FTS5 score component
870
+ graph_source: 'fts5',
871
+ });
872
+ }
873
+ }
874
+ }
875
+ // Re-sort by similarity
876
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
877
+ results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
878
+ searchMethod = disableRecency ? 'vector+fts5' : 'vector+recency+fts5';
879
+ }
880
+ }
881
+ catch {
882
+ // FTS5 not available, continue with embedding-only results
883
+ }
884
+ }
708
885
  // Stage 2: Graph expansion (NEW - Phase 1)
709
886
  // Expand candidates with supersedes chain and semantic edges
710
887
  if (results.length > 0) {
@@ -712,6 +889,14 @@ async function suggest(userQuestion, options = {}) {
712
889
  results = graphEnhanced;
713
890
  searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
714
891
  }
892
+ // Stage 2.5: is_static boost (after graph expansion to preserve sort order)
893
+ for (const result of results) {
894
+ if (result.is_static === 1) {
895
+ result.final_score = Math.min(1.0, (result.final_score ?? result.similarity ?? 0) + 0.2);
896
+ }
897
+ }
898
+ // Re-sort by final_score after is_static boost
899
+ results.sort((a, b) => (b.final_score ?? b.similarity ?? 0) - (a.final_score ?? a.similarity ?? 0));
715
900
  }
716
901
  catch (vectorError) {
717
902
  // Fallback to keyword search if vector search unavailable
@@ -873,13 +1058,30 @@ async function listDecisions(options = {}) {
873
1058
  const { limit = 10, format = 'json' } = options;
874
1059
  try {
875
1060
  const adapter = (0, memory_store_js_1.getAdapter)();
876
- const stmt = adapter.prepare(`
877
- SELECT * FROM decisions
878
- WHERE superseded_by IS NULL
879
- ORDER BY created_at DESC
880
- LIMIT ?
881
- `);
882
- const decisions = await stmt.all(limit);
1061
+ let decisions;
1062
+ if (options.scopes && options.scopes.length > 0) {
1063
+ // Scope-filtered query: JOIN memory_scope_bindings + memory_scopes
1064
+ const scopeIds = await Promise.all(options.scopes.map((s) => (0, db_manager_js_1.ensureMemoryScope)(s.kind, s.id)));
1065
+ const placeholders = scopeIds.map(() => '?').join(', ');
1066
+ const stmt = adapter.prepare(`
1067
+ SELECT DISTINCT d.* FROM decisions d
1068
+ JOIN memory_scope_bindings msb ON msb.memory_id = d.id
1069
+ WHERE msb.scope_id IN (${placeholders})
1070
+ AND d.superseded_by IS NULL
1071
+ ORDER BY d.created_at DESC
1072
+ LIMIT ?
1073
+ `);
1074
+ decisions = await stmt.all(...scopeIds, limit);
1075
+ }
1076
+ else {
1077
+ const stmt = adapter.prepare(`
1078
+ SELECT * FROM decisions
1079
+ WHERE superseded_by IS NULL
1080
+ ORDER BY created_at DESC
1081
+ LIMIT ?
1082
+ `);
1083
+ decisions = await stmt.all(limit);
1084
+ }
883
1085
  if (format === 'markdown') {
884
1086
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
885
1087
  return (0, decision_formatter_js_1.formatList)(decisions);
@@ -2158,9 +2360,21 @@ const mama = {
2158
2360
  // Core functions (used by 4 MCP tools)
2159
2361
  save,
2160
2362
  suggest,
2363
+ saveMemory: api_js_1.saveMemory,
2364
+ recallMemory: api_js_1.recallMemory,
2161
2365
  list: listDecisions,
2162
2366
  listCheckpoints,
2163
2367
  updateOutcome,
2368
+ buildProfile: api_js_1.buildProfile,
2369
+ ingestMemory: api_js_1.ingestMemory,
2370
+ evolveMemory: api_js_1.evolveMemory,
2371
+ buildMemoryBootstrap: api_js_1.buildMemoryBootstrap,
2372
+ createAuditAck: api_js_1.createAuditAck,
2373
+ recordMemoryAudit: api_js_1.recordMemoryAudit,
2374
+ upsertChannelSummary: api_js_1.upsertChannelSummary,
2375
+ getChannelSummary: api_js_1.getChannelSummary,
2376
+ listAuditFindings: finding_store_js_1.listOpenAuditFindings,
2377
+ listRecentMemoryEvents: event_store_js_1.listRecentMemoryEvents,
2164
2378
  saveCheckpoint,
2165
2379
  loadCheckpoint,
2166
2380
  // Legacy functions (retained for internal use, not exposed via MCP)
@@ -2184,6 +2398,7 @@ const mama = {
2184
2398
  verifyBackupExists,
2185
2399
  deleteAutoLinks,
2186
2400
  validateCleanupResult,
2401
+ expandWithGraph,
2187
2402
  };
2188
2403
  // Default export for backward compatibility
2189
2404
  exports.default = mama;
@@ -0,0 +1,81 @@
1
+ import { resolveMemoryEvolution } from './evolution-engine.js';
2
+ 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
+ interface SaveMemoryInput {
5
+ topic: string;
6
+ kind: MemoryKind;
7
+ summary: string;
8
+ details: string;
9
+ confidence?: number;
10
+ status?: MemoryStatus;
11
+ scopes: MemoryScopeRef[];
12
+ source: {
13
+ package: 'mama-core' | 'mcp-server' | 'standalone' | 'claude-code-plugin';
14
+ source_type: string;
15
+ user_id?: string;
16
+ channel_id?: string;
17
+ project_id?: string;
18
+ };
19
+ /** IDs to exclude from supersede candidates (e.g., sibling facts from same ingestion batch) */
20
+ excludeIds?: string[];
21
+ /**
22
+ * ISO 8601 date string for when the event actually occurred (e.g. "2023-01-15").
23
+ * Stored in the event_date column. Defaults to created_at if omitted.
24
+ */
25
+ eventDate?: string;
26
+ }
27
+ interface RecallMemoryOptions {
28
+ scopes?: MemoryScopeRef[];
29
+ includeProfile?: boolean;
30
+ includeHistory?: boolean;
31
+ skipGraphExpansion?: boolean;
32
+ topicPrefix?: string;
33
+ }
34
+ interface IngestMemoryInput {
35
+ content: string;
36
+ scopes?: MemoryScopeRef[];
37
+ source: SaveMemoryInput['source'];
38
+ eventDate?: string;
39
+ }
40
+ export declare const LEXICAL_STOPWORDS: Set<string>;
41
+ export declare function getLexicalQueryTokens(query: string): string[];
42
+ export declare function loadEdgesForIds(ids: string[]): Promise<MemoryEdge[]>;
43
+ export declare function saveMemory(input: SaveMemoryInput): Promise<{
44
+ success: boolean;
45
+ id: string;
46
+ }>;
47
+ export declare function buildProfile(scopes: MemoryScopeRef[]): Promise<ProfileSnapshot>;
48
+ export declare function recallMemory(query: string, options?: RecallMemoryOptions): Promise<RecallBundle>;
49
+ export declare function ingestMemory(input: IngestMemoryInput): Promise<{
50
+ success: boolean;
51
+ id: string;
52
+ }>;
53
+ export declare function evolveMemory(input: Parameters<typeof resolveMemoryEvolution>[0]): Promise<import("./evolution-engine.js").EvolutionResult>;
54
+ export declare function buildMemoryBootstrap(params: {
55
+ scopes: MemoryScopeRef[];
56
+ channelKey?: string;
57
+ currentGoal?: string;
58
+ mainAgentState?: MemoryAgentBootstrap['main_agent_state'];
59
+ }): Promise<MemoryAgentBootstrap>;
60
+ export declare function createAuditAck(input: MemoryAuditAck): MemoryAuditAck;
61
+ export declare function recordMemoryAudit(input: {
62
+ channelKey: string;
63
+ turnId: string;
64
+ topic: string;
65
+ scopeRefs: MemoryScopeRef[];
66
+ ack: MemoryAuditAck;
67
+ savedMemories?: Array<{
68
+ id: string;
69
+ topic: string;
70
+ summary: string;
71
+ }>;
72
+ }): Promise<{
73
+ eventIds: string[];
74
+ findingIds: string[];
75
+ state: import("./types.js").ChannelSummaryStateRecord;
76
+ }>;
77
+ declare function callExtractionLLM(prompt: string, options: NonNullable<IngestConversationInput['extract']>): Promise<ExtractedMemoryUnit[]>;
78
+ export declare function setExtractionFn(fn: typeof callExtractionLLM | null): void;
79
+ export declare function ingestConversation(input: IngestConversationInput): Promise<IngestConversationResult>;
80
+ export { upsertChannelSummary, getChannelSummary };
81
+ //# sourceMappingURL=api.d.ts.map