agent-working-memory 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +428 -399
  2. package/dist/api/routes.d.ts.map +1 -1
  3. package/dist/api/routes.js +60 -5
  4. package/dist/api/routes.js.map +1 -1
  5. package/dist/cli.js +468 -68
  6. package/dist/cli.js.map +1 -1
  7. package/dist/coordination/index.d.ts +11 -0
  8. package/dist/coordination/index.d.ts.map +1 -0
  9. package/dist/coordination/index.js +39 -0
  10. package/dist/coordination/index.js.map +1 -0
  11. package/dist/coordination/mcp-tools.d.ts +8 -0
  12. package/dist/coordination/mcp-tools.d.ts.map +1 -0
  13. package/dist/coordination/mcp-tools.js +221 -0
  14. package/dist/coordination/mcp-tools.js.map +1 -0
  15. package/dist/coordination/routes.d.ts +9 -0
  16. package/dist/coordination/routes.d.ts.map +1 -0
  17. package/dist/coordination/routes.js +573 -0
  18. package/dist/coordination/routes.js.map +1 -0
  19. package/dist/coordination/schema.d.ts +12 -0
  20. package/dist/coordination/schema.d.ts.map +1 -0
  21. package/dist/coordination/schema.js +125 -0
  22. package/dist/coordination/schema.js.map +1 -0
  23. package/dist/coordination/schemas.d.ts +227 -0
  24. package/dist/coordination/schemas.d.ts.map +1 -0
  25. package/dist/coordination/schemas.js +125 -0
  26. package/dist/coordination/schemas.js.map +1 -0
  27. package/dist/coordination/stale.d.ts +27 -0
  28. package/dist/coordination/stale.d.ts.map +1 -0
  29. package/dist/coordination/stale.js +58 -0
  30. package/dist/coordination/stale.js.map +1 -0
  31. package/dist/engine/activation.d.ts.map +1 -1
  32. package/dist/engine/activation.js +119 -23
  33. package/dist/engine/activation.js.map +1 -1
  34. package/dist/engine/consolidation.d.ts.map +1 -1
  35. package/dist/engine/consolidation.js +27 -6
  36. package/dist/engine/consolidation.js.map +1 -1
  37. package/dist/index.js +100 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp.js +149 -80
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/storage/sqlite.d.ts +21 -0
  42. package/dist/storage/sqlite.d.ts.map +1 -1
  43. package/dist/storage/sqlite.js +331 -282
  44. package/dist/storage/sqlite.js.map +1 -1
  45. package/dist/types/engram.d.ts +24 -0
  46. package/dist/types/engram.d.ts.map +1 -1
  47. package/dist/types/engram.js.map +1 -1
  48. package/package.json +57 -55
  49. package/src/api/index.ts +3 -3
  50. package/src/api/routes.ts +600 -536
  51. package/src/cli.ts +850 -397
  52. package/src/coordination/index.ts +47 -0
  53. package/src/coordination/mcp-tools.ts +318 -0
  54. package/src/coordination/routes.ts +846 -0
  55. package/src/coordination/schema.ts +120 -0
  56. package/src/coordination/schemas.ts +155 -0
  57. package/src/coordination/stale.ts +97 -0
  58. package/src/core/decay.ts +63 -63
  59. package/src/core/embeddings.ts +88 -88
  60. package/src/core/hebbian.ts +93 -93
  61. package/src/core/index.ts +5 -5
  62. package/src/core/logger.ts +36 -36
  63. package/src/core/query-expander.ts +66 -66
  64. package/src/core/reranker.ts +101 -101
  65. package/src/engine/activation.ts +758 -656
  66. package/src/engine/connections.ts +103 -103
  67. package/src/engine/consolidation-scheduler.ts +125 -125
  68. package/src/engine/consolidation.ts +29 -6
  69. package/src/engine/eval.ts +102 -102
  70. package/src/engine/eviction.ts +101 -101
  71. package/src/engine/index.ts +8 -8
  72. package/src/engine/retraction.ts +100 -100
  73. package/src/engine/staging.ts +74 -74
  74. package/src/index.ts +208 -121
  75. package/src/mcp.ts +1093 -1013
  76. package/src/storage/index.ts +3 -3
  77. package/src/storage/sqlite.ts +1017 -963
  78. package/src/types/agent.ts +67 -67
  79. package/src/types/checkpoint.ts +46 -46
  80. package/src/types/engram.ts +245 -217
  81. package/src/types/eval.ts +100 -100
  82. package/src/types/index.ts +6 -6
