agent-working-memory 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp.ts CHANGED
@@ -51,17 +51,41 @@ import { RetractionEngine } from './engine/retraction.js';
51
51
  import { EvalEngine } from './engine/eval.js';
52
52
  import { ConsolidationEngine } from './engine/consolidation.js';
53
53
  import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
54
- import { evaluateSalience } from './core/salience.js';
54
+ import { evaluateSalience, computeNovelty } from './core/salience.js';
55
55
  import type { ConsciousState } from './types/checkpoint.js';
56
56
  import type { SalienceEventType } from './core/salience.js';
57
57
  import type { TaskStatus, TaskPriority } from './types/engram.js';
58
58
  import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
59
59
  import { embed } from './core/embeddings.js';
60
+ import { startSidecar } from './hooks/sidecar.js';
61
+ import { initLogger, log, getLogPath } from './core/logger.js';
62
+
63
+ // --- Incognito Mode ---
64
+ // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
65
+ // No DB, no engines, no sidecar — just a bare MCP server that exposes nothing.
66
+
67
+ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO === 'true';
68
+
69
+ if (INCOGNITO) {
70
+ console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
71
+ const server = new McpServer({ name: 'agent-working-memory', version: '0.4.0' });
72
+ const transport = new StdioServerTransport();
73
+ server.connect(transport).catch(err => {
74
+ console.error('MCP server failed:', err);
75
+ process.exit(1);
76
+ });
77
+ // No tools registered — Claude won't see any memory_* tools
78
+ } else {
60
79
 
61
80
  // --- Setup ---
62
81
 
63
82
  const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
64
83
  const AGENT_ID = process.env.AWM_AGENT_ID ?? 'claude-code';
84
+ const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
85
+ const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
86
+
87
+ initLogger(DB_PATH);
88
+ log(AGENT_ID, 'startup', `MCP server starting (db: ${DB_PATH}, hooks: ${HOOK_PORT})`);
65
89
 
66
90
  const store = new EngramStore(DB_PATH);
67
91
  const activationEngine = new ActivationEngine(store);
@@ -78,7 +102,7 @@ consolidationScheduler.start();
78
102
 
79
103
  const server = new McpServer({
80
104
  name: 'agent-working-memory',
81
- version: '0.3.0',
105
+ version: '0.4.0',
82
106
  });
83
107
 
84
108
  // --- Tools ---
@@ -87,11 +111,12 @@ server.tool(
87
111
  'memory_write',
88
112
  `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
89
113
 
90
- Use this when you learn something that might be useful later:
91
- - Discoveries about the codebase, bugs, or architecture
92
- - Decisions you made and why
93
- - Errors encountered and how they were resolved
94
- - User preferences or project patterns
114
+ CALL THIS PROACTIVELY do not wait to be asked. Write memories when you:
115
+ - Discover something about the codebase, bugs, or architecture
116
+ - Make a decision and want to remember why
117
+ - Encounter and resolve an error
118
+ - Learn a user preference or project pattern
119
+ - Complete a significant piece of work
95
120
 
96
121
  The concept should be a short label (3-8 words). The content should be the full detail.`,
97
122
  {
@@ -111,6 +136,9 @@ The concept should be a short label (3-8 words). The content should be the full
111
136
  .describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
112
137
  },
113
138
  async (params) => {
139
+ // Check novelty — is this new information or a duplicate?
140
+ const novelty = computeNovelty(store, AGENT_ID, params.concept, params.content);
141
+
114
142
  const salience = evaluateSalience({
115
143
  content: params.content,
116
144
  eventType: params.event_type as SalienceEventType,
@@ -118,13 +146,15 @@ The concept should be a short label (3-8 words). The content should be the full
118
146
  decisionMade: params.decision_made,
119
147
  causalDepth: params.causal_depth,
120
148
  resolutionEffort: params.resolution_effort,
149
+ novelty,
121
150
  });
122
151
 
123
152
  if (salience.disposition === 'discard') {
153
+ log(AGENT_ID, 'write:discard', `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)}`);
124
154
  return {
125
155
  content: [{
126
156
  type: 'text' as const,
127
- text: `Memory discarded (salience ${salience.score.toFixed(2)} below threshold). Not worth storing.`,
157
+ text: `Discarded (salience ${salience.score.toFixed(2)}, novelty ${novelty.toFixed(1)})`,
128
158
  }],
129
159
  };
130
160
  }
@@ -154,10 +184,12 @@ The concept should be a short label (3-8 words). The content should be the full
154
184
  // Auto-checkpoint: track write
155
185
  try { store.updateAutoCheckpointWrite(AGENT_ID, engram.id); } catch { /* non-fatal */ }
156
186
 
187
+ log(AGENT_ID, `write:${salience.disposition}`, `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)} id=${engram.id}`);
188
+
157
189
  return {
158
190
  content: [{
159
191
  type: 'text' as const,
160
- text: `Memory stored (${salience.disposition}). ID: ${engram.id}\nConcept: ${params.concept}\nSalience: ${salience.score.toFixed(2)}`,
192
+ text: `Stored (${salience.disposition}) "${params.concept}" [${salience.score.toFixed(2)}]`,
161
193
  }],
162
194
  };
163
195
  }
@@ -165,16 +197,19 @@ The concept should be a short label (3-8 words). The content should be the full
165
197
 
166
198
  server.tool(
167
199
  'memory_recall',
168
- `Recall memories relevant to a context. Uses cognitive activation — not keyword search.
200
+ `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
169
201
 
170
- Use this when you need to remember something:
171
- - Starting a new task (recall relevant past experience)
202
+ ALWAYS call this when:
203
+ - Starting work on a project or topic (recall what you know)
172
204
  - Debugging (recall similar errors and solutions)
173
205
  - Making decisions (recall past decisions and outcomes)
206
+ - The user mentions a topic you might have stored memories about
174
207
 
175
- Returns the most relevant memories ranked by a composite score of text relevance, temporal recency, and associative strength.`,
208
+ Accepts either "query" or "context" parameter both work identically.
209
+ Returns the most relevant memories ranked by text relevance, temporal recency, and associative strength.`,
176
210
  {
177
- context: z.string().describe('What are you thinking about? Describe the current situation or question'),
211
+ query: z.string().optional().describe('What to search for describe the situation, question, or topic'),
212
+ context: z.string().optional().describe('Alias for query (either works)'),
178
213
  limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
179
214
  min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
180
215
  include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
@@ -182,9 +217,18 @@ Returns the most relevant memories ranked by a composite score of text relevance
182
217
  use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
183
218
  },
184
219
  async (params) => {
220
+ const queryText = params.query ?? params.context;
221
+ if (!queryText) {
222
+ return {
223
+ content: [{
224
+ type: 'text' as const,
225
+ text: 'Error: provide either "query" or "context" parameter with your search text.',
226
+ }],
227
+ };
228
+ }
185
229
  const results = await activationEngine.activate({
186
230
  agentId: AGENT_ID,
187
- context: params.context,
231
+ context: queryText,
188
232
  limit: params.limit,
189
233
  minScore: params.min_score,
190
234
  includeStaging: params.include_staging,
@@ -195,9 +239,11 @@ Returns the most relevant memories ranked by a composite score of text relevance
195
239
  // Auto-checkpoint: track recall
196
240
  try {
197
241
  const ids = results.map(r => r.engram.id);
198
- store.updateAutoCheckpointRecall(AGENT_ID, params.context, ids);
242
+ store.updateAutoCheckpointRecall(AGENT_ID, queryText, ids);
199
243
  } catch { /* non-fatal */ }
200
244
 
245
+ log(AGENT_ID, 'recall', `"${queryText.slice(0, 80)}" → ${results.length} results`);
246
+
201
247
  if (results.length === 0) {
202
248
  return {
203
249
  content: [{
@@ -208,14 +254,13 @@ Returns the most relevant memories ranked by a composite score of text relevance
208
254
  }
209
255
 
210
256
  const lines = results.map((r, i) => {
211
- const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
212
- return `${i + 1}. **${r.engram.concept}** (score: ${r.score.toFixed(3)})${tags}\n ${r.engram.content}\n _${r.why}_\n ID: ${r.engram.id}`;
257
+ return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)}): ${r.engram.content}`;
213
258
  });
214
259
 
215
260
  return {
216
261
  content: [{
217
262
  type: 'text' as const,
218
- text: `Recalled ${results.length} memories:\n\n${lines.join('\n\n')}`,
263
+ text: lines.join('\n'),
219
264
  }],
220
265
  };
221
266
  }
@@ -245,7 +290,7 @@ Always call this after using a recalled memory so the system learns what's valua
245
290
  return {
246
291
  content: [{
247
292
  type: 'text' as const,
248
- text: `Feedback recorded: ${params.useful ? 'useful' : 'not useful'}. Confidence ${params.useful ? 'increased' : 'decreased'}.`,
293
+ text: `Feedback: ${params.useful ? '+useful' : '-not useful'}`,
249
294
  }],
250
295
  };
251
296
  }
@@ -286,10 +331,12 @@ Use this when you discover a memory contains incorrect information.`,
286
331
 
287
332
  server.tool(
288
333
  'memory_stats',
289
- `Get memory health stats — how many memories, confidence levels, association count, and system performance.`,
334
+ `Get memory health stats — how many memories, confidence levels, association count, and system performance.
335
+ Also shows the activity log path so the user can tail it to see what's happening.`,
290
336
  {},
291
337
  async () => {
292
338
  const metrics = evalEngine.computeMetrics(AGENT_ID);
339
+ const checkpoint = store.getCheckpoint(AGENT_ID);
293
340
  const lines = [
294
341
  `Agent: ${AGENT_ID}`,
295
342
  `Active memories: ${metrics.activeEngramCount}`,
@@ -300,6 +347,14 @@ server.tool(
300
347
  `Edge utility: ${(metrics.edgeUtilityRate * 100).toFixed(1)}%`,
301
348
  `Activations (24h): ${metrics.activationCount}`,
302
349
  `Avg latency: ${metrics.avgLatencyMs.toFixed(1)}ms`,
350
+ ``,
351
+ `Session writes: ${checkpoint?.auto.writeCountSinceConsolidation ?? 0}`,
352
+ `Session recalls: ${checkpoint?.auto.recallCountSinceConsolidation ?? 0}`,
353
+ `Last activity: ${checkpoint?.auto.lastActivityAt?.toISOString() ?? 'never'}`,
354
+ `Checkpoint: ${checkpoint?.executionState ? checkpoint.executionState.currentTask : 'none'}`,
355
+ ``,
356
+ `Activity log: ${getLogPath() ?? 'not configured'}`,
357
+ `Hook sidecar: 127.0.0.1:${HOOK_PORT}`,
303
358
  ];
304
359
 
305
360
  return {
@@ -317,12 +372,12 @@ server.tool(
317
372
  'memory_checkpoint',
318
373
  `Save your current execution state so you can recover after context compaction.
319
374
 
320
- Use this when:
321
- - You're about to do something that might fill the context window
322
- - You've made important decisions you don't want to lose
323
- - You want to preserve your working state before a long operation
375
+ ALWAYS call this before:
376
+ - Long operations (multi-file generation, large refactors, overnight work)
377
+ - Anything that might fill the context window
378
+ - Switching to a different task
324
379
 
325
- The state is saved per-agent and overwrites any previous checkpoint.`,
380
+ Also call periodically during long sessions to avoid losing state. The state is saved per-agent and overwrites any previous checkpoint.`,
326
381
  {
327
382
  current_task: z.string().describe('What you are currently working on'),
328
383
  decisions: z.array(z.string()).optional().default([])
@@ -350,11 +405,12 @@ The state is saved per-agent and overwrites any previous checkpoint.`,
350
405
  };
351
406
 
352
407
  store.saveCheckpoint(AGENT_ID, state);
408
+ log(AGENT_ID, 'checkpoint', `"${params.current_task}" decisions=${params.decisions.length} files=${params.active_files.length}`);
353
409
 
354
410
  return {
355
411
  content: [{
356
412
  type: 'text' as const,
357
- text: `Checkpoint saved.\nTask: ${params.current_task}\nDecisions: ${params.decisions.length}\nNext steps: ${params.next_steps.length}\nFiles: ${params.active_files.length}`,
413
+ text: `Checkpoint saved: "${params.current_task}" (${params.decisions.length} decisions, ${params.active_files.length} files)`,
358
414
  }],
359
415
  };
360
416
  }
@@ -414,18 +470,44 @@ Use this at the start of every session or after compaction to pick up where you
414
470
  } catch { /* recall failure is non-fatal */ }
415
471
  }
416
472
 
417
- // Trigger mini-consolidation if idle >5min
473
+ // Consolidation on restore:
474
+ // - If idle >5min but last consolidation was recent (graceful exit ran it), skip
475
+ // - If idle >5min and no recent consolidation, run full cycle (non-graceful exit fallback)
418
476
  const MINI_IDLE_MS = 5 * 60_000;
477
+ const FULL_CONSOLIDATION_GAP_MS = 10 * 60_000; // 10 min — if last consolidation was longer ago, run full
419
478
  let miniConsolidationTriggered = false;
479
+ let fullConsolidationTriggered = false;
480
+
420
481
  if (idleMs > MINI_IDLE_MS) {
421
- miniConsolidationTriggered = true;
422
- consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => {});
482
+ const sinceLastConsolidation = checkpoint?.lastConsolidationAt
483
+ ? now - checkpoint.lastConsolidationAt.getTime()
484
+ : Infinity;
485
+
486
+ if (sinceLastConsolidation > FULL_CONSOLIDATION_GAP_MS) {
487
+ // No recent consolidation — graceful exit didn't happen, run full cycle
488
+ fullConsolidationTriggered = true;
489
+ try {
490
+ const result = consolidationEngine.consolidate(AGENT_ID);
491
+ store.markConsolidation(AGENT_ID, false);
492
+ 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`);
493
+ } catch { /* consolidation failure is non-fatal */ }
494
+ } else {
495
+ // Recent consolidation exists — graceful exit already handled it, just do mini
496
+ miniConsolidationTriggered = true;
497
+ consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => {});
498
+ }
423
499
  }
424
500
 
425
501
  // Format response
426
502
  const parts: string[] = [];
427
503
  const idleMin = Math.round(idleMs / 60_000);
428
- parts.push(`Idle: ${idleMin}min${miniConsolidationTriggered ? ' (mini-consolidation triggered)' : ''}`);
504
+ const consolidationNote = fullConsolidationTriggered
505
+ ? ' (full consolidation — no graceful exit detected)'
506
+ : miniConsolidationTriggered
507
+ ? ' (mini-consolidation triggered)'
508
+ : '';
509
+ log(AGENT_ID, 'restore', `idle=${idleMin}min checkpoint=${!!checkpoint?.executionState} recalled=${recalledMemories.length} lastWrite=${lastWrite?.concept ?? 'none'}${fullConsolidationTriggered ? ' FULL_CONSOLIDATION' : ''}`);
510
+ parts.push(`Idle: ${idleMin}min${consolidationNote}`);
429
511
 
430
512
  if (checkpoint?.executionState) {
431
513
  const s = checkpoint.executionState;
@@ -437,6 +519,7 @@ Use this at the start of every session or after compaction to pick up where you
437
519
  if (checkpoint.checkpointAt) parts.push(`_Saved at: ${checkpoint.checkpointAt.toISOString()}_`);
438
520
  } else {
439
521
  parts.push('\nNo explicit checkpoint saved.');
522
+ parts.push('\n**Tip:** Use memory_write to save important learnings, and memory_checkpoint before long operations so you can recover state.');
440
523
  }
441
524
 
442
525
  if (lastWrite) {
@@ -510,7 +593,7 @@ Tasks automatically get high salience so they won't be discarded.`,
510
593
  return {
511
594
  content: [{
512
595
  type: 'text' as const,
513
- text: `Task created: ${engram.id}\nTitle: ${params.concept}\nPriority: ${params.priority}\nStatus: ${engram.taskStatus}`,
596
+ text: `Task created: "${params.concept}" (${params.priority})`,
514
597
  }],
515
598
  };
516
599
  }
@@ -552,7 +635,7 @@ server.tool(
552
635
  return {
553
636
  content: [{
554
637
  type: 'text' as const,
555
- text: `Task updated: ${updated.concept}\nStatus: ${updated.taskStatus}\nPriority: ${updated.taskPriority}${updated.blockedBy ? `\nBlocked by: ${updated.blockedBy}` : ''}`,
638
+ text: `Updated: "${updated.concept}" ${updated.taskStatus} (${updated.taskPriority})`,
556
639
  }],
557
640
  };
558
641
  }
@@ -620,16 +703,182 @@ Use this when you finish a task or need to decide what to do next.`,
620
703
  }
621
704
  );
622
705
 
706
+ // --- Task Bracket Tools ---
707
+
708
+ server.tool(
709
+ 'memory_task_begin',
710
+ `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
711
+
712
+ CALL THIS when starting:
713
+ - A multi-step operation (doc generation, large refactor, migration)
714
+ - Work on a new topic or project area
715
+ - Anything that might fill the context window
716
+
717
+ This ensures your state is saved before you start, and primes recall with relevant context.`,
718
+ {
719
+ topic: z.string().describe('What task are you starting? (3-15 words)'),
720
+ files: z.array(z.string()).optional().default([])
721
+ .describe('Files you expect to work with'),
722
+ notes: z.string().optional().default('')
723
+ .describe('Any additional context'),
724
+ },
725
+ async (params) => {
726
+ // 1. Checkpoint current state
727
+ const checkpoint = store.getCheckpoint(AGENT_ID);
728
+ const prevTask = checkpoint?.executionState?.currentTask ?? 'None';
729
+
730
+ store.saveCheckpoint(AGENT_ID, {
731
+ currentTask: params.topic,
732
+ decisions: [],
733
+ activeFiles: params.files,
734
+ nextSteps: [],
735
+ relatedMemoryIds: [],
736
+ notes: params.notes || `Started via memory_task_begin. Previous task: ${prevTask}`,
737
+ episodeId: null,
738
+ });
739
+
740
+ // 2. Auto-recall relevant memories
741
+ let recalledSummary = '';
742
+ try {
743
+ const results = await activationEngine.activate({
744
+ agentId: AGENT_ID,
745
+ context: params.topic,
746
+ limit: 5,
747
+ minScore: 0.05,
748
+ useReranker: true,
749
+ useExpansion: true,
750
+ });
751
+
752
+ if (results.length > 0) {
753
+ const lines = results.map((r, i) => {
754
+ const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
755
+ return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${tags}\n ${r.engram.content.slice(0, 150)}${r.engram.content.length > 150 ? '...' : ''}`;
756
+ });
757
+ recalledSummary = `\n\n**Recalled memories (${results.length}):**\n${lines.join('\n')}`;
758
+
759
+ // Track recall
760
+ store.updateAutoCheckpointRecall(AGENT_ID, params.topic, results.map(r => r.engram.id));
761
+ }
762
+ } catch { /* recall failure is non-fatal */ }
763
+
764
+ log(AGENT_ID, 'task:begin', `"${params.topic}" prev="${prevTask}"`);
765
+
766
+ return {
767
+ content: [{
768
+ type: 'text' as const,
769
+ text: `Started: "${params.topic}" (prev: ${prevTask})${recalledSummary}`,
770
+ }],
771
+ };
772
+ }
773
+ );
774
+
775
+ server.tool(
776
+ 'memory_task_end',
777
+ `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
778
+
779
+ CALL THIS when you finish:
780
+ - A multi-step operation
781
+ - Before switching to a different topic
782
+ - At the end of a work session
783
+
784
+ This captures what was accomplished so future sessions can recall it.`,
785
+ {
786
+ summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
787
+ tags: z.array(z.string()).optional().default([])
788
+ .describe('Tags for the summary memory'),
789
+ },
790
+ async (params) => {
791
+ // 1. Write summary as a memory
792
+ const salience = evaluateSalience({
793
+ content: params.summary,
794
+ eventType: 'decision',
795
+ surprise: 0.3,
796
+ decisionMade: true,
797
+ causalDepth: 0.5,
798
+ resolutionEffort: 0.5,
799
+ });
800
+
801
+ const engram = store.createEngram({
802
+ agentId: AGENT_ID,
803
+ concept: 'Task completed',
804
+ content: params.summary,
805
+ tags: [...params.tags, 'task-summary'],
806
+ salience: Math.max(salience.score, 0.7), // Always high salience for task summaries
807
+ salienceFeatures: salience.features,
808
+ reasonCodes: [...salience.reasonCodes, 'task-end'],
809
+ });
810
+
811
+ connectionEngine.enqueue(engram.id);
812
+
813
+ // Generate embedding asynchronously
814
+ embed(`Task completed: ${params.summary}`).then(vec => {
815
+ store.updateEmbedding(engram.id, vec);
816
+ }).catch(() => {});
817
+
818
+ // 2. Update checkpoint to reflect completion
819
+ const checkpoint = store.getCheckpoint(AGENT_ID);
820
+ const completedTask = checkpoint?.executionState?.currentTask ?? 'Unknown task';
821
+
822
+ store.saveCheckpoint(AGENT_ID, {
823
+ currentTask: `Completed: ${completedTask}`,
824
+ decisions: checkpoint?.executionState?.decisions ?? [],
825
+ activeFiles: [],
826
+ nextSteps: [],
827
+ relatedMemoryIds: [engram.id],
828
+ notes: `Task completed. Summary memory: ${engram.id}`,
829
+ episodeId: null,
830
+ });
831
+
832
+ store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
833
+ log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)}`);
834
+
835
+ return {
836
+ content: [{
837
+ type: 'text' as const,
838
+ text: `Completed: "${completedTask}" [${salience.score.toFixed(2)}]`,
839
+ }],
840
+ };
841
+ }
842
+ );
843
+
623
844
  // --- Start ---
624
845
 
625
846
  async function main() {
626
847
  const transport = new StdioServerTransport();
627
848
  await server.connect(transport);
849
+
850
+ // Start hook sidecar (lightweight HTTP for Claude Code hooks)
851
+ const sidecar = startSidecar({
852
+ store,
853
+ agentId: AGENT_ID,
854
+ secret: HOOK_SECRET,
855
+ port: HOOK_PORT,
856
+ onConsolidate: (agentId, reason) => {
857
+ console.error(`[mcp] consolidation triggered: ${reason}`);
858
+ const result = consolidationEngine.consolidate(agentId);
859
+ store.markConsolidation(agentId, false);
860
+ console.error(`[mcp] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
861
+ },
862
+ });
863
+
628
864
  // Log to stderr (stdout is reserved for MCP protocol)
629
865
  console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
866
+ console.error(`Hook sidecar on 127.0.0.1:${HOOK_PORT}${HOOK_SECRET ? ' (auth enabled)' : ' (no auth — set AWM_HOOK_SECRET)'}`);
867
+
868
+ // Clean shutdown
869
+ const cleanup = () => {
870
+ sidecar.close();
871
+ consolidationScheduler.stop();
872
+ stagingBuffer.stop();
873
+ store.close();
874
+ };
875
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
876
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
630
877
  }
631
878
 
632
879
  main().catch(err => {
633
880
  console.error('MCP server failed:', err);
634
881
  process.exit(1);
635
882
  });
883
+
884
+ } // end else (non-incognito)