@jungjaehoon/mama-server 1.2.4 → 1.3.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/README.md +20 -10
- package/package.json +1 -1
- package/src/db/migrations/010-extend-edge-types.sql +56 -0
- package/src/embedding-http-server.js +6 -0
- package/src/mama/db-manager.js +36 -7
- package/src/mama/decision-tracker.js +231 -13
- package/src/mama/mama-api.js +247 -62
- package/src/server.js +18 -6
- package/src/tools/save-decision.js +43 -7
- package/src/tools/update-outcome.js +88 -13
package/README.md
CHANGED
|
@@ -74,17 +74,27 @@ Any MCP-compatible client can use MAMA with:
|
|
|
74
74
|
npx -y @jungjaehoon/mama-server
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
## Available Tools
|
|
77
|
+
## Available Tools (v1.3)
|
|
78
78
|
|
|
79
|
-
The MCP server exposes
|
|
79
|
+
The MCP server exposes 4 core tools:
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
| ----------------- | --------------------------------------------------------------------- |
|
|
83
|
+
| `save` | Save decision (`type='decision'`) or checkpoint (`type='checkpoint'`) |
|
|
84
|
+
| `search` | Semantic search (with `query`) or list recent items (without `query`) |
|
|
85
|
+
| `update` | Update decision outcome (case-insensitive: success/failed/partial) |
|
|
86
|
+
| `load_checkpoint` | Resume previous session |
|
|
87
|
+
|
|
88
|
+
### Edge Types
|
|
89
|
+
|
|
90
|
+
Decisions connect through relationships. Include patterns in your reasoning:
|
|
91
|
+
|
|
92
|
+
| Edge Type | Pattern | Meaning |
|
|
93
|
+
| ------------- | -------------------------- | -------------------------- |
|
|
94
|
+
| `supersedes` | (automatic for same topic) | Newer replaces older |
|
|
95
|
+
| `builds_on` | `builds_on: decision_xxx` | Extends prior work |
|
|
96
|
+
| `debates` | `debates: decision_xxx` | Alternative view |
|
|
97
|
+
| `synthesizes` | `synthesizes: [id1, id2]` | Merges multiple approaches |
|
|
88
98
|
|
|
89
99
|
## Usage Example
|
|
90
100
|
|
|
@@ -197,4 +207,4 @@ MAMA was inspired by [mem0](https://github.com/mem0ai/mem0) (Apache 2.0). While
|
|
|
197
207
|
---
|
|
198
208
|
|
|
199
209
|
**Author:** SpineLift Team
|
|
200
|
-
**Version:** 1.
|
|
210
|
+
**Version:** 1.3.0
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- ══════════════════════════════════════════════════════════════
|
|
2
|
+
-- MAMA Migration 010: Extend Edge Types
|
|
3
|
+
-- ══════════════════════════════════════════════════════════════
|
|
4
|
+
-- Version: 1.3
|
|
5
|
+
-- Date: 2025-11-26
|
|
6
|
+
-- Purpose: Add builds_on, debates, synthesizes relationship types
|
|
7
|
+
-- Story: 2.1 - Edge Type Extension
|
|
8
|
+
-- ══════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
-- SQLite doesn't support ALTER TABLE to modify CHECK constraints.
|
|
11
|
+
-- We need to recreate the table with expanded CHECK constraint.
|
|
12
|
+
|
|
13
|
+
-- Step 1: Create new table with extended edge types
|
|
14
|
+
CREATE TABLE IF NOT EXISTS decision_edges_new (
|
|
15
|
+
from_id TEXT NOT NULL,
|
|
16
|
+
to_id TEXT NOT NULL,
|
|
17
|
+
relationship TEXT NOT NULL,
|
|
18
|
+
reason TEXT,
|
|
19
|
+
weight REAL DEFAULT 1.0,
|
|
20
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
21
|
+
created_by TEXT DEFAULT 'user' CHECK (created_by IN ('llm', 'user')),
|
|
22
|
+
approved_by_user INTEGER DEFAULT 1 CHECK (approved_by_user IN (0, 1)),
|
|
23
|
+
decision_id TEXT,
|
|
24
|
+
evidence TEXT,
|
|
25
|
+
|
|
26
|
+
PRIMARY KEY (from_id, to_id, relationship),
|
|
27
|
+
FOREIGN KEY (from_id) REFERENCES decisions(id),
|
|
28
|
+
FOREIGN KEY (to_id) REFERENCES decisions(id),
|
|
29
|
+
|
|
30
|
+
-- Extended CHECK constraint: original + v1.3 types
|
|
31
|
+
CHECK (relationship IN ('supersedes', 'refines', 'contradicts', 'builds_on', 'debates', 'synthesizes')),
|
|
32
|
+
CHECK (weight >= 0.0 AND weight <= 1.0)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Step 2: Copy existing data (explicit columns to handle schema variations)
|
|
36
|
+
INSERT OR IGNORE INTO decision_edges_new (from_id, to_id, relationship, reason, weight, created_at, created_by, approved_by_user, decision_id, evidence)
|
|
37
|
+
SELECT from_id, to_id, relationship, reason, weight, created_at, created_by, approved_by_user, decision_id, evidence FROM decision_edges;
|
|
38
|
+
|
|
39
|
+
-- Step 3: Drop old table
|
|
40
|
+
DROP TABLE IF EXISTS decision_edges;
|
|
41
|
+
|
|
42
|
+
-- Step 4: Rename new table
|
|
43
|
+
ALTER TABLE decision_edges_new RENAME TO decision_edges;
|
|
44
|
+
|
|
45
|
+
-- Step 5: Recreate indexes
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON decision_edges(from_id);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON decision_edges(to_id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_edges_relationship ON decision_edges(relationship);
|
|
49
|
+
|
|
50
|
+
-- Update schema version
|
|
51
|
+
INSERT OR REPLACE INTO schema_version (version, description, applied_at)
|
|
52
|
+
VALUES (10, 'Extend edge types: builds_on, debates, synthesizes', unixepoch());
|
|
53
|
+
|
|
54
|
+
-- ══════════════════════════════════════════════════════════════
|
|
55
|
+
-- End of Migration 010
|
|
56
|
+
-- ══════════════════════════════════════════════════════════════
|
|
@@ -184,6 +184,12 @@ async function startEmbeddingServer(port = DEFAULT_PORT) {
|
|
|
184
184
|
);
|
|
185
185
|
// Not a fatal error - another server instance may be running
|
|
186
186
|
resolve(null);
|
|
187
|
+
} else if (error.code === 'EPERM' || error.code === 'EACCES') {
|
|
188
|
+
console.error(
|
|
189
|
+
`[EmbeddingHTTP] Permission denied opening port ${port}, skipping HTTP embedding server (sandboxed environment)`
|
|
190
|
+
);
|
|
191
|
+
// Some environments block listening on localhost; keep MCP server running without HTTP embeddings
|
|
192
|
+
resolve(null);
|
|
187
193
|
} else {
|
|
188
194
|
reject(error);
|
|
189
195
|
}
|
package/src/mama/db-manager.js
CHANGED
|
@@ -386,30 +386,46 @@ async function queryDecisionGraph(topic) {
|
|
|
386
386
|
* for refines and contradicts relationships
|
|
387
387
|
*
|
|
388
388
|
* @param {Array<string>} decisionIds - Decision IDs to query edges for
|
|
389
|
-
* @returns {Promise<Object>} Categorized edges { refines, refined_by, contradicts, contradicted_by }
|
|
389
|
+
* @returns {Promise<Object>} Categorized edges { refines, refined_by, contradicts, contradicted_by, builds_on, built_on_by, debates, debated_by, synthesizes, synthesized_by }
|
|
390
390
|
*/
|
|
391
391
|
async function querySemanticEdges(decisionIds) {
|
|
392
392
|
const adapter = getAdapter();
|
|
393
393
|
|
|
394
394
|
if (!decisionIds || decisionIds.length === 0) {
|
|
395
|
-
return {
|
|
395
|
+
return {
|
|
396
|
+
refines: [],
|
|
397
|
+
refined_by: [],
|
|
398
|
+
contradicts: [],
|
|
399
|
+
contradicted_by: [],
|
|
400
|
+
// Story 2.1: Extended edge types
|
|
401
|
+
builds_on: [],
|
|
402
|
+
built_on_by: [],
|
|
403
|
+
debates: [],
|
|
404
|
+
debated_by: [],
|
|
405
|
+
synthesizes: [],
|
|
406
|
+
synthesized_by: [],
|
|
407
|
+
};
|
|
396
408
|
}
|
|
397
409
|
|
|
398
410
|
try {
|
|
399
411
|
// Build placeholders for IN clause
|
|
400
412
|
const placeholders = decisionIds.map(() => '?').join(',');
|
|
401
413
|
|
|
414
|
+
// Story 2.1: Include new edge types in query
|
|
415
|
+
const edgeTypes = ['refines', 'contradicts', 'builds_on', 'debates', 'synthesizes'];
|
|
416
|
+
const edgeTypePlaceholders = edgeTypes.map(() => '?').join(',');
|
|
417
|
+
|
|
402
418
|
// Query outgoing edges (from_id = decision)
|
|
403
419
|
const outgoingStmt = adapter.prepare(`
|
|
404
420
|
SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
|
|
405
421
|
FROM decision_edges e
|
|
406
422
|
JOIN decisions d ON e.to_id = d.id
|
|
407
423
|
WHERE e.from_id IN (${placeholders})
|
|
408
|
-
AND e.relationship IN (
|
|
424
|
+
AND e.relationship IN (${edgeTypePlaceholders})
|
|
409
425
|
AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
|
|
410
426
|
ORDER BY e.created_at DESC
|
|
411
427
|
`);
|
|
412
|
-
const outgoingEdges = await outgoingStmt.all(...decisionIds);
|
|
428
|
+
const outgoingEdges = await outgoingStmt.all(...decisionIds, ...edgeTypes);
|
|
413
429
|
|
|
414
430
|
// Query incoming edges (to_id = decision)
|
|
415
431
|
const incomingStmt = adapter.prepare(`
|
|
@@ -417,23 +433,36 @@ async function querySemanticEdges(decisionIds) {
|
|
|
417
433
|
FROM decision_edges e
|
|
418
434
|
JOIN decisions d ON e.from_id = d.id
|
|
419
435
|
WHERE e.to_id IN (${placeholders})
|
|
420
|
-
AND e.relationship IN (
|
|
436
|
+
AND e.relationship IN (${edgeTypePlaceholders})
|
|
421
437
|
AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
|
|
422
438
|
ORDER BY e.created_at DESC
|
|
423
439
|
`);
|
|
424
|
-
const incomingEdges = await incomingStmt.all(...decisionIds);
|
|
440
|
+
const incomingEdges = await incomingStmt.all(...decisionIds, ...edgeTypes);
|
|
425
441
|
|
|
426
|
-
// Categorize edges
|
|
442
|
+
// Categorize edges (original + v1.3 extended)
|
|
427
443
|
const refines = outgoingEdges.filter((e) => e.relationship === 'refines');
|
|
428
444
|
const refined_by = incomingEdges.filter((e) => e.relationship === 'refines');
|
|
429
445
|
const contradicts = outgoingEdges.filter((e) => e.relationship === 'contradicts');
|
|
430
446
|
const contradicted_by = incomingEdges.filter((e) => e.relationship === 'contradicts');
|
|
447
|
+
// Story 2.1: New edge type categories
|
|
448
|
+
const builds_on = outgoingEdges.filter((e) => e.relationship === 'builds_on');
|
|
449
|
+
const built_on_by = incomingEdges.filter((e) => e.relationship === 'builds_on');
|
|
450
|
+
const debates = outgoingEdges.filter((e) => e.relationship === 'debates');
|
|
451
|
+
const debated_by = incomingEdges.filter((e) => e.relationship === 'debates');
|
|
452
|
+
const synthesizes = outgoingEdges.filter((e) => e.relationship === 'synthesizes');
|
|
453
|
+
const synthesized_by = incomingEdges.filter((e) => e.relationship === 'synthesizes');
|
|
431
454
|
|
|
432
455
|
return {
|
|
433
456
|
refines,
|
|
434
457
|
refined_by,
|
|
435
458
|
contradicts,
|
|
436
459
|
contradicted_by,
|
|
460
|
+
builds_on,
|
|
461
|
+
built_on_by,
|
|
462
|
+
debates,
|
|
463
|
+
debated_by,
|
|
464
|
+
synthesizes,
|
|
465
|
+
synthesized_by,
|
|
437
466
|
};
|
|
438
467
|
} catch (error) {
|
|
439
468
|
throw new Error(`Semantic edges query failed: ${error.message}`);
|
|
@@ -24,6 +24,21 @@ const {
|
|
|
24
24
|
getAdapter,
|
|
25
25
|
} = require('./memory-store');
|
|
26
26
|
|
|
27
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Story 2.1: Extended Edge Types
|
|
29
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// Valid relationship types for decision_edges
|
|
31
|
+
// Original: supersedes, refines, contradicts
|
|
32
|
+
// v1.3 Extension: builds_on, debates, synthesizes
|
|
33
|
+
const VALID_EDGE_TYPES = [
|
|
34
|
+
'supersedes', // Original: New decision replaces old one
|
|
35
|
+
'refines', // Original: Decision refines another
|
|
36
|
+
'contradicts', // Original: Decision contradicts another
|
|
37
|
+
'builds_on', // v1.3: Extends existing decision with new insights
|
|
38
|
+
'debates', // v1.3: Presents counter-argument with evidence
|
|
39
|
+
'synthesizes', // v1.3: Merges multiple decisions into unified approach
|
|
40
|
+
];
|
|
41
|
+
|
|
27
42
|
/**
|
|
28
43
|
* Generate decision ID
|
|
29
44
|
*
|
|
@@ -74,32 +89,68 @@ async function getPreviousDecision(topic) {
|
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
/**
|
|
77
|
-
* Create
|
|
92
|
+
* Create a decision edge with specified relationship type
|
|
78
93
|
*
|
|
79
|
-
*
|
|
80
|
-
* AC #2: Supersedes relationship creation
|
|
94
|
+
* Story 2.1: Generic edge creation supporting all relationship types
|
|
81
95
|
*
|
|
82
|
-
* @param {string} fromId -
|
|
83
|
-
* @param {string} toId -
|
|
84
|
-
* @param {string}
|
|
96
|
+
* @param {string} fromId - Source decision ID
|
|
97
|
+
* @param {string} toId - Target decision ID
|
|
98
|
+
* @param {string} relationship - Edge type (supersedes, builds_on, debates, synthesizes, etc.)
|
|
99
|
+
* @param {string} reason - Reason for the relationship
|
|
100
|
+
* @returns {Promise<boolean>} Success status
|
|
85
101
|
*/
|
|
86
|
-
async function
|
|
102
|
+
async function createEdge(fromId, toId, relationship, reason) {
|
|
87
103
|
const adapter = getAdapter();
|
|
88
104
|
|
|
105
|
+
// Story 2.1: Runtime validation of edge types
|
|
106
|
+
if (!VALID_EDGE_TYPES.includes(relationship)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid edge type: "${relationship}". Valid types: ${VALID_EDGE_TYPES.join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
try {
|
|
90
|
-
//
|
|
91
|
-
//
|
|
113
|
+
// Note: SQLite CHECK constraint only allows supersedes/refines/contradicts
|
|
114
|
+
// New types (builds_on, debates, synthesizes) bypass CHECK via runtime validation
|
|
115
|
+
// The INSERT will fail for new types due to CHECK constraint
|
|
116
|
+
// WORKAROUND: Use PRAGMA ignore_check_constraints or recreate table
|
|
117
|
+
// For now, we'll catch the error and handle gracefully
|
|
118
|
+
|
|
119
|
+
// Story 2.1: LLM auto-detected edges are approved by default (approved_by_user=1)
|
|
120
|
+
// This allows them to appear in search results via querySemanticEdges
|
|
92
121
|
const stmt = adapter.prepare(`
|
|
93
|
-
INSERT INTO decision_edges (from_id, to_id, relationship, reason, created_at, created_by, approved_by_user)
|
|
94
|
-
VALUES (?, ?,
|
|
122
|
+
INSERT OR REPLACE INTO decision_edges (from_id, to_id, relationship, reason, created_at, created_by, approved_by_user)
|
|
123
|
+
VALUES (?, ?, ?, ?, ?, 'llm', 1)
|
|
95
124
|
`);
|
|
96
125
|
|
|
97
|
-
await stmt.run(fromId, toId, reason, Date.now());
|
|
126
|
+
await stmt.run(fromId, toId, relationship, reason, Date.now());
|
|
127
|
+
return true;
|
|
98
128
|
} catch (error) {
|
|
99
|
-
|
|
129
|
+
// Handle CHECK constraint failure for new edge types
|
|
130
|
+
if (error.message.includes('CHECK constraint failed')) {
|
|
131
|
+
info(
|
|
132
|
+
`[decision-tracker] Edge type "${relationship}" not yet supported in schema, skipping edge creation`
|
|
133
|
+
);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Failed to create ${relationship} edge: ${error.message}`);
|
|
100
137
|
}
|
|
101
138
|
}
|
|
102
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Create supersedes edge
|
|
142
|
+
*
|
|
143
|
+
* Task 3.5: Create supersedes edge (INSERT INTO decision_edges)
|
|
144
|
+
* AC #2: Supersedes relationship creation
|
|
145
|
+
*
|
|
146
|
+
* @param {string} fromId - New decision ID
|
|
147
|
+
* @param {string} toId - Previous decision ID
|
|
148
|
+
* @param {string} reason - Reason for superseding
|
|
149
|
+
*/
|
|
150
|
+
async function createSupersedesEdge(fromId, toId, reason) {
|
|
151
|
+
return createEdge(fromId, toId, 'supersedes', reason);
|
|
152
|
+
}
|
|
153
|
+
|
|
103
154
|
/**
|
|
104
155
|
* Update previous decision's superseded_by field
|
|
105
156
|
*
|
|
@@ -175,6 +226,163 @@ function detectRefinement(_detection, _sessionContext) {
|
|
|
175
226
|
return null;
|
|
176
227
|
}
|
|
177
228
|
|
|
229
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
230
|
+
// Story 2.2: Reasoning Field Parsing
|
|
231
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse reasoning field for relationship references
|
|
235
|
+
*
|
|
236
|
+
* Story 2.2: Detect patterns like:
|
|
237
|
+
* - builds_on: <decision_id>
|
|
238
|
+
* - debates: <decision_id>
|
|
239
|
+
* - synthesizes: [id1, id2]
|
|
240
|
+
*
|
|
241
|
+
* @param {string} reasoning - Decision reasoning text
|
|
242
|
+
* @returns {Array<{type: string, targetIds: string[]}>} Detected relationships
|
|
243
|
+
*/
|
|
244
|
+
function parseReasoningForRelationships(reasoning) {
|
|
245
|
+
if (!reasoning || typeof reasoning !== 'string') {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const relationships = [];
|
|
250
|
+
|
|
251
|
+
// Pattern 1: builds_on: <id> or builds_on: decision_xxx
|
|
252
|
+
const buildsOnMatch = reasoning.match(/builds_on:\s*(decision_[a-z0-9_]+)/gi);
|
|
253
|
+
if (buildsOnMatch) {
|
|
254
|
+
buildsOnMatch.forEach((match) => {
|
|
255
|
+
const id = match.replace(/builds_on:\s*/i, '').trim();
|
|
256
|
+
if (id) {
|
|
257
|
+
relationships.push({ type: 'builds_on', targetIds: [id] });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Pattern 2: debates: <id> or debates: decision_xxx
|
|
263
|
+
const debatesMatch = reasoning.match(/debates:\s*(decision_[a-z0-9_]+)/gi);
|
|
264
|
+
if (debatesMatch) {
|
|
265
|
+
debatesMatch.forEach((match) => {
|
|
266
|
+
const id = match.replace(/debates:\s*/i, '').trim();
|
|
267
|
+
if (id) {
|
|
268
|
+
relationships.push({ type: 'debates', targetIds: [id] });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Pattern 3: synthesizes: [id1, id2] or synthesizes: decision_xxx, decision_yyy
|
|
274
|
+
const synthesizesMatch = reasoning.match(
|
|
275
|
+
/synthesizes:\s*\[?\s*(decision_[a-z0-9_]+(?:\s*,\s*decision_[a-z0-9_]+)*)\s*\]?/gi
|
|
276
|
+
);
|
|
277
|
+
if (synthesizesMatch) {
|
|
278
|
+
synthesizesMatch.forEach((match) => {
|
|
279
|
+
const idsStr = match.replace(/synthesizes:\s*\[?\s*/i, '').replace(/\s*\]?\s*$/, '');
|
|
280
|
+
const ids = idsStr.split(/\s*,\s*/).filter((id) => id.startsWith('decision_'));
|
|
281
|
+
if (ids.length > 0) {
|
|
282
|
+
relationships.push({ type: 'synthesizes', targetIds: ids });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return relationships;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create edges from parsed reasoning relationships
|
|
292
|
+
*
|
|
293
|
+
* Story 2.2: Auto-create edges when reasoning references other decisions
|
|
294
|
+
*
|
|
295
|
+
* @param {string} fromId - Source decision ID
|
|
296
|
+
* @param {string} reasoning - Decision reasoning text
|
|
297
|
+
* @returns {Promise<{created: number, failed: number}>} Edge creation stats
|
|
298
|
+
*/
|
|
299
|
+
async function createEdgesFromReasoning(fromId, reasoning) {
|
|
300
|
+
const relationships = parseReasoningForRelationships(reasoning);
|
|
301
|
+
let created = 0;
|
|
302
|
+
let failed = 0;
|
|
303
|
+
|
|
304
|
+
for (const rel of relationships) {
|
|
305
|
+
for (const targetId of rel.targetIds) {
|
|
306
|
+
try {
|
|
307
|
+
// Verify target decision exists
|
|
308
|
+
const adapter = getAdapter();
|
|
309
|
+
const stmt = adapter.prepare('SELECT id FROM decisions WHERE id = ?');
|
|
310
|
+
const target = await stmt.get(targetId);
|
|
311
|
+
|
|
312
|
+
if (!target) {
|
|
313
|
+
info(`[decision-tracker] Referenced decision not found: ${targetId}, skipping edge`);
|
|
314
|
+
failed++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Create the edge
|
|
319
|
+
const reason = `Auto-detected from reasoning: ${rel.type} reference`;
|
|
320
|
+
const success = await createEdge(fromId, targetId, rel.type, reason);
|
|
321
|
+
|
|
322
|
+
if (success) {
|
|
323
|
+
created++;
|
|
324
|
+
info(`[decision-tracker] Created ${rel.type} edge: ${fromId} -> ${targetId}`);
|
|
325
|
+
} else {
|
|
326
|
+
failed++;
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
info(`[decision-tracker] Failed to create edge to ${targetId}: ${error.message}`);
|
|
330
|
+
failed++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { created, failed };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get supersedes chain depth for a topic
|
|
340
|
+
*
|
|
341
|
+
* Story 2.2: Calculate how many times a topic has been superseded
|
|
342
|
+
*
|
|
343
|
+
* @param {string} topic - Decision topic
|
|
344
|
+
* @returns {Promise<{depth: number, chain: string[]}>} Chain depth and decision IDs
|
|
345
|
+
*/
|
|
346
|
+
async function getSupersededChainDepth(topic) {
|
|
347
|
+
const adapter = getAdapter();
|
|
348
|
+
const chain = [];
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
// Start from the latest decision (superseded_by IS NULL)
|
|
352
|
+
let stmt = adapter.prepare(`
|
|
353
|
+
SELECT id, supersedes FROM decisions
|
|
354
|
+
WHERE topic = ? AND superseded_by IS NULL
|
|
355
|
+
ORDER BY created_at DESC
|
|
356
|
+
LIMIT 1
|
|
357
|
+
`);
|
|
358
|
+
|
|
359
|
+
let current = await stmt.get(topic);
|
|
360
|
+
|
|
361
|
+
if (!current) {
|
|
362
|
+
return { depth: 0, chain: [] };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
chain.push(current.id);
|
|
366
|
+
|
|
367
|
+
// Walk back through supersedes chain
|
|
368
|
+
while (current && current.supersedes) {
|
|
369
|
+
stmt = adapter.prepare('SELECT id, supersedes FROM decisions WHERE id = ?');
|
|
370
|
+
current = await stmt.get(current.supersedes);
|
|
371
|
+
|
|
372
|
+
if (current) {
|
|
373
|
+
chain.push(current.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
depth: chain.length - 1, // depth = number of supersedes edges
|
|
379
|
+
chain: chain.reverse(), // oldest to newest
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw new Error(`Failed to get supersedes chain: ${error.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
178
386
|
// ════════════════════════════════════════════════════════════════════════════
|
|
179
387
|
// NOTE: Auto-link functions REMOVED in v1.2.0
|
|
180
388
|
//
|
|
@@ -387,7 +595,10 @@ function updateConfidence(prior, evidence) {
|
|
|
387
595
|
// NOTE: Auto-link functions (createRefinesEdge, createContradictsEdge,
|
|
388
596
|
// findRelatedDecisions, isConflicting, detectConflicts) removed from exports.
|
|
389
597
|
// LLM infers relationships from search results instead.
|
|
598
|
+
//
|
|
599
|
+
// Story 2.1/2.2: Added new edge type support and reasoning parsing
|
|
390
600
|
module.exports = {
|
|
601
|
+
// Core functions
|
|
391
602
|
learnDecision,
|
|
392
603
|
generateDecisionId,
|
|
393
604
|
getPreviousDecision,
|
|
@@ -396,4 +607,11 @@ module.exports = {
|
|
|
396
607
|
calculateCombinedConfidence,
|
|
397
608
|
detectRefinement,
|
|
398
609
|
updateConfidence,
|
|
610
|
+
// Story 2.1: Edge type extension
|
|
611
|
+
VALID_EDGE_TYPES,
|
|
612
|
+
createEdge,
|
|
613
|
+
// Story 2.2: Reasoning field parsing
|
|
614
|
+
parseReasoningForRelationships,
|
|
615
|
+
createEdgesFromReasoning,
|
|
616
|
+
getSupersededChainDepth,
|
|
399
617
|
};
|
package/src/mama/mama-api.js
CHANGED
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
* - MAMA stores (organize books), retrieves (find books), indexes (catalog)
|
|
9
9
|
* - Claude decides what to save and how to use recalled decisions
|
|
10
10
|
*
|
|
11
|
+
* v1.3 Update: Collaborative Reasoning Graph
|
|
12
|
+
* - Auto-search on save: Find similar decisions before saving
|
|
13
|
+
* - Collaborative invitation: Suggest build-on/debate/synthesize
|
|
14
|
+
* - AX-first: Soft warnings, not hard blocks
|
|
15
|
+
*
|
|
11
16
|
* @module mama-api
|
|
12
|
-
* @version 1.
|
|
13
|
-
* @date 2025-11-
|
|
17
|
+
* @version 1.3
|
|
18
|
+
* @date 2025-11-26
|
|
14
19
|
*/
|
|
15
20
|
|
|
21
|
+
// Session-level warning cooldown cache (Story 1.1, 1.2)
|
|
22
|
+
// Prevents spam by tracking warned topics per session
|
|
23
|
+
const warnedTopicsCache = new Map();
|
|
24
|
+
const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
|
+
|
|
16
26
|
const { learnDecision } = require('./decision-tracker');
|
|
17
27
|
// eslint-disable-next-line no-unused-vars
|
|
18
28
|
const { injectDecisionContext } = require('./memory-inject');
|
|
@@ -182,7 +192,158 @@ async function save({
|
|
|
182
192
|
await stmt.run(...values);
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
|
|
195
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
196
|
+
// Story 1.1: Auto-Search on Save
|
|
197
|
+
// Story 1.2: Response Enhancement
|
|
198
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
199
|
+
let similar_decisions = [];
|
|
200
|
+
let warning = null;
|
|
201
|
+
let collaboration_hint = null;
|
|
202
|
+
let reasoning_graph = null;
|
|
203
|
+
|
|
204
|
+
// Only run auto-search for decisions (not checkpoints) with a topic
|
|
205
|
+
if (topic) {
|
|
206
|
+
try {
|
|
207
|
+
// Story 1.1: Auto-search using suggest()
|
|
208
|
+
const searchResults = await suggest(topic, {
|
|
209
|
+
limit: 3,
|
|
210
|
+
threshold: 0.7,
|
|
211
|
+
disableRecency: true, // Pure semantic similarity for comparison
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (searchResults && searchResults.results) {
|
|
215
|
+
// Filter out the decision we just saved
|
|
216
|
+
similar_decisions = searchResults.results
|
|
217
|
+
.filter((d) => d.id !== decisionId)
|
|
218
|
+
.map((d) => ({
|
|
219
|
+
id: d.id,
|
|
220
|
+
topic: d.topic,
|
|
221
|
+
decision: d.decision,
|
|
222
|
+
similarity: d.similarity,
|
|
223
|
+
created_at: d.created_at,
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
// Story 1.2: Warning logic (similarity >= 0.85)
|
|
227
|
+
const highSimilarity = similar_decisions.find((d) => d.similarity >= 0.85);
|
|
228
|
+
if (highSimilarity && !_isTopicInCooldown(topic)) {
|
|
229
|
+
warning = `High similarity (${(highSimilarity.similarity * 100).toFixed(0)}%) with existing decision "${highSimilarity.decision.substring(0, 50)}..."`;
|
|
230
|
+
_markTopicWarned(topic);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Story 1.2: Collaboration hint
|
|
234
|
+
if (similar_decisions.length > 0) {
|
|
235
|
+
collaboration_hint = _generateCollaborationHint(similar_decisions);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Story 1.1 AC3: Best-effort - save succeeds even if auto-search fails
|
|
240
|
+
console.error('Auto-search failed:', error.message);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Story 1.2: Reasoning graph info
|
|
244
|
+
try {
|
|
245
|
+
reasoning_graph = await _getReasoningGraphInfo(topic, decisionId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('Reasoning graph query failed:', error.message);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Story 2.2: Parse reasoning for relationship edges (builds_on, debates, synthesizes)
|
|
251
|
+
if (reasoning) {
|
|
252
|
+
try {
|
|
253
|
+
const { createEdgesFromReasoning } = require('./decision-tracker.js');
|
|
254
|
+
await createEdgesFromReasoning(decisionId, reasoning);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Best-effort - save succeeds even if edge creation fails
|
|
257
|
+
console.error('Edge creation from reasoning failed:', error.message);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Story 1.2: Enhanced response (backward compatible)
|
|
263
|
+
return {
|
|
264
|
+
success: true,
|
|
265
|
+
id: decisionId,
|
|
266
|
+
...(similar_decisions.length > 0 && { similar_decisions }),
|
|
267
|
+
...(warning && { warning }),
|
|
268
|
+
...(collaboration_hint && { collaboration_hint }),
|
|
269
|
+
...(reasoning_graph && { reasoning_graph }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
274
|
+
// Story 1.2: Helper functions for Response Enhancement
|
|
275
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a topic is in warning cooldown
|
|
279
|
+
* @param {string} topic - Topic to check
|
|
280
|
+
* @returns {boolean} True if topic was warned recently
|
|
281
|
+
*/
|
|
282
|
+
function _isTopicInCooldown(topic) {
|
|
283
|
+
const lastWarned = warnedTopicsCache.get(topic);
|
|
284
|
+
if (!lastWarned) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
return Date.now() - lastWarned < WARNING_COOLDOWN_MS;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Mark a topic as warned (start cooldown)
|
|
292
|
+
* @param {string} topic - Topic to mark
|
|
293
|
+
*/
|
|
294
|
+
function _markTopicWarned(topic) {
|
|
295
|
+
warnedTopicsCache.set(topic, Date.now());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate collaboration hint message
|
|
300
|
+
* @param {Array} similarDecisions - Similar decisions found
|
|
301
|
+
* @returns {string} Collaboration hint message
|
|
302
|
+
*/
|
|
303
|
+
function _generateCollaborationHint(similarDecisions) {
|
|
304
|
+
const count = similarDecisions.length;
|
|
305
|
+
if (count === 0) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return `Found ${count} related decision(s). Consider:
|
|
310
|
+
- SUPERSEDE: Same topic replaces prior (automatic)
|
|
311
|
+
- BUILD-ON: Add "builds_on: <id>" in reasoning to extend
|
|
312
|
+
- DEBATE: Add "debates: <id>" in reasoning for alternative view
|
|
313
|
+
- SYNTHESIZE: Add "synthesizes: [id1, id2]" in reasoning to unify`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get reasoning graph info for a topic
|
|
318
|
+
* @param {string} topic - Topic to query
|
|
319
|
+
* @param {string} currentId - Current decision ID
|
|
320
|
+
* @returns {Object} Reasoning graph info
|
|
321
|
+
*/
|
|
322
|
+
async function _getReasoningGraphInfo(topic, currentId) {
|
|
323
|
+
try {
|
|
324
|
+
const { queryDecisionGraph } = require('./memory-store');
|
|
325
|
+
const chain = await queryDecisionGraph(topic);
|
|
326
|
+
|
|
327
|
+
if (!chain || chain.length === 0) {
|
|
328
|
+
return {
|
|
329
|
+
topic,
|
|
330
|
+
depth: 1,
|
|
331
|
+
latest: currentId,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
topic,
|
|
337
|
+
depth: chain.length,
|
|
338
|
+
latest: chain[0]?.id || currentId,
|
|
339
|
+
};
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return {
|
|
342
|
+
topic,
|
|
343
|
+
depth: 1,
|
|
344
|
+
latest: currentId,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
186
347
|
}
|
|
187
348
|
|
|
188
349
|
/**
|
|
@@ -333,7 +494,10 @@ async function updateOutcome(decisionId, { outcome, failure_reason, limitation }
|
|
|
333
494
|
throw new Error('mama.updateOutcome() requires decisionId (string)');
|
|
334
495
|
}
|
|
335
496
|
|
|
336
|
-
|
|
497
|
+
// AX Improvement: Be forgiving with case sensitivity
|
|
498
|
+
const normalizedOutcome = outcome ? outcome.toUpperCase() : null;
|
|
499
|
+
|
|
500
|
+
if (!normalizedOutcome || !['SUCCESS', 'FAILED', 'PARTIAL'].includes(normalizedOutcome)) {
|
|
337
501
|
throw new Error('mama.updateOutcome() outcome must be "SUCCESS", "FAILED", or "PARTIAL"');
|
|
338
502
|
}
|
|
339
503
|
|
|
@@ -353,7 +517,7 @@ async function updateOutcome(decisionId, { outcome, failure_reason, limitation }
|
|
|
353
517
|
`
|
|
354
518
|
);
|
|
355
519
|
const result = stmt.run(
|
|
356
|
-
|
|
520
|
+
normalizedOutcome,
|
|
357
521
|
failure_reason || null,
|
|
358
522
|
limitation || null,
|
|
359
523
|
Date.now(),
|
|
@@ -416,91 +580,112 @@ async function expandWithGraph(candidates) {
|
|
|
416
580
|
console.warn(`Failed to get supersedes chain for ${candidate.topic}: ${error.message}`);
|
|
417
581
|
}
|
|
418
582
|
|
|
419
|
-
// 2. Add semantic edges (refines, contradicts)
|
|
583
|
+
// 2. Add semantic edges (refines, contradicts, builds_on, debates, synthesizes)
|
|
420
584
|
try {
|
|
421
585
|
const edges = await querySemanticEdges([candidate.id]);
|
|
422
586
|
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
587
|
+
// Helper to add edge to graph
|
|
588
|
+
const addEdge = (edge, idField, source, rank, simFactor) => {
|
|
589
|
+
const id = edge[idField];
|
|
590
|
+
if (!graphEnhanced.has(id)) {
|
|
591
|
+
graphEnhanced.set(id, {
|
|
592
|
+
id: id,
|
|
428
593
|
topic: edge.topic,
|
|
429
594
|
decision: edge.decision,
|
|
430
595
|
confidence: edge.confidence,
|
|
431
596
|
created_at: edge.created_at,
|
|
432
|
-
graph_source:
|
|
433
|
-
graph_rank:
|
|
434
|
-
similarity: candidate.similarity *
|
|
597
|
+
graph_source: source,
|
|
598
|
+
graph_rank: rank,
|
|
599
|
+
similarity: candidate.similarity * simFactor,
|
|
435
600
|
related_to: candidate.id,
|
|
436
601
|
edge_reason: edge.reason,
|
|
437
602
|
});
|
|
438
603
|
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Add refines edges
|
|
607
|
+
for (const edge of edges.refines) {
|
|
608
|
+
addEdge(edge, 'to_id', 'refines', 0.7, 0.85);
|
|
439
609
|
}
|
|
440
610
|
|
|
441
611
|
// Add refined_by edges
|
|
442
612
|
for (const edge of edges.refined_by) {
|
|
443
|
-
|
|
444
|
-
graphEnhanced.set(edge.from_id, {
|
|
445
|
-
id: edge.from_id,
|
|
446
|
-
topic: edge.topic,
|
|
447
|
-
decision: edge.decision,
|
|
448
|
-
confidence: edge.confidence,
|
|
449
|
-
created_at: edge.created_at,
|
|
450
|
-
graph_source: 'refined_by',
|
|
451
|
-
graph_rank: 0.7,
|
|
452
|
-
similarity: candidate.similarity * 0.85,
|
|
453
|
-
related_to: candidate.id,
|
|
454
|
-
edge_reason: edge.reason,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
613
|
+
addEdge(edge, 'from_id', 'refined_by', 0.7, 0.85);
|
|
457
614
|
}
|
|
458
615
|
|
|
459
616
|
// Add contradicts edges (lower rank, but still relevant)
|
|
460
617
|
for (const edge of edges.contradicts) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
618
|
+
addEdge(edge, 'to_id', 'contradicts', 0.6, 0.8);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Story 2.1: Add builds_on edges (high relevance - extending prior work)
|
|
622
|
+
for (const edge of edges.builds_on) {
|
|
623
|
+
addEdge(edge, 'to_id', 'builds_on', 0.75, 0.9);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Add built_on_by edges (someone built on this decision)
|
|
627
|
+
for (const edge of edges.built_on_by) {
|
|
628
|
+
addEdge(edge, 'from_id', 'built_on_by', 0.75, 0.9);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Add debates edges (alternative view)
|
|
632
|
+
for (const edge of edges.debates) {
|
|
633
|
+
addEdge(edge, 'to_id', 'debates', 0.65, 0.85);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Add debated_by edges
|
|
637
|
+
for (const edge of edges.debated_by) {
|
|
638
|
+
addEdge(edge, 'from_id', 'debated_by', 0.65, 0.85);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Add synthesizes edges (unified approach)
|
|
642
|
+
for (const edge of edges.synthesizes) {
|
|
643
|
+
addEdge(edge, 'to_id', 'synthesizes', 0.7, 0.88);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Add synthesized_by edges
|
|
647
|
+
for (const edge of edges.synthesized_by) {
|
|
648
|
+
addEdge(edge, 'from_id', 'synthesized_by', 0.7, 0.88);
|
|
475
649
|
}
|
|
476
650
|
} catch (error) {
|
|
477
651
|
console.warn(`Failed to get semantic edges for ${candidate.id}: ${error.message}`);
|
|
478
652
|
}
|
|
479
653
|
}
|
|
480
654
|
|
|
481
|
-
// 3. Convert Map to Array
|
|
482
|
-
const
|
|
655
|
+
// 3. Convert Map to Array
|
|
656
|
+
const allResults = Array.from(graphEnhanced.values());
|
|
657
|
+
|
|
658
|
+
// 4. Sort: Interleave expanded results after their related primary
|
|
659
|
+
// This ensures edge-connected decisions appear near their source
|
|
660
|
+
const primaryResults = allResults
|
|
661
|
+
.filter((r) => primaryIds.has(r.id))
|
|
662
|
+
.sort((a, b) => {
|
|
663
|
+
const scoreA = a.final_score || a.similarity || 0;
|
|
664
|
+
const scoreB = b.final_score || b.similarity || 0;
|
|
665
|
+
return scoreB - scoreA;
|
|
666
|
+
});
|
|
483
667
|
|
|
484
|
-
|
|
485
|
-
results.sort((a, b) => {
|
|
486
|
-
// Primary candidates always first
|
|
487
|
-
if (primaryIds.has(a.id) && !primaryIds.has(b.id)) {
|
|
488
|
-
return -1;
|
|
489
|
-
}
|
|
490
|
-
if (!primaryIds.has(a.id) && primaryIds.has(b.id)) {
|
|
491
|
-
return 1;
|
|
492
|
-
}
|
|
668
|
+
const expandedResults = allResults.filter((r) => !primaryIds.has(r.id));
|
|
493
669
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
670
|
+
// Build final results: each primary followed by its related expanded results
|
|
671
|
+
const results = [];
|
|
672
|
+
for (const primary of primaryResults) {
|
|
673
|
+
results.push(primary);
|
|
498
674
|
|
|
499
|
-
//
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
675
|
+
// Find expanded results related to this primary
|
|
676
|
+
const relatedExpanded = expandedResults.filter((e) => e.related_to === primary.id);
|
|
677
|
+
|
|
678
|
+
// Sort related by graph_rank (higher first)
|
|
679
|
+
relatedExpanded.sort((a, b) => (b.graph_rank || 0) - (a.graph_rank || 0));
|
|
680
|
+
|
|
681
|
+
// Add related expanded results right after their primary
|
|
682
|
+
results.push(...relatedExpanded);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Add any orphaned expanded results (shouldn't happen, but safety net)
|
|
686
|
+
const includedIds = new Set(results.map((r) => r.id));
|
|
687
|
+
const orphaned = expandedResults.filter((e) => !includedIds.has(e.id));
|
|
688
|
+
results.push(...orphaned);
|
|
504
689
|
|
|
505
690
|
return results;
|
|
506
691
|
}
|
package/src/server.js
CHANGED
|
@@ -241,8 +241,8 @@ class MAMAServer {
|
|
|
241
241
|
},
|
|
242
242
|
outcome: {
|
|
243
243
|
type: 'string',
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
description:
|
|
245
|
+
"New outcome status (case-insensitive): 'success' or 'SUCCESS', 'failed' or 'FAILED', 'partial' or 'PARTIAL'.",
|
|
246
246
|
},
|
|
247
247
|
reason: {
|
|
248
248
|
type: 'string',
|
|
@@ -363,7 +363,8 @@ class MAMAServer {
|
|
|
363
363
|
let decisions;
|
|
364
364
|
if (query) {
|
|
365
365
|
// suggest() returns { results: [...] } object or null
|
|
366
|
-
|
|
366
|
+
// Note: suggest() takes options object as second parameter
|
|
367
|
+
const suggestResult = await mama.suggest(query, { limit });
|
|
367
368
|
decisions = suggestResult?.results || [];
|
|
368
369
|
} else {
|
|
369
370
|
decisions = await mama.list(limit);
|
|
@@ -406,6 +407,7 @@ class MAMAServer {
|
|
|
406
407
|
|
|
407
408
|
/**
|
|
408
409
|
* Handle update (decision outcome)
|
|
410
|
+
* Story 3.1: Case-insensitive outcome support
|
|
409
411
|
*/
|
|
410
412
|
async handleUpdate(args) {
|
|
411
413
|
const { id, outcome, reason } = args;
|
|
@@ -414,10 +416,20 @@ class MAMAServer {
|
|
|
414
416
|
return { success: false, message: '❌ Update requires: id, outcome' };
|
|
415
417
|
}
|
|
416
418
|
|
|
417
|
-
|
|
419
|
+
// Story 3.1: Normalize outcome - handle both 'failure' and 'failed' variants
|
|
420
|
+
let normalizedOutcome = outcome.toUpperCase();
|
|
421
|
+
if (normalizedOutcome === 'FAILURE') {
|
|
422
|
+
normalizedOutcome = 'FAILED';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await mama.updateOutcome(id, {
|
|
426
|
+
outcome: normalizedOutcome,
|
|
427
|
+
failure_reason: reason,
|
|
428
|
+
});
|
|
429
|
+
|
|
418
430
|
return {
|
|
419
431
|
success: true,
|
|
420
|
-
message: `✅ Updated ${id} → ${
|
|
432
|
+
message: `✅ Updated ${id} → ${normalizedOutcome}`,
|
|
421
433
|
};
|
|
422
434
|
}
|
|
423
435
|
|
|
@@ -440,7 +452,7 @@ class MAMAServer {
|
|
|
440
452
|
// Pre-warm model in background (don't block MCP startup)
|
|
441
453
|
warmModel().catch((err) => console.error('[MAMA MCP] Model warmup error:', err.message));
|
|
442
454
|
} else {
|
|
443
|
-
console.error('[MAMA MCP] HTTP embedding server skipped (port
|
|
455
|
+
console.error('[MAMA MCP] HTTP embedding server skipped (port unavailable or blocked)');
|
|
444
456
|
}
|
|
445
457
|
|
|
446
458
|
// Start server with stdio transport
|
|
@@ -17,8 +17,37 @@ const mama = require('../mama/mama-api.js');
|
|
|
17
17
|
*/
|
|
18
18
|
const createSaveDecisionTool = (mamaApi) => ({
|
|
19
19
|
name: 'save_decision',
|
|
20
|
-
description:
|
|
21
|
-
|
|
20
|
+
description: `Save your architectural decisions, lessons learned, or insights to MAMA's shared memory.
|
|
21
|
+
|
|
22
|
+
**WHY USE THIS:**
|
|
23
|
+
You are not just an AI assistant; you are a partner in this project. Your insights and reasoning are valuable assets. By saving them here, you allow future agents (including yourself in later sessions) to:
|
|
24
|
+
1. Understand *WHY* a decision was made (Reasoning), not just *WHAT* was done.
|
|
25
|
+
2. Avoid repeating the same mistakes (Lessons Learned).
|
|
26
|
+
3. Build a connected graph of knowledge.
|
|
27
|
+
|
|
28
|
+
**WHEN TO USE (Be Proactive!):**
|
|
29
|
+
- **Decisions**: Whenever you make a significant choice (e.g., "Use SQLite instead of JSON"), save it. Don't wait for the user to ask. If you thought hard about it, it's worth saving.
|
|
30
|
+
- **Insights**: If you discover something new ("Ah, this library conflicts with that one"), save it.
|
|
31
|
+
- **Requests**: If the user says "Remember this" or "Note that", use this tool immediately.
|
|
32
|
+
|
|
33
|
+
**COLLABORATION MODES:**
|
|
34
|
+
When you find similar past decisions (returned in similar_decisions), choose your approach:
|
|
35
|
+
- **build_on**: Extend the existing decision with new insights. Use same topic to create supersedes edge.
|
|
36
|
+
- **debate**: Present a counter-argument with evidence. Explain why the prior decision may be wrong.
|
|
37
|
+
- **synthesize**: Merge multiple decisions into a new unified approach.
|
|
38
|
+
|
|
39
|
+
**5-LAYER REASONING (CoT Guide):**
|
|
40
|
+
Structure your reasoning with these layers for maximum value:
|
|
41
|
+
1. **Context**: What problem/situation prompted this decision?
|
|
42
|
+
2. **Evidence**: What proves this works? (tests, benchmarks, prior experience)
|
|
43
|
+
3. **Alternatives**: What other options were considered and why rejected?
|
|
44
|
+
4. **Risks**: Known limitations or failure modes
|
|
45
|
+
5. **Rationale**: Final reasoning that ties it all together
|
|
46
|
+
|
|
47
|
+
**INSTRUCTIONS:**
|
|
48
|
+
1. **Search First**: Before saving, try to search for related past decisions.
|
|
49
|
+
2. **Link**: If you find a related decision, mention its ID or topic in the 'reasoning' field to create a mental link.
|
|
50
|
+
3. **Reasoning**: Explain your logic clearly so future agents can "empathize" with your decision.`,
|
|
22
51
|
inputSchema: {
|
|
23
52
|
type: 'object',
|
|
24
53
|
properties: {
|
|
@@ -104,24 +133,31 @@ const createSaveDecisionTool = (mamaApi) => ({
|
|
|
104
133
|
}
|
|
105
134
|
|
|
106
135
|
// Call MAMA API (mama.save will handle outcome mapping to DB format)
|
|
107
|
-
|
|
136
|
+
// Story 1.1/1.2: save() now returns enhanced response object
|
|
137
|
+
const result = await mamaApi.save({
|
|
108
138
|
topic,
|
|
109
139
|
decision,
|
|
110
140
|
reasoning,
|
|
111
141
|
confidence,
|
|
112
|
-
type,
|
|
142
|
+
type,
|
|
113
143
|
outcome,
|
|
114
144
|
evidence,
|
|
115
145
|
alternatives,
|
|
116
146
|
risks,
|
|
117
147
|
});
|
|
118
148
|
|
|
149
|
+
// Story 1.2: Return enhanced response with collaborative fields
|
|
119
150
|
return {
|
|
120
|
-
success:
|
|
121
|
-
decision_id: id,
|
|
151
|
+
success: result.success,
|
|
152
|
+
decision_id: result.id,
|
|
122
153
|
topic: topic,
|
|
123
|
-
message: `✅ Decision saved successfully (ID: ${id})`,
|
|
154
|
+
message: `✅ Decision saved successfully (ID: ${result.id})`,
|
|
124
155
|
recall_command: `To recall: mama.recall('${topic}')`,
|
|
156
|
+
// Story 1.1/1.2: Collaborative fields (optional)
|
|
157
|
+
...(result.similar_decisions && { similar_decisions: result.similar_decisions }),
|
|
158
|
+
...(result.warning && { warning: result.warning }),
|
|
159
|
+
...(result.collaboration_hint && { collaboration_hint: result.collaboration_hint }),
|
|
160
|
+
...(result.reasoning_graph && { reasoning_graph: result.reasoning_graph }),
|
|
125
161
|
};
|
|
126
162
|
} catch (error) {
|
|
127
163
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
@@ -12,13 +12,79 @@
|
|
|
12
12
|
|
|
13
13
|
const mama = require('../mama/mama-api.js');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Valid outcome values (uppercase canonical form)
|
|
17
|
+
*/
|
|
18
|
+
const VALID_OUTCOMES = ['SUCCESS', 'FAILED', 'PARTIAL'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Suggest the closest valid outcome for typos/case errors
|
|
22
|
+
* @param {string} input - User's input
|
|
23
|
+
* @returns {string} Suggested outcome
|
|
24
|
+
*/
|
|
25
|
+
function suggestOutcome(input) {
|
|
26
|
+
if (!input || typeof input !== 'string') {
|
|
27
|
+
return 'SUCCESS';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const normalized = input.toUpperCase().trim();
|
|
31
|
+
|
|
32
|
+
// Exact match after normalization
|
|
33
|
+
if (VALID_OUTCOMES.includes(normalized)) {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Prefix matching (e.g., "suc" -> "SUCCESS", "fail" -> "FAILED")
|
|
38
|
+
const prefixMatch = VALID_OUTCOMES.find((o) => o.startsWith(normalized.slice(0, 3)));
|
|
39
|
+
if (prefixMatch) {
|
|
40
|
+
return prefixMatch;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Common typos/variations
|
|
44
|
+
const typoMap = {
|
|
45
|
+
SUCCEED: 'SUCCESS',
|
|
46
|
+
SUCCEEDED: 'SUCCESS',
|
|
47
|
+
PASS: 'SUCCESS',
|
|
48
|
+
PASSED: 'SUCCESS',
|
|
49
|
+
OK: 'SUCCESS',
|
|
50
|
+
FAIL: 'FAILED',
|
|
51
|
+
FAILURE: 'FAILED',
|
|
52
|
+
ERROR: 'FAILED',
|
|
53
|
+
PART: 'PARTIAL',
|
|
54
|
+
PARTIALLY: 'PARTIAL',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return typoMap[normalized] || 'SUCCESS';
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
/**
|
|
16
61
|
* Update outcome tool definition
|
|
17
62
|
*/
|
|
18
63
|
const updateOutcomeTool = {
|
|
19
64
|
name: 'update_outcome',
|
|
20
|
-
description:
|
|
21
|
-
|
|
65
|
+
description: `Update decision outcome after real-world validation.
|
|
66
|
+
|
|
67
|
+
**WHEN TO USE:**
|
|
68
|
+
• Days/weeks later when issues are discovered → mark FAILED with reason
|
|
69
|
+
• After production deployment confirms success → mark SUCCESS
|
|
70
|
+
• After partial success with known limitations → mark PARTIAL with limitation
|
|
71
|
+
|
|
72
|
+
**WHY IMPORTANT:**
|
|
73
|
+
Tracks decision evolution - failure outcomes help future LLMs avoid same mistakes.
|
|
74
|
+
TIP: If decision failed, save a new decision with same topic to supersede it.
|
|
75
|
+
|
|
76
|
+
**OUTCOME TYPES (case-insensitive):**
|
|
77
|
+
• SUCCESS / success: Decision worked as expected
|
|
78
|
+
• FAILED / failed: Decision caused problems (provide failure_reason)
|
|
79
|
+
• PARTIAL / partial: Decision partially worked (provide limitation)
|
|
80
|
+
|
|
81
|
+
**EVIDENCE TYPES:**
|
|
82
|
+
When providing failure_reason or limitation, consider including:
|
|
83
|
+
• url: Link to documentation, PR, or external resource
|
|
84
|
+
• file_path: Path to relevant code file
|
|
85
|
+
• log_snippet: Relevant log output or error message
|
|
86
|
+
• observation: Direct observation or user feedback
|
|
87
|
+
• reasoning_ref: Reference to another decision's reasoning`,
|
|
22
88
|
inputSchema: {
|
|
23
89
|
type: 'object',
|
|
24
90
|
properties: {
|
|
@@ -29,9 +95,8 @@ const updateOutcomeTool = {
|
|
|
29
95
|
},
|
|
30
96
|
outcome: {
|
|
31
97
|
type: 'string',
|
|
32
|
-
enum: ['SUCCESS', 'FAILED', 'PARTIAL'],
|
|
33
98
|
description:
|
|
34
|
-
"Outcome status:\n• 'SUCCESS': Decision worked well in practice\n• 'FAILED': Decision caused problems (explain in failure_reason)\n• 'PARTIAL': Decision partially worked (explain in limitation)",
|
|
99
|
+
"Outcome status (case-insensitive):\n• 'SUCCESS' / 'success': Decision worked well in practice\n• 'FAILED' / 'failed': Decision caused problems (explain in failure_reason)\n• 'PARTIAL' / 'partial': Decision partially worked (explain in limitation)",
|
|
35
100
|
},
|
|
36
101
|
failure_reason: {
|
|
37
102
|
type: 'string',
|
|
@@ -55,23 +120,33 @@ const updateOutcomeTool = {
|
|
|
55
120
|
if (!decisionId || typeof decisionId !== 'string' || decisionId.trim() === '') {
|
|
56
121
|
return {
|
|
57
122
|
success: false,
|
|
58
|
-
message:
|
|
123
|
+
message:
|
|
124
|
+
'❌ Validation error: decisionId must be a non-empty string\n' +
|
|
125
|
+
' 💡 Use search tool to find valid decision IDs.',
|
|
59
126
|
};
|
|
60
127
|
}
|
|
61
128
|
|
|
62
|
-
|
|
129
|
+
// Story 3.1: Case-insensitive outcome normalization
|
|
130
|
+
const normalizedOutcome =
|
|
131
|
+
outcome && typeof outcome === 'string' ? outcome.toUpperCase().trim() : outcome;
|
|
132
|
+
|
|
133
|
+
if (!normalizedOutcome || !VALID_OUTCOMES.includes(normalizedOutcome)) {
|
|
134
|
+
const suggestion = suggestOutcome(outcome);
|
|
63
135
|
return {
|
|
64
136
|
success: false,
|
|
65
|
-
message:
|
|
137
|
+
message:
|
|
138
|
+
'❌ Validation error: outcome must be "SUCCESS", "FAILED", or "PARTIAL"\n' +
|
|
139
|
+
` 💡 Did you mean "${suggestion}"? (case-insensitive, e.g., "success" works too)`,
|
|
66
140
|
};
|
|
67
141
|
}
|
|
68
142
|
|
|
69
143
|
// Validation: failure_reason required for FAILED
|
|
70
|
-
if (
|
|
144
|
+
if (normalizedOutcome === 'FAILED' && (!failure_reason || failure_reason.trim() === '')) {
|
|
71
145
|
return {
|
|
72
146
|
success: false,
|
|
73
147
|
message:
|
|
74
|
-
'❌ Validation error: failure_reason is required when outcome="FAILED"
|
|
148
|
+
'❌ Validation error: failure_reason is required when outcome="FAILED"\n' +
|
|
149
|
+
' 💡 Explain what went wrong so future agents can learn from this.',
|
|
75
150
|
};
|
|
76
151
|
}
|
|
77
152
|
|
|
@@ -90,9 +165,9 @@ const updateOutcomeTool = {
|
|
|
90
165
|
};
|
|
91
166
|
}
|
|
92
167
|
|
|
93
|
-
// Call MAMA API
|
|
168
|
+
// Call MAMA API with normalized outcome
|
|
94
169
|
await mama.updateOutcome(decisionId, {
|
|
95
|
-
outcome,
|
|
170
|
+
outcome: normalizedOutcome,
|
|
96
171
|
failure_reason,
|
|
97
172
|
limitation,
|
|
98
173
|
});
|
|
@@ -101,8 +176,8 @@ const updateOutcomeTool = {
|
|
|
101
176
|
return {
|
|
102
177
|
success: true,
|
|
103
178
|
decision_id: decisionId,
|
|
104
|
-
outcome,
|
|
105
|
-
message: `✅ Decision outcome updated to ${
|
|
179
|
+
outcome: normalizedOutcome,
|
|
180
|
+
message: `✅ Decision outcome updated to ${normalizedOutcome}${
|
|
106
181
|
failure_reason
|
|
107
182
|
? `\n Reason: ${failure_reason.substring(0, 100)}${failure_reason.length > 100 ? '...' : ''}`
|
|
108
183
|
: ''
|