package/src/mcp.ts CHANGED
@@ -1,1013 +1,1093 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * MCP Server — Model Context Protocol interface for AgentWorkingMemory.
5
- *
6
- * Runs as a stdio-based MCP server that Claude Code connects to directly.
7
- * Uses the storage and engine layers in-process (no HTTP overhead).
8
- *
9
- * Tools exposed (12):
10
- * memory_write — store a memory (salience filter decides disposition)
11
- * memory_recall — activate memories by context (cognitive retrieval)
12
- * memory_feedback — report whether a recalled memory was useful
13
- * memory_retract — invalidate a wrong memory with optional correction
14
- * memory_supersede — replace an outdated memory with a current one
15
- * memory_stats — get memory health metrics
16
- * memory_checkpoint — save structured execution state (survives compaction)
17
- * memory_restore — restore state + targeted recall after compaction
18
- * memory_task_add — create a prioritized task
19
- * memory_task_update — change task status, priority, or blocking
20
- * memory_task_list — list tasks filtered by status
21
- * memory_task_next — get the highest-priority actionable task
22
- *
23
- * Run: npx tsx src/mcp.ts
24
- * Config: add to ~/.claude.json or .mcp.json
25
- */
26
-
27
- import { readFileSync } from 'node:fs';
28
- import { resolve } from 'node:path';
29
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
30
-
31
- // Load .env file if present (no external dependency)
32
- try {
33
- const envPath = resolve(process.cwd(), '.env');
34
- const envContent = readFileSync(envPath, 'utf-8');
35
- for (const line of envContent.split('\n')) {
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith('#')) continue;
38
- const eqIdx = trimmed.indexOf('=');
39
- if (eqIdx === -1) continue;
40
- const key = trimmed.slice(0, eqIdx).trim();
41
- const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
42
- if (!process.env[key]) process.env[key] = val;
43
- }
44
- } catch { /* No .env file */ }
45
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
46
- import { z } from 'zod';
47
-
48
- import { EngramStore } from './storage/sqlite.js';
49
- import { ActivationEngine } from './engine/activation.js';
50
- import { ConnectionEngine } from './engine/connections.js';
51
- import { StagingBuffer } from './engine/staging.js';
52
- import { EvictionEngine } from './engine/eviction.js';
53
- import { RetractionEngine } from './engine/retraction.js';
54
- import { EvalEngine } from './engine/eval.js';
55
- import { ConsolidationEngine } from './engine/consolidation.js';
56
- import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
57
- import { evaluateSalience, computeNovelty, computeNoveltyWithMatch } from './core/salience.js';
58
- import type { ConsciousState } from './types/checkpoint.js';
59
- import type { SalienceEventType } from './core/salience.js';
60
- import type { TaskStatus, TaskPriority } from './types/engram.js';
61
- import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
62
- import { embed } from './core/embeddings.js';
63
- import { startSidecar } from './hooks/sidecar.js';
64
- import { initLogger, log, getLogPath } from './core/logger.js';
65
-
66
- // --- Incognito Mode ---
67
- // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
68
- // No DB, no engines, no sidecar — just a bare MCP server that exposes nothing.
69
-
70
- const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO === 'true';
71
-
72
- if (INCOGNITO) {
73
- console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
74
- const server = new McpServer({ name: 'agent-working-memory', version: '0.5.4' });
75
- const transport = new StdioServerTransport();
76
- server.connect(transport).catch(err => {
77
- console.error('MCP server failed:', err);
78
- process.exit(1);
79
- });
80
- // No tools registered — Claude won't see any memory_* tools
81
- } else {
82
-
83
- // --- Setup ---
84
-
85
- const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
86
- const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? 'claude-code';
87
- const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
88
- const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
89
-
90
- initLogger(DB_PATH);
91
- log(AGENT_ID, 'startup', `MCP server starting (db: ${DB_PATH}, hooks: ${HOOK_PORT})`);
92
-
93
- const store = new EngramStore(DB_PATH);
94
- const activationEngine = new ActivationEngine(store);
95
- const connectionEngine = new ConnectionEngine(store, activationEngine);
96
- const stagingBuffer = new StagingBuffer(store, activationEngine);
97
- const evictionEngine = new EvictionEngine(store);
98
- const retractionEngine = new RetractionEngine(store);
99
- const evalEngine = new EvalEngine(store);
100
- const consolidationEngine = new ConsolidationEngine(store);
101
- const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
102
-
103
- stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
104
- consolidationScheduler.start();
105
-
106
- const server = new McpServer({
107
- name: 'agent-working-memory',
108
- version: '0.5.4',
109
- });
110
-
111
- // --- Tools ---
112
-
113
- server.tool(
114
- 'memory_write',
115
- `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
116
-
117
- CALL THIS PROACTIVELY — do not wait to be asked. Write memories when you:
118
- - Discover something about the codebase, bugs, or architecture
119
- - Make a decision and want to remember why
120
- - Encounter and resolve an error
121
- - Learn a user preference or project pattern
122
- - Complete a significant piece of work
123
-
124
- The concept should be a short label (3-8 words). The content should be the full detail.`,
125
- {
126
- concept: z.string().describe('Short label for this memory (3-8 words)'),
127
- content: z.string().describe('Full detail of what was learned'),
128
- tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
129
- event_type: z.enum(['observation', 'decision', 'friction', 'surprise', 'causal'])
130
- .optional().default('observation')
131
- .describe('Type of event: observation (default), decision, friction (error/blocker), surprise, causal (root cause)'),
132
- surprise: z.number().min(0).max(1).optional().default(0.3)
133
- .describe('How surprising was this? 0=expected, 1=very unexpected'),
134
- decision_made: z.boolean().optional().default(false)
135
- .describe('Was a decision made? True boosts importance'),
136
- causal_depth: z.number().min(0).max(1).optional().default(0.3)
137
- .describe('How deep is the causal understanding? 0=surface, 1=root cause'),
138
- resolution_effort: z.number().min(0).max(1).optional().default(0.3)
139
- .describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
140
- memory_class: z.enum(['canonical', 'working', 'ephemeral']).optional().default('working')
141
- .describe('Memory class: canonical (source-of-truth, never stages), working (default), ephemeral (temporary, decays faster)'),
142
- supersedes: z.string().optional()
143
- .describe('ID of an older memory this one replaces. The old memory is down-ranked, not deleted.'),
144
- },
145
- async (params) => {
146
- // Check novelty with match info for reinforcement
147
- const noveltyResult = computeNoveltyWithMatch(store, AGENT_ID, params.concept, params.content);
148
- const novelty = noveltyResult.novelty;
149
-
150
- // --- Reinforce-on-Duplicate check ---
151
- // Tightened thresholds: require near-exact match (novelty < 0.3, BM25 > 0.85, 60% content overlap)
152
- if (novelty < 0.3
153
- && noveltyResult.matchScore > 0.85
154
- && noveltyResult.matchedEngramId) {
155
- const matchedEngram = store.getEngram(noveltyResult.matchedEngramId);
156
- if (matchedEngram) {
157
- const existingTokens = new Set(matchedEngram.content.toLowerCase().split(/\s+/).filter(w => w.length > 3));
158
- const newTokens = new Set(params.content.toLowerCase().split(/\s+/).filter(w => w.length > 3));
159
- let overlap = 0;
160
- for (const t of newTokens) { if (existingTokens.has(t)) overlap++; }
161
- const contentOverlap = newTokens.size > 0 ? overlap / newTokens.size : 0;
162
-
163
- if (contentOverlap > 0.6) {
164
- // True duplicate reinforce existing and skip creation
165
- store.touchEngram(noveltyResult.matchedEngramId);
166
- try { store.updateAutoCheckpointWrite(AGENT_ID, noveltyResult.matchedEngramId); } catch { /* non-fatal */ }
167
- log(AGENT_ID, 'write:reinforce', `"${params.concept}" reinforced "${matchedEngram.concept}" (overlap=${contentOverlap.toFixed(2)})`);
168
- return {
169
- content: [{
170
- type: 'text' as const,
171
- text: `Reinforced existing memory "${matchedEngram.concept}" (overlap ${(contentOverlap * 100).toFixed(0)}%)`,
172
- }],
173
- };
174
- }
175
- // Partial match continue to create new memory
176
- log(AGENT_ID, 'write:partial-match', `"${params.concept}" partially matched "${matchedEngram.concept}" (overlap=${contentOverlap.toFixed(2)}), creating new memory`);
177
- }
178
- }
179
-
180
- const salience = evaluateSalience({
181
- content: params.content,
182
- eventType: params.event_type as SalienceEventType,
183
- surprise: params.surprise,
184
- decisionMade: params.decision_made,
185
- causalDepth: params.causal_depth,
186
- resolutionEffort: params.resolution_effort,
187
- novelty,
188
- memoryClass: params.memory_class,
189
- });
190
-
191
- // v0.5.4: No longer discard — store everything, use salience for ranking.
192
- // Low-salience memories get low confidence so they rank below high-salience
193
- // in retrieval, but remain available for recall when needed.
194
- const isLowSalience = salience.disposition === 'discard';
195
-
196
- const CONFIDENCE_PRIORS: Record<string, number> = {
197
- decision: 0.65,
198
- friction: 0.60,
199
- causal: 0.60,
200
- surprise: 0.55,
201
- observation: 0.45,
202
- };
203
- const confidencePrior = isLowSalience
204
- ? 0.25
205
- : salience.disposition === 'staging'
206
- ? 0.40
207
- : CONFIDENCE_PRIORS[params.event_type ?? 'observation'] ?? 0.45;
208
-
209
- const engram = store.createEngram({
210
- agentId: AGENT_ID,
211
- concept: params.concept,
212
- content: params.content,
213
- tags: isLowSalience ? [...(params.tags ?? []), 'low-salience'] : params.tags,
214
- salience: salience.score,
215
- confidence: confidencePrior,
216
- salienceFeatures: salience.features,
217
- reasonCodes: salience.reasonCodes,
218
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
219
- memoryClass: params.memory_class,
220
- supersedes: params.supersedes,
221
- });
222
-
223
- if (salience.disposition === 'staging') {
224
- store.updateStage(engram.id, 'staging');
225
- } else {
226
- connectionEngine.enqueue(engram.id);
227
- }
228
-
229
- // Handle supersession: mark old memory as superseded
230
- if (params.supersedes) {
231
- const oldEngram = store.getEngram(params.supersedes);
232
- if (oldEngram) {
233
- store.supersedeEngram(params.supersedes, engram.id);
234
- // Create supersession association
235
- store.upsertAssociation(engram.id, oldEngram.id, 0.8, 'causal', 0.9);
236
- }
237
- }
238
-
239
- // Generate embedding asynchronously (don't block response)
240
- embed(`${params.concept} ${params.content}`).then(vec => {
241
- store.updateEmbedding(engram.id, vec);
242
- }).catch(() => {}); // Embedding failure is non-fatal
243
-
244
- // Auto-checkpoint: track write
245
- try { store.updateAutoCheckpointWrite(AGENT_ID, engram.id); } catch { /* non-fatal */ }
246
-
247
- const logDisposition = isLowSalience ? 'low-salience' : salience.disposition;
248
- log(AGENT_ID, `write:${logDisposition}`, `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)} id=${engram.id}`);
249
-
250
- return {
251
- content: [{
252
- type: 'text' as const,
253
- text: `Stored (${salience.disposition}) "${params.concept}" [${salience.score.toFixed(2)}]`,
254
- }],
255
- };
256
- }
257
- );
258
-
259
- server.tool(
260
- 'memory_recall',
261
- `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
262
-
263
- ALWAYS call this when:
264
- - Starting work on a project or topic (recall what you know)
265
- - Debugging (recall similar errors and solutions)
266
- - Making decisions (recall past decisions and outcomes)
267
- - The user mentions a topic you might have stored memories about
268
-
269
- Accepts either "query" or "context" parameter both work identically.
270
- Returns the most relevant memories ranked by text relevance, temporal recency, and associative strength.`,
271
- {
272
- query: z.string().optional().describe('What to search for describe the situation, question, or topic'),
273
- context: z.string().optional().describe('Alias for query (either works)'),
274
- limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
275
- min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
276
- include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
277
- use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
278
- use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
279
- },
280
- async (params) => {
281
- const queryText = params.query ?? params.context;
282
- if (!queryText) {
283
- return {
284
- content: [{
285
- type: 'text' as const,
286
- text: 'Error: provide either "query" or "context" parameter with your search text.',
287
- }],
288
- };
289
- }
290
- const results = await activationEngine.activate({
291
- agentId: AGENT_ID,
292
- context: queryText,
293
- limit: params.limit,
294
- minScore: params.min_score,
295
- includeStaging: params.include_staging,
296
- useReranker: params.use_reranker,
297
- useExpansion: params.use_expansion,
298
- });
299
-
300
- // Auto-checkpoint: track recall
301
- try {
302
- const ids = results.map(r => r.engram.id);
303
- store.updateAutoCheckpointRecall(AGENT_ID, queryText, ids);
304
- } catch { /* non-fatal */ }
305
-
306
- log(AGENT_ID, 'recall', `"${queryText.slice(0, 80)}" ${results.length} results`);
307
-
308
- if (results.length === 0) {
309
- return {
310
- content: [{
311
- type: 'text' as const,
312
- text: 'No relevant memories found.',
313
- }],
314
- };
315
- }
316
-
317
- const lines = results.map((r, i) => {
318
- return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)}): ${r.engram.content}`;
319
- });
320
-
321
- return {
322
- content: [{
323
- type: 'text' as const,
324
- text: lines.join('\n'),
325
- }],
326
- };
327
- }
328
- );
329
-
330
- server.tool(
331
- 'memory_feedback',
332
- `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
333
-
334
- Always call this after using a recalled memory so the system learns what's valuable.`,
335
- {
336
- engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
337
- useful: z.boolean().describe('Was this memory actually helpful?'),
338
- context: z.string().optional().describe('Brief note on why it was/wasn\'t useful'),
339
- },
340
- async (params) => {
341
- store.logRetrievalFeedback(null, params.engram_id, params.useful, params.context ?? '');
342
-
343
- const engram = store.getEngram(params.engram_id);
344
- if (engram) {
345
- const delta = params.useful
346
- ? DEFAULT_AGENT_CONFIG.feedbackPositiveBoost
347
- : -DEFAULT_AGENT_CONFIG.feedbackNegativePenalty;
348
- store.updateConfidence(engram.id, engram.confidence + delta);
349
- }
350
-
351
- return {
352
- content: [{
353
- type: 'text' as const,
354
- text: `Feedback: ${params.useful ? '+useful' : '-not useful'}`,
355
- }],
356
- };
357
- }
358
- );
359
-
360
- server.tool(
361
- 'memory_retract',
362
- `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
363
-
364
- Use this when you discover a memory contains incorrect information.`,
365
- {
366
- engram_id: z.string().describe('ID of the wrong memory'),
367
- reason: z.string().describe('Why is this memory wrong?'),
368
- correction: z.string().optional().describe('What is the correct information? (creates a new memory)'),
369
- },
370
- async (params) => {
371
- const result = retractionEngine.retract({
372
- agentId: AGENT_ID,
373
- targetEngramId: params.engram_id,
374
- reason: params.reason,
375
- counterContent: params.correction,
376
- });
377
-
378
- const parts = [`Memory ${params.engram_id} retracted.`];
379
- if (result.correctionId) {
380
- parts.push(`Correction stored as ${result.correctionId}.`);
381
- }
382
- parts.push(`${result.associatesAffected} related memories had confidence reduced.`);
383
-
384
- return {
385
- content: [{
386
- type: 'text' as const,
387
- text: parts.join(' '),
388
- }],
389
- };
390
- }
391
- );
392
-
393
- server.tool(
394
- 'memory_supersede',
395
- `Replace an outdated memory with a newer one. Unlike retraction (which marks memories as wrong), supersession marks the old memory as outdated but historically correct.
396
-
397
- Use this when:
398
- - A status or count has changed (e.g., "5 reviews done" "7 reviews done")
399
- - Architecture or infrastructure evolved (e.g., "two-repo model" → "three-repo model")
400
- - A schedule or plan was updated
401
-
402
- The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`,
403
- {
404
- old_engram_id: z.string().describe('ID of the outdated memory'),
405
- new_engram_id: z.string().describe('ID of the replacement memory'),
406
- reason: z.string().optional().describe('Why the old memory is outdated'),
407
- },
408
- async (params) => {
409
- const oldEngram = store.getEngram(params.old_engram_id);
410
- if (!oldEngram) {
411
- return { content: [{ type: 'text' as const, text: `Old memory not found: ${params.old_engram_id}` }] };
412
- }
413
- const newEngram = store.getEngram(params.new_engram_id);
414
- if (!newEngram) {
415
- return { content: [{ type: 'text' as const, text: `New memory not found: ${params.new_engram_id}` }] };
416
- }
417
-
418
- store.supersedeEngram(params.old_engram_id, params.new_engram_id);
419
-
420
- // Create supersession association (new → old)
421
- store.upsertAssociation(params.new_engram_id, params.old_engram_id, 0.8, 'causal', 0.9);
422
-
423
- // Reduce old memory's confidence (not to zero — it's historical, not wrong)
424
- store.updateConfidence(params.old_engram_id, Math.max(0.2, oldEngram.confidence * 0.4));
425
-
426
- log(AGENT_ID, 'supersede', `"${oldEngram.concept}" "${newEngram.concept}"${params.reason ? ` (${params.reason})` : ''}`);
427
-
428
- return {
429
- content: [{
430
- type: 'text' as const,
431
- text: `Superseded: "${oldEngram.concept}" → "${newEngram.concept}"`,
432
- }],
433
- };
434
- }
435
- );
436
-
437
- server.tool(
438
- 'memory_stats',
439
- `Get memory health stats how many memories, confidence levels, association count, and system performance.
440
- Also shows the activity log path so the user can tail it to see what's happening.`,
441
- {},
442
- async () => {
443
- const metrics = evalEngine.computeMetrics(AGENT_ID);
444
- const checkpoint = store.getCheckpoint(AGENT_ID);
445
- const lines = [
446
- `Agent: ${AGENT_ID}`,
447
- `Active memories: ${metrics.activeEngramCount}`,
448
- `Staging: ${metrics.stagingEngramCount}`,
449
- `Retracted: ${metrics.retractedCount}`,
450
- `Avg confidence: ${metrics.avgConfidence.toFixed(3)}`,
451
- `Total edges: ${metrics.totalEdges}`,
452
- `Edge utility: ${(metrics.edgeUtilityRate * 100).toFixed(1)}%`,
453
- `Activations (24h): ${metrics.activationCount}`,
454
- `Avg latency: ${metrics.avgLatencyMs.toFixed(1)}ms`,
455
- ``,
456
- `Session writes: ${checkpoint?.auto.writeCountSinceConsolidation ?? 0}`,
457
- `Session recalls: ${checkpoint?.auto.recallCountSinceConsolidation ?? 0}`,
458
- `Last activity: ${checkpoint?.auto.lastActivityAt?.toISOString() ?? 'never'}`,
459
- `Checkpoint: ${checkpoint?.executionState ? checkpoint.executionState.currentTask : 'none'}`,
460
- ``,
461
- `Activity log: ${getLogPath() ?? 'not configured'}`,
462
- `Hook sidecar: 127.0.0.1:${HOOK_PORT}`,
463
- ];
464
-
465
- return {
466
- content: [{
467
- type: 'text' as const,
468
- text: lines.join('\n'),
469
- }],
470
- };
471
- }
472
- );
473
-
474
- // --- Checkpointing Tools ---
475
-
476
- server.tool(
477
- 'memory_checkpoint',
478
- `Save your current execution state so you can recover after context compaction.
479
-
480
- ALWAYS call this before:
481
- - Long operations (multi-file generation, large refactors, overnight work)
482
- - Anything that might fill the context window
483
- - Switching to a different task
484
-
485
- Also call periodically during long sessions to avoid losing state. The state is saved per-agent and overwrites any previous checkpoint.`,
486
- {
487
- current_task: z.string().describe('What you are currently working on'),
488
- decisions: z.array(z.string()).optional().default([])
489
- .describe('Key decisions made so far'),
490
- active_files: z.array(z.string()).optional().default([])
491
- .describe('Files you are currently working with'),
492
- next_steps: z.array(z.string()).optional().default([])
493
- .describe('What needs to happen next'),
494
- related_memory_ids: z.array(z.string()).optional().default([])
495
- .describe('IDs of memories relevant to current work'),
496
- notes: z.string().optional().default('')
497
- .describe('Any other context worth preserving'),
498
- episode_id: z.string().optional()
499
- .describe('Current episode ID if known'),
500
- },
501
- async (params) => {
502
- const state: ConsciousState = {
503
- currentTask: params.current_task,
504
- decisions: params.decisions,
505
- activeFiles: params.active_files,
506
- nextSteps: params.next_steps,
507
- relatedMemoryIds: params.related_memory_ids,
508
- notes: params.notes,
509
- episodeId: params.episode_id ?? null,
510
- };
511
-
512
- store.saveCheckpoint(AGENT_ID, state);
513
- log(AGENT_ID, 'checkpoint', `"${params.current_task}" decisions=${params.decisions.length} files=${params.active_files.length}`);
514
-
515
- return {
516
- content: [{
517
- type: 'text' as const,
518
- text: `Checkpoint saved: "${params.current_task}" (${params.decisions.length} decisions, ${params.active_files.length} files)`,
519
- }],
520
- };
521
- }
522
- );
523
-
524
- server.tool(
525
- 'memory_restore',
526
- `Restore your previous execution state after context compaction or at session start.
527
-
528
- Returns:
529
- - Your saved execution state (task, decisions, next steps, files)
530
- - Recently recalled memories for context
531
- - Your last write for continuity
532
- - How long you were idle
533
-
534
- Use this at the start of every session or after compaction to pick up where you left off.`,
535
- {},
536
- async () => {
537
- const checkpoint = store.getCheckpoint(AGENT_ID);
538
-
539
- const now = Date.now();
540
- const idleMs = checkpoint
541
- ? now - checkpoint.auto.lastActivityAt.getTime()
542
- : 0;
543
-
544
- // Get last written engram
545
- let lastWrite: { id: string; concept: string; content: string } | null = null;
546
- if (checkpoint?.auto.lastWriteId) {
547
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
548
- if (engram) {
549
- lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
550
- }
551
- }
552
-
553
- // Recall memories using last context
554
- let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
555
- const recallContext = checkpoint?.auto.lastRecallContext
556
- ?? checkpoint?.executionState?.currentTask
557
- ?? null;
558
-
559
- if (recallContext) {
560
- try {
561
- const results = await activationEngine.activate({
562
- agentId: AGENT_ID,
563
- context: recallContext,
564
- limit: 5,
565
- minScore: 0.05,
566
- useReranker: true,
567
- useExpansion: true,
568
- });
569
- recalledMemories = results.map(r => ({
570
- id: r.engram.id,
571
- concept: r.engram.concept,
572
- content: r.engram.content,
573
- score: r.score,
574
- }));
575
- } catch { /* recall failure is non-fatal */ }
576
- }
577
-
578
- // Consolidation on restore:
579
- // - If idle >5min but last consolidation was recent (graceful exit ran it), skip
580
- // - If idle >5min and no recent consolidation, run full cycle (non-graceful exit fallback)
581
- const MINI_IDLE_MS = 5 * 60_000;
582
- const FULL_CONSOLIDATION_GAP_MS = 10 * 60_000; // 10 min — if last consolidation was longer ago, run full
583
- let miniConsolidationTriggered = false;
584
- let fullConsolidationTriggered = false;
585
-
586
- if (idleMs > MINI_IDLE_MS) {
587
- const sinceLastConsolidation = checkpoint?.lastConsolidationAt
588
- ? now - checkpoint.lastConsolidationAt.getTime()
589
- : Infinity;
590
-
591
- if (sinceLastConsolidation > FULL_CONSOLIDATION_GAP_MS) {
592
- // No recent consolidation — graceful exit didn't happen, run full cycle
593
- fullConsolidationTriggered = true;
594
- try {
595
- const result = await consolidationEngine.consolidate(AGENT_ID);
596
- store.markConsolidation(AGENT_ID, false);
597
- log(AGENT_ID, 'consolidation', `full sleep cycle on restore (no graceful exit, idle ${Math.round(idleMs / 60_000)}min, last consolidation ${Math.round(sinceLastConsolidation / 60_000)}min ago) — ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
598
- } catch { /* consolidation failure is non-fatal */ }
599
- } else {
600
- // Recent consolidation exists — graceful exit already handled it, just do mini
601
- miniConsolidationTriggered = true;
602
- consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => {});
603
- }
604
- }
605
-
606
- // Format response
607
- const parts: string[] = [];
608
- const idleMin = Math.round(idleMs / 60_000);
609
- const consolidationNote = fullConsolidationTriggered
610
- ? ' (full consolidation — no graceful exit detected)'
611
- : miniConsolidationTriggered
612
- ? ' (mini-consolidation triggered)'
613
- : '';
614
- log(AGENT_ID, 'restore', `idle=${idleMin}min checkpoint=${!!checkpoint?.executionState} recalled=${recalledMemories.length} lastWrite=${lastWrite?.concept ?? 'none'}${fullConsolidationTriggered ? ' FULL_CONSOLIDATION' : ''}`);
615
- parts.push(`Idle: ${idleMin}min${consolidationNote}`);
616
-
617
- if (checkpoint?.executionState) {
618
- const s = checkpoint.executionState;
619
- parts.push(`\n**Current task:** ${s.currentTask}`);
620
- if (s.decisions.length) parts.push(`**Decisions:** ${s.decisions.join('; ')}`);
621
- if (s.nextSteps.length) parts.push(`**Next steps:** ${s.nextSteps.map((st, i) => `${i + 1}. ${st}`).join(', ')}`);
622
- if (s.activeFiles.length) parts.push(`**Active files:** ${s.activeFiles.join(', ')}`);
623
- if (s.notes) parts.push(`**Notes:** ${s.notes}`);
624
- if (checkpoint.checkpointAt) parts.push(`_Saved at: ${checkpoint.checkpointAt.toISOString()}_`);
625
- } else {
626
- parts.push('\nNo explicit checkpoint saved.');
627
- parts.push('\n**Tip:** Use memory_write to save important learnings, and memory_checkpoint before long operations so you can recover state.');
628
- }
629
-
630
- if (lastWrite) {
631
- parts.push(`\n**Last write:** ${lastWrite.concept}\n${lastWrite.content}`);
632
- }
633
-
634
- if (recalledMemories.length > 0) {
635
- parts.push(`\n**Recalled memories (${recalledMemories.length}):**`);
636
- for (const m of recalledMemories) {
637
- parts.push(`- **${m.concept}** (${m.score.toFixed(3)}): ${m.content.slice(0, 150)}${m.content.length > 150 ? '...' : ''}`);
638
- }
639
- }
640
-
641
- return {
642
- content: [{
643
- type: 'text' as const,
644
- text: parts.join('\n'),
645
- }],
646
- };
647
- }
648
- );
649
-
650
- // --- Task Management Tools ---
651
-
652
- server.tool(
653
- 'memory_task_add',
654
- `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
655
-
656
- Use this when:
657
- - You identify work that needs doing but can't do it right now
658
- - The user mentions something to do later
659
- - You want to park a sub-task while focusing on something more urgent
660
-
661
- Tasks automatically get high salience so they won't be discarded.`,
662
- {
663
- concept: z.string().describe('Short task title (3-10 words)'),
664
- content: z.string().describe('Full task description — what needs doing, context, acceptance criteria'),
665
- tags: z.array(z.string()).optional().describe('Tags for categorization'),
666
- priority: z.enum(['urgent', 'high', 'medium', 'low']).default('medium')
667
- .describe('Task priority: urgent (do now), high (do soon), medium (normal), low (backlog)'),
668
- blocked_by: z.string().optional().describe('ID of a task that must finish first'),
669
- },
670
- async (params) => {
671
- const engram = store.createEngram({
672
- agentId: AGENT_ID,
673
- concept: params.concept,
674
- content: params.content,
675
- tags: [...(params.tags ?? []), 'task'],
676
- salience: 0.9, // Tasks always high salience
677
- confidence: 0.8,
678
- salienceFeatures: {
679
- surprise: 0.5,
680
- decisionMade: true,
681
- causalDepth: 0.5,
682
- resolutionEffort: 0.5,
683
- eventType: 'decision',
684
- },
685
- reasonCodes: ['task-created'],
686
- taskStatus: params.blocked_by ? 'blocked' : 'open',
687
- taskPriority: params.priority as TaskPriority,
688
- blockedBy: params.blocked_by,
689
- });
690
-
691
- connectionEngine.enqueue(engram.id);
692
-
693
- // Generate embedding asynchronously
694
- embed(`${params.concept} ${params.content}`).then(vec => {
695
- store.updateEmbedding(engram.id, vec);
696
- }).catch(() => {});
697
-
698
- return {
699
- content: [{
700
- type: 'text' as const,
701
- text: `Task created: "${params.concept}" (${params.priority})`,
702
- }],
703
- };
704
- }
705
- );
706
-
707
- server.tool(
708
- 'memory_task_update',
709
- `Update a task's status or priority. Use this to:
710
- - Start working on a task (open → in_progress)
711
- - Mark a task done (→ done)
712
- - Block a task on another (→ blocked)
713
- - Reprioritize (change priority)
714
- - Unblock a task (clear blocked_by)`,
715
- {
716
- task_id: z.string().describe('ID of the task to update'),
717
- status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
718
- .describe('New status'),
719
- priority: z.enum(['urgent', 'high', 'medium', 'low']).optional()
720
- .describe('New priority'),
721
- blocked_by: z.string().optional().describe('ID of blocking task (set to empty string to unblock)'),
722
- },
723
- async (params) => {
724
- const engram = store.getEngram(params.task_id);
725
- if (!engram || !engram.taskStatus) {
726
- return { content: [{ type: 'text' as const, text: `Task not found: ${params.task_id}` }] };
727
- }
728
-
729
- if (params.blocked_by !== undefined) {
730
- store.updateBlockedBy(params.task_id, params.blocked_by || null);
731
- }
732
- if (params.status) {
733
- store.updateTaskStatus(params.task_id, params.status as TaskStatus);
734
- }
735
- if (params.priority) {
736
- store.updateTaskPriority(params.task_id, params.priority as TaskPriority);
737
- }
738
-
739
- const updated = store.getEngram(params.task_id)!;
740
- return {
741
- content: [{
742
- type: 'text' as const,
743
- text: `Updated: "${updated.concept}" ${updated.taskStatus} (${updated.taskPriority})`,
744
- }],
745
- };
746
- }
747
- );
748
-
749
- server.tool(
750
- 'memory_task_list',
751
- `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
752
-
753
- Use at the start of a session to see what's pending, or to check blocked/done tasks.`,
754
- {
755
- status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
756
- .describe('Filter by status (omit to see all active tasks)'),
757
- include_done: z.boolean().optional().default(false)
758
- .describe('Include completed tasks?'),
759
- },
760
- async (params) => {
761
- let tasks = store.getTasks(AGENT_ID, params.status as TaskStatus | undefined);
762
- if (!params.include_done && !params.status) {
763
- tasks = tasks.filter(t => t.taskStatus !== 'done');
764
- }
765
-
766
- if (tasks.length === 0) {
767
- return { content: [{ type: 'text' as const, text: 'No tasks found.' }] };
768
- }
769
-
770
- const lines = tasks.map((t, i) => {
771
- const blocked = t.blockedBy ? ` [blocked by ${t.blockedBy}]` : '';
772
- const tags = t.tags?.filter(tag => tag !== 'task').join(', ');
773
- return `${i + 1}. [${t.taskStatus}] **${t.concept}** (${t.taskPriority})${blocked}\n ${t.content.slice(0, 120)}${t.content.length > 120 ? '...' : ''}\n ${tags ? `Tags: ${tags} | ` : ''}ID: ${t.id}`;
774
- });
775
-
776
- return {
777
- content: [{
778
- type: 'text' as const,
779
- text: `Tasks (${tasks.length}):\n\n${lines.join('\n\n')}`,
780
- }],
781
- };
782
- }
783
- );
784
-
785
- server.tool(
786
- 'memory_task_next',
787
- `Get the single most important task to work on next.
788
-
789
- Prioritizes: in_progress tasks first (finish what you started), then by priority level, then oldest first. Skips blocked and done tasks.
790
-
791
- Use this when you finish a task or need to decide what to do next.`,
792
- {},
793
- async () => {
794
- const next = store.getNextTask(AGENT_ID);
795
- if (!next) {
796
- return { content: [{ type: 'text' as const, text: 'No actionable tasks. All clear!' }] };
797
- }
798
-
799
- const blocked = next.blockedBy ? `\nBlocked by: ${next.blockedBy}` : '';
800
- const tags = next.tags?.filter(tag => tag !== 'task').join(', ');
801
-
802
- return {
803
- content: [{
804
- type: 'text' as const,
805
- text: `Next task:\n**${next.concept}** (${next.taskPriority})\nStatus: ${next.taskStatus}\n${next.content}${blocked}\n${tags ? `Tags: ${tags}\n` : ''}ID: ${next.id}`,
806
- }],
807
- };
808
- }
809
- );
810
-
811
- // --- Task Bracket Tools ---
812
-
813
- server.tool(
814
- 'memory_task_begin',
815
- `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
816
-
817
- CALL THIS when starting:
818
- - A multi-step operation (doc generation, large refactor, migration)
819
- - Work on a new topic or project area
820
- - Anything that might fill the context window
821
-
822
- This ensures your state is saved before you start, and primes recall with relevant context.`,
823
- {
824
- topic: z.string().describe('What task are you starting? (3-15 words)'),
825
- files: z.array(z.string()).optional().default([])
826
- .describe('Files you expect to work with'),
827
- notes: z.string().optional().default('')
828
- .describe('Any additional context'),
829
- },
830
- async (params) => {
831
- // 1. Checkpoint current state
832
- const checkpoint = store.getCheckpoint(AGENT_ID);
833
- const prevTask = checkpoint?.executionState?.currentTask ?? 'None';
834
-
835
- store.saveCheckpoint(AGENT_ID, {
836
- currentTask: params.topic,
837
- decisions: [],
838
- activeFiles: params.files,
839
- nextSteps: [],
840
- relatedMemoryIds: [],
841
- notes: params.notes || `Started via memory_task_begin. Previous task: ${prevTask}`,
842
- episodeId: null,
843
- });
844
-
845
- // 2. Auto-recall relevant memories
846
- let recalledSummary = '';
847
- try {
848
- const results = await activationEngine.activate({
849
- agentId: AGENT_ID,
850
- context: params.topic,
851
- limit: 5,
852
- minScore: 0.05,
853
- useReranker: true,
854
- useExpansion: true,
855
- });
856
-
857
- if (results.length > 0) {
858
- const lines = results.map((r, i) => {
859
- const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
860
- return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${tags}\n ${r.engram.content.slice(0, 150)}${r.engram.content.length > 150 ? '...' : ''}`;
861
- });
862
- recalledSummary = `\n\n**Recalled memories (${results.length}):**\n${lines.join('\n')}`;
863
-
864
- // Track recall
865
- store.updateAutoCheckpointRecall(AGENT_ID, params.topic, results.map(r => r.engram.id));
866
- }
867
- } catch { /* recall failure is non-fatal */ }
868
-
869
- log(AGENT_ID, 'task:begin', `"${params.topic}" prev="${prevTask}"`);
870
-
871
- return {
872
- content: [{
873
- type: 'text' as const,
874
- text: `Started: "${params.topic}" (prev: ${prevTask})${recalledSummary}`,
875
- }],
876
- };
877
- }
878
- );
879
-
880
- server.tool(
881
- 'memory_task_end',
882
- `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
883
-
884
- CALL THIS when you finish:
885
- - A multi-step operation
886
- - Before switching to a different topic
887
- - At the end of a work session
888
-
889
- This captures what was accomplished so future sessions can recall it.`,
890
- {
891
- summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
892
- tags: z.array(z.string()).optional().default([])
893
- .describe('Tags for the summary memory'),
894
- supersedes: z.array(z.string()).optional().default([])
895
- .describe('IDs of older memories this task summary replaces (marks them as superseded)'),
896
- },
897
- async (params) => {
898
- // 1. Write summary as a memory
899
- const salience = evaluateSalience({
900
- content: params.summary,
901
- eventType: 'decision',
902
- surprise: 0.3,
903
- decisionMade: true,
904
- causalDepth: 0.5,
905
- resolutionEffort: 0.5,
906
- });
907
-
908
- // Determine the real task name for the summary engram
909
- const checkpoint = store.getCheckpoint(AGENT_ID);
910
- const rawTask = checkpoint?.executionState?.currentTask ?? 'Unknown task';
911
- // Strip any "Completed: " prefixes to avoid cascading
912
- const cleanedTask = rawTask.replace(/^(Completed: )+/, '');
913
- // Don't use auto-checkpoint or already-completed tasks as real task names
914
- const isNamedTask = !cleanedTask.startsWith('Auto-checkpoint') && cleanedTask !== 'Unknown task';
915
- const completedTask = isNamedTask
916
- ? cleanedTask
917
- : params.summary.slice(0, 60).replace(/\n/g, ' ');
918
-
919
- const engram = store.createEngram({
920
- agentId: AGENT_ID,
921
- concept: completedTask.slice(0, 80),
922
- content: params.summary,
923
- tags: [...params.tags, 'task-summary'],
924
- salience: isNamedTask ? Math.max(salience.score, 0.7) : salience.score, // Only floor salience for named tasks
925
- confidence: 0.65, // Task summaries are decision-grade (completed work)
926
- salienceFeatures: salience.features,
927
- reasonCodes: [...salience.reasonCodes, 'task-end'],
928
- });
929
-
930
- connectionEngine.enqueue(engram.id);
931
-
932
- // 2. Handle supersessions — mark old memories as outdated
933
- let supersededCount = 0;
934
- for (const oldId of params.supersedes) {
935
- const oldEngram = store.getEngram(oldId);
936
- if (oldEngram) {
937
- store.supersedeEngram(oldId, engram.id);
938
- store.upsertAssociation(engram.id, oldId, 0.8, 'causal', 0.9);
939
- store.updateConfidence(oldId, Math.max(0.2, oldEngram.confidence * 0.4));
940
- supersededCount++;
941
- }
942
- }
943
-
944
- // Generate embedding asynchronously
945
- embed(`Task completed: ${params.summary}`).then(vec => {
946
- store.updateEmbedding(engram.id, vec);
947
- }).catch(() => {});
948
-
949
- // 2. Update checkpoint to reflect completion
950
- store.saveCheckpoint(AGENT_ID, {
951
- currentTask: `Completed: ${completedTask}`,
952
- decisions: checkpoint?.executionState?.decisions ?? [],
953
- activeFiles: [],
954
- nextSteps: [],
955
- relatedMemoryIds: [engram.id],
956
- notes: `Task completed. Summary memory: ${engram.id}`,
957
- episodeId: null,
958
- });
959
-
960
- store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
961
- log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)} superseded=${supersededCount}`);
962
-
963
- const supersededNote = supersededCount > 0 ? ` (${supersededCount} old memories superseded)` : '';
964
- return {
965
- content: [{
966
- type: 'text' as const,
967
- text: `Completed: "${completedTask}" [${salience.score.toFixed(2)}]${supersededNote}`,
968
- }],
969
- };
970
- }
971
- );
972
-
973
- // --- Start ---
974
-
975
- async function main() {
976
- const transport = new StdioServerTransport();
977
- await server.connect(transport);
978
-
979
- // Start hook sidecar (lightweight HTTP for Claude Code hooks)
980
- const sidecar = startSidecar({
981
- store,
982
- agentId: AGENT_ID,
983
- secret: HOOK_SECRET,
984
- port: HOOK_PORT,
985
- onConsolidate: async (agentId, reason) => {
986
- console.error(`[mcp] consolidation triggered: ${reason}`);
987
- const result = await consolidationEngine.consolidate(agentId);
988
- store.markConsolidation(agentId, false);
989
- console.error(`[mcp] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
990
- },
991
- });
992
-
993
- // Log to stderr (stdout is reserved for MCP protocol)
994
- console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
995
- console.error(`Hook sidecar on 127.0.0.1:${HOOK_PORT}${HOOK_SECRET ? ' (auth enabled)' : ' (no auth — set AWM_HOOK_SECRET)'}`);
996
-
997
- // Clean shutdown
998
- const cleanup = () => {
999
- sidecar.close();
1000
- consolidationScheduler.stop();
1001
- stagingBuffer.stop();
1002
- store.close();
1003
- };
1004
- process.on('SIGINT', () => { cleanup(); process.exit(0); });
1005
- process.on('SIGTERM', () => { cleanup(); process.exit(0); });
1006
- }
1007
-
1008
- main().catch(err => {
1009
- console.error('MCP server failed:', err);
1010
- process.exit(1);
1011
- });
1012
-
1013
- } // end else (non-incognito)
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * MCP Server — Model Context Protocol interface for AgentWorkingMemory.
5
+ *
6
+ * Runs as a stdio-based MCP server that Claude Code connects to directly.
7
+ * Uses the storage and engine layers in-process (no HTTP overhead).
8
+ *
9
+ * Tools exposed (12):
10
+ * memory_write — store a memory (salience filter decides disposition)
11
+ * memory_recall — activate memories by context (cognitive retrieval)
12
+ * memory_feedback — report whether a recalled memory was useful
13
+ * memory_retract — invalidate a wrong memory with optional correction
14
+ * memory_supersede — replace an outdated memory with a current one
15
+ * memory_stats — get memory health metrics
16
+ * memory_checkpoint — save structured execution state (survives compaction)
17
+ * memory_restore — restore state + targeted recall after compaction
18
+ * memory_task_add — create a prioritized task
19
+ * memory_task_update — change task status, priority, or blocking
20
+ * memory_task_list — list tasks filtered by status
21
+ * memory_task_next — get the highest-priority actionable task
22
+ *
23
+ * Run: npx tsx src/mcp.ts
24
+ * Config: add to ~/.claude.json or .mcp.json
25
+ */
26
+
27
+ import { readFileSync } from 'node:fs';
28
+ import { resolve } from 'node:path';
29
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
30
+
31
+ // Load .env file if present (no external dependency)
32
+ try {
33
+ const envPath = resolve(process.cwd(), '.env');
34
+ const envContent = readFileSync(envPath, 'utf-8');
35
+ for (const line of envContent.split('\n')) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed || trimmed.startsWith('#')) continue;
38
+ const eqIdx = trimmed.indexOf('=');
39
+ if (eqIdx === -1) continue;
40
+ const key = trimmed.slice(0, eqIdx).trim();
41
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
42
+ if (!process.env[key]) process.env[key] = val;
43
+ }
44
+ } catch { /* No .env file */ }
45
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
46
+ import { z } from 'zod';
47
+
48
+ import { EngramStore } from './storage/sqlite.js';
49
+ import { ActivationEngine } from './engine/activation.js';
50
+ import { ConnectionEngine } from './engine/connections.js';
51
+ import { StagingBuffer } from './engine/staging.js';
52
+ import { EvictionEngine } from './engine/eviction.js';
53
+ import { RetractionEngine } from './engine/retraction.js';
54
+ import { EvalEngine } from './engine/eval.js';
55
+ import { ConsolidationEngine } from './engine/consolidation.js';
56
+ import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
57
+ import { evaluateSalience, computeNovelty, computeNoveltyWithMatch } from './core/salience.js';
58
+ import type { ConsciousState } from './types/checkpoint.js';
59
+ import type { SalienceEventType } from './core/salience.js';
60
+ import type { TaskStatus, TaskPriority } from './types/engram.js';
61
+ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
62
+ import { embed } from './core/embeddings.js';
63
+ import { startSidecar } from './hooks/sidecar.js';
64
+ import { initLogger, log, getLogPath } from './core/logger.js';
65
+
66
+ // --- Incognito Mode ---
67
+ // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
68
+ // No DB, no engines, no sidecar — just a bare MCP server that exposes nothing.
69
+
70
+ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO === 'true';
71
+
72
+ if (INCOGNITO) {
73
+ console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
74
+ const server = new McpServer({ name: 'agent-working-memory', version: '0.6.0' });
75
+ const transport = new StdioServerTransport();
76
+ server.connect(transport).catch(err => {
77
+ console.error('MCP server failed:', err);
78
+ process.exit(1);
79
+ });
80
+ // No tools registered — Claude won't see any memory_* tools
81
+ } else {
82
+
83
+ // --- Setup ---
84
+
85
+ const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
86
+ const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? 'claude-code';
87
+ const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
88
+ const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
89
+
90
+ initLogger(DB_PATH);
91
+ log(AGENT_ID, 'startup', `MCP server starting (db: ${DB_PATH}, hooks: ${HOOK_PORT})`);
92
+
93
+ const store = new EngramStore(DB_PATH);
94
+ const activationEngine = new ActivationEngine(store);
95
+ const connectionEngine = new ConnectionEngine(store, activationEngine);
96
+ const stagingBuffer = new StagingBuffer(store, activationEngine);
97
+ const evictionEngine = new EvictionEngine(store);
98
+ const retractionEngine = new RetractionEngine(store);
99
+ const evalEngine = new EvalEngine(store);
100
+ const consolidationEngine = new ConsolidationEngine(store);
101
+ const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
102
+
103
+ stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
104
+ consolidationScheduler.start();
105
+
106
+ // Coordination DB handle — set when AWM_COORDINATION=true, used by memory_write for decision propagation
107
+ let coordDb: import('better-sqlite3').Database | null = null;
108
+
109
+ const server = new McpServer({
110
+ name: 'agent-working-memory',
111
+ version: '0.6.0',
112
+ });
113
+
114
+ // --- Auto-classification for memory types ---
115
+
116
+ function classifyMemoryType(content: string): 'episodic' | 'semantic' | 'procedural' | 'unclassified' {
117
+ const lower = content.toLowerCase();
118
+ // Procedural: how-to, steps, numbered lists
119
+ if (/\bhow to\b|\bsteps?:/i.test(content) || /^\s*\d+[\.\)]\s/m.test(content) || /\bthen run\b|\bfirst,?\s/i.test(content)) {
120
+ return 'procedural';
121
+ }
122
+ // Episodic: past tense events, incidents, specific time references
123
+ if (/\b(discovered|debugged|fixed|encountered|happened|resolved|found that|we did|i did|yesterday|last week|today)\b/i.test(content)) {
124
+ return 'episodic';
125
+ }
126
+ // Semantic: facts, decisions, rules, patterns
127
+ if (/\b(is|are|should|always|never|must|uses?|requires?|means|pattern|decision|rule|convention)\b/i.test(content) && content.length < 500) {
128
+ return 'semantic';
129
+ }
130
+ return 'unclassified';
131
+ }
132
+
133
+ // --- Tools ---
134
+
135
+ server.tool(
136
+ 'memory_write',
137
+ `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
138
+
139
+ CALL THIS PROACTIVELY — do not wait to be asked. Write memories when you:
140
+ - Discover something about the codebase, bugs, or architecture
141
+ - Make a decision and want to remember why
142
+ - Encounter and resolve an error
143
+ - Learn a user preference or project pattern
144
+ - Complete a significant piece of work
145
+
146
+ The concept should be a short label (3-8 words). The content should be the full detail.`,
147
+ {
148
+ concept: z.string().describe('Short label for this memory (3-8 words)'),
149
+ content: z.string().describe('Full detail of what was learned'),
150
+ tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
151
+ event_type: z.enum(['observation', 'decision', 'friction', 'surprise', 'causal'])
152
+ .optional().default('observation')
153
+ .describe('Type of event: observation (default), decision, friction (error/blocker), surprise, causal (root cause)'),
154
+ surprise: z.number().min(0).max(1).optional().default(0.3)
155
+ .describe('How surprising was this? 0=expected, 1=very unexpected'),
156
+ decision_made: z.boolean().optional().default(false)
157
+ .describe('Was a decision made? True boosts importance'),
158
+ causal_depth: z.number().min(0).max(1).optional().default(0.3)
159
+ .describe('How deep is the causal understanding? 0=surface, 1=root cause'),
160
+ resolution_effort: z.number().min(0).max(1).optional().default(0.3)
161
+ .describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
162
+ memory_class: z.enum(['canonical', 'working', 'ephemeral']).optional().default('working')
163
+ .describe('Memory class: canonical (source-of-truth, never stages), working (default), ephemeral (temporary, decays faster)'),
164
+ memory_type: z.enum(['episodic', 'semantic', 'procedural', 'unclassified']).optional()
165
+ .describe('Memory type: episodic (events/incidents), semantic (facts/decisions), procedural (how-to/steps). Auto-classified if omitted.'),
166
+ supersedes: z.string().optional()
167
+ .describe('ID of an older memory this one replaces. The old memory is down-ranked, not deleted.'),
168
+ },
169
+ async (params) => {
170
+ // Check novelty with match info for reinforcement
171
+ const noveltyResult = computeNoveltyWithMatch(store, AGENT_ID, params.concept, params.content);
172
+ const novelty = noveltyResult.novelty;
173
+
174
+ // --- Reinforce-on-Duplicate check ---
175
+ // Tightened thresholds: require near-exact match (novelty < 0.3, BM25 > 0.85, 60% content overlap)
176
+ if (novelty < 0.3
177
+ && noveltyResult.matchScore > 0.85
178
+ && noveltyResult.matchedEngramId) {
179
+ const matchedEngram = store.getEngram(noveltyResult.matchedEngramId);
180
+ if (matchedEngram) {
181
+ const existingTokens = new Set(matchedEngram.content.toLowerCase().split(/\s+/).filter(w => w.length > 3));
182
+ const newTokens = new Set(params.content.toLowerCase().split(/\s+/).filter(w => w.length > 3));
183
+ let overlap = 0;
184
+ for (const t of newTokens) { if (existingTokens.has(t)) overlap++; }
185
+ const contentOverlap = newTokens.size > 0 ? overlap / newTokens.size : 0;
186
+
187
+ if (contentOverlap > 0.6) {
188
+ // True duplicate — reinforce existing and skip creation
189
+ store.touchEngram(noveltyResult.matchedEngramId);
190
+ try { store.updateAutoCheckpointWrite(AGENT_ID, noveltyResult.matchedEngramId); } catch { /* non-fatal */ }
191
+ log(AGENT_ID, 'write:reinforce', `"${params.concept}" reinforced "${matchedEngram.concept}" (overlap=${contentOverlap.toFixed(2)})`);
192
+ return {
193
+ content: [{
194
+ type: 'text' as const,
195
+ text: `Reinforced existing memory "${matchedEngram.concept}" (overlap ${(contentOverlap * 100).toFixed(0)}%)`,
196
+ }],
197
+ };
198
+ }
199
+ // Partial match — continue to create new memory
200
+ log(AGENT_ID, 'write:partial-match', `"${params.concept}" partially matched "${matchedEngram.concept}" (overlap=${contentOverlap.toFixed(2)}), creating new memory`);
201
+ }
202
+ }
203
+
204
+ const salience = evaluateSalience({
205
+ content: params.content,
206
+ eventType: params.event_type as SalienceEventType,
207
+ surprise: params.surprise,
208
+ decisionMade: params.decision_made,
209
+ causalDepth: params.causal_depth,
210
+ resolutionEffort: params.resolution_effort,
211
+ novelty,
212
+ memoryClass: params.memory_class,
213
+ });
214
+
215
+ // v0.5.4: No longer discard — store everything, use salience for ranking.
216
+ // Low-salience memories get low confidence so they rank below high-salience
217
+ // in retrieval, but remain available for recall when needed.
218
+ const isLowSalience = salience.disposition === 'discard';
219
+
220
+ const CONFIDENCE_PRIORS: Record<string, number> = {
221
+ decision: 0.65,
222
+ friction: 0.60,
223
+ causal: 0.60,
224
+ surprise: 0.55,
225
+ observation: 0.45,
226
+ };
227
+ const confidencePrior = isLowSalience
228
+ ? 0.25
229
+ : salience.disposition === 'staging'
230
+ ? 0.40
231
+ : CONFIDENCE_PRIORS[params.event_type ?? 'observation'] ?? 0.45;
232
+
233
+ const memoryType = params.memory_type ?? classifyMemoryType(params.content);
234
+
235
+ const engram = store.createEngram({
236
+ agentId: AGENT_ID,
237
+ concept: params.concept,
238
+ content: params.content,
239
+ tags: isLowSalience ? [...(params.tags ?? []), 'low-salience'] : params.tags,
240
+ salience: salience.score,
241
+ confidence: confidencePrior,
242
+ salienceFeatures: salience.features,
243
+ reasonCodes: salience.reasonCodes,
244
+ ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
245
+ memoryClass: params.memory_class,
246
+ memoryType,
247
+ supersedes: params.supersedes,
248
+ });
249
+
250
+ if (salience.disposition === 'staging') {
251
+ store.updateStage(engram.id, 'staging');
252
+ } else {
253
+ connectionEngine.enqueue(engram.id);
254
+ }
255
+
256
+ // Handle supersession: mark old memory as superseded
257
+ if (params.supersedes) {
258
+ const oldEngram = store.getEngram(params.supersedes);
259
+ if (oldEngram) {
260
+ store.supersedeEngram(params.supersedes, engram.id);
261
+ // Create supersession association
262
+ store.upsertAssociation(engram.id, oldEngram.id, 0.8, 'causal', 0.9);
263
+ }
264
+ }
265
+
266
+ // Generate embedding asynchronously (don't block response)
267
+ embed(`${params.concept} ${params.content}`).then(vec => {
268
+ store.updateEmbedding(engram.id, vec);
269
+ }).catch(() => {}); // Embedding failure is non-fatal
270
+
271
+ // Auto-checkpoint: track write
272
+ try { store.updateAutoCheckpointWrite(AGENT_ID, engram.id); } catch { /* non-fatal */ }
273
+
274
+ // Decision propagation: when decision_made=true and coordination is enabled,
275
+ // broadcast to coord_decisions so other agents can discover it
276
+ if (params.decision_made && coordDb) {
277
+ try {
278
+ const agent = coordDb.prepare(
279
+ `SELECT id, current_task FROM coord_agents WHERE name = ? AND status != 'dead' ORDER BY last_seen DESC LIMIT 1`
280
+ ).get(AGENT_ID) as { id: string; current_task: string | null } | undefined;
281
+ if (agent) {
282
+ coordDb.prepare(
283
+ `INSERT INTO coord_decisions (author_id, assignment_id, tags, summary) VALUES (?, ?, ?, ?)`
284
+ ).run(agent.id, agent.current_task, params.tags ? JSON.stringify(params.tags) : null, params.concept);
285
+ }
286
+ } catch { /* decision propagation is non-fatal */ }
287
+ }
288
+
289
+ const logDisposition = isLowSalience ? 'low-salience' : salience.disposition;
290
+ log(AGENT_ID, `write:${logDisposition}`, `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)} id=${engram.id}`);
291
+
292
+ return {
293
+ content: [{
294
+ type: 'text' as const,
295
+ text: `Stored (${salience.disposition}) "${params.concept}" [${salience.score.toFixed(2)}]\nID: ${engram.id}`,
296
+ }],
297
+ };
298
+ }
299
+ );
300
+
301
+ server.tool(
302
+ 'memory_recall',
303
+ `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
304
+
305
+ ALWAYS call this when:
306
+ - Starting work on a project or topic (recall what you know)
307
+ - Debugging (recall similar errors and solutions)
308
+ - Making decisions (recall past decisions and outcomes)
309
+ - The user mentions a topic you might have stored memories about
310
+
311
+ Accepts either "query" or "context" parameter — both work identically.
312
+ Returns the most relevant memories ranked by text relevance, temporal recency, and associative strength.`,
313
+ {
314
+ query: z.string().optional().describe('What to search for — describe the situation, question, or topic'),
315
+ context: z.string().optional().describe('Alias for query (either works)'),
316
+ limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
317
+ min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
318
+ include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
319
+ use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
320
+ use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
321
+ memory_type: z.enum(['episodic', 'semantic', 'procedural']).optional().describe('Filter by memory type (omit to search all types)'),
322
+ },
323
+ async (params) => {
324
+ const queryText = params.query ?? params.context;
325
+ if (!queryText) {
326
+ return {
327
+ content: [{
328
+ type: 'text' as const,
329
+ text: 'Error: provide either "query" or "context" parameter with your search text.',
330
+ }],
331
+ };
332
+ }
333
+ const results = await activationEngine.activate({
334
+ agentId: AGENT_ID,
335
+ context: queryText,
336
+ limit: params.limit,
337
+ minScore: params.min_score,
338
+ includeStaging: params.include_staging,
339
+ useReranker: params.use_reranker,
340
+ useExpansion: params.use_expansion,
341
+ memoryType: params.memory_type,
342
+ });
343
+
344
+ // Auto-checkpoint: track recall
345
+ try {
346
+ const ids = results.map(r => r.engram.id);
347
+ store.updateAutoCheckpointRecall(AGENT_ID, queryText, ids);
348
+ } catch { /* non-fatal */ }
349
+
350
+ log(AGENT_ID, 'recall', `"${queryText.slice(0, 80)}" → ${results.length} results`);
351
+
352
+ if (results.length === 0) {
353
+ return {
354
+ content: [{
355
+ type: 'text' as const,
356
+ text: 'No relevant memories found.',
357
+ }],
358
+ };
359
+ }
360
+
361
+ const lines = results.map((r, i) => {
362
+ return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)}): ${r.engram.content}`;
363
+ });
364
+
365
+ return {
366
+ content: [{
367
+ type: 'text' as const,
368
+ text: lines.join('\n'),
369
+ }],
370
+ };
371
+ }
372
+ );
373
+
374
+ server.tool(
375
+ 'memory_feedback',
376
+ `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
377
+
378
+ Always call this after using a recalled memory so the system learns what's valuable.`,
379
+ {
380
+ engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
381
+ useful: z.boolean().describe('Was this memory actually helpful?'),
382
+ context: z.string().optional().describe('Brief note on why it was/wasn\'t useful'),
383
+ },
384
+ async (params) => {
385
+ store.logRetrievalFeedback(null, params.engram_id, params.useful, params.context ?? '');
386
+
387
+ const engram = store.getEngram(params.engram_id);
388
+ if (engram) {
389
+ const delta = params.useful
390
+ ? DEFAULT_AGENT_CONFIG.feedbackPositiveBoost
391
+ : -DEFAULT_AGENT_CONFIG.feedbackNegativePenalty;
392
+ store.updateConfidence(engram.id, engram.confidence + delta);
393
+ }
394
+
395
+ return {
396
+ content: [{
397
+ type: 'text' as const,
398
+ text: `Feedback: ${params.useful ? '+useful' : '-not useful'}`,
399
+ }],
400
+ };
401
+ }
402
+ );
403
+
404
+ server.tool(
405
+ 'memory_retract',
406
+ `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
407
+
408
+ Use this when you discover a memory contains incorrect information.`,
409
+ {
410
+ engram_id: z.string().describe('ID of the wrong memory'),
411
+ reason: z.string().describe('Why is this memory wrong?'),
412
+ correction: z.string().optional().describe('What is the correct information? (creates a new memory)'),
413
+ },
414
+ async (params) => {
415
+ const result = retractionEngine.retract({
416
+ agentId: AGENT_ID,
417
+ targetEngramId: params.engram_id,
418
+ reason: params.reason,
419
+ counterContent: params.correction,
420
+ });
421
+
422
+ const parts = [`Memory ${params.engram_id} retracted.`];
423
+ if (result.correctionId) {
424
+ parts.push(`Correction stored as ${result.correctionId}.`);
425
+ }
426
+ parts.push(`${result.associatesAffected} related memories had confidence reduced.`);
427
+
428
+ return {
429
+ content: [{
430
+ type: 'text' as const,
431
+ text: parts.join(' '),
432
+ }],
433
+ };
434
+ }
435
+ );
436
+
437
+ server.tool(
438
+ 'memory_supersede',
439
+ `Replace an outdated memory with a newer one. Unlike retraction (which marks memories as wrong), supersession marks the old memory as outdated but historically correct.
440
+
441
+ Use this when:
442
+ - A status or count has changed (e.g., "5 reviews done" → "7 reviews done")
443
+ - Architecture or infrastructure evolved (e.g., "two-repo model" → "three-repo model")
444
+ - A schedule or plan was updated
445
+
446
+ The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`,
447
+ {
448
+ old_engram_id: z.string().describe('ID of the outdated memory'),
449
+ new_engram_id: z.string().describe('ID of the replacement memory'),
450
+ reason: z.string().optional().describe('Why the old memory is outdated'),
451
+ },
452
+ async (params) => {
453
+ const oldEngram = store.getEngram(params.old_engram_id);
454
+ if (!oldEngram) {
455
+ return { content: [{ type: 'text' as const, text: `Old memory not found: ${params.old_engram_id}` }] };
456
+ }
457
+ const newEngram = store.getEngram(params.new_engram_id);
458
+ if (!newEngram) {
459
+ return { content: [{ type: 'text' as const, text: `New memory not found: ${params.new_engram_id}` }] };
460
+ }
461
+
462
+ store.supersedeEngram(params.old_engram_id, params.new_engram_id);
463
+
464
+ // Create supersession association (new → old)
465
+ store.upsertAssociation(params.new_engram_id, params.old_engram_id, 0.8, 'causal', 0.9);
466
+
467
+ // Reduce old memory's confidence (not to zero — it's historical, not wrong)
468
+ store.updateConfidence(params.old_engram_id, Math.max(0.2, oldEngram.confidence * 0.4));
469
+
470
+ log(AGENT_ID, 'supersede', `"${oldEngram.concept}" → "${newEngram.concept}"${params.reason ? ` (${params.reason})` : ''}`);
471
+
472
+ return {
473
+ content: [{
474
+ type: 'text' as const,
475
+ text: `Superseded: "${oldEngram.concept}" → "${newEngram.concept}"`,
476
+ }],
477
+ };
478
+ }
479
+ );
480
+
481
+ server.tool(
482
+ 'memory_stats',
483
+ `Get memory health stats how many memories, confidence levels, association count, and system performance.
484
+ Also shows the activity log path so the user can tail it to see what's happening.`,
485
+ {},
486
+ async () => {
487
+ const metrics = evalEngine.computeMetrics(AGENT_ID);
488
+ const checkpoint = store.getCheckpoint(AGENT_ID);
489
+ const lines = [
490
+ `Agent: ${AGENT_ID}`,
491
+ `Active memories: ${metrics.activeEngramCount}`,
492
+ `Staging: ${metrics.stagingEngramCount}`,
493
+ `Retracted: ${metrics.retractedCount}`,
494
+ `Avg confidence: ${metrics.avgConfidence.toFixed(3)}`,
495
+ `Total edges: ${metrics.totalEdges}`,
496
+ `Edge utility: ${(metrics.edgeUtilityRate * 100).toFixed(1)}%`,
497
+ `Activations (24h): ${metrics.activationCount}`,
498
+ `Avg latency: ${metrics.avgLatencyMs.toFixed(1)}ms`,
499
+ ``,
500
+ `Session writes: ${checkpoint?.auto.writeCountSinceConsolidation ?? 0}`,
501
+ `Session recalls: ${checkpoint?.auto.recallCountSinceConsolidation ?? 0}`,
502
+ `Last activity: ${checkpoint?.auto.lastActivityAt?.toISOString() ?? 'never'}`,
503
+ `Checkpoint: ${checkpoint?.executionState ? checkpoint.executionState.currentTask : 'none'}`,
504
+ ``,
505
+ `Activity log: ${getLogPath() ?? 'not configured'}`,
506
+ `Hook sidecar: 127.0.0.1:${HOOK_PORT}`,
507
+ ];
508
+
509
+ return {
510
+ content: [{
511
+ type: 'text' as const,
512
+ text: lines.join('\n'),
513
+ }],
514
+ };
515
+ }
516
+ );
517
+
518
+ // --- Checkpointing Tools ---
519
+
520
+ server.tool(
521
+ 'memory_checkpoint',
522
+ `Save your current execution state so you can recover after context compaction.
523
+
524
+ ALWAYS call this before:
525
+ - Long operations (multi-file generation, large refactors, overnight work)
526
+ - Anything that might fill the context window
527
+ - Switching to a different task
528
+
529
+ Also call periodically during long sessions to avoid losing state. The state is saved per-agent and overwrites any previous checkpoint.`,
530
+ {
531
+ current_task: z.string().describe('What you are currently working on'),
532
+ decisions: z.array(z.string()).optional().default([])
533
+ .describe('Key decisions made so far'),
534
+ active_files: z.array(z.string()).optional().default([])
535
+ .describe('Files you are currently working with'),
536
+ next_steps: z.array(z.string()).optional().default([])
537
+ .describe('What needs to happen next'),
538
+ related_memory_ids: z.array(z.string()).optional().default([])
539
+ .describe('IDs of memories relevant to current work'),
540
+ notes: z.string().optional().default('')
541
+ .describe('Any other context worth preserving'),
542
+ episode_id: z.string().optional()
543
+ .describe('Current episode ID if known'),
544
+ },
545
+ async (params) => {
546
+ const state: ConsciousState = {
547
+ currentTask: params.current_task,
548
+ decisions: params.decisions,
549
+ activeFiles: params.active_files,
550
+ nextSteps: params.next_steps,
551
+ relatedMemoryIds: params.related_memory_ids,
552
+ notes: params.notes,
553
+ episodeId: params.episode_id ?? null,
554
+ };
555
+
556
+ store.saveCheckpoint(AGENT_ID, state);
557
+ log(AGENT_ID, 'checkpoint', `"${params.current_task}" decisions=${params.decisions.length} files=${params.active_files.length}`);
558
+
559
+ return {
560
+ content: [{
561
+ type: 'text' as const,
562
+ text: `Checkpoint saved: "${params.current_task}" (${params.decisions.length} decisions, ${params.active_files.length} files)`,
563
+ }],
564
+ };
565
+ }
566
+ );
567
+
568
+ server.tool(
569
+ 'memory_restore',
570
+ `Restore your previous execution state after context compaction or at session start.
571
+
572
+ Returns:
573
+ - Your saved execution state (task, decisions, next steps, files)
574
+ - Recently recalled memories for context
575
+ - Your last write for continuity
576
+ - How long you were idle
577
+
578
+ Use this at the start of every session or after compaction to pick up where you left off.`,
579
+ {},
580
+ async () => {
581
+ const checkpoint = store.getCheckpoint(AGENT_ID);
582
+
583
+ const now = Date.now();
584
+ const idleMs = checkpoint
585
+ ? now - checkpoint.auto.lastActivityAt.getTime()
586
+ : 0;
587
+
588
+ // Get last written engram
589
+ let lastWrite: { id: string; concept: string; content: string } | null = null;
590
+ if (checkpoint?.auto.lastWriteId) {
591
+ const engram = store.getEngram(checkpoint.auto.lastWriteId);
592
+ if (engram) {
593
+ lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
594
+ }
595
+ }
596
+
597
+ // Recall memories using last context
598
+ let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
599
+ const recallContext = checkpoint?.auto.lastRecallContext
600
+ ?? checkpoint?.executionState?.currentTask
601
+ ?? null;
602
+
603
+ if (recallContext) {
604
+ try {
605
+ const results = await activationEngine.activate({
606
+ agentId: AGENT_ID,
607
+ context: recallContext,
608
+ limit: 5,
609
+ minScore: 0.05,
610
+ useReranker: true,
611
+ useExpansion: true,
612
+ });
613
+ recalledMemories = results.map(r => ({
614
+ id: r.engram.id,
615
+ concept: r.engram.concept,
616
+ content: r.engram.content,
617
+ score: r.score,
618
+ }));
619
+ } catch { /* recall failure is non-fatal */ }
620
+ }
621
+
622
+ // Consolidation on restore:
623
+ // - If idle >5min but last consolidation was recent (graceful exit ran it), skip
624
+ // - If idle >5min and no recent consolidation, run full cycle (non-graceful exit fallback)
625
+ const MINI_IDLE_MS = 5 * 60_000;
626
+ const FULL_CONSOLIDATION_GAP_MS = 10 * 60_000; // 10 min — if last consolidation was longer ago, run full
627
+ let miniConsolidationTriggered = false;
628
+ let fullConsolidationTriggered = false;
629
+
630
+ if (idleMs > MINI_IDLE_MS) {
631
+ const sinceLastConsolidation = checkpoint?.lastConsolidationAt
632
+ ? now - checkpoint.lastConsolidationAt.getTime()
633
+ : Infinity;
634
+
635
+ if (sinceLastConsolidation > FULL_CONSOLIDATION_GAP_MS) {
636
+ // No recent consolidation graceful exit didn't happen, run full cycle
637
+ fullConsolidationTriggered = true;
638
+ try {
639
+ const result = await consolidationEngine.consolidate(AGENT_ID);
640
+ store.markConsolidation(AGENT_ID, false);
641
+ log(AGENT_ID, 'consolidation', `full sleep cycle on restore (no graceful exit, idle ${Math.round(idleMs / 60_000)}min, last consolidation ${Math.round(sinceLastConsolidation / 60_000)}min ago) — ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
642
+ } catch { /* consolidation failure is non-fatal */ }
643
+ } else {
644
+ // Recent consolidation exists — graceful exit already handled it, just do mini
645
+ miniConsolidationTriggered = true;
646
+ consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => {});
647
+ }
648
+ }
649
+
650
+ // Format response
651
+ const parts: string[] = [];
652
+ const idleMin = Math.round(idleMs / 60_000);
653
+ const consolidationNote = fullConsolidationTriggered
654
+ ? ' (full consolidation no graceful exit detected)'
655
+ : miniConsolidationTriggered
656
+ ? ' (mini-consolidation triggered)'
657
+ : '';
658
+ log(AGENT_ID, 'restore', `idle=${idleMin}min checkpoint=${!!checkpoint?.executionState} recalled=${recalledMemories.length} lastWrite=${lastWrite?.concept ?? 'none'}${fullConsolidationTriggered ? ' FULL_CONSOLIDATION' : ''}`);
659
+ parts.push(`Idle: ${idleMin}min${consolidationNote}`);
660
+
661
+ if (checkpoint?.executionState) {
662
+ const s = checkpoint.executionState;
663
+ parts.push(`\n**Current task:** ${s.currentTask}`);
664
+ if (s.decisions.length) parts.push(`**Decisions:** ${s.decisions.join('; ')}`);
665
+ if (s.nextSteps.length) parts.push(`**Next steps:** ${s.nextSteps.map((st, i) => `${i + 1}. ${st}`).join(', ')}`);
666
+ if (s.activeFiles.length) parts.push(`**Active files:** ${s.activeFiles.join(', ')}`);
667
+ if (s.notes) parts.push(`**Notes:** ${s.notes}`);
668
+ if (checkpoint.checkpointAt) parts.push(`_Saved at: ${checkpoint.checkpointAt.toISOString()}_`);
669
+ } else {
670
+ parts.push('\nNo explicit checkpoint saved.');
671
+ parts.push('\n**Tip:** Use memory_write to save important learnings, and memory_checkpoint before long operations so you can recover state.');
672
+ }
673
+
674
+ if (lastWrite) {
675
+ parts.push(`\n**Last write:** ${lastWrite.concept}\n${lastWrite.content}`);
676
+ }
677
+
678
+ if (recalledMemories.length > 0) {
679
+ parts.push(`\n**Recalled memories (${recalledMemories.length}):**`);
680
+ for (const m of recalledMemories) {
681
+ parts.push(`- **${m.concept}** (${m.score.toFixed(3)}): ${m.content.slice(0, 150)}${m.content.length > 150 ? '...' : ''}`);
682
+ }
683
+ }
684
+
685
+ // Peer decisions: show recent decisions from other agents (last 30 min)
686
+ if (coordDb) {
687
+ try {
688
+ const myAgent = coordDb.prepare(
689
+ `SELECT id FROM coord_agents WHERE name = ? AND status != 'dead' ORDER BY last_seen DESC LIMIT 1`
690
+ ).get(AGENT_ID) as { id: string } | undefined;
691
+
692
+ const peerDecisions = coordDb.prepare(
693
+ `SELECT d.summary, a.name AS author_name, d.created_at
694
+ FROM coord_decisions d JOIN coord_agents a ON d.author_id = a.id
695
+ WHERE d.author_id != ? AND d.created_at > datetime('now', '-30 minutes')
696
+ ORDER BY d.created_at DESC LIMIT 10`
697
+ ).all(myAgent?.id ?? '') as Array<{ summary: string; author_name: string; created_at: string }>;
698
+
699
+ if (peerDecisions.length > 0) {
700
+ parts.push(`\n**Peer decisions (last 30 min):**`);
701
+ for (const d of peerDecisions) {
702
+ parts.push(`- [${d.author_name}] ${d.summary} (${d.created_at})`);
703
+ }
704
+ }
705
+ } catch { /* peer decisions are non-fatal */ }
706
+ }
707
+
708
+ return {
709
+ content: [{
710
+ type: 'text' as const,
711
+ text: parts.join('\n'),
712
+ }],
713
+ };
714
+ }
715
+ );
716
+
717
+ // --- Task Management Tools ---
718
+
719
+ server.tool(
720
+ 'memory_task_add',
721
+ `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
722
+
723
+ Use this when:
724
+ - You identify work that needs doing but can't do it right now
725
+ - The user mentions something to do later
726
+ - You want to park a sub-task while focusing on something more urgent
727
+
728
+ Tasks automatically get high salience so they won't be discarded.`,
729
+ {
730
+ concept: z.string().describe('Short task title (3-10 words)'),
731
+ content: z.string().describe('Full task description — what needs doing, context, acceptance criteria'),
732
+ tags: z.array(z.string()).optional().describe('Tags for categorization'),
733
+ priority: z.enum(['urgent', 'high', 'medium', 'low']).default('medium')
734
+ .describe('Task priority: urgent (do now), high (do soon), medium (normal), low (backlog)'),
735
+ blocked_by: z.string().optional().describe('ID of a task that must finish first'),
736
+ },
737
+ async (params) => {
738
+ const engram = store.createEngram({
739
+ agentId: AGENT_ID,
740
+ concept: params.concept,
741
+ content: params.content,
742
+ tags: [...(params.tags ?? []), 'task'],
743
+ salience: 0.9, // Tasks always high salience
744
+ confidence: 0.8,
745
+ salienceFeatures: {
746
+ surprise: 0.5,
747
+ decisionMade: true,
748
+ causalDepth: 0.5,
749
+ resolutionEffort: 0.5,
750
+ eventType: 'decision',
751
+ },
752
+ reasonCodes: ['task-created'],
753
+ taskStatus: params.blocked_by ? 'blocked' : 'open',
754
+ taskPriority: params.priority as TaskPriority,
755
+ blockedBy: params.blocked_by,
756
+ });
757
+
758
+ connectionEngine.enqueue(engram.id);
759
+
760
+ // Generate embedding asynchronously
761
+ embed(`${params.concept} ${params.content}`).then(vec => {
762
+ store.updateEmbedding(engram.id, vec);
763
+ }).catch(() => {});
764
+
765
+ return {
766
+ content: [{
767
+ type: 'text' as const,
768
+ text: `Task created: "${params.concept}" (${params.priority})`,
769
+ }],
770
+ };
771
+ }
772
+ );
773
+
774
+ server.tool(
775
+ 'memory_task_update',
776
+ `Update a task's status or priority. Use this to:
777
+ - Start working on a task (open → in_progress)
778
+ - Mark a task done (→ done)
779
+ - Block a task on another (→ blocked)
780
+ - Reprioritize (change priority)
781
+ - Unblock a task (clear blocked_by)`,
782
+ {
783
+ task_id: z.string().describe('ID of the task to update'),
784
+ status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
785
+ .describe('New status'),
786
+ priority: z.enum(['urgent', 'high', 'medium', 'low']).optional()
787
+ .describe('New priority'),
788
+ blocked_by: z.string().optional().describe('ID of blocking task (set to empty string to unblock)'),
789
+ },
790
+ async (params) => {
791
+ const engram = store.getEngram(params.task_id);
792
+ if (!engram || !engram.taskStatus) {
793
+ return { content: [{ type: 'text' as const, text: `Task not found: ${params.task_id}` }] };
794
+ }
795
+
796
+ if (params.blocked_by !== undefined) {
797
+ store.updateBlockedBy(params.task_id, params.blocked_by || null);
798
+ }
799
+ if (params.status) {
800
+ store.updateTaskStatus(params.task_id, params.status as TaskStatus);
801
+ }
802
+ if (params.priority) {
803
+ store.updateTaskPriority(params.task_id, params.priority as TaskPriority);
804
+ }
805
+
806
+ const updated = store.getEngram(params.task_id)!;
807
+ return {
808
+ content: [{
809
+ type: 'text' as const,
810
+ text: `Updated: "${updated.concept}" → ${updated.taskStatus} (${updated.taskPriority})`,
811
+ }],
812
+ };
813
+ }
814
+ );
815
+
816
+ server.tool(
817
+ 'memory_task_list',
818
+ `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
819
+
820
+ Use at the start of a session to see what's pending, or to check blocked/done tasks.`,
821
+ {
822
+ status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
823
+ .describe('Filter by status (omit to see all active tasks)'),
824
+ include_done: z.boolean().optional().default(false)
825
+ .describe('Include completed tasks?'),
826
+ },
827
+ async (params) => {
828
+ let tasks = store.getTasks(AGENT_ID, params.status as TaskStatus | undefined);
829
+ if (!params.include_done && !params.status) {
830
+ tasks = tasks.filter(t => t.taskStatus !== 'done');
831
+ }
832
+
833
+ if (tasks.length === 0) {
834
+ return { content: [{ type: 'text' as const, text: 'No tasks found.' }] };
835
+ }
836
+
837
+ const lines = tasks.map((t, i) => {
838
+ const blocked = t.blockedBy ? ` [blocked by ${t.blockedBy}]` : '';
839
+ const tags = t.tags?.filter(tag => tag !== 'task').join(', ');
840
+ return `${i + 1}. [${t.taskStatus}] **${t.concept}** (${t.taskPriority})${blocked}\n ${t.content.slice(0, 120)}${t.content.length > 120 ? '...' : ''}\n ${tags ? `Tags: ${tags} | ` : ''}ID: ${t.id}`;
841
+ });
842
+
843
+ return {
844
+ content: [{
845
+ type: 'text' as const,
846
+ text: `Tasks (${tasks.length}):\n\n${lines.join('\n\n')}`,
847
+ }],
848
+ };
849
+ }
850
+ );
851
+
852
+ server.tool(
853
+ 'memory_task_next',
854
+ `Get the single most important task to work on next.
855
+
856
+ Prioritizes: in_progress tasks first (finish what you started), then by priority level, then oldest first. Skips blocked and done tasks.
857
+
858
+ Use this when you finish a task or need to decide what to do next.`,
859
+ {},
860
+ async () => {
861
+ const next = store.getNextTask(AGENT_ID);
862
+ if (!next) {
863
+ return { content: [{ type: 'text' as const, text: 'No actionable tasks. All clear!' }] };
864
+ }
865
+
866
+ const blocked = next.blockedBy ? `\nBlocked by: ${next.blockedBy}` : '';
867
+ const tags = next.tags?.filter(tag => tag !== 'task').join(', ');
868
+
869
+ return {
870
+ content: [{
871
+ type: 'text' as const,
872
+ text: `Next task:\n**${next.concept}** (${next.taskPriority})\nStatus: ${next.taskStatus}\n${next.content}${blocked}\n${tags ? `Tags: ${tags}\n` : ''}ID: ${next.id}`,
873
+ }],
874
+ };
875
+ }
876
+ );
877
+
878
+ // --- Task Bracket Tools ---
879
+
880
+ server.tool(
881
+ 'memory_task_begin',
882
+ `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
883
+
884
+ CALL THIS when starting:
885
+ - A multi-step operation (doc generation, large refactor, migration)
886
+ - Work on a new topic or project area
887
+ - Anything that might fill the context window
888
+
889
+ This ensures your state is saved before you start, and primes recall with relevant context.`,
890
+ {
891
+ topic: z.string().describe('What task are you starting? (3-15 words)'),
892
+ files: z.array(z.string()).optional().default([])
893
+ .describe('Files you expect to work with'),
894
+ notes: z.string().optional().default('')
895
+ .describe('Any additional context'),
896
+ },
897
+ async (params) => {
898
+ // 1. Checkpoint current state
899
+ const checkpoint = store.getCheckpoint(AGENT_ID);
900
+ const prevTask = checkpoint?.executionState?.currentTask ?? 'None';
901
+
902
+ store.saveCheckpoint(AGENT_ID, {
903
+ currentTask: params.topic,
904
+ decisions: [],
905
+ activeFiles: params.files,
906
+ nextSteps: [],
907
+ relatedMemoryIds: [],
908
+ notes: params.notes || `Started via memory_task_begin. Previous task: ${prevTask}`,
909
+ episodeId: null,
910
+ });
911
+
912
+ // 2. Auto-recall relevant memories
913
+ let recalledSummary = '';
914
+ try {
915
+ const results = await activationEngine.activate({
916
+ agentId: AGENT_ID,
917
+ context: params.topic,
918
+ limit: 5,
919
+ minScore: 0.05,
920
+ useReranker: true,
921
+ useExpansion: true,
922
+ });
923
+
924
+ if (results.length > 0) {
925
+ const lines = results.map((r, i) => {
926
+ const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
927
+ return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${tags}\n ${r.engram.content.slice(0, 150)}${r.engram.content.length > 150 ? '...' : ''}`;
928
+ });
929
+ recalledSummary = `\n\n**Recalled memories (${results.length}):**\n${lines.join('\n')}`;
930
+
931
+ // Track recall
932
+ store.updateAutoCheckpointRecall(AGENT_ID, params.topic, results.map(r => r.engram.id));
933
+ }
934
+ } catch { /* recall failure is non-fatal */ }
935
+
936
+ log(AGENT_ID, 'task:begin', `"${params.topic}" prev="${prevTask}"`);
937
+
938
+ return {
939
+ content: [{
940
+ type: 'text' as const,
941
+ text: `Started: "${params.topic}" (prev: ${prevTask})${recalledSummary}`,
942
+ }],
943
+ };
944
+ }
945
+ );
946
+
947
+ server.tool(
948
+ 'memory_task_end',
949
+ `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
950
+
951
+ CALL THIS when you finish:
952
+ - A multi-step operation
953
+ - Before switching to a different topic
954
+ - At the end of a work session
955
+
956
+ This captures what was accomplished so future sessions can recall it.`,
957
+ {
958
+ summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
959
+ tags: z.array(z.string()).optional().default([])
960
+ .describe('Tags for the summary memory'),
961
+ supersedes: z.array(z.string()).optional().default([])
962
+ .describe('IDs of older memories this task summary replaces (marks them as superseded)'),
963
+ },
964
+ async (params) => {
965
+ // 1. Write summary as a memory
966
+ const salience = evaluateSalience({
967
+ content: params.summary,
968
+ eventType: 'decision',
969
+ surprise: 0.3,
970
+ decisionMade: true,
971
+ causalDepth: 0.5,
972
+ resolutionEffort: 0.5,
973
+ });
974
+
975
+ // Determine the real task name for the summary engram
976
+ const checkpoint = store.getCheckpoint(AGENT_ID);
977
+ const rawTask = checkpoint?.executionState?.currentTask ?? 'Unknown task';
978
+ // Strip any "Completed: " prefixes to avoid cascading
979
+ const cleanedTask = rawTask.replace(/^(Completed: )+/, '');
980
+ // Don't use auto-checkpoint or already-completed tasks as real task names
981
+ const isNamedTask = !cleanedTask.startsWith('Auto-checkpoint') && cleanedTask !== 'Unknown task';
982
+ const completedTask = isNamedTask
983
+ ? cleanedTask
984
+ : params.summary.slice(0, 60).replace(/\n/g, ' ');
985
+
986
+ const engram = store.createEngram({
987
+ agentId: AGENT_ID,
988
+ concept: completedTask.slice(0, 80),
989
+ content: params.summary,
990
+ tags: [...params.tags, 'task-summary'],
991
+ salience: isNamedTask ? Math.max(salience.score, 0.7) : salience.score, // Only floor salience for named tasks
992
+ confidence: 0.65, // Task summaries are decision-grade (completed work)
993
+ salienceFeatures: salience.features,
994
+ reasonCodes: [...salience.reasonCodes, 'task-end'],
995
+ });
996
+
997
+ connectionEngine.enqueue(engram.id);
998
+
999
+ // 2. Handle supersessions — mark old memories as outdated
1000
+ let supersededCount = 0;
1001
+ for (const oldId of params.supersedes) {
1002
+ const oldEngram = store.getEngram(oldId);
1003
+ if (oldEngram) {
1004
+ store.supersedeEngram(oldId, engram.id);
1005
+ store.upsertAssociation(engram.id, oldId, 0.8, 'causal', 0.9);
1006
+ store.updateConfidence(oldId, Math.max(0.2, oldEngram.confidence * 0.4));
1007
+ supersededCount++;
1008
+ }
1009
+ }
1010
+
1011
+ // Generate embedding asynchronously
1012
+ embed(`Task completed: ${params.summary}`).then(vec => {
1013
+ store.updateEmbedding(engram.id, vec);
1014
+ }).catch(() => {});
1015
+
1016
+ // 2. Update checkpoint to reflect completion
1017
+ store.saveCheckpoint(AGENT_ID, {
1018
+ currentTask: `Completed: ${completedTask}`,
1019
+ decisions: checkpoint?.executionState?.decisions ?? [],
1020
+ activeFiles: [],
1021
+ nextSteps: [],
1022
+ relatedMemoryIds: [engram.id],
1023
+ notes: `Task completed. Summary memory: ${engram.id}`,
1024
+ episodeId: null,
1025
+ });
1026
+
1027
+ store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
1028
+ log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)} superseded=${supersededCount}`);
1029
+
1030
+ const supersededNote = supersededCount > 0 ? ` (${supersededCount} old memories superseded)` : '';
1031
+ return {
1032
+ content: [{
1033
+ type: 'text' as const,
1034
+ text: `Completed: "${completedTask}" [${salience.score.toFixed(2)}]${supersededNote}`,
1035
+ }],
1036
+ };
1037
+ }
1038
+ );
1039
+
1040
+ // --- Start ---
1041
+
1042
+ async function main() {
1043
+ const transport = new StdioServerTransport();
1044
+ await server.connect(transport);
1045
+
1046
+ // Start hook sidecar (lightweight HTTP for Claude Code hooks)
1047
+ const sidecar = startSidecar({
1048
+ store,
1049
+ agentId: AGENT_ID,
1050
+ secret: HOOK_SECRET,
1051
+ port: HOOK_PORT,
1052
+ onConsolidate: async (agentId, reason) => {
1053
+ console.error(`[mcp] consolidation triggered: ${reason}`);
1054
+ const result = await consolidationEngine.consolidate(agentId);
1055
+ store.markConsolidation(agentId, false);
1056
+ console.error(`[mcp] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
1057
+ },
1058
+ });
1059
+
1060
+ // Coordination MCP tools (opt-in via AWM_COORDINATION=true)
1061
+ const coordEnabled = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
1062
+ if (coordEnabled) {
1063
+ const { initCoordinationTables } = await import('./coordination/schema.js');
1064
+ const { registerCoordinationTools } = await import('./coordination/mcp-tools.js');
1065
+ initCoordinationTables(store.getDb());
1066
+ registerCoordinationTools(server, store.getDb());
1067
+ coordDb = store.getDb();
1068
+ } else {
1069
+ console.error('AWM: coordination tools disabled (set AWM_COORDINATION=true to enable)');
1070
+ }
1071
+
1072
+ // Log to stderr (stdout is reserved for MCP protocol)
1073
+ console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
1074
+ console.error(`Hook sidecar on 127.0.0.1:${HOOK_PORT}${HOOK_SECRET ? ' (auth enabled)' : ' (no auth — set AWM_HOOK_SECRET)'}`);
1075
+
1076
+ // Clean shutdown
1077
+ const cleanup = () => {
1078
+ sidecar.close();
1079
+ consolidationScheduler.stop();
1080
+ stagingBuffer.stop();
1081
+ try { store.walCheckpoint(); } catch { /* non-fatal */ }
1082
+ store.close();
1083
+ };
1084
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
1085
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
1086
+ }
1087
+
1088
+ main().catch(err => {
1089
+ console.error('MCP server failed:', err);
1090
+ process.exit(1);
1091
+ });
1092
+
1093
+ } // end else (non-incognito)