@jungjaehoon/mama-server 1.2.0 → 1.2.2
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
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Migration: 008-fix-auto-link-defaults.sql
|
|
2
|
+
-- Description: Fix auto-generated links and update default values
|
|
3
|
+
-- Date: 2025-11-25
|
|
4
|
+
--
|
|
5
|
+
-- Problem: Migration 006 set incorrect defaults:
|
|
6
|
+
-- - created_by DEFAULT 'user' (should be 'llm' for auto-generated)
|
|
7
|
+
-- - approved_by_user DEFAULT 1 (should be 0 for pending approval)
|
|
8
|
+
--
|
|
9
|
+
-- This caused auto-generated links to be indistinguishable from user-approved links.
|
|
10
|
+
--
|
|
11
|
+
-- Solution:
|
|
12
|
+
-- 1. Identify auto-generated links by pattern matching reason field
|
|
13
|
+
-- 2. Mark them as created_by='llm', approved_by_user=0
|
|
14
|
+
-- 3. Note: Cannot change column defaults in SQLite, so application code must handle this
|
|
15
|
+
|
|
16
|
+
-- Step 1: Mark auto-generated links (identifiable by pattern in reason field)
|
|
17
|
+
-- Pattern: "Refines previous approach (similarity: X.XX)" or similar auto-generated reasons
|
|
18
|
+
UPDATE decision_edges
|
|
19
|
+
SET
|
|
20
|
+
created_by = 'llm',
|
|
21
|
+
approved_by_user = 0
|
|
22
|
+
WHERE reason LIKE '%similarity:%'
|
|
23
|
+
OR reason LIKE '%Refines previous approach%'
|
|
24
|
+
OR reason LIKE '%auto%'
|
|
25
|
+
OR reason LIKE '%Auto%';
|
|
26
|
+
|
|
27
|
+
-- Step 2: Log the migration action
|
|
28
|
+
INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
|
|
29
|
+
SELECT
|
|
30
|
+
from_id,
|
|
31
|
+
to_id,
|
|
32
|
+
relationship,
|
|
33
|
+
'deprecated',
|
|
34
|
+
'system',
|
|
35
|
+
'Migration 008: Marked as auto-generated link requiring approval',
|
|
36
|
+
unixepoch()
|
|
37
|
+
FROM decision_edges
|
|
38
|
+
WHERE created_by = 'llm' AND approved_by_user = 0;
|
|
39
|
+
|
|
40
|
+
-- Note: After this migration:
|
|
41
|
+
-- - Auto-generated links will require explicit approval via approve_link tool
|
|
42
|
+
-- - Users can review pending links with get_pending_links tool
|
|
43
|
+
-- - New auto-generated links should set created_by='llm', approved_by_user=0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Migration: 009-remove-auto-links.sql
|
|
2
|
+
-- Description: Remove all auto-generated links (noise cleanup)
|
|
3
|
+
-- Date: 2025-11-25
|
|
4
|
+
--
|
|
5
|
+
-- Context: 2025-11-25 architecture decision
|
|
6
|
+
-- - Analyzed 462 links: 366 refines (100% cross-topic noise), 56 supersedes (valid), 40 contradicts (mostly noise)
|
|
7
|
+
-- - Key insight: LLM can infer decision evolution from time-ordered search results
|
|
8
|
+
-- - Auto-link generation removed from decision-tracker.js
|
|
9
|
+
-- - MCP tools consolidated from 11 to 4 (save, search, update, load_checkpoint)
|
|
10
|
+
--
|
|
11
|
+
-- This migration removes:
|
|
12
|
+
-- 1. All 'refines' links (100% cross-topic noise)
|
|
13
|
+
-- 2. All 'contradicts' links (unreliable without LLM judgment)
|
|
14
|
+
-- 3. Keeps 'supersedes' links (same-topic, reliable)
|
|
15
|
+
|
|
16
|
+
-- Step 1: Backup to audit log before deletion
|
|
17
|
+
INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
|
|
18
|
+
SELECT
|
|
19
|
+
from_id,
|
|
20
|
+
to_id,
|
|
21
|
+
relationship,
|
|
22
|
+
'deprecated',
|
|
23
|
+
'system',
|
|
24
|
+
'Migration 009: Removed auto-generated noise links (refines, contradicts)',
|
|
25
|
+
unixepoch()
|
|
26
|
+
FROM decision_edges
|
|
27
|
+
WHERE relationship IN ('refines', 'contradicts');
|
|
28
|
+
|
|
29
|
+
-- Step 2: Delete noise links
|
|
30
|
+
DELETE FROM decision_edges
|
|
31
|
+
WHERE relationship IN ('refines', 'contradicts');
|
|
32
|
+
|
|
33
|
+
-- Step 3: Verify remaining links are valid (supersedes only)
|
|
34
|
+
-- This is a sanity check - should only have supersedes links remaining
|
|
35
|
+
-- SELECT relationship, COUNT(*) FROM decision_edges GROUP BY relationship;
|
|
36
|
+
|
|
37
|
+
-- Note: After this migration:
|
|
38
|
+
-- - Only 'supersedes' edges remain (same-topic, created when decision replaces previous)
|
|
39
|
+
-- - LLM will infer refines/contradicts from search results
|
|
40
|
+
-- - No more auto-link generation in decision-tracker.js
|
|
@@ -87,9 +87,11 @@ async function createSupersedesEdge(fromId, toId, reason) {
|
|
|
87
87
|
const adapter = getAdapter();
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
|
+
// Auto-generated links: created_by='llm', approved_by_user=0 (pending approval)
|
|
91
|
+
// This ensures they appear in get_pending_links and require explicit approval
|
|
90
92
|
const stmt = adapter.prepare(`
|
|
91
|
-
INSERT INTO decision_edges (from_id, to_id, relationship, reason, created_at)
|
|
92
|
-
VALUES (?, ?, 'supersedes', ?,
|
|
93
|
+
INSERT INTO decision_edges (from_id, to_id, relationship, reason, created_at, created_by, approved_by_user)
|
|
94
|
+
VALUES (?, ?, 'supersedes', ?, ?, 'llm', 0)
|
|
93
95
|
`);
|
|
94
96
|
|
|
95
97
|
await stmt.run(fromId, toId, reason, Date.now());
|
|
@@ -173,154 +175,22 @@ function detectRefinement(_detection, _sessionContext) {
|
|
|
173
175
|
return null;
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
VALUES (?, ?, 'refines', ?, ?)
|
|
193
|
-
`);
|
|
194
|
-
|
|
195
|
-
await stmt.run(fromId, toId, reason, Date.now());
|
|
196
|
-
} catch (error) {
|
|
197
|
-
throw new Error(`Failed to create refines edge: ${error.message}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Detect conflicting decisions (same topic, different decision)
|
|
203
|
-
*
|
|
204
|
-
* Task 5.4: Detect conflicting decisions
|
|
205
|
-
* AC #2, #5: Relationship types
|
|
206
|
-
*
|
|
207
|
-
* @param {string} topic - Decision topic
|
|
208
|
-
* @param {string} newDecision - New decision value
|
|
209
|
-
* @param {string} newId - New decision ID (to exclude from search)
|
|
210
|
-
* @returns {Promise<Array<Object>>} Conflicting decisions
|
|
211
|
-
*/
|
|
212
|
-
async function detectConflicts(topic, newDecision, newId) {
|
|
213
|
-
const adapter = getAdapter();
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
// Find active decisions on same topic with different decision value
|
|
217
|
-
const stmt = adapter.prepare(`
|
|
218
|
-
SELECT * FROM decisions
|
|
219
|
-
WHERE topic = ?
|
|
220
|
-
AND decision != ?
|
|
221
|
-
AND id != ?
|
|
222
|
-
AND superseded_by IS NULL
|
|
223
|
-
AND outcome IS NULL
|
|
224
|
-
ORDER BY created_at DESC
|
|
225
|
-
`);
|
|
226
|
-
|
|
227
|
-
const conflicts = await stmt.all(topic, newDecision, newId);
|
|
228
|
-
return conflicts || [];
|
|
229
|
-
} catch (error) {
|
|
230
|
-
throw new Error(`Failed to detect conflicts: ${error.message}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Create contradicts edge (conflicting relationship)
|
|
236
|
-
*
|
|
237
|
-
* Task 5.5: Create contradicts edges for conflicts
|
|
238
|
-
* AC #2, #5: Relationship types
|
|
239
|
-
*
|
|
240
|
-
* @param {string} fromId - New decision ID
|
|
241
|
-
* @param {string} toId - Conflicting decision ID
|
|
242
|
-
* @param {string} reason - Reason for contradiction
|
|
243
|
-
*/
|
|
244
|
-
async function createContradictsEdge(fromId, toId, reason) {
|
|
245
|
-
const adapter = getAdapter();
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const stmt = adapter.prepare(`
|
|
249
|
-
INSERT INTO decision_edges (from_id, to_id, relationship, reason, created_at)
|
|
250
|
-
VALUES (?, ?, 'contradicts', ?, ?)
|
|
251
|
-
`);
|
|
252
|
-
|
|
253
|
-
await stmt.run(fromId, toId, reason, Date.now());
|
|
254
|
-
} catch (error) {
|
|
255
|
-
throw new Error(`Failed to create contradicts edge: ${error.message}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Find semantically related decisions using vector search
|
|
261
|
-
*
|
|
262
|
-
* Story 014.14: AC #1 - Vector Search for Related Decisions
|
|
263
|
-
*
|
|
264
|
-
* @param {string} decisionId - New decision ID (exclude from search)
|
|
265
|
-
* @param {string} topic - Decision topic
|
|
266
|
-
* @param {string} decision - Decision text
|
|
267
|
-
* @param {string} reasoning - Decision reasoning
|
|
268
|
-
* @returns {Promise<Array<Object>>} Related decisions with similarity scores
|
|
269
|
-
*/
|
|
270
|
-
async function findRelatedDecisions(decisionId, topic, decision, reasoning) {
|
|
271
|
-
const { queryVectorSearch } = require('./memory-store');
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
// Combine decision + reasoning for semantic search
|
|
275
|
-
const searchText = `${decision}. ${reasoning}`;
|
|
276
|
-
|
|
277
|
-
// Vector search params
|
|
278
|
-
const params = {
|
|
279
|
-
query: searchText,
|
|
280
|
-
limit: 10, // Get more candidates for filtering
|
|
281
|
-
threshold: 0.75, // Minimum similarity (Story 014.14: AC #1)
|
|
282
|
-
timeWindow: 90 * 24 * 60 * 60 * 1000, // Last 90 days (Story 014.14: AC #1)
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// Query vector database
|
|
286
|
-
const results = await queryVectorSearch(params);
|
|
287
|
-
|
|
288
|
-
// Filter out self and return top 5
|
|
289
|
-
return results.filter((r) => r.id !== decisionId).slice(0, 5);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
info(`[decision-tracker] Vector search failed, returning empty: ${error.message}`);
|
|
292
|
-
return []; // Graceful degradation
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Detect if reasoning contains conflict keywords
|
|
298
|
-
*
|
|
299
|
-
* Story 014.14: AC #3 - Contradicts Edge Detection
|
|
300
|
-
*
|
|
301
|
-
* @param {string} newReasoning - New decision reasoning
|
|
302
|
-
* @param {string} oldReasoning - Previous decision reasoning
|
|
303
|
-
* @returns {boolean} True if conflicting
|
|
304
|
-
*/
|
|
305
|
-
function isConflicting(newReasoning, oldReasoning) {
|
|
306
|
-
const conflictKeywords = [
|
|
307
|
-
'instead of',
|
|
308
|
-
'replace',
|
|
309
|
-
'not',
|
|
310
|
-
'contrary to',
|
|
311
|
-
'different from',
|
|
312
|
-
'opposite',
|
|
313
|
-
'revert',
|
|
314
|
-
'undo',
|
|
315
|
-
'abandon',
|
|
316
|
-
'deprecate',
|
|
317
|
-
'remove',
|
|
318
|
-
];
|
|
319
|
-
|
|
320
|
-
const combined = `${newReasoning} ${oldReasoning}`.toLowerCase();
|
|
321
|
-
|
|
322
|
-
return conflictKeywords.some((keyword) => combined.includes(keyword));
|
|
323
|
-
}
|
|
178
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
// NOTE: Auto-link functions REMOVED in v1.2.0
|
|
180
|
+
//
|
|
181
|
+
// Removed functions:
|
|
182
|
+
// - createRefinesEdge
|
|
183
|
+
// - detectConflicts
|
|
184
|
+
// - createContradictsEdge
|
|
185
|
+
// - findRelatedDecisions
|
|
186
|
+
// - isConflicting
|
|
187
|
+
//
|
|
188
|
+
// Reason: LLM can infer decision evolution from time-ordered search results.
|
|
189
|
+
// Auto-links created 366 noise edges (100% cross-topic).
|
|
190
|
+
// Only supersedes (same topic) is reliable.
|
|
191
|
+
//
|
|
192
|
+
// See: CHANGELOG.md v1.2.0 - 2025-11-25
|
|
193
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
324
194
|
|
|
325
195
|
/**
|
|
326
196
|
* Learn Decision Function (Main API)
|
|
@@ -447,62 +317,14 @@ async function learnDecision(detection, toolExecution, sessionContext) {
|
|
|
447
317
|
}
|
|
448
318
|
|
|
449
319
|
// ════════════════════════════════════════════════════════
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
for (const related of relatedDecisions) {
|
|
460
|
-
const similarity = related.similarity;
|
|
461
|
-
|
|
462
|
-
// Skip if already has supersedes relationship
|
|
463
|
-
if (related.superseded_by || related.id === previous?.id) {
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// High similarity → refines edge (Story 014.14: AC #2)
|
|
468
|
-
if (similarity > 0.85) {
|
|
469
|
-
const reason = `Refines previous approach (similarity: ${similarity.toFixed(2)})`;
|
|
470
|
-
await createRefinesEdge(decisionId, related.id, reason);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Medium similarity + conflict keywords → contradicts edge (Story 014.14: AC #3)
|
|
474
|
-
else if (similarity > 0.75 && isConflicting(detection.reasoning, related.reasoning)) {
|
|
475
|
-
const reason = `Contradicts previous approach (similarity: ${similarity.toFixed(2)})`;
|
|
476
|
-
await createContradictsEdge(decisionId, related.id, reason);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ════════════════════════════════════════════════════════
|
|
481
|
-
// Task 5.3: Create Refines Edges (if multi-parent refinement)
|
|
482
|
-
// ════════════════════════════════════════════════════════
|
|
483
|
-
if (refinedFrom && refinedFrom.length > 0) {
|
|
484
|
-
// AC #5: Multi-parent refinement
|
|
485
|
-
await Promise.all(
|
|
486
|
-
refinedFrom.map(async (parentId) => {
|
|
487
|
-
const reason = `Refined decision from multiple parents`;
|
|
488
|
-
await createRefinesEdge(decisionId, parentId, reason);
|
|
489
|
-
})
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ════════════════════════════════════════════════════════
|
|
494
|
-
// Task 5.4, 5.5: Detect and Create Contradicts Edges
|
|
320
|
+
// NOTE: Auto-link generation (refines, contradicts) REMOVED
|
|
321
|
+
//
|
|
322
|
+
// Reason: LLM can infer decision evolution from time-ordered
|
|
323
|
+
// search results. Auto-links created 366 noise edges (100%
|
|
324
|
+
// cross-topic). Only supersedes (same topic) is reliable.
|
|
325
|
+
//
|
|
326
|
+
// See: 2025-11-25 discussion on decision tracking algorithm
|
|
495
327
|
// ════════════════════════════════════════════════════════
|
|
496
|
-
const conflicts = await detectConflicts(detection.topic, detection.decision, decisionId);
|
|
497
|
-
if (conflicts.length > 0) {
|
|
498
|
-
// AC #2, #5: Conflicting decisions
|
|
499
|
-
await Promise.all(
|
|
500
|
-
conflicts.map(async (conflict) => {
|
|
501
|
-
const reason = `Conflicting decision: "${conflict.decision}" vs "${detection.decision}"`;
|
|
502
|
-
await createContradictsEdge(decisionId, conflict.id, reason);
|
|
503
|
-
})
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
328
|
|
|
507
329
|
// ════════════════════════════════════════════════════════
|
|
508
330
|
// Story 014.7.6: Generate notification if needs validation
|
|
@@ -562,6 +384,9 @@ function updateConfidence(prior, evidence) {
|
|
|
562
384
|
}
|
|
563
385
|
|
|
564
386
|
// Export API
|
|
387
|
+
// NOTE: Auto-link functions (createRefinesEdge, createContradictsEdge,
|
|
388
|
+
// findRelatedDecisions, isConflicting, detectConflicts) removed from exports.
|
|
389
|
+
// LLM infers relationships from search results instead.
|
|
565
390
|
module.exports = {
|
|
566
391
|
learnDecision,
|
|
567
392
|
generateDecisionId,
|
|
@@ -571,9 +396,4 @@ module.exports = {
|
|
|
571
396
|
calculateCombinedConfidence,
|
|
572
397
|
detectRefinement,
|
|
573
398
|
updateConfidence,
|
|
574
|
-
createRefinesEdge,
|
|
575
|
-
detectConflicts,
|
|
576
|
-
createContradictsEdge,
|
|
577
|
-
findRelatedDecisions, // Story 014.14: AC #1
|
|
578
|
-
isConflicting, // Story 014.14: AC #3
|
|
579
399
|
};
|
package/src/mama/mama-api.js
CHANGED
|
@@ -905,6 +905,36 @@ async function loadCheckpoint() {
|
|
|
905
905
|
}
|
|
906
906
|
}
|
|
907
907
|
|
|
908
|
+
/**
|
|
909
|
+
* List recent checkpoints (New Feature: Session Continuity)
|
|
910
|
+
*
|
|
911
|
+
* @param {number} limit - Max number of checkpoints to return
|
|
912
|
+
* @returns {Promise<Array>} Recent checkpoints
|
|
913
|
+
*/
|
|
914
|
+
async function listCheckpoints(limit = 10) {
|
|
915
|
+
try {
|
|
916
|
+
const adapter = getAdapter();
|
|
917
|
+
const stmt = adapter.prepare(`
|
|
918
|
+
SELECT * FROM checkpoints
|
|
919
|
+
ORDER BY timestamp DESC
|
|
920
|
+
LIMIT ?
|
|
921
|
+
`);
|
|
922
|
+
|
|
923
|
+
const checkpoints = stmt.all(limit);
|
|
924
|
+
|
|
925
|
+
return checkpoints.map((c) => {
|
|
926
|
+
try {
|
|
927
|
+
c.open_files = JSON.parse(c.open_files);
|
|
928
|
+
} catch (e) {
|
|
929
|
+
c.open_files = [];
|
|
930
|
+
}
|
|
931
|
+
return c;
|
|
932
|
+
});
|
|
933
|
+
} catch (error) {
|
|
934
|
+
throw new Error(`Failed to list checkpoints: ${error.message}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
908
938
|
/**
|
|
909
939
|
* Propose a new link between decisions (Epic 3 - Story 3.1)
|
|
910
940
|
*
|
|
@@ -2307,35 +2337,42 @@ function formatQualityReportMarkdown(report) {
|
|
|
2307
2337
|
* 3. Claude-First Design - Claude decides what to save
|
|
2308
2338
|
* 4. Non-Intrusive - Silent failures for helpers (suggest)
|
|
2309
2339
|
*/
|
|
2340
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
2341
|
+
// MAMA API - Simplified to 4 MCP tools (2025-11-25)
|
|
2342
|
+
//
|
|
2343
|
+
// Design: LLM can infer decision evolution from time-ordered search results
|
|
2344
|
+
// More tools = more constraints = less LLM flexibility
|
|
2345
|
+
//
|
|
2346
|
+
// Retained internal functions for future use, but MCP exposes only:
|
|
2347
|
+
// save, search, update, load_checkpoint
|
|
2348
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
2310
2349
|
const mama = {
|
|
2350
|
+
// Core functions (used by 4 MCP tools)
|
|
2311
2351
|
save,
|
|
2312
|
-
recall,
|
|
2313
|
-
updateOutcome,
|
|
2314
2352
|
suggest,
|
|
2315
2353
|
list: listDecisions,
|
|
2354
|
+
listCheckpoints,
|
|
2355
|
+
updateOutcome,
|
|
2316
2356
|
saveCheckpoint,
|
|
2317
2357
|
loadCheckpoint,
|
|
2318
|
-
//
|
|
2358
|
+
// Legacy functions (retained for internal use, not exposed via MCP)
|
|
2359
|
+
recall,
|
|
2319
2360
|
proposeLink,
|
|
2320
2361
|
approveLink,
|
|
2321
2362
|
rejectLink,
|
|
2322
2363
|
getPendingLinks,
|
|
2323
2364
|
deprecateAutoLinks,
|
|
2324
|
-
// Epic 4: Quality Metrics & Observability
|
|
2325
2365
|
calculateCoverage,
|
|
2326
2366
|
calculateQuality,
|
|
2327
2367
|
generateQualityReport,
|
|
2328
|
-
// Epic 4: Restart Metrics (Story 4.2)
|
|
2329
2368
|
logRestartAttempt,
|
|
2330
2369
|
calculateRestartSuccessRate,
|
|
2331
2370
|
calculateRestartLatency,
|
|
2332
2371
|
getRestartMetrics,
|
|
2333
|
-
// Epic 5: Migration & Cleanup (Story 5.1)
|
|
2334
2372
|
scanAutoLinks,
|
|
2335
2373
|
createLinkBackup,
|
|
2336
2374
|
generatePreCleanupReport,
|
|
2337
2375
|
restoreLinkBackup,
|
|
2338
|
-
// Epic 5: Migration & Cleanup (Story 5.2)
|
|
2339
2376
|
verifyBackupExists,
|
|
2340
2377
|
deleteAutoLinks,
|
|
2341
2378
|
validateCleanupResult,
|
package/src/server.js
CHANGED
|
@@ -27,30 +27,10 @@ const {
|
|
|
27
27
|
ListToolsRequestSchema,
|
|
28
28
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
29
29
|
|
|
30
|
-
// Import MAMA tools
|
|
31
|
-
|
|
32
|
-
const {
|
|
33
|
-
const
|
|
34
|
-
const { listDecisionsTool } = require('./tools/list-decisions.js');
|
|
35
|
-
const { updateOutcomeTool } = require('./tools/update-outcome.js');
|
|
36
|
-
const { saveCheckpointTool, loadCheckpointTool } = require('./tools/checkpoint-tools.js');
|
|
37
|
-
const {
|
|
38
|
-
proposeLinkTool,
|
|
39
|
-
approveLinkTool,
|
|
40
|
-
rejectLinkTool,
|
|
41
|
-
getPendingLinksTool,
|
|
42
|
-
deprecateAutoLinksTool,
|
|
43
|
-
scanAutoLinksTool,
|
|
44
|
-
createLinkBackupTool,
|
|
45
|
-
generateCleanupReportTool,
|
|
46
|
-
restoreLinkBackupTool,
|
|
47
|
-
executeLinkCleanupTool,
|
|
48
|
-
validateCleanupResultTool,
|
|
49
|
-
} = require('./tools/link-tools.js');
|
|
50
|
-
const {
|
|
51
|
-
generateQualityReportTool,
|
|
52
|
-
getRestartMetricsTool,
|
|
53
|
-
} = require('./tools/quality-metrics-tools.js');
|
|
30
|
+
// Import MAMA tools - Simplified to 4 core tools (2025-11-25 refactor)
|
|
31
|
+
// Rationale: LLM can infer relationships from search results, fewer tools = more flexibility
|
|
32
|
+
const { loadCheckpointTool } = require('./tools/checkpoint-tools.js');
|
|
33
|
+
const mama = require('./mama/mama-api.js');
|
|
54
34
|
|
|
55
35
|
// Import core modules
|
|
56
36
|
const { initDB } = require('./mama/db-manager.js');
|
|
@@ -162,227 +142,129 @@ class MAMAServer {
|
|
|
162
142
|
}
|
|
163
143
|
|
|
164
144
|
setupHandlers() {
|
|
165
|
-
// List available tools
|
|
145
|
+
// List available tools - Simplified to 4 core tools (2025-11-25)
|
|
146
|
+
// Design principle: LLM infers relationships from search results
|
|
147
|
+
// Fewer tools = more flexibility, less constraint
|
|
166
148
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
167
149
|
tools: [
|
|
150
|
+
// 1. SAVE - Unified save for decisions and checkpoints
|
|
168
151
|
{
|
|
169
|
-
name: '
|
|
170
|
-
description:
|
|
152
|
+
name: 'save',
|
|
153
|
+
description:
|
|
154
|
+
"Save a decision or checkpoint to MAMA's memory. type='decision': architectural choices, lessons learned (same topic = newer supersedes older, tracking evolution). type='checkpoint': session state to resume later.",
|
|
171
155
|
inputSchema: {
|
|
172
156
|
type: 'object',
|
|
173
157
|
properties: {
|
|
158
|
+
type: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
enum: ['decision', 'checkpoint'],
|
|
161
|
+
description: "What to save: 'decision' or 'checkpoint'",
|
|
162
|
+
},
|
|
163
|
+
// Decision fields
|
|
174
164
|
topic: {
|
|
175
165
|
type: 'string',
|
|
176
166
|
description:
|
|
177
|
-
"Decision
|
|
167
|
+
"[Decision] Topic identifier (e.g., 'auth_strategy'). Same topic = new decision supersedes previous, creating evolution chain.",
|
|
178
168
|
},
|
|
179
169
|
decision: {
|
|
180
170
|
type: 'string',
|
|
181
|
-
description: "The decision made (e.g., 'Use JWT with refresh tokens').",
|
|
171
|
+
description: "[Decision] The decision made (e.g., 'Use JWT with refresh tokens').",
|
|
182
172
|
},
|
|
183
173
|
reasoning: {
|
|
184
174
|
type: 'string',
|
|
185
175
|
description:
|
|
186
|
-
'Why this decision was made.
|
|
176
|
+
'[Decision] Why this decision was made. Include 5-layer narrative: (1) Context - what problem/situation; (2) Evidence - what proves this works (tests, benchmarks, prior experience); (3) Alternatives - what other options were considered and why rejected; (4) Risks - known limitations or failure modes; (5) Rationale - final reasoning for this choice.',
|
|
187
177
|
},
|
|
188
178
|
confidence: {
|
|
189
179
|
type: 'number',
|
|
190
|
-
description: 'Confidence
|
|
180
|
+
description: '[Decision] Confidence 0.0-1.0. Default: 0.5',
|
|
191
181
|
minimum: 0,
|
|
192
182
|
maximum: 1,
|
|
193
183
|
},
|
|
194
|
-
|
|
184
|
+
// Checkpoint fields
|
|
185
|
+
summary: {
|
|
195
186
|
type: 'string',
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
description:
|
|
188
|
+
'[Checkpoint] Session state summary. Use 4-section format: (1) 🎯 Goal & Progress - what was the goal, where did you stop; (2) ✅ Evidence - mark each item as Verified/Not run/Assumed with proof; (3) ⏳ Unfinished & Risks - incomplete work, blockers, unknowns; (4) 🚦 Next Agent Briefing - Definition of Done, quick health checks to run first.',
|
|
198
189
|
},
|
|
199
|
-
|
|
190
|
+
next_steps: {
|
|
200
191
|
type: 'string',
|
|
201
|
-
|
|
202
|
-
|
|
192
|
+
description:
|
|
193
|
+
'[Checkpoint] Instructions for next session: DoD (Definition of Done), quick verification commands (npm test, curl health), constraints/cautions.',
|
|
203
194
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
name: 'recall_decision',
|
|
210
|
-
description: 'Recall full decision history for a specific topic.',
|
|
211
|
-
inputSchema: {
|
|
212
|
-
type: 'object',
|
|
213
|
-
properties: {
|
|
214
|
-
topic: {
|
|
215
|
-
type: 'string',
|
|
216
|
-
description: 'Decision topic to recall',
|
|
195
|
+
open_files: {
|
|
196
|
+
type: 'array',
|
|
197
|
+
items: { type: 'string' },
|
|
198
|
+
description: '[Checkpoint] Currently relevant files.',
|
|
217
199
|
},
|
|
218
200
|
},
|
|
219
|
-
required: ['
|
|
201
|
+
required: ['type'],
|
|
220
202
|
},
|
|
221
203
|
},
|
|
204
|
+
// 2. SEARCH - Unified search across decisions and checkpoints
|
|
222
205
|
{
|
|
223
|
-
name: '
|
|
224
|
-
description:
|
|
206
|
+
name: 'search',
|
|
207
|
+
description:
|
|
208
|
+
'Search decisions and checkpoints. With query: semantic search. Without query: returns recent items by time.',
|
|
225
209
|
inputSchema: {
|
|
226
210
|
type: 'object',
|
|
227
211
|
properties: {
|
|
228
|
-
|
|
212
|
+
query: {
|
|
229
213
|
type: 'string',
|
|
230
|
-
description:
|
|
214
|
+
description:
|
|
215
|
+
'Search query (optional). If empty, returns recent items sorted by time.',
|
|
231
216
|
},
|
|
232
|
-
|
|
233
|
-
type: '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
maximum: 1,
|
|
217
|
+
type: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
enum: ['all', 'decision', 'checkpoint'],
|
|
220
|
+
description: "Filter by type. Default: 'all'",
|
|
237
221
|
},
|
|
238
|
-
},
|
|
239
|
-
required: ['userQuestion'],
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
name: 'list_decisions',
|
|
244
|
-
description: 'List recent decisions with optional limit',
|
|
245
|
-
inputSchema: {
|
|
246
|
-
type: 'object',
|
|
247
|
-
properties: {
|
|
248
222
|
limit: {
|
|
249
223
|
type: 'number',
|
|
250
|
-
description: 'Maximum
|
|
224
|
+
description: 'Maximum results. Default: 10',
|
|
251
225
|
},
|
|
252
226
|
},
|
|
253
227
|
},
|
|
254
228
|
},
|
|
229
|
+
// 3. UPDATE - Update decision outcome
|
|
255
230
|
{
|
|
256
|
-
name: '
|
|
257
|
-
description:
|
|
231
|
+
name: 'update',
|
|
232
|
+
description:
|
|
233
|
+
'Update an existing decision outcome (success/failure/partial). Use after trying a decision to track what worked.',
|
|
258
234
|
inputSchema: {
|
|
259
235
|
type: 'object',
|
|
260
236
|
properties: {
|
|
261
|
-
|
|
237
|
+
id: {
|
|
262
238
|
type: 'string',
|
|
263
|
-
description:
|
|
239
|
+
description: 'Decision ID to update.',
|
|
264
240
|
},
|
|
265
241
|
outcome: {
|
|
266
242
|
type: 'string',
|
|
267
|
-
enum: ['
|
|
268
|
-
description: 'New outcome status',
|
|
269
|
-
},
|
|
270
|
-
failure_reason: {
|
|
271
|
-
type: 'string',
|
|
272
|
-
description: "Why the decision failed (REQUIRED if outcome='FAILED').",
|
|
243
|
+
enum: ['success', 'failure', 'partial'],
|
|
244
|
+
description: 'New outcome status.',
|
|
273
245
|
},
|
|
274
|
-
|
|
246
|
+
reason: {
|
|
275
247
|
type: 'string',
|
|
276
|
-
description:
|
|
248
|
+
description: 'Why it succeeded/failed/was partial.',
|
|
277
249
|
},
|
|
278
250
|
},
|
|
279
|
-
required: ['
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: 'save_checkpoint',
|
|
284
|
-
description:
|
|
285
|
-
'Save the current session state (checkpoint). Format: 1) Goal & Progress (honest about unfinished), 2) Evidence with status [Verified/Not run/Assumed], 3) Unfinished & Risks, 4) Next Agent briefing (Definition of Done + quick health-check commands).',
|
|
286
|
-
inputSchema: {
|
|
287
|
-
type: 'object',
|
|
288
|
-
properties: {
|
|
289
|
-
summary: {
|
|
290
|
-
type: 'string',
|
|
291
|
-
description:
|
|
292
|
-
'Summary of the current session state, what was accomplished, and what is pending.',
|
|
293
|
-
},
|
|
294
|
-
open_files: {
|
|
295
|
-
type: 'array',
|
|
296
|
-
items: { type: 'string' },
|
|
297
|
-
description: 'List of currently relevant or open files.',
|
|
298
|
-
},
|
|
299
|
-
next_steps: {
|
|
300
|
-
type: 'string',
|
|
301
|
-
description: 'Clear instructions for the next session on what to do next.',
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
required: ['summary'],
|
|
251
|
+
required: ['id', 'outcome'],
|
|
305
252
|
},
|
|
306
253
|
},
|
|
254
|
+
// 4. LOAD_CHECKPOINT - Resume previous session
|
|
307
255
|
{
|
|
308
256
|
name: 'load_checkpoint',
|
|
309
257
|
description:
|
|
310
|
-
'Load the latest
|
|
258
|
+
'Load the latest checkpoint to resume a previous session. Use at session start.',
|
|
311
259
|
inputSchema: {
|
|
312
260
|
type: 'object',
|
|
313
261
|
properties: {},
|
|
314
262
|
},
|
|
315
263
|
},
|
|
316
|
-
// Epic 3: Link Collaboration & Governance
|
|
317
|
-
{
|
|
318
|
-
name: 'propose_link',
|
|
319
|
-
description: proposeLinkTool.description,
|
|
320
|
-
inputSchema: proposeLinkTool.inputSchema,
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
name: 'approve_link',
|
|
324
|
-
description: approveLinkTool.description,
|
|
325
|
-
inputSchema: approveLinkTool.inputSchema,
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: 'reject_link',
|
|
329
|
-
description: rejectLinkTool.description,
|
|
330
|
-
inputSchema: rejectLinkTool.inputSchema,
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
name: 'get_pending_links',
|
|
334
|
-
description: getPendingLinksTool.description,
|
|
335
|
-
inputSchema: getPendingLinksTool.inputSchema,
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
name: 'deprecate_auto_links',
|
|
339
|
-
description: deprecateAutoLinksTool.description,
|
|
340
|
-
inputSchema: deprecateAutoLinksTool.inputSchema,
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: 'scan_auto_links',
|
|
344
|
-
description: scanAutoLinksTool.description,
|
|
345
|
-
inputSchema: scanAutoLinksTool.inputSchema,
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
name: 'create_link_backup',
|
|
349
|
-
description: createLinkBackupTool.description,
|
|
350
|
-
inputSchema: createLinkBackupTool.inputSchema,
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
name: 'generate_cleanup_report',
|
|
354
|
-
description: generateCleanupReportTool.description,
|
|
355
|
-
inputSchema: generateCleanupReportTool.inputSchema,
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: 'restore_link_backup',
|
|
359
|
-
description: restoreLinkBackupTool.description,
|
|
360
|
-
inputSchema: restoreLinkBackupTool.inputSchema,
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
name: 'generate_quality_report',
|
|
364
|
-
description: generateQualityReportTool.description,
|
|
365
|
-
inputSchema: generateQualityReportTool.inputSchema,
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
name: 'get_restart_metrics',
|
|
369
|
-
description: getRestartMetricsTool.description,
|
|
370
|
-
inputSchema: getRestartMetricsTool.inputSchema,
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
name: 'execute_link_cleanup',
|
|
374
|
-
description: executeLinkCleanupTool.description,
|
|
375
|
-
inputSchema: executeLinkCleanupTool.inputSchema,
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: 'validate_cleanup_result',
|
|
379
|
-
description: validateCleanupResultTool.description,
|
|
380
|
-
inputSchema: validateCleanupResultTool.inputSchema,
|
|
381
|
-
},
|
|
382
264
|
],
|
|
383
265
|
}));
|
|
384
266
|
|
|
385
|
-
// Handle tool execution
|
|
267
|
+
// Handle tool execution - 4 core tools only
|
|
386
268
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
387
269
|
const { name, arguments: args } = request.params;
|
|
388
270
|
|
|
@@ -390,66 +272,18 @@ class MAMAServer {
|
|
|
390
272
|
let result;
|
|
391
273
|
|
|
392
274
|
switch (name) {
|
|
393
|
-
case '
|
|
394
|
-
result = await
|
|
395
|
-
break;
|
|
396
|
-
case 'recall_decision':
|
|
397
|
-
result = await recallDecisionTool.handler(args);
|
|
275
|
+
case 'save':
|
|
276
|
+
result = await this.handleSave(args);
|
|
398
277
|
break;
|
|
399
|
-
case '
|
|
400
|
-
result = await
|
|
278
|
+
case 'search':
|
|
279
|
+
result = await this.handleSearch(args);
|
|
401
280
|
break;
|
|
402
|
-
case '
|
|
403
|
-
result = await
|
|
404
|
-
break;
|
|
405
|
-
case 'update_outcome':
|
|
406
|
-
result = await updateOutcomeTool.handler(args);
|
|
407
|
-
break;
|
|
408
|
-
case 'save_checkpoint':
|
|
409
|
-
result = await saveCheckpointTool.handler(args);
|
|
281
|
+
case 'update':
|
|
282
|
+
result = await this.handleUpdate(args);
|
|
410
283
|
break;
|
|
411
284
|
case 'load_checkpoint':
|
|
412
285
|
result = await loadCheckpointTool.handler(args);
|
|
413
286
|
break;
|
|
414
|
-
case 'propose_link':
|
|
415
|
-
result = await proposeLinkTool.handler(args);
|
|
416
|
-
break;
|
|
417
|
-
case 'approve_link':
|
|
418
|
-
result = await approveLinkTool.handler(args);
|
|
419
|
-
break;
|
|
420
|
-
case 'reject_link':
|
|
421
|
-
result = await rejectLinkTool.handler(args);
|
|
422
|
-
break;
|
|
423
|
-
case 'get_pending_links':
|
|
424
|
-
result = await getPendingLinksTool.handler(args);
|
|
425
|
-
break;
|
|
426
|
-
case 'deprecate_auto_links':
|
|
427
|
-
result = await deprecateAutoLinksTool.handler(args);
|
|
428
|
-
break;
|
|
429
|
-
case 'scan_auto_links':
|
|
430
|
-
result = await scanAutoLinksTool.handler(args);
|
|
431
|
-
break;
|
|
432
|
-
case 'create_link_backup':
|
|
433
|
-
result = await createLinkBackupTool.handler(args);
|
|
434
|
-
break;
|
|
435
|
-
case 'generate_cleanup_report':
|
|
436
|
-
result = await generateCleanupReportTool.handler(args);
|
|
437
|
-
break;
|
|
438
|
-
case 'restore_link_backup':
|
|
439
|
-
result = await restoreLinkBackupTool.handler(args);
|
|
440
|
-
break;
|
|
441
|
-
case 'generate_quality_report':
|
|
442
|
-
result = await generateQualityReportTool.handler(args);
|
|
443
|
-
break;
|
|
444
|
-
case 'get_restart_metrics':
|
|
445
|
-
result = await getRestartMetricsTool.handler(args);
|
|
446
|
-
break;
|
|
447
|
-
case 'execute_link_cleanup':
|
|
448
|
-
result = await executeLinkCleanupTool.handler(args);
|
|
449
|
-
break;
|
|
450
|
-
case 'validate_cleanup_result':
|
|
451
|
-
result = await validateCleanupResultTool.handler(args);
|
|
452
|
-
break;
|
|
453
287
|
default:
|
|
454
288
|
throw new Error(`Unknown tool: ${name}`);
|
|
455
289
|
}
|
|
@@ -477,6 +311,114 @@ class MAMAServer {
|
|
|
477
311
|
});
|
|
478
312
|
}
|
|
479
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Handle unified save (decision or checkpoint)
|
|
316
|
+
*/
|
|
317
|
+
async handleSave(args) {
|
|
318
|
+
const { type } = args;
|
|
319
|
+
|
|
320
|
+
if (type === 'decision') {
|
|
321
|
+
const { topic, decision, reasoning, confidence = 0.5 } = args;
|
|
322
|
+
if (!topic || !decision || !reasoning) {
|
|
323
|
+
return { success: false, message: '❌ Decision requires: topic, decision, reasoning' };
|
|
324
|
+
}
|
|
325
|
+
const id = await mama.save({ topic, decision, reasoning, confidence });
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
id,
|
|
329
|
+
type: 'decision',
|
|
330
|
+
message: `✅ Decision saved: ${topic}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type === 'checkpoint') {
|
|
335
|
+
const { summary, next_steps, open_files } = args;
|
|
336
|
+
if (!summary) {
|
|
337
|
+
return { success: false, message: '❌ Checkpoint requires: summary' };
|
|
338
|
+
}
|
|
339
|
+
const id = await mama.saveCheckpoint({ summary, next_steps, open_files });
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
id,
|
|
343
|
+
type: 'checkpoint',
|
|
344
|
+
message: '✅ Checkpoint saved',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { success: false, message: "❌ type must be 'decision' or 'checkpoint'" };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Handle unified search (decisions + checkpoints)
|
|
353
|
+
*/
|
|
354
|
+
async handleSearch(args) {
|
|
355
|
+
const { query, type = 'all', limit = 10 } = args;
|
|
356
|
+
|
|
357
|
+
const results = [];
|
|
358
|
+
|
|
359
|
+
// Search decisions
|
|
360
|
+
if (type === 'all' || type === 'decision') {
|
|
361
|
+
let decisions;
|
|
362
|
+
if (query) {
|
|
363
|
+
// suggest() returns { results: [...] } object or null
|
|
364
|
+
const suggestResult = await mama.suggest(query, limit);
|
|
365
|
+
decisions = suggestResult?.results || [];
|
|
366
|
+
} else {
|
|
367
|
+
decisions = await mama.list(limit);
|
|
368
|
+
}
|
|
369
|
+
// Ensure decisions is an array
|
|
370
|
+
if (Array.isArray(decisions)) {
|
|
371
|
+
results.push(
|
|
372
|
+
...decisions.map((d) => ({
|
|
373
|
+
...d,
|
|
374
|
+
_type: 'decision',
|
|
375
|
+
}))
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Search checkpoints
|
|
381
|
+
if (type === 'all' || type === 'checkpoint') {
|
|
382
|
+
const checkpoints = await mama.listCheckpoints(limit);
|
|
383
|
+
results.push(
|
|
384
|
+
...checkpoints.map((c) => ({
|
|
385
|
+
id: `checkpoint_${c.id}`,
|
|
386
|
+
summary: c.summary,
|
|
387
|
+
next_steps: c.next_steps,
|
|
388
|
+
created_at: c.timestamp,
|
|
389
|
+
_type: 'checkpoint',
|
|
390
|
+
}))
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Sort by time (newest first) and limit
|
|
395
|
+
results.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
|
396
|
+
const limited = results.slice(0, limit);
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
count: limited.length,
|
|
401
|
+
results: limited,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Handle update (decision outcome)
|
|
407
|
+
*/
|
|
408
|
+
async handleUpdate(args) {
|
|
409
|
+
const { id, outcome, reason } = args;
|
|
410
|
+
|
|
411
|
+
if (!id || !outcome) {
|
|
412
|
+
return { success: false, message: '❌ Update requires: id, outcome' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await mama.updateOutcome(id, outcome.toUpperCase(), reason);
|
|
416
|
+
return {
|
|
417
|
+
success: true,
|
|
418
|
+
message: `✅ Updated ${id} → ${outcome}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
480
422
|
async start() {
|
|
481
423
|
try {
|
|
482
424
|
setupLogging();
|