agent-working-memory 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -52,257 +52,308 @@ import { RetractionEngine } from './engine/retraction.js';
52
52
  import { EvalEngine } from './engine/eval.js';
53
53
  import { ConsolidationEngine } from './engine/consolidation.js';
54
54
  import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
55
- import { evaluateSalience } from './core/salience.js';
55
+ import { evaluateSalience, computeNovelty } from './core/salience.js';
56
56
  import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
57
57
  import { embed } from './core/embeddings.js';
58
- // --- Setup ---
59
- const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
60
- const AGENT_ID = process.env.AWM_AGENT_ID ?? 'claude-code';
61
- const store = new EngramStore(DB_PATH);
62
- const activationEngine = new ActivationEngine(store);
63
- const connectionEngine = new ConnectionEngine(store, activationEngine);
64
- const stagingBuffer = new StagingBuffer(store, activationEngine);
65
- const evictionEngine = new EvictionEngine(store);
66
- const retractionEngine = new RetractionEngine(store);
67
- const evalEngine = new EvalEngine(store);
68
- const consolidationEngine = new ConsolidationEngine(store);
69
- const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
70
- stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
71
- consolidationScheduler.start();
72
- const server = new McpServer({
73
- name: 'agent-working-memory',
74
- version: '0.3.0',
75
- });
76
- // --- Tools ---
77
- server.tool('memory_write', `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
58
+ import { startSidecar } from './hooks/sidecar.js';
59
+ import { initLogger, log, getLogPath } from './core/logger.js';
60
+ // --- Incognito Mode ---
61
+ // When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
62
+ // No DB, no engines, no sidecar — just a bare MCP server that exposes nothing.
63
+ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO === 'true';
64
+ if (INCOGNITO) {
65
+ console.error('AWM: incognito mode all memory tools disabled, nothing will be recorded');
66
+ const server = new McpServer({ name: 'agent-working-memory', version: '0.4.0' });
67
+ const transport = new StdioServerTransport();
68
+ server.connect(transport).catch(err => {
69
+ console.error('MCP server failed:', err);
70
+ process.exit(1);
71
+ });
72
+ // No tools registered — Claude won't see any memory_* tools
73
+ }
74
+ else {
75
+ // --- Setup ---
76
+ const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
77
+ const AGENT_ID = process.env.AWM_AGENT_ID ?? 'claude-code';
78
+ const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
79
+ const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
80
+ initLogger(DB_PATH);
81
+ log(AGENT_ID, 'startup', `MCP server starting (db: ${DB_PATH}, hooks: ${HOOK_PORT})`);
82
+ const store = new EngramStore(DB_PATH);
83
+ const activationEngine = new ActivationEngine(store);
84
+ const connectionEngine = new ConnectionEngine(store, activationEngine);
85
+ const stagingBuffer = new StagingBuffer(store, activationEngine);
86
+ const evictionEngine = new EvictionEngine(store);
87
+ const retractionEngine = new RetractionEngine(store);
88
+ const evalEngine = new EvalEngine(store);
89
+ const consolidationEngine = new ConsolidationEngine(store);
90
+ const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
91
+ stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
92
+ consolidationScheduler.start();
93
+ const server = new McpServer({
94
+ name: 'agent-working-memory',
95
+ version: '0.4.0',
96
+ });
97
+ // --- Tools ---
98
+ server.tool('memory_write', `Store a memory. The salience filter decides whether it's worth keeping (active), needs more evidence (staging), or should be discarded.
78
99
 
79
- Use this when you learn something that might be useful later:
80
- - Discoveries about the codebase, bugs, or architecture
81
- - Decisions you made and why
82
- - Errors encountered and how they were resolved
83
- - User preferences or project patterns
100
+ CALL THIS PROACTIVELY do not wait to be asked. Write memories when you:
101
+ - Discover something about the codebase, bugs, or architecture
102
+ - Make a decision and want to remember why
103
+ - Encounter and resolve an error
104
+ - Learn a user preference or project pattern
105
+ - Complete a significant piece of work
84
106
 
85
107
  The concept should be a short label (3-8 words). The content should be the full detail.`, {
86
- concept: z.string().describe('Short label for this memory (3-8 words)'),
87
- content: z.string().describe('Full detail of what was learned'),
88
- tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
89
- event_type: z.enum(['observation', 'decision', 'friction', 'surprise', 'causal'])
90
- .optional().default('observation')
91
- .describe('Type of event: observation (default), decision, friction (error/blocker), surprise, causal (root cause)'),
92
- surprise: z.number().min(0).max(1).optional().default(0.3)
93
- .describe('How surprising was this? 0=expected, 1=very unexpected'),
94
- decision_made: z.boolean().optional().default(false)
95
- .describe('Was a decision made? True boosts importance'),
96
- causal_depth: z.number().min(0).max(1).optional().default(0.3)
97
- .describe('How deep is the causal understanding? 0=surface, 1=root cause'),
98
- resolution_effort: z.number().min(0).max(1).optional().default(0.3)
99
- .describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
100
- }, async (params) => {
101
- const salience = evaluateSalience({
102
- content: params.content,
103
- eventType: params.event_type,
104
- surprise: params.surprise,
105
- decisionMade: params.decision_made,
106
- causalDepth: params.causal_depth,
107
- resolutionEffort: params.resolution_effort,
108
- });
109
- if (salience.disposition === 'discard') {
108
+ concept: z.string().describe('Short label for this memory (3-8 words)'),
109
+ content: z.string().describe('Full detail of what was learned'),
110
+ tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
111
+ event_type: z.enum(['observation', 'decision', 'friction', 'surprise', 'causal'])
112
+ .optional().default('observation')
113
+ .describe('Type of event: observation (default), decision, friction (error/blocker), surprise, causal (root cause)'),
114
+ surprise: z.number().min(0).max(1).optional().default(0.3)
115
+ .describe('How surprising was this? 0=expected, 1=very unexpected'),
116
+ decision_made: z.boolean().optional().default(false)
117
+ .describe('Was a decision made? True boosts importance'),
118
+ causal_depth: z.number().min(0).max(1).optional().default(0.3)
119
+ .describe('How deep is the causal understanding? 0=surface, 1=root cause'),
120
+ resolution_effort: z.number().min(0).max(1).optional().default(0.3)
121
+ .describe('How much effort to resolve? 0=trivial, 1=significant debugging'),
122
+ }, async (params) => {
123
+ // Check novelty — is this new information or a duplicate?
124
+ const novelty = computeNovelty(store, AGENT_ID, params.concept, params.content);
125
+ const salience = evaluateSalience({
126
+ content: params.content,
127
+ eventType: params.event_type,
128
+ surprise: params.surprise,
129
+ decisionMade: params.decision_made,
130
+ causalDepth: params.causal_depth,
131
+ resolutionEffort: params.resolution_effort,
132
+ novelty,
133
+ });
134
+ if (salience.disposition === 'discard') {
135
+ log(AGENT_ID, 'write:discard', `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)}`);
136
+ return {
137
+ content: [{
138
+ type: 'text',
139
+ text: `Memory discarded (salience ${salience.score.toFixed(2)} below threshold, novelty ${novelty.toFixed(1)}). Not worth storing.`,
140
+ }],
141
+ };
142
+ }
143
+ const engram = store.createEngram({
144
+ agentId: AGENT_ID,
145
+ concept: params.concept,
146
+ content: params.content,
147
+ tags: params.tags,
148
+ salience: salience.score,
149
+ salienceFeatures: salience.features,
150
+ reasonCodes: salience.reasonCodes,
151
+ ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
152
+ });
153
+ if (salience.disposition === 'staging') {
154
+ store.updateStage(engram.id, 'staging');
155
+ }
156
+ else {
157
+ connectionEngine.enqueue(engram.id);
158
+ }
159
+ // Generate embedding asynchronously (don't block response)
160
+ embed(`${params.concept} ${params.content}`).then(vec => {
161
+ store.updateEmbedding(engram.id, vec);
162
+ }).catch(() => { }); // Embedding failure is non-fatal
163
+ // Auto-checkpoint: track write
164
+ try {
165
+ store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
166
+ }
167
+ catch { /* non-fatal */ }
168
+ log(AGENT_ID, `write:${salience.disposition}`, `"${params.concept}" salience=${salience.score.toFixed(2)} novelty=${novelty.toFixed(1)} id=${engram.id}`);
110
169
  return {
111
170
  content: [{
112
171
  type: 'text',
113
- text: `Memory discarded (salience ${salience.score.toFixed(2)} below threshold). Not worth storing.`,
172
+ text: `Memory stored (${salience.disposition}). ID: ${engram.id}\nConcept: ${params.concept}\nSalience: ${salience.score.toFixed(2)}`,
114
173
  }],
115
174
  };
116
- }
117
- const engram = store.createEngram({
118
- agentId: AGENT_ID,
119
- concept: params.concept,
120
- content: params.content,
121
- tags: params.tags,
122
- salience: salience.score,
123
- salienceFeatures: salience.features,
124
- reasonCodes: salience.reasonCodes,
125
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
126
175
  });
127
- if (salience.disposition === 'staging') {
128
- store.updateStage(engram.id, 'staging');
129
- }
130
- else {
131
- connectionEngine.enqueue(engram.id);
132
- }
133
- // Generate embedding asynchronously (don't block response)
134
- embed(`${params.concept} ${params.content}`).then(vec => {
135
- store.updateEmbedding(engram.id, vec);
136
- }).catch(() => { }); // Embedding failure is non-fatal
137
- // Auto-checkpoint: track write
138
- try {
139
- store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
140
- }
141
- catch { /* non-fatal */ }
142
- return {
143
- content: [{
144
- type: 'text',
145
- text: `Memory stored (${salience.disposition}). ID: ${engram.id}\nConcept: ${params.concept}\nSalience: ${salience.score.toFixed(2)}`,
146
- }],
147
- };
148
- });
149
- server.tool('memory_recall', `Recall memories relevant to a context. Uses cognitive activation — not keyword search.
176
+ server.tool('memory_recall', `Recall memories relevant to a query. Uses cognitive activation — not keyword search.
150
177
 
151
- Use this when you need to remember something:
152
- - Starting a new task (recall relevant past experience)
178
+ ALWAYS call this when:
179
+ - Starting work on a project or topic (recall what you know)
153
180
  - Debugging (recall similar errors and solutions)
154
181
  - Making decisions (recall past decisions and outcomes)
182
+ - The user mentions a topic you might have stored memories about
155
183
 
156
- Returns the most relevant memories ranked by a composite score of text relevance, temporal recency, and associative strength.`, {
157
- context: z.string().describe('What are you thinking about? Describe the current situation or question'),
158
- limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
159
- min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
160
- include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
161
- use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
162
- use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
163
- }, async (params) => {
164
- const results = await activationEngine.activate({
165
- agentId: AGENT_ID,
166
- context: params.context,
167
- limit: params.limit,
168
- minScore: params.min_score,
169
- includeStaging: params.include_staging,
170
- useReranker: params.use_reranker,
171
- useExpansion: params.use_expansion,
172
- });
173
- // Auto-checkpoint: track recall
174
- try {
175
- const ids = results.map(r => r.engram.id);
176
- store.updateAutoCheckpointRecall(AGENT_ID, params.context, ids);
177
- }
178
- catch { /* non-fatal */ }
179
- if (results.length === 0) {
184
+ Accepts either "query" or "context" parameter both work identically.
185
+ Returns the most relevant memories ranked by text relevance, temporal recency, and associative strength.`, {
186
+ query: z.string().optional().describe('What to search for — describe the situation, question, or topic'),
187
+ context: z.string().optional().describe('Alias for query (either works)'),
188
+ limit: z.number().optional().default(5).describe('Max memories to return (default 5)'),
189
+ min_score: z.number().optional().default(0.05).describe('Minimum relevance score (default 0.05)'),
190
+ include_staging: z.boolean().optional().default(false).describe('Include weak/unconfirmed memories?'),
191
+ use_reranker: z.boolean().optional().default(true).describe('Use cross-encoder re-ranking for better relevance (default true)'),
192
+ use_expansion: z.boolean().optional().default(true).describe('Expand query with synonyms for better recall (default true)'),
193
+ }, async (params) => {
194
+ const queryText = params.query ?? params.context;
195
+ if (!queryText) {
196
+ return {
197
+ content: [{
198
+ type: 'text',
199
+ text: 'Error: provide either "query" or "context" parameter with your search text.',
200
+ }],
201
+ };
202
+ }
203
+ const results = await activationEngine.activate({
204
+ agentId: AGENT_ID,
205
+ context: queryText,
206
+ limit: params.limit,
207
+ minScore: params.min_score,
208
+ includeStaging: params.include_staging,
209
+ useReranker: params.use_reranker,
210
+ useExpansion: params.use_expansion,
211
+ });
212
+ // Auto-checkpoint: track recall
213
+ try {
214
+ const ids = results.map(r => r.engram.id);
215
+ store.updateAutoCheckpointRecall(AGENT_ID, queryText, ids);
216
+ }
217
+ catch { /* non-fatal */ }
218
+ log(AGENT_ID, 'recall', `"${queryText.slice(0, 80)}" → ${results.length} results`);
219
+ if (results.length === 0) {
220
+ return {
221
+ content: [{
222
+ type: 'text',
223
+ text: 'No relevant memories found.',
224
+ }],
225
+ };
226
+ }
227
+ const lines = results.map((r, i) => {
228
+ const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
229
+ return `${i + 1}. **${r.engram.concept}** (score: ${r.score.toFixed(3)})${tags}\n ${r.engram.content}\n _${r.why}_\n ID: ${r.engram.id}`;
230
+ });
180
231
  return {
181
232
  content: [{
182
233
  type: 'text',
183
- text: 'No relevant memories found.',
234
+ text: `Recalled ${results.length} memories:\n\n${lines.join('\n\n')}`,
184
235
  }],
185
236
  };
186
- }
187
- const lines = results.map((r, i) => {
188
- const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
189
- return `${i + 1}. **${r.engram.concept}** (score: ${r.score.toFixed(3)})${tags}\n ${r.engram.content}\n _${r.why}_\n ID: ${r.engram.id}`;
190
237
  });
191
- return {
192
- content: [{
193
- type: 'text',
194
- text: `Recalled ${results.length} memories:\n\n${lines.join('\n\n')}`,
195
- }],
196
- };
197
- });
198
- server.tool('memory_feedback', `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
238
+ server.tool('memory_feedback', `Report whether a recalled memory was actually useful. This updates the memory's confidence score — useful memories become stronger, useless ones weaken.
199
239
 
200
240
  Always call this after using a recalled memory so the system learns what's valuable.`, {
201
- engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
202
- useful: z.boolean().describe('Was this memory actually helpful?'),
203
- context: z.string().optional().describe('Brief note on why it was/wasn\'t useful'),
204
- }, async (params) => {
205
- store.logRetrievalFeedback(null, params.engram_id, params.useful, params.context ?? '');
206
- const engram = store.getEngram(params.engram_id);
207
- if (engram) {
208
- const delta = params.useful
209
- ? DEFAULT_AGENT_CONFIG.feedbackPositiveBoost
210
- : -DEFAULT_AGENT_CONFIG.feedbackNegativePenalty;
211
- store.updateConfidence(engram.id, engram.confidence + delta);
212
- }
213
- return {
214
- content: [{
215
- type: 'text',
216
- text: `Feedback recorded: ${params.useful ? 'useful' : 'not useful'}. Confidence ${params.useful ? 'increased' : 'decreased'}.`,
217
- }],
218
- };
219
- });
220
- server.tool('memory_retract', `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
241
+ engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
242
+ useful: z.boolean().describe('Was this memory actually helpful?'),
243
+ context: z.string().optional().describe('Brief note on why it was/wasn\'t useful'),
244
+ }, async (params) => {
245
+ store.logRetrievalFeedback(null, params.engram_id, params.useful, params.context ?? '');
246
+ const engram = store.getEngram(params.engram_id);
247
+ if (engram) {
248
+ const delta = params.useful
249
+ ? DEFAULT_AGENT_CONFIG.feedbackPositiveBoost
250
+ : -DEFAULT_AGENT_CONFIG.feedbackNegativePenalty;
251
+ store.updateConfidence(engram.id, engram.confidence + delta);
252
+ }
253
+ return {
254
+ content: [{
255
+ type: 'text',
256
+ text: `Feedback recorded: ${params.useful ? 'useful' : 'not useful'}. Confidence ${params.useful ? 'increased' : 'decreased'}.`,
257
+ }],
258
+ };
259
+ });
260
+ server.tool('memory_retract', `Retract a memory that turned out to be wrong. Creates a correction and reduces confidence of related memories.
221
261
 
222
262
  Use this when you discover a memory contains incorrect information.`, {
223
- engram_id: z.string().describe('ID of the wrong memory'),
224
- reason: z.string().describe('Why is this memory wrong?'),
225
- correction: z.string().optional().describe('What is the correct information? (creates a new memory)'),
226
- }, async (params) => {
227
- const result = retractionEngine.retract({
228
- agentId: AGENT_ID,
229
- targetEngramId: params.engram_id,
230
- reason: params.reason,
231
- counterContent: params.correction,
263
+ engram_id: z.string().describe('ID of the wrong memory'),
264
+ reason: z.string().describe('Why is this memory wrong?'),
265
+ correction: z.string().optional().describe('What is the correct information? (creates a new memory)'),
266
+ }, async (params) => {
267
+ const result = retractionEngine.retract({
268
+ agentId: AGENT_ID,
269
+ targetEngramId: params.engram_id,
270
+ reason: params.reason,
271
+ counterContent: params.correction,
272
+ });
273
+ const parts = [`Memory ${params.engram_id} retracted.`];
274
+ if (result.correctionId) {
275
+ parts.push(`Correction stored as ${result.correctionId}.`);
276
+ }
277
+ parts.push(`${result.associatesAffected} related memories had confidence reduced.`);
278
+ return {
279
+ content: [{
280
+ type: 'text',
281
+ text: parts.join(' '),
282
+ }],
283
+ };
232
284
  });
233
- const parts = [`Memory ${params.engram_id} retracted.`];
234
- if (result.correctionId) {
235
- parts.push(`Correction stored as ${result.correctionId}.`);
236
- }
237
- parts.push(`${result.associatesAffected} related memories had confidence reduced.`);
238
- return {
239
- content: [{
240
- type: 'text',
241
- text: parts.join(' '),
242
- }],
243
- };
244
- });
245
- server.tool('memory_stats', `Get memory health stats — how many memories, confidence levels, association count, and system performance.`, {}, async () => {
246
- const metrics = evalEngine.computeMetrics(AGENT_ID);
247
- const lines = [
248
- `Agent: ${AGENT_ID}`,
249
- `Active memories: ${metrics.activeEngramCount}`,
250
- `Staging: ${metrics.stagingEngramCount}`,
251
- `Retracted: ${metrics.retractedCount}`,
252
- `Avg confidence: ${metrics.avgConfidence.toFixed(3)}`,
253
- `Total edges: ${metrics.totalEdges}`,
254
- `Edge utility: ${(metrics.edgeUtilityRate * 100).toFixed(1)}%`,
255
- `Activations (24h): ${metrics.activationCount}`,
256
- `Avg latency: ${metrics.avgLatencyMs.toFixed(1)}ms`,
257
- ];
258
- return {
259
- content: [{
260
- type: 'text',
261
- text: lines.join('\n'),
262
- }],
263
- };
264
- });
265
- // --- Checkpointing Tools ---
266
- server.tool('memory_checkpoint', `Save your current execution state so you can recover after context compaction.
285
+ server.tool('memory_stats', `Get memory health stats — how many memories, confidence levels, association count, and system performance.
286
+ Also shows the activity log path so the user can tail it to see what's happening.`, {}, async () => {
287
+ const metrics = evalEngine.computeMetrics(AGENT_ID);
288
+ const checkpoint = store.getCheckpoint(AGENT_ID);
289
+ const lines = [
290
+ `Agent: ${AGENT_ID}`,
291
+ `Active memories: ${metrics.activeEngramCount}`,
292
+ `Staging: ${metrics.stagingEngramCount}`,
293
+ `Retracted: ${metrics.retractedCount}`,
294
+ `Avg confidence: ${metrics.avgConfidence.toFixed(3)}`,
295
+ `Total edges: ${metrics.totalEdges}`,
296
+ `Edge utility: ${(metrics.edgeUtilityRate * 100).toFixed(1)}%`,
297
+ `Activations (24h): ${metrics.activationCount}`,
298
+ `Avg latency: ${metrics.avgLatencyMs.toFixed(1)}ms`,
299
+ ``,
300
+ `Session writes: ${checkpoint?.auto.writeCountSinceConsolidation ?? 0}`,
301
+ `Session recalls: ${checkpoint?.auto.recallCountSinceConsolidation ?? 0}`,
302
+ `Last activity: ${checkpoint?.auto.lastActivityAt?.toISOString() ?? 'never'}`,
303
+ `Checkpoint: ${checkpoint?.executionState ? checkpoint.executionState.currentTask : 'none'}`,
304
+ ``,
305
+ `Activity log: ${getLogPath() ?? 'not configured'}`,
306
+ `Hook sidecar: 127.0.0.1:${HOOK_PORT}`,
307
+ ];
308
+ return {
309
+ content: [{
310
+ type: 'text',
311
+ text: lines.join('\n'),
312
+ }],
313
+ };
314
+ });
315
+ // --- Checkpointing Tools ---
316
+ server.tool('memory_checkpoint', `Save your current execution state so you can recover after context compaction.
267
317
 
268
- Use this when:
269
- - You're about to do something that might fill the context window
270
- - You've made important decisions you don't want to lose
271
- - You want to preserve your working state before a long operation
318
+ ALWAYS call this before:
319
+ - Long operations (multi-file generation, large refactors, overnight work)
320
+ - Anything that might fill the context window
321
+ - Switching to a different task
272
322
 
273
- The state is saved per-agent and overwrites any previous checkpoint.`, {
274
- current_task: z.string().describe('What you are currently working on'),
275
- decisions: z.array(z.string()).optional().default([])
276
- .describe('Key decisions made so far'),
277
- active_files: z.array(z.string()).optional().default([])
278
- .describe('Files you are currently working with'),
279
- next_steps: z.array(z.string()).optional().default([])
280
- .describe('What needs to happen next'),
281
- related_memory_ids: z.array(z.string()).optional().default([])
282
- .describe('IDs of memories relevant to current work'),
283
- notes: z.string().optional().default('')
284
- .describe('Any other context worth preserving'),
285
- episode_id: z.string().optional()
286
- .describe('Current episode ID if known'),
287
- }, async (params) => {
288
- const state = {
289
- currentTask: params.current_task,
290
- decisions: params.decisions,
291
- activeFiles: params.active_files,
292
- nextSteps: params.next_steps,
293
- relatedMemoryIds: params.related_memory_ids,
294
- notes: params.notes,
295
- episodeId: params.episode_id ?? null,
296
- };
297
- store.saveCheckpoint(AGENT_ID, state);
298
- return {
299
- content: [{
300
- type: 'text',
301
- text: `Checkpoint saved.\nTask: ${params.current_task}\nDecisions: ${params.decisions.length}\nNext steps: ${params.next_steps.length}\nFiles: ${params.active_files.length}`,
302
- }],
303
- };
304
- });
305
- server.tool('memory_restore', `Restore your previous execution state after context compaction or at session start.
323
+ Also call periodically during long sessions to avoid losing state. The state is saved per-agent and overwrites any previous checkpoint.`, {
324
+ current_task: z.string().describe('What you are currently working on'),
325
+ decisions: z.array(z.string()).optional().default([])
326
+ .describe('Key decisions made so far'),
327
+ active_files: z.array(z.string()).optional().default([])
328
+ .describe('Files you are currently working with'),
329
+ next_steps: z.array(z.string()).optional().default([])
330
+ .describe('What needs to happen next'),
331
+ related_memory_ids: z.array(z.string()).optional().default([])
332
+ .describe('IDs of memories relevant to current work'),
333
+ notes: z.string().optional().default('')
334
+ .describe('Any other context worth preserving'),
335
+ episode_id: z.string().optional()
336
+ .describe('Current episode ID if known'),
337
+ }, async (params) => {
338
+ const state = {
339
+ currentTask: params.current_task,
340
+ decisions: params.decisions,
341
+ activeFiles: params.active_files,
342
+ nextSteps: params.next_steps,
343
+ relatedMemoryIds: params.related_memory_ids,
344
+ notes: params.notes,
345
+ episodeId: params.episode_id ?? null,
346
+ };
347
+ store.saveCheckpoint(AGENT_ID, state);
348
+ log(AGENT_ID, 'checkpoint', `"${params.current_task}" decisions=${params.decisions.length} files=${params.active_files.length}`);
349
+ return {
350
+ content: [{
351
+ type: 'text',
352
+ text: `Checkpoint saved.\nTask: ${params.current_task}\nDecisions: ${params.decisions.length}\nNext steps: ${params.next_steps.length}\nFiles: ${params.active_files.length}`,
353
+ }],
354
+ };
355
+ });
356
+ server.tool('memory_restore', `Restore your previous execution state after context compaction or at session start.
306
357
 
307
358
  Returns:
308
359
  - Your saved execution state (task, decisions, next steps, files)
@@ -311,89 +362,116 @@ Returns:
311
362
  - How long you were idle
312
363
 
313
364
  Use this at the start of every session or after compaction to pick up where you left off.`, {}, async () => {
314
- const checkpoint = store.getCheckpoint(AGENT_ID);
315
- const now = Date.now();
316
- const idleMs = checkpoint
317
- ? now - checkpoint.auto.lastActivityAt.getTime()
318
- : 0;
319
- // Get last written engram
320
- let lastWrite = null;
321
- if (checkpoint?.auto.lastWriteId) {
322
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
323
- if (engram) {
324
- lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
365
+ const checkpoint = store.getCheckpoint(AGENT_ID);
366
+ const now = Date.now();
367
+ const idleMs = checkpoint
368
+ ? now - checkpoint.auto.lastActivityAt.getTime()
369
+ : 0;
370
+ // Get last written engram
371
+ let lastWrite = null;
372
+ if (checkpoint?.auto.lastWriteId) {
373
+ const engram = store.getEngram(checkpoint.auto.lastWriteId);
374
+ if (engram) {
375
+ lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
376
+ }
325
377
  }
326
- }
327
- // Recall memories using last context
328
- let recalledMemories = [];
329
- const recallContext = checkpoint?.auto.lastRecallContext
330
- ?? checkpoint?.executionState?.currentTask
331
- ?? null;
332
- if (recallContext) {
333
- try {
334
- const results = await activationEngine.activate({
335
- agentId: AGENT_ID,
336
- context: recallContext,
337
- limit: 5,
338
- minScore: 0.05,
339
- useReranker: true,
340
- useExpansion: true,
341
- });
342
- recalledMemories = results.map(r => ({
343
- id: r.engram.id,
344
- concept: r.engram.concept,
345
- content: r.engram.content,
346
- score: r.score,
347
- }));
378
+ // Recall memories using last context
379
+ let recalledMemories = [];
380
+ const recallContext = checkpoint?.auto.lastRecallContext
381
+ ?? checkpoint?.executionState?.currentTask
382
+ ?? null;
383
+ if (recallContext) {
384
+ try {
385
+ const results = await activationEngine.activate({
386
+ agentId: AGENT_ID,
387
+ context: recallContext,
388
+ limit: 5,
389
+ minScore: 0.05,
390
+ useReranker: true,
391
+ useExpansion: true,
392
+ });
393
+ recalledMemories = results.map(r => ({
394
+ id: r.engram.id,
395
+ concept: r.engram.concept,
396
+ content: r.engram.content,
397
+ score: r.score,
398
+ }));
399
+ }
400
+ catch { /* recall failure is non-fatal */ }
348
401
  }
349
- catch { /* recall failure is non-fatal */ }
350
- }
351
- // Trigger mini-consolidation if idle >5min
352
- const MINI_IDLE_MS = 5 * 60_000;
353
- let miniConsolidationTriggered = false;
354
- if (idleMs > MINI_IDLE_MS) {
355
- miniConsolidationTriggered = true;
356
- consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => { });
357
- }
358
- // Format response
359
- const parts = [];
360
- const idleMin = Math.round(idleMs / 60_000);
361
- parts.push(`Idle: ${idleMin}min${miniConsolidationTriggered ? ' (mini-consolidation triggered)' : ''}`);
362
- if (checkpoint?.executionState) {
363
- const s = checkpoint.executionState;
364
- parts.push(`\n**Current task:** ${s.currentTask}`);
365
- if (s.decisions.length)
366
- parts.push(`**Decisions:** ${s.decisions.join('; ')}`);
367
- if (s.nextSteps.length)
368
- parts.push(`**Next steps:** ${s.nextSteps.map((st, i) => `${i + 1}. ${st}`).join(', ')}`);
369
- if (s.activeFiles.length)
370
- parts.push(`**Active files:** ${s.activeFiles.join(', ')}`);
371
- if (s.notes)
372
- parts.push(`**Notes:** ${s.notes}`);
373
- if (checkpoint.checkpointAt)
374
- parts.push(`_Saved at: ${checkpoint.checkpointAt.toISOString()}_`);
375
- }
376
- else {
377
- parts.push('\nNo explicit checkpoint saved.');
378
- }
379
- if (lastWrite) {
380
- parts.push(`\n**Last write:** ${lastWrite.concept}\n${lastWrite.content}`);
381
- }
382
- if (recalledMemories.length > 0) {
383
- parts.push(`\n**Recalled memories (${recalledMemories.length}):**`);
384
- for (const m of recalledMemories) {
385
- parts.push(`- **${m.concept}** (${m.score.toFixed(3)}): ${m.content.slice(0, 150)}${m.content.length > 150 ? '...' : ''}`);
402
+ // Consolidation on restore:
403
+ // - If idle >5min but last consolidation was recent (graceful exit ran it), skip
404
+ // - If idle >5min and no recent consolidation, run full cycle (non-graceful exit fallback)
405
+ const MINI_IDLE_MS = 5 * 60_000;
406
+ const FULL_CONSOLIDATION_GAP_MS = 10 * 60_000; // 10 min — if last consolidation was longer ago, run full
407
+ let miniConsolidationTriggered = false;
408
+ let fullConsolidationTriggered = false;
409
+ if (idleMs > MINI_IDLE_MS) {
410
+ const sinceLastConsolidation = checkpoint?.lastConsolidationAt
411
+ ? now - checkpoint.lastConsolidationAt.getTime()
412
+ : Infinity;
413
+ if (sinceLastConsolidation > FULL_CONSOLIDATION_GAP_MS) {
414
+ // No recent consolidation — graceful exit didn't happen, run full cycle
415
+ fullConsolidationTriggered = true;
416
+ try {
417
+ const result = consolidationEngine.consolidate(AGENT_ID);
418
+ store.markConsolidation(AGENT_ID, false);
419
+ 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`);
420
+ }
421
+ catch { /* consolidation failure is non-fatal */ }
422
+ }
423
+ else {
424
+ // Recent consolidation exists — graceful exit already handled it, just do mini
425
+ miniConsolidationTriggered = true;
426
+ consolidationScheduler.runMiniConsolidation(AGENT_ID).catch(() => { });
427
+ }
386
428
  }
387
- }
388
- return {
389
- content: [{
390
- type: 'text',
391
- text: parts.join('\n'),
392
- }],
393
- };
394
- });
395
- // --- Task Management Tools ---
396
- server.tool('memory_task_add', `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
429
+ // Format response
430
+ const parts = [];
431
+ const idleMin = Math.round(idleMs / 60_000);
432
+ const consolidationNote = fullConsolidationTriggered
433
+ ? ' (full consolidation — no graceful exit detected)'
434
+ : miniConsolidationTriggered
435
+ ? ' (mini-consolidation triggered)'
436
+ : '';
437
+ log(AGENT_ID, 'restore', `idle=${idleMin}min checkpoint=${!!checkpoint?.executionState} recalled=${recalledMemories.length} lastWrite=${lastWrite?.concept ?? 'none'}${fullConsolidationTriggered ? ' FULL_CONSOLIDATION' : ''}`);
438
+ parts.push(`Idle: ${idleMin}min${consolidationNote}`);
439
+ if (checkpoint?.executionState) {
440
+ const s = checkpoint.executionState;
441
+ parts.push(`\n**Current task:** ${s.currentTask}`);
442
+ if (s.decisions.length)
443
+ parts.push(`**Decisions:** ${s.decisions.join('; ')}`);
444
+ if (s.nextSteps.length)
445
+ parts.push(`**Next steps:** ${s.nextSteps.map((st, i) => `${i + 1}. ${st}`).join(', ')}`);
446
+ if (s.activeFiles.length)
447
+ parts.push(`**Active files:** ${s.activeFiles.join(', ')}`);
448
+ if (s.notes)
449
+ parts.push(`**Notes:** ${s.notes}`);
450
+ if (checkpoint.checkpointAt)
451
+ parts.push(`_Saved at: ${checkpoint.checkpointAt.toISOString()}_`);
452
+ }
453
+ else {
454
+ parts.push('\nNo explicit checkpoint saved.');
455
+ parts.push('\n**Tip:** Use memory_write to save important learnings, and memory_checkpoint before long operations so you can recover state.');
456
+ }
457
+ if (lastWrite) {
458
+ parts.push(`\n**Last write:** ${lastWrite.concept}\n${lastWrite.content}`);
459
+ }
460
+ if (recalledMemories.length > 0) {
461
+ parts.push(`\n**Recalled memories (${recalledMemories.length}):**`);
462
+ for (const m of recalledMemories) {
463
+ parts.push(`- **${m.concept}** (${m.score.toFixed(3)}): ${m.content.slice(0, 150)}${m.content.length > 150 ? '...' : ''}`);
464
+ }
465
+ }
466
+ return {
467
+ content: [{
468
+ type: 'text',
469
+ text: parts.join('\n'),
470
+ }],
471
+ };
472
+ });
473
+ // --- Task Management Tools ---
474
+ server.tool('memory_task_add', `Create a task that you need to come back to. Tasks are memories with status and priority tracking.
397
475
 
398
476
  Use this when:
399
477
  - You identify work that needs doing but can't do it right now
@@ -401,132 +479,269 @@ Use this when:
401
479
  - You want to park a sub-task while focusing on something more urgent
402
480
 
403
481
  Tasks automatically get high salience so they won't be discarded.`, {
404
- concept: z.string().describe('Short task title (3-10 words)'),
405
- content: z.string().describe('Full task description — what needs doing, context, acceptance criteria'),
406
- tags: z.array(z.string()).optional().describe('Tags for categorization'),
407
- priority: z.enum(['urgent', 'high', 'medium', 'low']).default('medium')
408
- .describe('Task priority: urgent (do now), high (do soon), medium (normal), low (backlog)'),
409
- blocked_by: z.string().optional().describe('ID of a task that must finish first'),
410
- }, async (params) => {
411
- const engram = store.createEngram({
412
- agentId: AGENT_ID,
413
- concept: params.concept,
414
- content: params.content,
415
- tags: [...(params.tags ?? []), 'task'],
416
- salience: 0.9, // Tasks always high salience
417
- confidence: 0.8,
418
- salienceFeatures: {
419
- surprise: 0.5,
420
- decisionMade: true,
421
- causalDepth: 0.5,
422
- resolutionEffort: 0.5,
423
- eventType: 'decision',
424
- },
425
- reasonCodes: ['task-created'],
426
- taskStatus: params.blocked_by ? 'blocked' : 'open',
427
- taskPriority: params.priority,
428
- blockedBy: params.blocked_by,
482
+ concept: z.string().describe('Short task title (3-10 words)'),
483
+ content: z.string().describe('Full task description — what needs doing, context, acceptance criteria'),
484
+ tags: z.array(z.string()).optional().describe('Tags for categorization'),
485
+ priority: z.enum(['urgent', 'high', 'medium', 'low']).default('medium')
486
+ .describe('Task priority: urgent (do now), high (do soon), medium (normal), low (backlog)'),
487
+ blocked_by: z.string().optional().describe('ID of a task that must finish first'),
488
+ }, async (params) => {
489
+ const engram = store.createEngram({
490
+ agentId: AGENT_ID,
491
+ concept: params.concept,
492
+ content: params.content,
493
+ tags: [...(params.tags ?? []), 'task'],
494
+ salience: 0.9, // Tasks always high salience
495
+ confidence: 0.8,
496
+ salienceFeatures: {
497
+ surprise: 0.5,
498
+ decisionMade: true,
499
+ causalDepth: 0.5,
500
+ resolutionEffort: 0.5,
501
+ eventType: 'decision',
502
+ },
503
+ reasonCodes: ['task-created'],
504
+ taskStatus: params.blocked_by ? 'blocked' : 'open',
505
+ taskPriority: params.priority,
506
+ blockedBy: params.blocked_by,
507
+ });
508
+ connectionEngine.enqueue(engram.id);
509
+ // Generate embedding asynchronously
510
+ embed(`${params.concept} ${params.content}`).then(vec => {
511
+ store.updateEmbedding(engram.id, vec);
512
+ }).catch(() => { });
513
+ return {
514
+ content: [{
515
+ type: 'text',
516
+ text: `Task created: ${engram.id}\nTitle: ${params.concept}\nPriority: ${params.priority}\nStatus: ${engram.taskStatus}`,
517
+ }],
518
+ };
429
519
  });
430
- connectionEngine.enqueue(engram.id);
431
- // Generate embedding asynchronously
432
- embed(`${params.concept} ${params.content}`).then(vec => {
433
- store.updateEmbedding(engram.id, vec);
434
- }).catch(() => { });
435
- return {
436
- content: [{
437
- type: 'text',
438
- text: `Task created: ${engram.id}\nTitle: ${params.concept}\nPriority: ${params.priority}\nStatus: ${engram.taskStatus}`,
439
- }],
440
- };
441
- });
442
- server.tool('memory_task_update', `Update a task's status or priority. Use this to:
520
+ server.tool('memory_task_update', `Update a task's status or priority. Use this to:
443
521
  - Start working on a task (open → in_progress)
444
522
  - Mark a task done (→ done)
445
523
  - Block a task on another (→ blocked)
446
524
  - Reprioritize (change priority)
447
525
  - Unblock a task (clear blocked_by)`, {
448
- task_id: z.string().describe('ID of the task to update'),
449
- status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
450
- .describe('New status'),
451
- priority: z.enum(['urgent', 'high', 'medium', 'low']).optional()
452
- .describe('New priority'),
453
- blocked_by: z.string().optional().describe('ID of blocking task (set to empty string to unblock)'),
454
- }, async (params) => {
455
- const engram = store.getEngram(params.task_id);
456
- if (!engram || !engram.taskStatus) {
457
- return { content: [{ type: 'text', text: `Task not found: ${params.task_id}` }] };
458
- }
459
- if (params.blocked_by !== undefined) {
460
- store.updateBlockedBy(params.task_id, params.blocked_by || null);
461
- }
462
- if (params.status) {
463
- store.updateTaskStatus(params.task_id, params.status);
464
- }
465
- if (params.priority) {
466
- store.updateTaskPriority(params.task_id, params.priority);
467
- }
468
- const updated = store.getEngram(params.task_id);
469
- return {
470
- content: [{
471
- type: 'text',
472
- text: `Task updated: ${updated.concept}\nStatus: ${updated.taskStatus}\nPriority: ${updated.taskPriority}${updated.blockedBy ? `\nBlocked by: ${updated.blockedBy}` : ''}`,
473
- }],
474
- };
475
- });
476
- server.tool('memory_task_list', `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
526
+ task_id: z.string().describe('ID of the task to update'),
527
+ status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
528
+ .describe('New status'),
529
+ priority: z.enum(['urgent', 'high', 'medium', 'low']).optional()
530
+ .describe('New priority'),
531
+ blocked_by: z.string().optional().describe('ID of blocking task (set to empty string to unblock)'),
532
+ }, async (params) => {
533
+ const engram = store.getEngram(params.task_id);
534
+ if (!engram || !engram.taskStatus) {
535
+ return { content: [{ type: 'text', text: `Task not found: ${params.task_id}` }] };
536
+ }
537
+ if (params.blocked_by !== undefined) {
538
+ store.updateBlockedBy(params.task_id, params.blocked_by || null);
539
+ }
540
+ if (params.status) {
541
+ store.updateTaskStatus(params.task_id, params.status);
542
+ }
543
+ if (params.priority) {
544
+ store.updateTaskPriority(params.task_id, params.priority);
545
+ }
546
+ const updated = store.getEngram(params.task_id);
547
+ return {
548
+ content: [{
549
+ type: 'text',
550
+ text: `Task updated: ${updated.concept}\nStatus: ${updated.taskStatus}\nPriority: ${updated.taskPriority}${updated.blockedBy ? `\nBlocked by: ${updated.blockedBy}` : ''}`,
551
+ }],
552
+ };
553
+ });
554
+ server.tool('memory_task_list', `List tasks with optional status filter. Shows tasks ordered by priority (urgent first).
477
555
 
478
556
  Use at the start of a session to see what's pending, or to check blocked/done tasks.`, {
479
- status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
480
- .describe('Filter by status (omit to see all active tasks)'),
481
- include_done: z.boolean().optional().default(false)
482
- .describe('Include completed tasks?'),
483
- }, async (params) => {
484
- let tasks = store.getTasks(AGENT_ID, params.status);
485
- if (!params.include_done && !params.status) {
486
- tasks = tasks.filter(t => t.taskStatus !== 'done');
487
- }
488
- if (tasks.length === 0) {
489
- return { content: [{ type: 'text', text: 'No tasks found.' }] };
490
- }
491
- const lines = tasks.map((t, i) => {
492
- const blocked = t.blockedBy ? ` [blocked by ${t.blockedBy}]` : '';
493
- const tags = t.tags?.filter(tag => tag !== 'task').join(', ');
494
- 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}`;
557
+ status: z.enum(['open', 'in_progress', 'blocked', 'done']).optional()
558
+ .describe('Filter by status (omit to see all active tasks)'),
559
+ include_done: z.boolean().optional().default(false)
560
+ .describe('Include completed tasks?'),
561
+ }, async (params) => {
562
+ let tasks = store.getTasks(AGENT_ID, params.status);
563
+ if (!params.include_done && !params.status) {
564
+ tasks = tasks.filter(t => t.taskStatus !== 'done');
565
+ }
566
+ if (tasks.length === 0) {
567
+ return { content: [{ type: 'text', text: 'No tasks found.' }] };
568
+ }
569
+ const lines = tasks.map((t, i) => {
570
+ const blocked = t.blockedBy ? ` [blocked by ${t.blockedBy}]` : '';
571
+ const tags = t.tags?.filter(tag => tag !== 'task').join(', ');
572
+ 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}`;
573
+ });
574
+ return {
575
+ content: [{
576
+ type: 'text',
577
+ text: `Tasks (${tasks.length}):\n\n${lines.join('\n\n')}`,
578
+ }],
579
+ };
495
580
  });
496
- return {
497
- content: [{
498
- type: 'text',
499
- text: `Tasks (${tasks.length}):\n\n${lines.join('\n\n')}`,
500
- }],
501
- };
502
- });
503
- server.tool('memory_task_next', `Get the single most important task to work on next.
581
+ server.tool('memory_task_next', `Get the single most important task to work on next.
504
582
 
505
583
  Prioritizes: in_progress tasks first (finish what you started), then by priority level, then oldest first. Skips blocked and done tasks.
506
584
 
507
585
  Use this when you finish a task or need to decide what to do next.`, {}, async () => {
508
- const next = store.getNextTask(AGENT_ID);
509
- if (!next) {
510
- return { content: [{ type: 'text', text: 'No actionable tasks. All clear!' }] };
586
+ const next = store.getNextTask(AGENT_ID);
587
+ if (!next) {
588
+ return { content: [{ type: 'text', text: 'No actionable tasks. All clear!' }] };
589
+ }
590
+ const blocked = next.blockedBy ? `\nBlocked by: ${next.blockedBy}` : '';
591
+ const tags = next.tags?.filter(tag => tag !== 'task').join(', ');
592
+ return {
593
+ content: [{
594
+ type: 'text',
595
+ text: `Next task:\n**${next.concept}** (${next.taskPriority})\nStatus: ${next.taskStatus}\n${next.content}${blocked}\n${tags ? `Tags: ${tags}\n` : ''}ID: ${next.id}`,
596
+ }],
597
+ };
598
+ });
599
+ // --- Task Bracket Tools ---
600
+ server.tool('memory_task_begin', `Signal that you're starting a significant task. Auto-checkpoints current state and recalls relevant memories.
601
+
602
+ CALL THIS when starting:
603
+ - A multi-step operation (doc generation, large refactor, migration)
604
+ - Work on a new topic or project area
605
+ - Anything that might fill the context window
606
+
607
+ This ensures your state is saved before you start, and primes recall with relevant context.`, {
608
+ topic: z.string().describe('What task are you starting? (3-15 words)'),
609
+ files: z.array(z.string()).optional().default([])
610
+ .describe('Files you expect to work with'),
611
+ notes: z.string().optional().default('')
612
+ .describe('Any additional context'),
613
+ }, async (params) => {
614
+ // 1. Checkpoint current state
615
+ const checkpoint = store.getCheckpoint(AGENT_ID);
616
+ const prevTask = checkpoint?.executionState?.currentTask ?? 'None';
617
+ store.saveCheckpoint(AGENT_ID, {
618
+ currentTask: params.topic,
619
+ decisions: [],
620
+ activeFiles: params.files,
621
+ nextSteps: [],
622
+ relatedMemoryIds: [],
623
+ notes: params.notes || `Started via memory_task_begin. Previous task: ${prevTask}`,
624
+ episodeId: null,
625
+ });
626
+ // 2. Auto-recall relevant memories
627
+ let recalledSummary = '';
628
+ try {
629
+ const results = await activationEngine.activate({
630
+ agentId: AGENT_ID,
631
+ context: params.topic,
632
+ limit: 5,
633
+ minScore: 0.05,
634
+ useReranker: true,
635
+ useExpansion: true,
636
+ });
637
+ if (results.length > 0) {
638
+ const lines = results.map((r, i) => {
639
+ const tags = r.engram.tags?.length ? ` [${r.engram.tags.join(', ')}]` : '';
640
+ return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${tags}\n ${r.engram.content.slice(0, 150)}${r.engram.content.length > 150 ? '...' : ''}`;
641
+ });
642
+ recalledSummary = `\n\n**Recalled memories (${results.length}):**\n${lines.join('\n')}`;
643
+ // Track recall
644
+ store.updateAutoCheckpointRecall(AGENT_ID, params.topic, results.map(r => r.engram.id));
645
+ }
646
+ }
647
+ catch { /* recall failure is non-fatal */ }
648
+ log(AGENT_ID, 'task:begin', `"${params.topic}" prev="${prevTask}"`);
649
+ return {
650
+ content: [{
651
+ type: 'text',
652
+ text: `Task started: ${params.topic}\nCheckpoint saved (previous: ${prevTask}).${params.files.length ? `\nFiles: ${params.files.join(', ')}` : ''}${recalledSummary}`,
653
+ }],
654
+ };
655
+ });
656
+ server.tool('memory_task_end', `Signal that you've finished a significant task. Writes a summary memory and auto-checkpoints.
657
+
658
+ CALL THIS when you finish:
659
+ - A multi-step operation
660
+ - Before switching to a different topic
661
+ - At the end of a work session
662
+
663
+ This captures what was accomplished so future sessions can recall it.`, {
664
+ summary: z.string().describe('What was accomplished? Include key outcomes, decisions, and any issues.'),
665
+ tags: z.array(z.string()).optional().default([])
666
+ .describe('Tags for the summary memory'),
667
+ }, async (params) => {
668
+ // 1. Write summary as a memory
669
+ const salience = evaluateSalience({
670
+ content: params.summary,
671
+ eventType: 'decision',
672
+ surprise: 0.3,
673
+ decisionMade: true,
674
+ causalDepth: 0.5,
675
+ resolutionEffort: 0.5,
676
+ });
677
+ const engram = store.createEngram({
678
+ agentId: AGENT_ID,
679
+ concept: 'Task completed',
680
+ content: params.summary,
681
+ tags: [...params.tags, 'task-summary'],
682
+ salience: Math.max(salience.score, 0.7), // Always high salience for task summaries
683
+ salienceFeatures: salience.features,
684
+ reasonCodes: [...salience.reasonCodes, 'task-end'],
685
+ });
686
+ connectionEngine.enqueue(engram.id);
687
+ // Generate embedding asynchronously
688
+ embed(`Task completed: ${params.summary}`).then(vec => {
689
+ store.updateEmbedding(engram.id, vec);
690
+ }).catch(() => { });
691
+ // 2. Update checkpoint to reflect completion
692
+ const checkpoint = store.getCheckpoint(AGENT_ID);
693
+ const completedTask = checkpoint?.executionState?.currentTask ?? 'Unknown task';
694
+ store.saveCheckpoint(AGENT_ID, {
695
+ currentTask: `Completed: ${completedTask}`,
696
+ decisions: checkpoint?.executionState?.decisions ?? [],
697
+ activeFiles: [],
698
+ nextSteps: [],
699
+ relatedMemoryIds: [engram.id],
700
+ notes: `Task completed. Summary memory: ${engram.id}`,
701
+ episodeId: null,
702
+ });
703
+ store.updateAutoCheckpointWrite(AGENT_ID, engram.id);
704
+ log(AGENT_ID, 'task:end', `"${completedTask}" summary=${engram.id} salience=${salience.score.toFixed(2)}`);
705
+ return {
706
+ content: [{
707
+ type: 'text',
708
+ text: `Task completed and saved.\nSummary memory: ${engram.id}\nSalience: ${salience.score.toFixed(2)}\nCheckpoint updated.`,
709
+ }],
710
+ };
711
+ });
712
+ // --- Start ---
713
+ async function main() {
714
+ const transport = new StdioServerTransport();
715
+ await server.connect(transport);
716
+ // Start hook sidecar (lightweight HTTP for Claude Code hooks)
717
+ const sidecar = startSidecar({
718
+ store,
719
+ agentId: AGENT_ID,
720
+ secret: HOOK_SECRET,
721
+ port: HOOK_PORT,
722
+ onConsolidate: (agentId, reason) => {
723
+ console.error(`[mcp] consolidation triggered: ${reason}`);
724
+ const result = consolidationEngine.consolidate(agentId);
725
+ store.markConsolidation(agentId, false);
726
+ console.error(`[mcp] consolidation done: ${result.edgesStrengthened} strengthened, ${result.memoriesForgotten} forgotten`);
727
+ },
728
+ });
729
+ // Log to stderr (stdout is reserved for MCP protocol)
730
+ console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
731
+ console.error(`Hook sidecar on 127.0.0.1:${HOOK_PORT}${HOOK_SECRET ? ' (auth enabled)' : ' (no auth — set AWM_HOOK_SECRET)'}`);
732
+ // Clean shutdown
733
+ const cleanup = () => {
734
+ sidecar.close();
735
+ consolidationScheduler.stop();
736
+ stagingBuffer.stop();
737
+ store.close();
738
+ };
739
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
740
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
511
741
  }
512
- const blocked = next.blockedBy ? `\nBlocked by: ${next.blockedBy}` : '';
513
- const tags = next.tags?.filter(tag => tag !== 'task').join(', ');
514
- return {
515
- content: [{
516
- type: 'text',
517
- text: `Next task:\n**${next.concept}** (${next.taskPriority})\nStatus: ${next.taskStatus}\n${next.content}${blocked}\n${tags ? `Tags: ${tags}\n` : ''}ID: ${next.id}`,
518
- }],
519
- };
520
- });
521
- // --- Start ---
522
- async function main() {
523
- const transport = new StdioServerTransport();
524
- await server.connect(transport);
525
- // Log to stderr (stdout is reserved for MCP protocol)
526
- console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
527
- }
528
- main().catch(err => {
529
- console.error('MCP server failed:', err);
530
- process.exit(1);
531
- });
742
+ main().catch(err => {
743
+ console.error('MCP server failed:', err);
744
+ process.exit(1);
745
+ });
746
+ } // end else (non-incognito)
532
747
  //# sourceMappingURL=mcp.js.map