@jungjaehoon/mama-server 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.
- package/package.json +53 -0
- package/src/mama/config-loader.js +218 -0
- package/src/mama/db-adapter/README.md +105 -0
- package/src/mama/db-adapter/base-adapter.js +91 -0
- package/src/mama/db-adapter/index.js +31 -0
- package/src/mama/db-adapter/sqlite-adapter.js +342 -0
- package/src/mama/db-adapter/statement.js +127 -0
- package/src/mama/db-manager.js +584 -0
- package/src/mama/debug-logger.js +78 -0
- package/src/mama/decision-formatter.js +1180 -0
- package/src/mama/decision-tracker.js +565 -0
- package/src/mama/embedding-cache.js +221 -0
- package/src/mama/embeddings.js +265 -0
- package/src/mama/hook-metrics.js +403 -0
- package/src/mama/mama-api.js +913 -0
- package/src/mama/memory-inject.js +243 -0
- package/src/mama/memory-store.js +89 -0
- package/src/mama/ollama-client.js +387 -0
- package/src/mama/outcome-tracker.js +349 -0
- package/src/mama/query-intent.js +236 -0
- package/src/mama/relevance-scorer.js +283 -0
- package/src/mama/time-formatter.js +82 -0
- package/src/mama/transparency-banner.js +301 -0
- package/src/server.js +290 -0
- package/src/tools/checkpoint-tools.js +76 -0
- package/src/tools/index.js +54 -0
- package/src/tools/list-decisions.js +76 -0
- package/src/tools/recall-decision.js +75 -0
- package/src/tools/save-decision.js +113 -0
- package/src/tools/suggest-decision.js +84 -0
- package/src/tools/update-outcome.js +128 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Simple Public API
|
|
3
|
+
*
|
|
4
|
+
* Clean wrapper around MAMA's internal functions
|
|
5
|
+
* Follows Claude-First Design: Simple, Transparent, Non-Intrusive
|
|
6
|
+
*
|
|
7
|
+
* Core Principle: MAMA = Librarian, Claude = Researcher
|
|
8
|
+
* - MAMA stores (organize books), retrieves (find books), indexes (catalog)
|
|
9
|
+
* - Claude decides what to save and how to use recalled decisions
|
|
10
|
+
*
|
|
11
|
+
* @module mama-api
|
|
12
|
+
* @version 1.0
|
|
13
|
+
* @date 2025-11-14
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { learnDecision } = require('./decision-tracker');
|
|
17
|
+
const { injectDecisionContext } = require('./memory-inject');
|
|
18
|
+
const { queryDecisionGraph, querySemanticEdges, getDB, getAdapter } = require('./memory-store');
|
|
19
|
+
const { formatRecall, formatList } = require('./decision-formatter');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Save a decision or insight to MAMA's memory
|
|
23
|
+
*
|
|
24
|
+
* Simple API for Claude to save insights without complex configuration
|
|
25
|
+
* AC #1: Simple API - no complex configuration required
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} params - Decision parameters
|
|
28
|
+
* @param {string} params.topic - Decision topic (e.g., 'auth_strategy', 'date_format')
|
|
29
|
+
* @param {string} params.decision - The decision made (e.g., 'JWT', 'ISO 8601 + Unix')
|
|
30
|
+
* @param {string} params.reasoning - Why this decision was made
|
|
31
|
+
* @param {number} [params.confidence=0.5] - Confidence score 0.0-1.0 (optional)
|
|
32
|
+
* @param {string} [params.type='user_decision'] - 'user_decision' or 'assistant_insight' (optional)
|
|
33
|
+
* @param {string} [params.outcome='pending'] - 'pending', 'success', 'failure', 'partial', 'superseded' (optional)
|
|
34
|
+
* @param {string} [params.failure_reason] - Why this decision failed (optional, used with outcome='failure')
|
|
35
|
+
* @param {string} [params.limitation] - Known limitations of this decision (optional)
|
|
36
|
+
* @returns {Promise<string>} Decision ID
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const decisionId = await mama.save({
|
|
40
|
+
* topic: 'date_calculation_format',
|
|
41
|
+
* decision: 'Support both ISO 8601 and Unix timestamp formats',
|
|
42
|
+
* reasoning: 'Bootstrap data stored as ISO 8601 causing NaN errors',
|
|
43
|
+
* confidence: 0.95,
|
|
44
|
+
* type: 'assistant_insight',
|
|
45
|
+
* outcome: 'success'
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
async function save({
|
|
49
|
+
topic,
|
|
50
|
+
decision,
|
|
51
|
+
reasoning,
|
|
52
|
+
confidence = 0.5,
|
|
53
|
+
type = 'user_decision',
|
|
54
|
+
outcome = 'pending',
|
|
55
|
+
failure_reason = null,
|
|
56
|
+
limitation = null,
|
|
57
|
+
trust_context = null,
|
|
58
|
+
}) {
|
|
59
|
+
// Validate required fields
|
|
60
|
+
if (!topic || typeof topic !== 'string') {
|
|
61
|
+
throw new Error('mama.save() requires topic (string)');
|
|
62
|
+
}
|
|
63
|
+
if (!decision || typeof decision !== 'string') {
|
|
64
|
+
throw new Error('mama.save() requires decision (string)');
|
|
65
|
+
}
|
|
66
|
+
if (!reasoning || typeof reasoning !== 'string') {
|
|
67
|
+
throw new Error('mama.save() requires reasoning (string)');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate confidence range
|
|
71
|
+
if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
|
|
72
|
+
throw new Error('mama.save() confidence must be a number between 0.0 and 1.0');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate type
|
|
76
|
+
if (type !== 'user_decision' && type !== 'assistant_insight') {
|
|
77
|
+
throw new Error('mama.save() type must be "user_decision" or "assistant_insight"');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate outcome
|
|
81
|
+
const validOutcomes = ['pending', 'success', 'failure', 'partial', 'superseded'];
|
|
82
|
+
if (outcome && !validOutcomes.includes(outcome)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`mama.save() outcome must be one of: ${validOutcomes.join(', ')} (got: ${outcome})`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Map type to user_involvement field
|
|
89
|
+
// Note: Current schema uses user_involvement ('requested', 'approved', 'rejected')
|
|
90
|
+
// Future: Will use decision_type column for proper distinction
|
|
91
|
+
const userInvolvement = type === 'user_decision' ? 'approved' : null;
|
|
92
|
+
|
|
93
|
+
// Create detection object for learnDecision()
|
|
94
|
+
const detection = {
|
|
95
|
+
topic,
|
|
96
|
+
decision,
|
|
97
|
+
reasoning,
|
|
98
|
+
confidence,
|
|
99
|
+
outcome,
|
|
100
|
+
failure_reason,
|
|
101
|
+
limitation,
|
|
102
|
+
trust_context,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Create tool execution context
|
|
106
|
+
// Use current timestamp and generate session ID
|
|
107
|
+
const sessionId = `mama_api_${Date.now()}`;
|
|
108
|
+
const toolExecution = {
|
|
109
|
+
tool_name: 'mama.save',
|
|
110
|
+
tool_input: { topic, decision },
|
|
111
|
+
exit_code: 0,
|
|
112
|
+
session_id: sessionId,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Create session context
|
|
117
|
+
const sessionContext = {
|
|
118
|
+
session_id: sessionId,
|
|
119
|
+
latest_user_message: `Save ${type}: ${topic}`,
|
|
120
|
+
recent_exchange: `Claude: ${reasoning.substring(0, 100)}...`,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Call internal learnDecision function
|
|
124
|
+
// Note: learnDecision returns { decisionId, notification }
|
|
125
|
+
const { decisionId } = await learnDecision(detection, toolExecution, sessionContext);
|
|
126
|
+
|
|
127
|
+
// Update user_involvement, outcome, failure_reason, limitation
|
|
128
|
+
// Note: learnDecision always sets 'requested', we need to override it
|
|
129
|
+
const adapter = getAdapter();
|
|
130
|
+
|
|
131
|
+
// Build UPDATE query dynamically based on what fields are provided
|
|
132
|
+
const updates = [];
|
|
133
|
+
const values = [];
|
|
134
|
+
|
|
135
|
+
// user_involvement based on type
|
|
136
|
+
if (type === 'assistant_insight') {
|
|
137
|
+
updates.push('user_involvement = NULL');
|
|
138
|
+
} else if (type === 'user_decision') {
|
|
139
|
+
updates.push('user_involvement = ?');
|
|
140
|
+
values.push('approved');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// outcome (always set, default is 'pending')
|
|
144
|
+
// Story M4.1 fix: Map to DB format (uppercase, pending → NULL)
|
|
145
|
+
if (outcome) {
|
|
146
|
+
const outcomeMap = {
|
|
147
|
+
pending: null,
|
|
148
|
+
success: 'SUCCESS',
|
|
149
|
+
failure: 'FAILED',
|
|
150
|
+
partial: 'PARTIAL',
|
|
151
|
+
superseded: null,
|
|
152
|
+
};
|
|
153
|
+
const dbOutcome = outcomeMap[outcome] !== undefined ? outcomeMap[outcome] : outcome;
|
|
154
|
+
|
|
155
|
+
updates.push('outcome = ?');
|
|
156
|
+
values.push(dbOutcome);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// failure_reason (optional)
|
|
160
|
+
if (failure_reason) {
|
|
161
|
+
updates.push('failure_reason = ?');
|
|
162
|
+
values.push(failure_reason);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// limitation (optional)
|
|
166
|
+
if (limitation) {
|
|
167
|
+
updates.push('limitation = ?');
|
|
168
|
+
values.push(limitation);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Execute UPDATE if we have any fields to update
|
|
172
|
+
if (updates.length > 0) {
|
|
173
|
+
values.push(decisionId); // WHERE id = ?
|
|
174
|
+
const stmt = adapter.prepare(`
|
|
175
|
+
UPDATE decisions
|
|
176
|
+
SET ${updates.join(', ')}
|
|
177
|
+
WHERE id = ?
|
|
178
|
+
`);
|
|
179
|
+
await stmt.run(...values);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return decisionId;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Recall decisions by topic
|
|
187
|
+
*
|
|
188
|
+
* DEFAULT: Returns JSON object with decisions and edges (LLM-first design)
|
|
189
|
+
* OPTIONAL: Returns Markdown string if format='markdown' (for human display)
|
|
190
|
+
*
|
|
191
|
+
* @param {string} topic - Decision topic to recall
|
|
192
|
+
* @param {Object} [options] - Options
|
|
193
|
+
* @param {string} [options.format='json'] - Output format: 'json' (default) or 'markdown'
|
|
194
|
+
* @returns {Promise<Object|string>} Decision history as JSON or Markdown
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* // LLM usage (default)
|
|
198
|
+
* const data = await mama.recall('auth_strategy');
|
|
199
|
+
* // → { topic, decisions: [...], edges: [...], meta: {...} }
|
|
200
|
+
*
|
|
201
|
+
* // Human display
|
|
202
|
+
* const markdown = await mama.recall('auth_strategy', { format: 'markdown' });
|
|
203
|
+
* // → "📋 Decision History: auth_strategy\n━━━━━━━━..."
|
|
204
|
+
*/
|
|
205
|
+
async function recall(topic, options = {}) {
|
|
206
|
+
if (!topic || typeof topic !== 'string') {
|
|
207
|
+
throw new Error('mama.recall() requires topic (string)');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { format = 'json' } = options;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const decisions = await queryDecisionGraph(topic);
|
|
214
|
+
|
|
215
|
+
if (!decisions || decisions.length === 0) {
|
|
216
|
+
if (format === 'markdown') {
|
|
217
|
+
return `❌ No decisions found for topic: ${topic}`;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
topic,
|
|
221
|
+
supersedes_chain: [],
|
|
222
|
+
semantic_edges: { refines: [], refined_by: [], contradicts: [], contradicted_by: [] },
|
|
223
|
+
meta: { count: 0 },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Query semantic edges for all decisions
|
|
228
|
+
const decisionIds = decisions.map((d) => d.id);
|
|
229
|
+
const semanticEdges = await querySemanticEdges(decisionIds);
|
|
230
|
+
|
|
231
|
+
// Markdown format (for human display)
|
|
232
|
+
if (format === 'markdown') {
|
|
233
|
+
// Pass semantic edges to formatter
|
|
234
|
+
return formatRecall(decisions, semanticEdges);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// JSON format (default - LLM-first)
|
|
238
|
+
// Separate supersedes chain from semantic edges
|
|
239
|
+
return {
|
|
240
|
+
topic,
|
|
241
|
+
supersedes_chain: decisions.map((d) => ({
|
|
242
|
+
id: d.id,
|
|
243
|
+
decision: d.decision,
|
|
244
|
+
reasoning: d.reasoning,
|
|
245
|
+
confidence: d.confidence,
|
|
246
|
+
outcome: d.outcome,
|
|
247
|
+
failure_reason: d.failure_reason,
|
|
248
|
+
limitation: d.limitation,
|
|
249
|
+
created_at: d.created_at,
|
|
250
|
+
updated_at: d.updated_at,
|
|
251
|
+
superseded_by: d.superseded_by,
|
|
252
|
+
supersedes: d.supersedes,
|
|
253
|
+
trust_context: d.trust_context,
|
|
254
|
+
})),
|
|
255
|
+
semantic_edges: {
|
|
256
|
+
refines: semanticEdges.refines.map((e) => ({
|
|
257
|
+
to_topic: e.topic,
|
|
258
|
+
to_decision: e.decision,
|
|
259
|
+
to_id: e.to_id,
|
|
260
|
+
reason: e.reason,
|
|
261
|
+
confidence: e.confidence,
|
|
262
|
+
created_at: e.created_at,
|
|
263
|
+
})),
|
|
264
|
+
refined_by: semanticEdges.refined_by.map((e) => ({
|
|
265
|
+
from_topic: e.topic,
|
|
266
|
+
from_decision: e.decision,
|
|
267
|
+
from_id: e.from_id,
|
|
268
|
+
reason: e.reason,
|
|
269
|
+
confidence: e.confidence,
|
|
270
|
+
created_at: e.created_at,
|
|
271
|
+
})),
|
|
272
|
+
contradicts: semanticEdges.contradicts.map((e) => ({
|
|
273
|
+
to_topic: e.topic,
|
|
274
|
+
to_decision: e.decision,
|
|
275
|
+
to_id: e.to_id,
|
|
276
|
+
reason: e.reason,
|
|
277
|
+
created_at: e.created_at,
|
|
278
|
+
})),
|
|
279
|
+
contradicted_by: semanticEdges.contradicted_by.map((e) => ({
|
|
280
|
+
from_topic: e.topic,
|
|
281
|
+
from_decision: e.decision,
|
|
282
|
+
from_id: e.from_id,
|
|
283
|
+
reason: e.reason,
|
|
284
|
+
created_at: e.created_at,
|
|
285
|
+
})),
|
|
286
|
+
},
|
|
287
|
+
meta: {
|
|
288
|
+
count: decisions.length,
|
|
289
|
+
latest_id: decisions[0]?.id,
|
|
290
|
+
has_supersedes_chain: decisions.some((d) => d.supersedes),
|
|
291
|
+
has_semantic_edges:
|
|
292
|
+
semanticEdges.refines.length > 0 ||
|
|
293
|
+
semanticEdges.refined_by.length > 0 ||
|
|
294
|
+
semanticEdges.contradicts.length > 0 ||
|
|
295
|
+
semanticEdges.contradicted_by.length > 0,
|
|
296
|
+
semantic_edges_count: {
|
|
297
|
+
refines: semanticEdges.refines.length,
|
|
298
|
+
refined_by: semanticEdges.refined_by.length,
|
|
299
|
+
contradicts: semanticEdges.contradicts.length,
|
|
300
|
+
contradicted_by: semanticEdges.contradicted_by.length,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
throw new Error(`mama.recall() failed: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Update outcome of a decision
|
|
311
|
+
*
|
|
312
|
+
* Track whether a decision succeeded, failed, or partially worked
|
|
313
|
+
* AC: Evolutionary Decision Memory - Learn from outcomes
|
|
314
|
+
*
|
|
315
|
+
* @param {string} decisionId - Decision ID to update
|
|
316
|
+
* @param {Object} outcome - Outcome details
|
|
317
|
+
* @param {string} outcome.outcome - 'SUCCESS', 'FAILED', or 'PARTIAL'
|
|
318
|
+
* @param {string} [outcome.failure_reason] - Reason for failure (if FAILED)
|
|
319
|
+
* @param {string} [outcome.limitation] - Limitation description (if PARTIAL)
|
|
320
|
+
* @returns {Promise<void>}
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* await mama.updateOutcome('decision_auth_strategy_123456_abc', {
|
|
324
|
+
* outcome: 'FAILED',
|
|
325
|
+
* failure_reason: 'Missing token expiration handling'
|
|
326
|
+
* });
|
|
327
|
+
*/
|
|
328
|
+
async function updateOutcome(decisionId, { outcome, failure_reason, limitation }) {
|
|
329
|
+
if (!decisionId || typeof decisionId !== 'string') {
|
|
330
|
+
throw new Error('mama.updateOutcome() requires decisionId (string)');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!outcome || !['SUCCESS', 'FAILED', 'PARTIAL'].includes(outcome)) {
|
|
334
|
+
throw new Error('mama.updateOutcome() outcome must be "SUCCESS", "FAILED", or "PARTIAL"');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const adapter = getAdapter();
|
|
339
|
+
|
|
340
|
+
// Update outcome and related fields
|
|
341
|
+
const stmt = adapter.prepare(
|
|
342
|
+
`
|
|
343
|
+
UPDATE decisions
|
|
344
|
+
SET
|
|
345
|
+
outcome = ?,
|
|
346
|
+
failure_reason = ?,
|
|
347
|
+
limitation = ?,
|
|
348
|
+
updated_at = ?
|
|
349
|
+
WHERE id = ?
|
|
350
|
+
`
|
|
351
|
+
);
|
|
352
|
+
await stmt.run(outcome, failure_reason || null, limitation || null, Date.now(), decisionId);
|
|
353
|
+
|
|
354
|
+
return;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
throw new Error(`mama.updateOutcome() failed: ${error.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Expand search results with graph context (Phase 1 - Graph-Enhanced Retrieval)
|
|
362
|
+
*
|
|
363
|
+
* For each candidate decision:
|
|
364
|
+
* 1. Add supersedes chain (evolution history)
|
|
365
|
+
* 2. Add semantic edges (refines, contradicts)
|
|
366
|
+
* 3. Deduplicate by ID
|
|
367
|
+
* 4. Re-rank by relevance (primary candidates ranked higher)
|
|
368
|
+
*
|
|
369
|
+
* @param {Array} candidates - Initial search results from vector/keyword search
|
|
370
|
+
* @returns {Promise<Array>} Graph-enhanced results with evolution context
|
|
371
|
+
*/
|
|
372
|
+
async function expandWithGraph(candidates) {
|
|
373
|
+
const graphEnhanced = new Map(); // Use Map for deduplication by ID
|
|
374
|
+
const primaryIds = new Set(candidates.map((c) => c.id)); // Track primary candidates
|
|
375
|
+
|
|
376
|
+
// Process each candidate
|
|
377
|
+
for (const candidate of candidates) {
|
|
378
|
+
// Add primary candidate with higher rank
|
|
379
|
+
if (!graphEnhanced.has(candidate.id)) {
|
|
380
|
+
graphEnhanced.set(candidate.id, {
|
|
381
|
+
...candidate,
|
|
382
|
+
graph_source: 'primary', // Mark as primary result
|
|
383
|
+
graph_rank: 1.0, // Highest rank
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 1. Add supersedes chain (evolution history)
|
|
388
|
+
try {
|
|
389
|
+
const chain = await queryDecisionGraph(candidate.topic);
|
|
390
|
+
for (const decision of chain) {
|
|
391
|
+
if (!graphEnhanced.has(decision.id)) {
|
|
392
|
+
graphEnhanced.set(decision.id, {
|
|
393
|
+
...decision,
|
|
394
|
+
graph_source: 'supersedes_chain',
|
|
395
|
+
graph_rank: 0.8, // Lower rank than primary
|
|
396
|
+
similarity: candidate.similarity * 0.9, // Inherit similarity, slightly reduced
|
|
397
|
+
related_to: candidate.id, // Track relationship
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.warn(`Failed to get supersedes chain for ${candidate.topic}: ${error.message}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 2. Add semantic edges (refines, contradicts)
|
|
406
|
+
try {
|
|
407
|
+
const edges = await querySemanticEdges([candidate.id]);
|
|
408
|
+
|
|
409
|
+
// Add refines edges
|
|
410
|
+
for (const edge of edges.refines) {
|
|
411
|
+
if (!graphEnhanced.has(edge.to_id)) {
|
|
412
|
+
graphEnhanced.set(edge.to_id, {
|
|
413
|
+
id: edge.to_id,
|
|
414
|
+
topic: edge.topic,
|
|
415
|
+
decision: edge.decision,
|
|
416
|
+
confidence: edge.confidence,
|
|
417
|
+
created_at: edge.created_at,
|
|
418
|
+
graph_source: 'refines',
|
|
419
|
+
graph_rank: 0.7,
|
|
420
|
+
similarity: candidate.similarity * 0.85,
|
|
421
|
+
related_to: candidate.id,
|
|
422
|
+
edge_reason: edge.reason,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Add refined_by edges
|
|
428
|
+
for (const edge of edges.refined_by) {
|
|
429
|
+
if (!graphEnhanced.has(edge.from_id)) {
|
|
430
|
+
graphEnhanced.set(edge.from_id, {
|
|
431
|
+
id: edge.from_id,
|
|
432
|
+
topic: edge.topic,
|
|
433
|
+
decision: edge.decision,
|
|
434
|
+
confidence: edge.confidence,
|
|
435
|
+
created_at: edge.created_at,
|
|
436
|
+
graph_source: 'refined_by',
|
|
437
|
+
graph_rank: 0.7,
|
|
438
|
+
similarity: candidate.similarity * 0.85,
|
|
439
|
+
related_to: candidate.id,
|
|
440
|
+
edge_reason: edge.reason,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Add contradicts edges (lower rank, but still relevant)
|
|
446
|
+
for (const edge of edges.contradicts) {
|
|
447
|
+
if (!graphEnhanced.has(edge.to_id)) {
|
|
448
|
+
graphEnhanced.set(edge.to_id, {
|
|
449
|
+
id: edge.to_id,
|
|
450
|
+
topic: edge.topic,
|
|
451
|
+
decision: edge.decision,
|
|
452
|
+
confidence: edge.confidence,
|
|
453
|
+
created_at: edge.created_at,
|
|
454
|
+
graph_source: 'contradicts',
|
|
455
|
+
graph_rank: 0.6,
|
|
456
|
+
similarity: candidate.similarity * 0.8,
|
|
457
|
+
related_to: candidate.id,
|
|
458
|
+
edge_reason: edge.reason,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.warn(`Failed to get semantic edges for ${candidate.id}: ${error.message}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 3. Convert Map to Array and sort by graph_rank + similarity
|
|
468
|
+
const results = Array.from(graphEnhanced.values());
|
|
469
|
+
|
|
470
|
+
// 4. Sort: Primary first, then by graph_rank, then by final_score (or similarity)
|
|
471
|
+
results.sort((a, b) => {
|
|
472
|
+
// Primary candidates always first
|
|
473
|
+
if (primaryIds.has(a.id) && !primaryIds.has(b.id)) return -1;
|
|
474
|
+
if (!primaryIds.has(a.id) && primaryIds.has(b.id)) return 1;
|
|
475
|
+
|
|
476
|
+
// Then by graph_rank
|
|
477
|
+
if (a.graph_rank !== b.graph_rank) {
|
|
478
|
+
return b.graph_rank - a.graph_rank;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Finally by final_score (recency-boosted) or similarity (fallback)
|
|
482
|
+
const scoreA = a.final_score || a.similarity || 0;
|
|
483
|
+
const scoreB = b.final_score || b.similarity || 0;
|
|
484
|
+
return scoreB - scoreA;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return results;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Apply Gaussian Decay recency boosting (Elasticsearch-style)
|
|
492
|
+
* Allows Claude to dynamically adjust search strategy based on results
|
|
493
|
+
*
|
|
494
|
+
* @param {Array} results - Search results with similarity scores
|
|
495
|
+
* @param {Object} options - Recency boosting options
|
|
496
|
+
* @returns {Array} Results with recency-boosted final scores
|
|
497
|
+
*/
|
|
498
|
+
function applyRecencyBoost(results, options = {}) {
|
|
499
|
+
const {
|
|
500
|
+
recencyWeight = 0.3,
|
|
501
|
+
recencyScale = 7,
|
|
502
|
+
recencyDecay = 0.5,
|
|
503
|
+
disableRecency = false,
|
|
504
|
+
} = options;
|
|
505
|
+
|
|
506
|
+
if (disableRecency || recencyWeight === 0) {
|
|
507
|
+
return results;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const now = Date.now(); // Current timestamp in milliseconds
|
|
511
|
+
|
|
512
|
+
return results
|
|
513
|
+
.map((r) => {
|
|
514
|
+
// created_at is stored in milliseconds in the database
|
|
515
|
+
const ageInDays = (now - r.created_at) / (86400 * 1000);
|
|
516
|
+
|
|
517
|
+
// Gaussian Decay: exp(-((age / scale)^2) / (2 * ln(1 / decay)))
|
|
518
|
+
// At scale days: score = decay (e.g., 7 days = 50%)
|
|
519
|
+
const gaussianDecay = Math.exp(
|
|
520
|
+
-Math.pow(ageInDays / recencyScale, 2) / (2 * Math.log(1 / recencyDecay))
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// Combine semantic similarity with recency
|
|
524
|
+
const finalScore = r.similarity * (1 - recencyWeight) + gaussianDecay * recencyWeight;
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
...r,
|
|
528
|
+
recency_score: gaussianDecay,
|
|
529
|
+
recency_age_days: Math.round(ageInDays * 10) / 10,
|
|
530
|
+
final_score: finalScore,
|
|
531
|
+
};
|
|
532
|
+
})
|
|
533
|
+
.sort((a, b) => b.final_score - a.final_score);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Suggest relevant decisions based on user question
|
|
538
|
+
*
|
|
539
|
+
* DEFAULT: Returns JSON object with search results (LLM-first design)
|
|
540
|
+
* OPTIONAL: Returns Markdown string if format='markdown' (for human display)
|
|
541
|
+
*
|
|
542
|
+
* Simplified: Direct vector search without LLM intent analysis
|
|
543
|
+
* Works with short queries, long questions, Korean/English
|
|
544
|
+
*
|
|
545
|
+
* @param {string} userQuestion - User's question or intent
|
|
546
|
+
* @param {Object} options - Search options
|
|
547
|
+
* @param {string} [options.format='json'] - Output format: 'json' (default) or 'markdown'
|
|
548
|
+
* @param {number} [options.limit=5] - Max results to return
|
|
549
|
+
* @param {number} [options.threshold=0.6] - Minimum similarity (adaptive by query length)
|
|
550
|
+
* @param {boolean} [options.useReranking=false] - Use LLM re-ranking (optional, slower)
|
|
551
|
+
* @returns {Promise<Object|string|null>} Search results as JSON or Markdown, null if no results
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* // LLM usage (default)
|
|
555
|
+
* const data = await mama.suggest('Why did we choose JWT?');
|
|
556
|
+
* // → { query, results: [...], meta: {...} }
|
|
557
|
+
*
|
|
558
|
+
* // Human display
|
|
559
|
+
* const markdown = await mama.suggest('mesh optimization', { format: 'markdown' });
|
|
560
|
+
* // → "💡 MAMA found 3 related topics:\n1. ..."
|
|
561
|
+
*/
|
|
562
|
+
async function suggest(userQuestion, options = {}) {
|
|
563
|
+
if (!userQuestion || typeof userQuestion !== 'string') {
|
|
564
|
+
throw new Error('mama.suggest() requires userQuestion (string)');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const {
|
|
568
|
+
format = 'json',
|
|
569
|
+
limit = 5,
|
|
570
|
+
threshold,
|
|
571
|
+
useReranking = false,
|
|
572
|
+
// Recency boosting parameters (Gaussian Decay - Elasticsearch style)
|
|
573
|
+
recencyWeight = 0.3, // 0-1: How much to weight recency (0.3 = 70% semantic, 30% recency)
|
|
574
|
+
recencyScale = 7, // Days until recency score drops to 50%
|
|
575
|
+
recencyDecay = 0.5, // Score at scale point (0.5 = 50%)
|
|
576
|
+
disableRecency = false, // Set true to disable recency boosting entirely
|
|
577
|
+
} = options;
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
// 1. Try vector search first (if sqlite-vss is available)
|
|
581
|
+
const { getPreparedStmt, getDB } = require('./memory-store');
|
|
582
|
+
let results = [];
|
|
583
|
+
let searchMethod = 'vector';
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
// Check if vectorSearch prepared statement exists
|
|
587
|
+
getPreparedStmt('vectorSearch');
|
|
588
|
+
|
|
589
|
+
// Generate query embedding
|
|
590
|
+
const { generateEmbedding } = require('./embeddings');
|
|
591
|
+
const queryEmbedding = await generateEmbedding(userQuestion);
|
|
592
|
+
|
|
593
|
+
// Adaptive threshold (shorter queries need higher confidence)
|
|
594
|
+
const wordCount = userQuestion.split(/\s+/).length;
|
|
595
|
+
const adaptiveThreshold = threshold !== undefined ? threshold : wordCount < 3 ? 0.7 : 0.6;
|
|
596
|
+
|
|
597
|
+
// Vector search
|
|
598
|
+
const { vectorSearch } = require('./memory-store');
|
|
599
|
+
results = await vectorSearch(queryEmbedding, limit * 2, 0.5); // Get more candidates
|
|
600
|
+
|
|
601
|
+
// Filter by adaptive threshold
|
|
602
|
+
results = results.filter((r) => r.similarity >= adaptiveThreshold);
|
|
603
|
+
|
|
604
|
+
// Stage 1.5: Apply recency boosting (Gaussian Decay)
|
|
605
|
+
// Allows Claude to adjust search strategy (recent vs historical)
|
|
606
|
+
if (results.length > 0 && !disableRecency) {
|
|
607
|
+
results = applyRecencyBoost(results, {
|
|
608
|
+
recencyWeight,
|
|
609
|
+
recencyScale,
|
|
610
|
+
recencyDecay,
|
|
611
|
+
disableRecency,
|
|
612
|
+
});
|
|
613
|
+
searchMethod = 'vector+recency';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Stage 2: Graph expansion (NEW - Phase 1)
|
|
617
|
+
// Expand candidates with supersedes chain and semantic edges
|
|
618
|
+
if (results.length > 0) {
|
|
619
|
+
const graphEnhanced = await expandWithGraph(results);
|
|
620
|
+
results = graphEnhanced;
|
|
621
|
+
searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
|
|
622
|
+
}
|
|
623
|
+
} catch (vectorError) {
|
|
624
|
+
// Fallback to keyword search if vector search unavailable
|
|
625
|
+
console.warn(`Vector search failed: ${vectorError.message}, falling back to keyword search`);
|
|
626
|
+
searchMethod = 'keyword';
|
|
627
|
+
|
|
628
|
+
// Keyword search fallback
|
|
629
|
+
const adapter = getAdapter();
|
|
630
|
+
const keywords = userQuestion
|
|
631
|
+
.toLowerCase()
|
|
632
|
+
.split(/\s+/)
|
|
633
|
+
.filter((w) => w.length > 2); // Filter short words
|
|
634
|
+
|
|
635
|
+
if (keywords.length === 0) {
|
|
636
|
+
return `💡 Hint: Please be more specific.\nExample: "Railway Volume settings" or "mesh parameter optimization"`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Build LIKE query for each keyword
|
|
640
|
+
const likeConditions = keywords.map(() => '(topic LIKE ? OR decision LIKE ?)').join(' OR ');
|
|
641
|
+
const likeParams = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
|
|
642
|
+
|
|
643
|
+
const stmt = adapter.prepare(`
|
|
644
|
+
SELECT * FROM decisions
|
|
645
|
+
WHERE ${likeConditions}
|
|
646
|
+
AND superseded_by IS NULL
|
|
647
|
+
ORDER BY created_at DESC
|
|
648
|
+
LIMIT ?
|
|
649
|
+
`);
|
|
650
|
+
|
|
651
|
+
const rows = await stmt.all(...likeParams, limit);
|
|
652
|
+
results = rows.map((row) => ({
|
|
653
|
+
...row,
|
|
654
|
+
similarity: 0.75, // Assign moderate similarity for keyword matches
|
|
655
|
+
}));
|
|
656
|
+
|
|
657
|
+
// Stage 2: Graph expansion for keyword results (Phase 1)
|
|
658
|
+
if (results.length > 0) {
|
|
659
|
+
const graphEnhanced = await expandWithGraph(results);
|
|
660
|
+
results = graphEnhanced;
|
|
661
|
+
searchMethod = 'keyword+graph';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (results.length === 0) {
|
|
666
|
+
if (format === 'markdown') {
|
|
667
|
+
const wordCount = userQuestion.split(/\s+/).length;
|
|
668
|
+
if (wordCount < 3) {
|
|
669
|
+
return `💡 Hint: Please be more specific.\nExample: "Why did we choose COMPLEX mesh structure?" or "What parameters are used for large layers?"`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 5. Optional: LLM re-ranking (only if requested)
|
|
676
|
+
if (useReranking) {
|
|
677
|
+
results = await rerankWithLLM(userQuestion, results);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Slice to limit
|
|
681
|
+
const finalResults = results.slice(0, limit);
|
|
682
|
+
|
|
683
|
+
// Markdown format (for human display)
|
|
684
|
+
if (format === 'markdown') {
|
|
685
|
+
const { formatContext } = require('./decision-formatter');
|
|
686
|
+
const context = formatContext(finalResults, { maxTokens: 500 });
|
|
687
|
+
|
|
688
|
+
// Add graph expansion summary if applicable
|
|
689
|
+
let graphSummary = '';
|
|
690
|
+
if (searchMethod.includes('graph')) {
|
|
691
|
+
const primaryCount = finalResults.filter((r) => r.graph_source === 'primary').length;
|
|
692
|
+
const expandedCount = finalResults.filter((r) => r.graph_source !== 'primary').length;
|
|
693
|
+
|
|
694
|
+
graphSummary = `\n📊 Graph expansion: ${primaryCount} primary + ${expandedCount} related (supersedes/refines/contradicts)\n`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return `🔍 Search method: ${searchMethod}${graphSummary}\n${context}`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Calculate graph expansion stats
|
|
701
|
+
const graphStats = {
|
|
702
|
+
total_results: finalResults.length,
|
|
703
|
+
primary_count: finalResults.filter((r) => r.graph_source === 'primary').length,
|
|
704
|
+
expanded_count: finalResults.filter((r) => r.graph_source !== 'primary').length,
|
|
705
|
+
sources: {
|
|
706
|
+
primary: finalResults.filter((r) => r.graph_source === 'primary').length,
|
|
707
|
+
supersedes_chain: finalResults.filter((r) => r.graph_source === 'supersedes_chain').length,
|
|
708
|
+
refines: finalResults.filter((r) => r.graph_source === 'refines').length,
|
|
709
|
+
refined_by: finalResults.filter((r) => r.graph_source === 'refined_by').length,
|
|
710
|
+
contradicts: finalResults.filter((r) => r.graph_source === 'contradicts').length,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// JSON format (default - LLM-first)
|
|
715
|
+
return {
|
|
716
|
+
query: userQuestion,
|
|
717
|
+
results: finalResults.map((r) => ({
|
|
718
|
+
id: r.id,
|
|
719
|
+
topic: r.topic,
|
|
720
|
+
decision: r.decision,
|
|
721
|
+
reasoning: r.reasoning,
|
|
722
|
+
confidence: r.confidence,
|
|
723
|
+
similarity: r.similarity,
|
|
724
|
+
created_at: r.created_at,
|
|
725
|
+
// Recency metadata (NEW - Gaussian Decay)
|
|
726
|
+
recency_score: r.recency_score,
|
|
727
|
+
recency_age_days: r.recency_age_days,
|
|
728
|
+
final_score: r.final_score || r.similarity, // Falls back to similarity if no recency
|
|
729
|
+
// Graph metadata (NEW - Phase 1)
|
|
730
|
+
graph_source: r.graph_source || 'primary',
|
|
731
|
+
graph_rank: r.graph_rank || 1.0,
|
|
732
|
+
related_to: r.related_to || null,
|
|
733
|
+
edge_reason: r.edge_reason || null,
|
|
734
|
+
})),
|
|
735
|
+
meta: {
|
|
736
|
+
count: finalResults.length,
|
|
737
|
+
search_method: searchMethod,
|
|
738
|
+
threshold: threshold || 'adaptive',
|
|
739
|
+
// Recency boosting config (NEW - Gaussian Decay)
|
|
740
|
+
recency_boost: disableRecency
|
|
741
|
+
? null
|
|
742
|
+
: {
|
|
743
|
+
weight: recencyWeight,
|
|
744
|
+
scale: recencyScale,
|
|
745
|
+
decay: recencyDecay,
|
|
746
|
+
},
|
|
747
|
+
// Graph expansion stats (NEW - Phase 1)
|
|
748
|
+
graph_expansion: searchMethod.includes('graph') ? graphStats : null,
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
} catch (error) {
|
|
752
|
+
// Graceful degradation
|
|
753
|
+
console.warn(`mama.suggest() failed: ${error.message}`);
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Re-rank search results using local LLM (optional enhancement)
|
|
760
|
+
*
|
|
761
|
+
* @param {string} userQuestion - User's question
|
|
762
|
+
* @param {Array} results - Vector search results
|
|
763
|
+
* @returns {Promise<Array>} Re-ranked results
|
|
764
|
+
*/
|
|
765
|
+
async function rerankWithLLM(userQuestion, results) {
|
|
766
|
+
try {
|
|
767
|
+
const { generate } = require('./ollama-client');
|
|
768
|
+
|
|
769
|
+
const prompt = `User asked: "${userQuestion}"
|
|
770
|
+
|
|
771
|
+
Found decisions (ranked by vector similarity):
|
|
772
|
+
${results.map((r, i) => `${i + 1}. [${r.similarity.toFixed(3)}] ${r.topic}: ${r.decision.substring(0, 60)}...`).join('\n')}
|
|
773
|
+
|
|
774
|
+
Re-rank these by actual relevance to the user's intent (not just keyword similarity).
|
|
775
|
+
Return JSON: { "ranking": [index1, index2, ...] } (0-based indices)
|
|
776
|
+
|
|
777
|
+
Example: { "ranking": [2, 0, 4, 1, 3] } means 3rd is most relevant, then 1st, then 5th...`;
|
|
778
|
+
|
|
779
|
+
const response = await generate(prompt, {
|
|
780
|
+
format: 'json',
|
|
781
|
+
temperature: 0.3,
|
|
782
|
+
max_tokens: 100,
|
|
783
|
+
timeout: 3000,
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const parsed = typeof response === 'string' ? JSON.parse(response) : response;
|
|
787
|
+
|
|
788
|
+
// Reorder results based on LLM ranking
|
|
789
|
+
return parsed.ranking.map((idx) => results[idx]).filter(Boolean);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
console.warn(`Re-ranking failed: ${error.message}, using vector ranking`);
|
|
792
|
+
return results; // Fallback to vector ranking
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* List recent decisions (all topics, chronological)
|
|
798
|
+
*
|
|
799
|
+
* DEFAULT: Returns JSON array with recent decisions (LLM-first design)
|
|
800
|
+
* OPTIONAL: Returns Markdown string if format='markdown' (for human display)
|
|
801
|
+
*
|
|
802
|
+
* @param {Object} [options] - Options
|
|
803
|
+
* @param {number} [options.limit=10] - Max results
|
|
804
|
+
* @param {string} [options.format='json'] - Output format
|
|
805
|
+
* @returns {Promise<Array|string>} Recent decisions
|
|
806
|
+
*/
|
|
807
|
+
async function listDecisions(options = {}) {
|
|
808
|
+
const { limit = 10, format = 'json' } = options;
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
const adapter = getAdapter();
|
|
812
|
+
const stmt = adapter.prepare(`
|
|
813
|
+
SELECT * FROM decisions
|
|
814
|
+
WHERE superseded_by IS NULL
|
|
815
|
+
ORDER BY created_at DESC
|
|
816
|
+
LIMIT ?
|
|
817
|
+
`);
|
|
818
|
+
const decisions = await stmt.all(limit);
|
|
819
|
+
|
|
820
|
+
if (format === 'markdown') {
|
|
821
|
+
return formatList(decisions);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return decisions;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
throw new Error(`mama.listDecisions() failed: ${error.message}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Save current session checkpoint (New Feature: Session Continuity)
|
|
832
|
+
*
|
|
833
|
+
* @param {string} summary - Summary of current session state
|
|
834
|
+
* @param {Array<string>} openFiles - List of currently open files
|
|
835
|
+
* @param {string} nextSteps - Next steps to be taken
|
|
836
|
+
* @returns {Promise<number>} Checkpoint ID
|
|
837
|
+
*/
|
|
838
|
+
async function saveCheckpoint(summary, openFiles = [], nextSteps = '') {
|
|
839
|
+
if (!summary) throw new Error('Summary is required for checkpoint');
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
const adapter = getAdapter();
|
|
843
|
+
const stmt = adapter.prepare(`
|
|
844
|
+
INSERT INTO checkpoints (timestamp, summary, open_files, next_steps, status)
|
|
845
|
+
VALUES (?, ?, ?, ?, 'active')
|
|
846
|
+
`);
|
|
847
|
+
|
|
848
|
+
const result = stmt.run(
|
|
849
|
+
Date.now(),
|
|
850
|
+
summary,
|
|
851
|
+
JSON.stringify(openFiles),
|
|
852
|
+
nextSteps
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
return result.lastInsertRowid;
|
|
856
|
+
} catch (error) {
|
|
857
|
+
throw new Error(`Failed to save checkpoint: ${error.message}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Load latest active checkpoint (New Feature: Session Continuity)
|
|
863
|
+
*
|
|
864
|
+
* @returns {Promise<Object|null>} Latest checkpoint or null
|
|
865
|
+
*/
|
|
866
|
+
async function loadCheckpoint() {
|
|
867
|
+
try {
|
|
868
|
+
const adapter = getAdapter();
|
|
869
|
+
const stmt = adapter.prepare(`
|
|
870
|
+
SELECT * FROM checkpoints
|
|
871
|
+
WHERE status = 'active'
|
|
872
|
+
ORDER BY timestamp DESC
|
|
873
|
+
LIMIT 1
|
|
874
|
+
`);
|
|
875
|
+
|
|
876
|
+
const checkpoint = stmt.get();
|
|
877
|
+
|
|
878
|
+
if (checkpoint) {
|
|
879
|
+
try {
|
|
880
|
+
checkpoint.open_files = JSON.parse(checkpoint.open_files);
|
|
881
|
+
} catch (e) {
|
|
882
|
+
checkpoint.open_files = [];
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return checkpoint || null;
|
|
887
|
+
} catch (error) {
|
|
888
|
+
throw new Error(`Failed to load checkpoint: ${error.message}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* MAMA Public API
|
|
893
|
+
*
|
|
894
|
+
* Simple, clean interface for Claude to interact with MAMA
|
|
895
|
+
* Hides complex implementation details (embeddings, vector search, graph queries)
|
|
896
|
+
*
|
|
897
|
+
* Key Principles:
|
|
898
|
+
* 1. Simple API First - No complex configuration
|
|
899
|
+
* 2. Transparent Process - Each step is visible
|
|
900
|
+
* 3. Claude-First Design - Claude decides what to save
|
|
901
|
+
* 4. Non-Intrusive - Silent failures for helpers (suggest)
|
|
902
|
+
*/
|
|
903
|
+
const mama = {
|
|
904
|
+
save,
|
|
905
|
+
recall,
|
|
906
|
+
updateOutcome,
|
|
907
|
+
suggest,
|
|
908
|
+
list: listDecisions,
|
|
909
|
+
saveCheckpoint,
|
|
910
|
+
loadCheckpoint,
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
module.exports = mama;
|