agent-working-memory 0.7.16 → 0.7.17

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 (38) hide show
  1. package/dist/adapters/claude-code.d.ts.map +1 -1
  2. package/dist/adapters/claude-code.js +2 -16
  3. package/dist/adapters/claude-code.js.map +1 -1
  4. package/dist/adapters/codex.d.ts.map +1 -1
  5. package/dist/adapters/codex.js +2 -11
  6. package/dist/adapters/codex.js.map +1 -1
  7. package/dist/adapters/common.d.ts +18 -0
  8. package/dist/adapters/common.d.ts.map +1 -1
  9. package/dist/adapters/common.js +127 -14
  10. package/dist/adapters/common.js.map +1 -1
  11. package/dist/adapters/cursor.d.ts.map +1 -1
  12. package/dist/adapters/cursor.js +2 -15
  13. package/dist/adapters/cursor.js.map +1 -1
  14. package/dist/adapters/http.d.ts.map +1 -1
  15. package/dist/adapters/http.js +6 -12
  16. package/dist/adapters/http.js.map +1 -1
  17. package/dist/api/routes.d.ts.map +1 -1
  18. package/dist/api/routes.js +45 -57
  19. package/dist/api/routes.js.map +1 -1
  20. package/dist/cli.js +103 -103
  21. package/dist/core/write-pipeline.d.ts +120 -0
  22. package/dist/core/write-pipeline.d.ts.map +1 -0
  23. package/dist/core/write-pipeline.js +236 -0
  24. package/dist/core/write-pipeline.js.map +1 -0
  25. package/dist/index.js +1 -1
  26. package/dist/mcp.js +107 -178
  27. package/dist/mcp.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/adapters/claude-code.ts +2 -18
  30. package/src/adapters/codex.ts +2 -12
  31. package/src/adapters/common.ts +141 -14
  32. package/src/adapters/cursor.ts +2 -17
  33. package/src/adapters/http.ts +5 -12
  34. package/src/api/routes.ts +714 -723
  35. package/src/cli.ts +719 -719
  36. package/src/core/write-pipeline.ts +343 -0
  37. package/src/index.ts +212 -212
  38. package/src/mcp.ts +1121 -1192
package/src/api/routes.ts CHANGED
@@ -1,723 +1,714 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * API Routes — the black box interface agents interact with.
5
- *
6
- * Core (agent-facing):
7
- * POST /memory/write — write a memory (salience filter decides disposition)
8
- * POST /memory/activate — retrieve by context activation
9
- * POST /memory/feedback — report whether a memory was useful
10
- * POST /memory/retract — invalidate a wrong memory
11
- *
12
- * Checkpointing:
13
- * POST /memory/checkpoint — save explicit execution state
14
- * GET /memory/restore/:agentId — restore state + targeted recall + async mini-consolidation
15
- *
16
- * Task management:
17
- * POST /task/create — create a prioritized task
18
- * POST /task/update — update status, priority, or blocking
19
- * GET /task/list/:agentId — list tasks (filtered by status)
20
- * GET /task/next/:agentId — get highest-priority actionable task
21
- *
22
- * Diagnostic (debugging/eval):
23
- * POST /memory/search — deterministic search (not cognitive)
24
- * GET /memory/:id — get a specific engram
25
- * GET /agent/:id/stats — memory stats for an agent
26
- * GET /agent/:id/metrics — eval metrics
27
- * POST /agent/register — register a new agent
28
- *
29
- * System:
30
- * POST /system/evict — trigger eviction check
31
- * POST /system/decay — trigger edge decay
32
- * POST /system/consolidate — run sleep cycle (strengthen, decay, sweep)
33
- * GET /health — health check
34
- */
35
-
36
- import type { FastifyInstance } from 'fastify';
37
- import type { EngramStore } from '../storage/sqlite.js';
38
- import type { ActivationEngine } from '../engine/activation.js';
39
- import type { ConnectionEngine } from '../engine/connections.js';
40
- import type { EvictionEngine } from '../engine/eviction.js';
41
- import type { RetractionEngine } from '../engine/retraction.js';
42
- import type { EvalEngine } from '../engine/eval.js';
43
- import type { ConsolidationEngine } from '../engine/consolidation.js';
44
- import type { ConsolidationScheduler } from '../engine/consolidation-scheduler.js';
45
- import { evaluateSalience, computeNovelty } from '../core/salience.js';
46
- import type { SalienceEventType } from '../core/salience.js';
47
- import type { TaskStatus, TaskPriority } from '../types/engram.js';
48
- import type { ConsciousState } from '../types/checkpoint.js';
49
- import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
50
- import { embed, embedBatch } from '../core/embeddings.js';
51
-
52
- export interface MemoryDeps {
53
- store: EngramStore;
54
- activationEngine: ActivationEngine;
55
- connectionEngine: ConnectionEngine;
56
- evictionEngine: EvictionEngine;
57
- retractionEngine: RetractionEngine;
58
- evalEngine: EvalEngine;
59
- consolidationEngine: ConsolidationEngine;
60
- consolidationScheduler: ConsolidationScheduler;
61
- }
62
-
63
- export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
64
- const { store, activationEngine, connectionEngine, evictionEngine, retractionEngine, evalEngine, consolidationEngine, consolidationScheduler } = deps;
65
-
66
- // ============================================================
67
- // CORE — Agent-facing endpoints
68
- // ============================================================
69
-
70
- app.post('/memory/write', async (req, reply) => {
71
- const body = req.body as {
72
- agentId: string;
73
- concept: string;
74
- content: string;
75
- tags?: string[];
76
- eventType?: SalienceEventType;
77
- surprise?: number;
78
- decisionMade?: boolean;
79
- causalDepth?: number;
80
- resolutionEffort?: number;
81
- confidence?: number;
82
- // Agent-provided metadata (stored as searchable tags)
83
- project?: string;
84
- topic?: string;
85
- source?: string;
86
- confidenceLevel?: string;
87
- sessionId?: string;
88
- intent?: string;
89
- };
90
-
91
- if (!body.agentId || typeof body.agentId !== 'string' ||
92
- !body.concept || typeof body.concept !== 'string' ||
93
- !body.content || typeof body.content !== 'string') {
94
- return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
95
- }
96
-
97
- const novelty = computeNovelty(store, body.agentId, body.concept, body.content);
98
-
99
- const salience = evaluateSalience({
100
- content: body.content,
101
- eventType: body.eventType,
102
- surprise: body.surprise,
103
- decisionMade: body.decisionMade,
104
- causalDepth: body.causalDepth,
105
- resolutionEffort: body.resolutionEffort,
106
- novelty,
107
- });
108
-
109
- // v0.5.4: No longer discard — store with low confidence for ranking.
110
- const isLowSalience = salience.disposition === 'discard';
111
- const confidence = isLowSalience
112
- ? 0.25
113
- : body.confidence ?? (salience.disposition === 'staging' ? 0.40 : 0.50);
114
-
115
- // Assemble tags: user-provided + agent metadata
116
- const userTags = body.tags ?? [];
117
- const metaTags: string[] = [];
118
- if (body.project) metaTags.push(`proj=${body.project}`);
119
- if (body.topic) metaTags.push(`topic=${body.topic}`);
120
- if (body.source) metaTags.push(`src=${body.source}`);
121
- if (body.confidenceLevel) metaTags.push(`conf=${body.confidenceLevel}`);
122
- if (body.sessionId) metaTags.push(`sid=${body.sessionId}`);
123
- if (body.intent) metaTags.push(`intent=${body.intent}`);
124
- const allTags = isLowSalience
125
- ? [...userTags, ...metaTags, 'low-salience']
126
- : [...userTags, ...metaTags];
127
-
128
- const engram = store.createEngram({
129
- agentId: body.agentId,
130
- concept: body.concept,
131
- content: body.content,
132
- tags: allTags,
133
- salience: salience.score,
134
- confidence,
135
- salienceFeatures: salience.features,
136
- reasonCodes: salience.reasonCodes,
137
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
138
- });
139
-
140
- if (salience.disposition === 'staging') {
141
- store.updateStage(engram.id, 'staging');
142
- }
143
-
144
- // Create temporal adjacency edge to previous memory (conversation thread graph)
145
- // This enables multi-hop graph walk through conversation sequences
146
- try {
147
- const prev = store.getLatestEngram(body.agentId, engram.id);
148
- if (prev) {
149
- store.upsertAssociation(prev.id, engram.id, 0.3, 'temporal', 0.8);
150
- }
151
- } catch { /* Temporal edge creation is non-fatal */ }
152
-
153
- if (salience.disposition === 'active' || isLowSalience) {
154
- connectionEngine.enqueue(engram.id);
155
-
156
- // Auto-assign to episode (1-hour window per agent)
157
- try {
158
- let episode = store.getActiveEpisode(body.agentId, 3600_000);
159
- if (!episode) {
160
- episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
161
- }
162
- store.addEngramToEpisode(engram.id, episode.id);
163
- } catch { /* Episode assignment is non-fatal */ }
164
- }
165
-
166
- // Generate embedding asynchronously (don't block response)
167
- embed(`${body.concept} ${body.content}`).then(vec => {
168
- store.updateEmbedding(engram.id, vec);
169
- }).catch(() => {}); // Embedding failure is non-fatal
170
-
171
- // Auto-checkpoint: track write for consolidation scheduling
172
- try { store.updateAutoCheckpointWrite(body.agentId, engram.id); } catch { /* non-fatal */ }
173
-
174
- return reply.code(201).send({
175
- stored: true,
176
- disposition: isLowSalience ? 'low-salience' : salience.disposition,
177
- salience: salience.score,
178
- reasonCodes: salience.reasonCodes,
179
- engram,
180
- });
181
- });
182
-
183
- /**
184
- * Bulk write — accepts many facts in one request.
185
- * Creates engrams in a single transaction, embeds in batch.
186
- * Returns all IDs for downstream supersession calls.
187
- */
188
- app.post('/memory/write-batch', async (req, reply) => {
189
- const body = req.body as {
190
- agentId: string;
191
- sessionId?: string; // Shared session ID for all memories in this batch
192
- memories: Array<{
193
- concept: string;
194
- content: string;
195
- tags?: string[];
196
- supersedes?: string;
197
- sessionId?: string; // Per-memory session override
198
- }>;
199
- };
200
-
201
- if (!body.agentId || !body.memories || body.memories.length === 0) {
202
- return reply.code(400).send({ error: 'agentId and non-empty memories array required' });
203
- }
204
-
205
- const results: Array<{ id: string; concept: string; disposition: string }> = [];
206
-
207
- for (const mem of body.memories) {
208
- // Add session ID tag if provided (batch-level or per-memory)
209
- const sid = mem.sessionId ?? body.sessionId;
210
- const memTags = [...(mem.tags ?? [])];
211
- if (sid) memTags.push(`sid=${sid}`);
212
-
213
- const engram = store.createEngram({
214
- agentId: body.agentId,
215
- concept: mem.concept,
216
- content: mem.content,
217
- tags: memTags,
218
- salience: 0.5,
219
- confidence: 0.5,
220
- supersedes: mem.supersedes ?? undefined,
221
- });
222
-
223
- // Handle supersession inline — archive superseded memory to remove from active pool
224
- if (mem.supersedes) {
225
- store.supersedeEngram(mem.supersedes, engram.id);
226
- store.updateConfidence(mem.supersedes, 0.1);
227
- store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
228
- }
229
-
230
- results.push({ id: engram.id, concept: mem.concept, disposition: 'active' });
231
- }
232
-
233
- // Batch embed synchronously ensures embeddings are ready before queries hit
234
- const texts = body.memories.map((m, i) => `${m.concept} ${m.content}`);
235
- try {
236
- const vecs = await embedBatch(texts);
237
- for (let i = 0; i < vecs.length; i++) {
238
- if (results[i]) {
239
- store.updateEmbedding(results[i].id, vecs[i]);
240
- }
241
- }
242
- } catch { /* Embedding failure is non-fatal */ }
243
-
244
- try { store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? ''); } catch {}
245
-
246
- return reply.code(201).send({
247
- stored: results.length,
248
- results,
249
- });
250
- });
251
-
252
- app.post('/memory/activate', async (req, reply) => {
253
- const body = req.body as {
254
- agentId: string;
255
- context: string;
256
- limit?: number;
257
- minScore?: number;
258
- includeStaging?: boolean;
259
- useReranker?: boolean;
260
- useExpansion?: boolean;
261
- abstentionThreshold?: number;
262
- workspace?: string;
263
- bm25Only?: boolean;
264
- };
265
-
266
- const results = await activationEngine.activate({
267
- agentId: body.agentId,
268
- context: body.context,
269
- limit: body.limit,
270
- minScore: body.minScore,
271
- includeStaging: body.includeStaging,
272
- useReranker: body.useReranker,
273
- useExpansion: body.useExpansion,
274
- abstentionThreshold: body.abstentionThreshold,
275
- workspace: body.workspace,
276
- bm25Only: body.bm25Only,
277
- });
278
-
279
- // Auto-checkpoint: track recall for consolidation scheduling
280
- try {
281
- const ids = results.map(r => r.engram.id);
282
- store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
283
- } catch { /* non-fatal */ }
284
-
285
- return reply.send({ results });
286
- });
287
-
288
- app.post('/memory/feedback', async (req, reply) => {
289
- const body = req.body as {
290
- activationEventId?: string;
291
- engramId: string;
292
- useful: boolean;
293
- context?: string;
294
- };
295
-
296
- store.logRetrievalFeedback(
297
- body.activationEventId ?? null,
298
- body.engramId,
299
- body.useful,
300
- body.context ?? ''
301
- );
302
-
303
- // Update engram confidence based on feedback
304
- const engram = store.getEngram(body.engramId);
305
- if (engram) {
306
- const config = DEFAULT_AGENT_CONFIG;
307
- const delta = body.useful
308
- ? config.feedbackPositiveBoost
309
- : -config.feedbackNegativePenalty;
310
- store.updateConfidence(engram.id, engram.confidence + delta);
311
- }
312
-
313
- // Touch activity for consolidation scheduling
314
- if (engram) {
315
- try { store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
316
- }
317
-
318
- return reply.send({ recorded: true });
319
- });
320
-
321
- app.post('/memory/retract', async (req, reply) => {
322
- const body = req.body as {
323
- agentId: string;
324
- targetEngramId: string;
325
- reason: string;
326
- counterContent?: string;
327
- };
328
-
329
- const result = retractionEngine.retract({
330
- agentId: body.agentId,
331
- targetEngramId: body.targetEngramId,
332
- reason: body.reason,
333
- counterContent: body.counterContent,
334
- });
335
-
336
- // Touch activity for consolidation scheduling
337
- try { store.touchActivity(body.agentId); } catch { /* non-fatal */ }
338
-
339
- return reply.send(result);
340
- });
341
-
342
- app.post('/memory/supersede', async (req, reply) => {
343
- const body = req.body as {
344
- oldEngramId: string;
345
- newEngramId: string;
346
- reason?: string;
347
- };
348
-
349
- const oldEngram = store.getEngram(body.oldEngramId);
350
- const newEngram = store.getEngram(body.newEngramId);
351
- if (!oldEngram) return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
352
- if (!newEngram) return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
353
-
354
- // Create causal association (new old)
355
- store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
356
-
357
- // Reduce old engram confidence to 20% (keep for historical reference)
358
- store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
359
-
360
- // Mark supersession via store method
361
- store.supersedeEngram(body.oldEngramId, body.newEngramId);
362
-
363
- try { store.touchActivity(oldEngram.agentId); } catch { /* non-fatal */ }
364
-
365
- return reply.send({
366
- superseded: body.oldEngramId,
367
- supersededBy: body.newEngramId,
368
- reason: body.reason ?? 'outdated',
369
- });
370
- });
371
-
372
- // ============================================================
373
- // DIAGNOSTIC — Debugging and inspection
374
- // ============================================================
375
-
376
- app.post('/memory/search', async (req, reply) => {
377
- const body = req.body as {
378
- agentId: string;
379
- text?: string;
380
- concept?: string;
381
- tags?: string[];
382
- stage?: string;
383
- retracted?: boolean;
384
- limit?: number;
385
- offset?: number;
386
- };
387
-
388
- const results = store.search({
389
- agentId: body.agentId,
390
- text: body.text,
391
- concept: body.concept,
392
- tags: body.tags,
393
- stage: body.stage as any,
394
- retracted: body.retracted,
395
- limit: body.limit,
396
- offset: body.offset,
397
- });
398
-
399
- return reply.send({ results, count: results.length });
400
- });
401
-
402
- app.get('/memory/:id', async (req, reply) => {
403
- const { id } = req.params as { id: string };
404
- const engram = store.getEngram(id);
405
- if (!engram) return reply.code(404).send({ error: 'Not found' });
406
-
407
- const associations = store.getAssociationsFor(id);
408
- return reply.send({ engram, associations });
409
- });
410
-
411
- app.get('/agent/:id/stats', async (req, reply) => {
412
- const { id } = req.params as { id: string };
413
- const active = store.getEngramsByAgent(id, 'active');
414
- const staging = store.getEngramsByAgent(id, 'staging');
415
- const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
416
- const associations = store.getAllAssociations(id);
417
-
418
- return reply.send({
419
- agentId: id,
420
- engrams: {
421
- active: active.length,
422
- staging: staging.length,
423
- retracted: retracted.length,
424
- total: active.length + staging.length + retracted.length,
425
- },
426
- associations: associations.length,
427
- avgConfidence: active.length > 0
428
- ? +(active.reduce((s, e) => s + e.confidence, 0) / active.length).toFixed(3)
429
- : 0,
430
- });
431
- });
432
-
433
- app.get('/agent/:id/metrics', async (req, reply) => {
434
- const { id } = req.params as { id: string };
435
- const windowHours = parseInt((req.query as any).window ?? '24', 10);
436
- const metrics = evalEngine.computeMetrics(id, windowHours);
437
- return reply.send({ metrics });
438
- });
439
-
440
- app.post('/agent/register', async (req, reply) => {
441
- const body = req.body as { name: string };
442
- const id = crypto.randomUUID();
443
- return reply.code(201).send({
444
- id,
445
- name: body.name,
446
- config: DEFAULT_AGENT_CONFIG,
447
- });
448
- });
449
-
450
- // ============================================================
451
- // SYSTEM Maintenance operations
452
- // ============================================================
453
-
454
- app.post('/system/evict', async (req, reply) => {
455
- const body = req.body as { agentId: string };
456
- const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
457
- return reply.send(result);
458
- });
459
-
460
- app.post('/system/decay', async (req, reply) => {
461
- const body = req.body as { agentId: string; halfLifeDays?: number };
462
- const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
463
- return reply.send({ edgesDecayed: decayed });
464
- });
465
-
466
- app.post('/system/consolidate', async (req, reply) => {
467
- const body = req.body as { agentId: string };
468
- const result = await consolidationEngine.consolidate(body.agentId);
469
- return reply.send(result);
470
- });
471
-
472
- // ============================================================
473
- // CHECKPOINTING — Conscious state preservation
474
- // ============================================================
475
-
476
- app.post('/memory/checkpoint', async (req, reply) => {
477
- const body = req.body as {
478
- agentId: string;
479
- currentTask: string;
480
- decisions?: string[];
481
- activeFiles?: string[];
482
- nextSteps?: string[];
483
- relatedMemoryIds?: string[];
484
- notes?: string;
485
- episodeId?: string | null;
486
- };
487
-
488
- const state: ConsciousState = {
489
- currentTask: body.currentTask,
490
- decisions: body.decisions ?? [],
491
- activeFiles: body.activeFiles ?? [],
492
- nextSteps: body.nextSteps ?? [],
493
- relatedMemoryIds: body.relatedMemoryIds ?? [],
494
- notes: body.notes ?? '',
495
- episodeId: body.episodeId ?? null,
496
- };
497
-
498
- store.saveCheckpoint(body.agentId, state);
499
- return reply.send({ saved: true, agentId: body.agentId });
500
- });
501
-
502
- app.get('/memory/restore/:agentId', async (req, reply) => {
503
- const { agentId } = req.params as { agentId: string };
504
- const checkpoint = store.getCheckpoint(agentId);
505
-
506
- const now = Date.now();
507
- const idleMs = checkpoint
508
- ? now - checkpoint.auto.lastActivityAt.getTime()
509
- : 0;
510
-
511
- // Get last written engram for context
512
- let lastWrite: { id: string; concept: string; content: string } | null = null;
513
- if (checkpoint?.auto.lastWriteId) {
514
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
515
- if (engram) {
516
- lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
517
- }
518
- }
519
-
520
- // Recall memories using last context (if available)
521
- let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
522
- const recallContext = checkpoint?.auto.lastRecallContext
523
- ?? checkpoint?.executionState?.currentTask
524
- ?? null;
525
-
526
- if (recallContext) {
527
- try {
528
- const results = await activationEngine.activate({
529
- agentId,
530
- context: recallContext,
531
- limit: 5,
532
- minScore: 0.05,
533
- useReranker: true,
534
- useExpansion: true,
535
- });
536
- recalledMemories = results.map(r => ({
537
- id: r.engram.id,
538
- concept: r.engram.concept,
539
- content: r.engram.content,
540
- score: r.score,
541
- }));
542
- } catch { /* recall failure is non-fatal */ }
543
- }
544
-
545
- // Trigger mini-consolidation if idle >5min (async, fire-and-forget)
546
- const MINI_CONSOLIDATION_IDLE_MS = 5 * 60_000;
547
- let miniConsolidationTriggered = false;
548
- if (idleMs > MINI_CONSOLIDATION_IDLE_MS) {
549
- miniConsolidationTriggered = true;
550
- consolidationScheduler.runMiniConsolidation(agentId).catch(() => {});
551
- }
552
-
553
- return reply.send({
554
- executionState: checkpoint?.executionState ?? null,
555
- checkpointAt: checkpoint?.checkpointAt ?? null,
556
- recalledMemories,
557
- lastWrite,
558
- idleMs,
559
- miniConsolidationTriggered,
560
- });
561
- });
562
-
563
- // ============================================================
564
- // TASK MANAGEMENT
565
- // ============================================================
566
-
567
- app.post('/task/create', async (req, reply) => {
568
- const body = req.body as {
569
- agentId: string;
570
- concept: string;
571
- content: string;
572
- tags?: string[];
573
- priority?: TaskPriority;
574
- blockedBy?: string;
575
- };
576
-
577
- const engram = store.createEngram({
578
- agentId: body.agentId,
579
- concept: body.concept,
580
- content: body.content,
581
- tags: [...(body.tags ?? []), 'task'],
582
- salience: 0.9,
583
- confidence: 0.8,
584
- salienceFeatures: {
585
- surprise: 0.5, decisionMade: true, causalDepth: 0.5,
586
- resolutionEffort: 0.5, eventType: 'decision',
587
- },
588
- reasonCodes: ['task-created'],
589
- taskStatus: body.blockedBy ? 'blocked' : 'open',
590
- taskPriority: body.priority ?? 'medium',
591
- blockedBy: body.blockedBy,
592
- });
593
-
594
- connectionEngine.enqueue(engram.id);
595
- embed(`${body.concept} ${body.content}`).then(vec => {
596
- store.updateEmbedding(engram.id, vec);
597
- }).catch(() => {});
598
-
599
- return reply.send(engram);
600
- });
601
-
602
- app.post('/task/update', async (req, reply) => {
603
- const body = req.body as {
604
- taskId: string;
605
- status?: TaskStatus;
606
- priority?: TaskPriority;
607
- blockedBy?: string | null;
608
- };
609
-
610
- const engram = store.getEngram(body.taskId);
611
- if (!engram || !engram.taskStatus) {
612
- return reply.code(404).send({ error: 'Task not found' });
613
- }
614
-
615
- if (body.blockedBy !== undefined) {
616
- store.updateBlockedBy(body.taskId, body.blockedBy);
617
- }
618
- if (body.status) {
619
- store.updateTaskStatus(body.taskId, body.status);
620
- }
621
- if (body.priority) {
622
- store.updateTaskPriority(body.taskId, body.priority);
623
- }
624
-
625
- return reply.send(store.getEngram(body.taskId));
626
- });
627
-
628
- app.get('/task/list/:agentId', async (req, reply) => {
629
- const { agentId } = req.params as { agentId: string };
630
- const { status, includeDone } = req.query as { status?: TaskStatus; includeDone?: string };
631
-
632
- let tasks = store.getTasks(agentId, status);
633
- if (includeDone !== 'true' && !status) {
634
- tasks = tasks.filter(t => t.taskStatus !== 'done');
635
- }
636
-
637
- return reply.send({ tasks, count: tasks.length });
638
- });
639
-
640
- app.get('/task/next/:agentId', async (req, reply) => {
641
- const { agentId } = req.params as { agentId: string };
642
- const next = store.getNextTask(agentId);
643
- return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
644
- });
645
-
646
- // Time warp — shift all timestamps backward by N days (for testing)
647
- app.post('/system/time-warp', async (req, reply) => {
648
- const body = req.body as { agentId: string; days: number };
649
- const ms = body.days * 24 * 60 * 60 * 1000;
650
- const shifted = store.timeWarp(body.agentId, ms);
651
- return reply.send({ shifted, days: body.days });
652
- });
653
-
654
- // ─── Export ─────────────────────────────────────────────────────────────
655
-
656
- app.get('/memory/export', async (req, reply) => {
657
- const { agentId, all } = req.query as { agentId?: string; all?: string };
658
- const includeAll = all === 'true';
659
- const db = store.getDb();
660
-
661
- let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
662
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
663
- retracted, retracted_by, retracted_at, tags
664
- FROM engrams`;
665
- const conditions: string[] = [];
666
- const params: string[] = [];
667
-
668
- if (agentId) {
669
- conditions.push('agent_id = ?');
670
- params.push(agentId);
671
- }
672
- if (!includeAll) {
673
- conditions.push('retracted = 0');
674
- conditions.push("stage = 'active'");
675
- }
676
- if (conditions.length > 0) {
677
- engramSql += ' WHERE ' + conditions.join(' AND ');
678
- }
679
- engramSql += ' ORDER BY created_at ASC';
680
-
681
- const engrams = db.prepare(engramSql).all(...params) as { id: string }[];
682
-
683
- const engramIds = new Set(engrams.map(e => e.id));
684
- const allAssocs = db.prepare(
685
- `SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
686
- FROM associations`
687
- ).all() as { from_engram_id: string; to_engram_id: string }[];
688
- const associations = allAssocs.filter(a => engramIds.has(a.from_engram_id) && engramIds.has(a.to_engram_id));
689
-
690
- return reply.send({
691
- exported_at: new Date().toISOString(),
692
- agent_id: agentId ?? null,
693
- include_all: includeAll,
694
- engrams_count: engrams.length,
695
- associations_count: associations.length,
696
- engrams,
697
- associations,
698
- });
699
- });
700
-
701
- // ─── Health ─────────────────────────────────────────────────────────────
702
-
703
- app.get('/health', async () => {
704
- const coordEnabled = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
705
- const base: Record<string, unknown> = {
706
- status: 'ok',
707
- timestamp: new Date().toISOString(),
708
- version: '0.7.16',
709
- coordination: coordEnabled,
710
- };
711
- if (coordEnabled) {
712
- try {
713
- const db = deps.store.getDb();
714
- const stats = db.prepare(`SELECT
715
- (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
716
- (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
717
- (SELECT COUNT(*) FROM coord_locks) AS active_locks`).get() as { agents_alive: number; pending_tasks: number; active_locks: number };
718
- Object.assign(base, stats);
719
- } catch { /* tables may not exist yet */ }
720
- }
721
- return base;
722
- });
723
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * API Routes — the black box interface agents interact with.
5
+ *
6
+ * Core (agent-facing):
7
+ * POST /memory/write — write a memory (salience filter decides disposition)
8
+ * POST /memory/activate — retrieve by context activation
9
+ * POST /memory/feedback — report whether a memory was useful
10
+ * POST /memory/retract — invalidate a wrong memory
11
+ *
12
+ * Checkpointing:
13
+ * POST /memory/checkpoint — save explicit execution state
14
+ * GET /memory/restore/:agentId — restore state + targeted recall + async mini-consolidation
15
+ *
16
+ * Task management:
17
+ * POST /task/create — create a prioritized task
18
+ * POST /task/update — update status, priority, or blocking
19
+ * GET /task/list/:agentId — list tasks (filtered by status)
20
+ * GET /task/next/:agentId — get highest-priority actionable task
21
+ *
22
+ * Diagnostic (debugging/eval):
23
+ * POST /memory/search — deterministic search (not cognitive)
24
+ * GET /memory/:id — get a specific engram
25
+ * GET /agent/:id/stats — memory stats for an agent
26
+ * GET /agent/:id/metrics — eval metrics
27
+ * POST /agent/register — register a new agent
28
+ *
29
+ * System:
30
+ * POST /system/evict — trigger eviction check
31
+ * POST /system/decay — trigger edge decay
32
+ * POST /system/consolidate — run sleep cycle (strengthen, decay, sweep)
33
+ * GET /health — health check
34
+ */
35
+
36
+ import type { FastifyInstance } from 'fastify';
37
+ import type { EngramStore } from '../storage/sqlite.js';
38
+ import type { ActivationEngine } from '../engine/activation.js';
39
+ import type { ConnectionEngine } from '../engine/connections.js';
40
+ import type { EvictionEngine } from '../engine/eviction.js';
41
+ import type { RetractionEngine } from '../engine/retraction.js';
42
+ import type { EvalEngine } from '../engine/eval.js';
43
+ import type { ConsolidationEngine } from '../engine/consolidation.js';
44
+ import type { ConsolidationScheduler } from '../engine/consolidation-scheduler.js';
45
+ import { evaluateSalience, computeNovelty } from '../core/salience.js';
46
+ import type { SalienceEventType } from '../core/salience.js';
47
+ import { performWrite } from '../core/write-pipeline.js';
48
+ import type { TaskStatus, TaskPriority } from '../types/engram.js';
49
+ import type { ConsciousState } from '../types/checkpoint.js';
50
+ import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
51
+ import { embed, embedBatch } from '../core/embeddings.js';
52
+
53
+ export interface MemoryDeps {
54
+ store: EngramStore;
55
+ activationEngine: ActivationEngine;
56
+ connectionEngine: ConnectionEngine;
57
+ evictionEngine: EvictionEngine;
58
+ retractionEngine: RetractionEngine;
59
+ evalEngine: EvalEngine;
60
+ consolidationEngine: ConsolidationEngine;
61
+ consolidationScheduler: ConsolidationScheduler;
62
+ }
63
+
64
+ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
65
+ const { store, activationEngine, connectionEngine, evictionEngine, retractionEngine, evalEngine, consolidationEngine, consolidationScheduler } = deps;
66
+
67
+ // ============================================================
68
+ // CORE — Agent-facing endpoints
69
+ // ============================================================
70
+
71
+ app.post('/memory/write', async (req, reply) => {
72
+ const body = req.body as {
73
+ agentId: string;
74
+ concept: string;
75
+ content: string;
76
+ tags?: string[];
77
+ eventType?: SalienceEventType;
78
+ surprise?: number;
79
+ decisionMade?: boolean;
80
+ causalDepth?: number;
81
+ resolutionEffort?: number;
82
+ confidence?: number;
83
+ // Memory class — canonical bypasses salience filter; structural is
84
+ // for system-written event-log records (see 0.8 spec). Restored in
85
+ // 0.7.17 after the field was dropped from the HTTP body schema during
86
+ // the 0.7.x refactor — core/salience.ts:88-89,124,187 and
87
+ // core/write-pipeline.ts:77 still expect and honor it, so HTTP
88
+ // callers were silently losing the canonical-bypass signal.
89
+ memory_class?: 'canonical' | 'working' | 'ephemeral';
90
+ // Agent-provided metadata (stored as searchable tags)
91
+ project?: string;
92
+ topic?: string;
93
+ source?: string;
94
+ confidenceLevel?: string;
95
+ sessionId?: string;
96
+ intent?: string;
97
+ };
98
+
99
+ if (!body.agentId || typeof body.agentId !== 'string' ||
100
+ !body.concept || typeof body.concept !== 'string' ||
101
+ !body.content || typeof body.content !== 'string') {
102
+ return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
103
+ }
104
+
105
+ // Assemble tags: user-provided + agent metadata
106
+ const userTags = body.tags ?? [];
107
+ const metaTags: string[] = [];
108
+ if (body.project) metaTags.push(`proj=${body.project}`);
109
+ if (body.topic) metaTags.push(`topic=${body.topic}`);
110
+ if (body.source) metaTags.push(`src=${body.source}`);
111
+ if (body.confidenceLevel) metaTags.push(`conf=${body.confidenceLevel}`);
112
+ if (body.sessionId) metaTags.push(`sid=${body.sessionId}`);
113
+ if (body.intent) metaTags.push(`intent=${body.intent}`);
114
+
115
+ const result = performWrite({ store, connectionEngine }, {
116
+ agentId: body.agentId,
117
+ concept: body.concept,
118
+ content: body.content,
119
+ tags: [...userTags, ...metaTags],
120
+ memoryClass: body.memory_class,
121
+ eventType: body.eventType,
122
+ surprise: body.surprise,
123
+ decisionMade: body.decisionMade,
124
+ causalDepth: body.causalDepth,
125
+ resolutionEffort: body.resolutionEffort,
126
+ confidence: body.confidence,
127
+ });
128
+
129
+ // Auto-checkpoint always (covers create, reinforce, and supersede).
130
+ try { store.updateAutoCheckpointWrite(body.agentId, result.engram.id); } catch { /* non-fatal */ }
131
+
132
+ if (result.action === 'reinforce') {
133
+ return reply.code(200).send({
134
+ stored: false,
135
+ action: 'reinforce',
136
+ disposition: 'reinforced',
137
+ engram: result.engram,
138
+ reinforce: result.reinforce,
139
+ novelty: result.noveltyResult.novelty,
140
+ });
141
+ }
142
+
143
+ // create / supersede paths follow legacy temporal-edge + episode logic.
144
+ try {
145
+ const prev = store.getLatestEngram(body.agentId, result.engram.id);
146
+ if (prev) {
147
+ store.upsertAssociation(prev.id, result.engram.id, 0.3, 'temporal', 0.8);
148
+ }
149
+ } catch { /* Temporal edge creation is non-fatal */ }
150
+
151
+ if (result.salience
152
+ && (result.salience.disposition === 'active' || result.salience.disposition === 'discard')) {
153
+ try {
154
+ let episode = store.getActiveEpisode(body.agentId, 3600_000);
155
+ if (!episode) {
156
+ episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
157
+ }
158
+ store.addEngramToEpisode(result.engram.id, episode.id);
159
+ } catch { /* Episode assignment is non-fatal */ }
160
+ }
161
+
162
+ const isLowSalience = result.salience?.disposition === 'discard';
163
+ return reply.code(201).send({
164
+ stored: true,
165
+ action: result.action,
166
+ disposition: isLowSalience ? 'low-salience' : (result.salience?.disposition ?? 'active'),
167
+ salience: result.salience?.score ?? 0,
168
+ reasonCodes: result.salience?.reasonCodes ?? [],
169
+ engram: result.engram,
170
+ supersedeOf: result.supersedeOf,
171
+ });
172
+ });
173
+
174
+ /**
175
+ * Bulk write — accepts many facts in one request.
176
+ * Creates engrams in a single transaction, embeds in batch.
177
+ * Returns all IDs for downstream supersession calls.
178
+ */
179
+ app.post('/memory/write-batch', async (req, reply) => {
180
+ const body = req.body as {
181
+ agentId: string;
182
+ sessionId?: string; // Shared session ID for all memories in this batch
183
+ memories: Array<{
184
+ concept: string;
185
+ content: string;
186
+ tags?: string[];
187
+ supersedes?: string;
188
+ sessionId?: string; // Per-memory session override
189
+ }>;
190
+ };
191
+
192
+ if (!body.agentId || !body.memories || body.memories.length === 0) {
193
+ return reply.code(400).send({ error: 'agentId and non-empty memories array required' });
194
+ }
195
+
196
+ const results: Array<{ id: string; concept: string; disposition: string }> = [];
197
+
198
+ for (const mem of body.memories) {
199
+ // Add session ID tag if provided (batch-level or per-memory)
200
+ const sid = mem.sessionId ?? body.sessionId;
201
+ const memTags = [...(mem.tags ?? [])];
202
+ if (sid) memTags.push(`sid=${sid}`);
203
+
204
+ const engram = store.createEngram({
205
+ agentId: body.agentId,
206
+ concept: mem.concept,
207
+ content: mem.content,
208
+ tags: memTags,
209
+ salience: 0.5,
210
+ confidence: 0.5,
211
+ supersedes: mem.supersedes ?? undefined,
212
+ });
213
+
214
+ // Handle supersession inline — archive superseded memory to remove from active pool
215
+ if (mem.supersedes) {
216
+ store.supersedeEngram(mem.supersedes, engram.id);
217
+ store.updateConfidence(mem.supersedes, 0.1);
218
+ store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
219
+ }
220
+
221
+ results.push({ id: engram.id, concept: mem.concept, disposition: 'active' });
222
+ }
223
+
224
+ // Batch embed synchronously — ensures embeddings are ready before queries hit
225
+ const texts = body.memories.map((m, i) => `${m.concept} ${m.content}`);
226
+ try {
227
+ const vecs = await embedBatch(texts);
228
+ for (let i = 0; i < vecs.length; i++) {
229
+ if (results[i]) {
230
+ store.updateEmbedding(results[i].id, vecs[i]);
231
+ }
232
+ }
233
+ } catch { /* Embedding failure is non-fatal */ }
234
+
235
+ try { store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? ''); } catch {}
236
+
237
+ return reply.code(201).send({
238
+ stored: results.length,
239
+ results,
240
+ });
241
+ });
242
+
243
+ app.post('/memory/activate', async (req, reply) => {
244
+ const body = req.body as {
245
+ agentId: string;
246
+ context: string;
247
+ limit?: number;
248
+ minScore?: number;
249
+ includeStaging?: boolean;
250
+ useReranker?: boolean;
251
+ useExpansion?: boolean;
252
+ abstentionThreshold?: number;
253
+ workspace?: string;
254
+ bm25Only?: boolean;
255
+ };
256
+
257
+ const results = await activationEngine.activate({
258
+ agentId: body.agentId,
259
+ context: body.context,
260
+ limit: body.limit,
261
+ minScore: body.minScore,
262
+ includeStaging: body.includeStaging,
263
+ useReranker: body.useReranker,
264
+ useExpansion: body.useExpansion,
265
+ abstentionThreshold: body.abstentionThreshold,
266
+ workspace: body.workspace,
267
+ bm25Only: body.bm25Only,
268
+ });
269
+
270
+ // Auto-checkpoint: track recall for consolidation scheduling
271
+ try {
272
+ const ids = results.map(r => r.engram.id);
273
+ store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
274
+ } catch { /* non-fatal */ }
275
+
276
+ return reply.send({ results });
277
+ });
278
+
279
+ app.post('/memory/feedback', async (req, reply) => {
280
+ const body = req.body as {
281
+ activationEventId?: string;
282
+ engramId: string;
283
+ useful: boolean;
284
+ context?: string;
285
+ };
286
+
287
+ store.logRetrievalFeedback(
288
+ body.activationEventId ?? null,
289
+ body.engramId,
290
+ body.useful,
291
+ body.context ?? ''
292
+ );
293
+
294
+ // Update engram confidence based on feedback
295
+ const engram = store.getEngram(body.engramId);
296
+ if (engram) {
297
+ const config = DEFAULT_AGENT_CONFIG;
298
+ const delta = body.useful
299
+ ? config.feedbackPositiveBoost
300
+ : -config.feedbackNegativePenalty;
301
+ store.updateConfidence(engram.id, engram.confidence + delta);
302
+ }
303
+
304
+ // Touch activity for consolidation scheduling
305
+ if (engram) {
306
+ try { store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
307
+ }
308
+
309
+ return reply.send({ recorded: true });
310
+ });
311
+
312
+ app.post('/memory/retract', async (req, reply) => {
313
+ const body = req.body as {
314
+ agentId: string;
315
+ targetEngramId: string;
316
+ reason: string;
317
+ counterContent?: string;
318
+ };
319
+
320
+ const result = retractionEngine.retract({
321
+ agentId: body.agentId,
322
+ targetEngramId: body.targetEngramId,
323
+ reason: body.reason,
324
+ counterContent: body.counterContent,
325
+ });
326
+
327
+ // Touch activity for consolidation scheduling
328
+ try { store.touchActivity(body.agentId); } catch { /* non-fatal */ }
329
+
330
+ return reply.send(result);
331
+ });
332
+
333
+ app.post('/memory/supersede', async (req, reply) => {
334
+ const body = req.body as {
335
+ oldEngramId: string;
336
+ newEngramId: string;
337
+ reason?: string;
338
+ };
339
+
340
+ const oldEngram = store.getEngram(body.oldEngramId);
341
+ const newEngram = store.getEngram(body.newEngramId);
342
+ if (!oldEngram) return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
343
+ if (!newEngram) return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
344
+
345
+ // Create causal association (new → old)
346
+ store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
347
+
348
+ // Reduce old engram confidence to 20% (keep for historical reference)
349
+ store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
350
+
351
+ // Mark supersession via store method
352
+ store.supersedeEngram(body.oldEngramId, body.newEngramId);
353
+
354
+ try { store.touchActivity(oldEngram.agentId); } catch { /* non-fatal */ }
355
+
356
+ return reply.send({
357
+ superseded: body.oldEngramId,
358
+ supersededBy: body.newEngramId,
359
+ reason: body.reason ?? 'outdated',
360
+ });
361
+ });
362
+
363
+ // ============================================================
364
+ // DIAGNOSTIC — Debugging and inspection
365
+ // ============================================================
366
+
367
+ app.post('/memory/search', async (req, reply) => {
368
+ const body = req.body as {
369
+ agentId: string;
370
+ text?: string;
371
+ concept?: string;
372
+ tags?: string[];
373
+ stage?: string;
374
+ retracted?: boolean;
375
+ limit?: number;
376
+ offset?: number;
377
+ };
378
+
379
+ const results = store.search({
380
+ agentId: body.agentId,
381
+ text: body.text,
382
+ concept: body.concept,
383
+ tags: body.tags,
384
+ stage: body.stage as any,
385
+ retracted: body.retracted,
386
+ limit: body.limit,
387
+ offset: body.offset,
388
+ });
389
+
390
+ return reply.send({ results, count: results.length });
391
+ });
392
+
393
+ app.get('/memory/:id', async (req, reply) => {
394
+ const { id } = req.params as { id: string };
395
+ const engram = store.getEngram(id);
396
+ if (!engram) return reply.code(404).send({ error: 'Not found' });
397
+
398
+ const associations = store.getAssociationsFor(id);
399
+ return reply.send({ engram, associations });
400
+ });
401
+
402
+ app.get('/agent/:id/stats', async (req, reply) => {
403
+ const { id } = req.params as { id: string };
404
+ const active = store.getEngramsByAgent(id, 'active');
405
+ const staging = store.getEngramsByAgent(id, 'staging');
406
+ const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
407
+ const associations = store.getAllAssociations(id);
408
+
409
+ return reply.send({
410
+ agentId: id,
411
+ engrams: {
412
+ active: active.length,
413
+ staging: staging.length,
414
+ retracted: retracted.length,
415
+ total: active.length + staging.length + retracted.length,
416
+ },
417
+ associations: associations.length,
418
+ avgConfidence: active.length > 0
419
+ ? +(active.reduce((s, e) => s + e.confidence, 0) / active.length).toFixed(3)
420
+ : 0,
421
+ });
422
+ });
423
+
424
+ app.get('/agent/:id/metrics', async (req, reply) => {
425
+ const { id } = req.params as { id: string };
426
+ const windowHours = parseInt((req.query as any).window ?? '24', 10);
427
+ const metrics = evalEngine.computeMetrics(id, windowHours);
428
+ return reply.send({ metrics });
429
+ });
430
+
431
+ app.post('/agent/register', async (req, reply) => {
432
+ const body = req.body as { name: string };
433
+ const id = crypto.randomUUID();
434
+ return reply.code(201).send({
435
+ id,
436
+ name: body.name,
437
+ config: DEFAULT_AGENT_CONFIG,
438
+ });
439
+ });
440
+
441
+ // ============================================================
442
+ // SYSTEM Maintenance operations
443
+ // ============================================================
444
+
445
+ app.post('/system/evict', async (req, reply) => {
446
+ const body = req.body as { agentId: string };
447
+ const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
448
+ return reply.send(result);
449
+ });
450
+
451
+ app.post('/system/decay', async (req, reply) => {
452
+ const body = req.body as { agentId: string; halfLifeDays?: number };
453
+ const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
454
+ return reply.send({ edgesDecayed: decayed });
455
+ });
456
+
457
+ app.post('/system/consolidate', async (req, reply) => {
458
+ const body = req.body as { agentId: string };
459
+ const result = await consolidationEngine.consolidate(body.agentId);
460
+ return reply.send(result);
461
+ });
462
+
463
+ // ============================================================
464
+ // CHECKPOINTING — Conscious state preservation
465
+ // ============================================================
466
+
467
+ app.post('/memory/checkpoint', async (req, reply) => {
468
+ const body = req.body as {
469
+ agentId: string;
470
+ currentTask: string;
471
+ decisions?: string[];
472
+ activeFiles?: string[];
473
+ nextSteps?: string[];
474
+ relatedMemoryIds?: string[];
475
+ notes?: string;
476
+ episodeId?: string | null;
477
+ };
478
+
479
+ const state: ConsciousState = {
480
+ currentTask: body.currentTask,
481
+ decisions: body.decisions ?? [],
482
+ activeFiles: body.activeFiles ?? [],
483
+ nextSteps: body.nextSteps ?? [],
484
+ relatedMemoryIds: body.relatedMemoryIds ?? [],
485
+ notes: body.notes ?? '',
486
+ episodeId: body.episodeId ?? null,
487
+ };
488
+
489
+ store.saveCheckpoint(body.agentId, state);
490
+ return reply.send({ saved: true, agentId: body.agentId });
491
+ });
492
+
493
+ app.get('/memory/restore/:agentId', async (req, reply) => {
494
+ const { agentId } = req.params as { agentId: string };
495
+ const checkpoint = store.getCheckpoint(agentId);
496
+
497
+ const now = Date.now();
498
+ const idleMs = checkpoint
499
+ ? now - checkpoint.auto.lastActivityAt.getTime()
500
+ : 0;
501
+
502
+ // Get last written engram for context
503
+ let lastWrite: { id: string; concept: string; content: string } | null = null;
504
+ if (checkpoint?.auto.lastWriteId) {
505
+ const engram = store.getEngram(checkpoint.auto.lastWriteId);
506
+ if (engram) {
507
+ lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
508
+ }
509
+ }
510
+
511
+ // Recall memories using last context (if available)
512
+ let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
513
+ const recallContext = checkpoint?.auto.lastRecallContext
514
+ ?? checkpoint?.executionState?.currentTask
515
+ ?? null;
516
+
517
+ if (recallContext) {
518
+ try {
519
+ const results = await activationEngine.activate({
520
+ agentId,
521
+ context: recallContext,
522
+ limit: 5,
523
+ minScore: 0.05,
524
+ useReranker: true,
525
+ useExpansion: true,
526
+ });
527
+ recalledMemories = results.map(r => ({
528
+ id: r.engram.id,
529
+ concept: r.engram.concept,
530
+ content: r.engram.content,
531
+ score: r.score,
532
+ }));
533
+ } catch { /* recall failure is non-fatal */ }
534
+ }
535
+
536
+ // Trigger mini-consolidation if idle >5min (async, fire-and-forget)
537
+ const MINI_CONSOLIDATION_IDLE_MS = 5 * 60_000;
538
+ let miniConsolidationTriggered = false;
539
+ if (idleMs > MINI_CONSOLIDATION_IDLE_MS) {
540
+ miniConsolidationTriggered = true;
541
+ consolidationScheduler.runMiniConsolidation(agentId).catch(() => {});
542
+ }
543
+
544
+ return reply.send({
545
+ executionState: checkpoint?.executionState ?? null,
546
+ checkpointAt: checkpoint?.checkpointAt ?? null,
547
+ recalledMemories,
548
+ lastWrite,
549
+ idleMs,
550
+ miniConsolidationTriggered,
551
+ });
552
+ });
553
+
554
+ // ============================================================
555
+ // TASK MANAGEMENT
556
+ // ============================================================
557
+
558
+ app.post('/task/create', async (req, reply) => {
559
+ const body = req.body as {
560
+ agentId: string;
561
+ concept: string;
562
+ content: string;
563
+ tags?: string[];
564
+ priority?: TaskPriority;
565
+ blockedBy?: string;
566
+ };
567
+
568
+ const engram = store.createEngram({
569
+ agentId: body.agentId,
570
+ concept: body.concept,
571
+ content: body.content,
572
+ tags: [...(body.tags ?? []), 'task'],
573
+ salience: 0.9,
574
+ confidence: 0.8,
575
+ salienceFeatures: {
576
+ surprise: 0.5, decisionMade: true, causalDepth: 0.5,
577
+ resolutionEffort: 0.5, eventType: 'decision',
578
+ },
579
+ reasonCodes: ['task-created'],
580
+ taskStatus: body.blockedBy ? 'blocked' : 'open',
581
+ taskPriority: body.priority ?? 'medium',
582
+ blockedBy: body.blockedBy,
583
+ });
584
+
585
+ connectionEngine.enqueue(engram.id);
586
+ embed(`${body.concept} ${body.content}`).then(vec => {
587
+ store.updateEmbedding(engram.id, vec);
588
+ }).catch(() => {});
589
+
590
+ return reply.send(engram);
591
+ });
592
+
593
+ app.post('/task/update', async (req, reply) => {
594
+ const body = req.body as {
595
+ taskId: string;
596
+ status?: TaskStatus;
597
+ priority?: TaskPriority;
598
+ blockedBy?: string | null;
599
+ };
600
+
601
+ const engram = store.getEngram(body.taskId);
602
+ if (!engram || !engram.taskStatus) {
603
+ return reply.code(404).send({ error: 'Task not found' });
604
+ }
605
+
606
+ if (body.blockedBy !== undefined) {
607
+ store.updateBlockedBy(body.taskId, body.blockedBy);
608
+ }
609
+ if (body.status) {
610
+ store.updateTaskStatus(body.taskId, body.status);
611
+ }
612
+ if (body.priority) {
613
+ store.updateTaskPriority(body.taskId, body.priority);
614
+ }
615
+
616
+ return reply.send(store.getEngram(body.taskId));
617
+ });
618
+
619
+ app.get('/task/list/:agentId', async (req, reply) => {
620
+ const { agentId } = req.params as { agentId: string };
621
+ const { status, includeDone } = req.query as { status?: TaskStatus; includeDone?: string };
622
+
623
+ let tasks = store.getTasks(agentId, status);
624
+ if (includeDone !== 'true' && !status) {
625
+ tasks = tasks.filter(t => t.taskStatus !== 'done');
626
+ }
627
+
628
+ return reply.send({ tasks, count: tasks.length });
629
+ });
630
+
631
+ app.get('/task/next/:agentId', async (req, reply) => {
632
+ const { agentId } = req.params as { agentId: string };
633
+ const next = store.getNextTask(agentId);
634
+ return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
635
+ });
636
+
637
+ // Time warp shift all timestamps backward by N days (for testing)
638
+ app.post('/system/time-warp', async (req, reply) => {
639
+ const body = req.body as { agentId: string; days: number };
640
+ const ms = body.days * 24 * 60 * 60 * 1000;
641
+ const shifted = store.timeWarp(body.agentId, ms);
642
+ return reply.send({ shifted, days: body.days });
643
+ });
644
+
645
+ // ─── Export ─────────────────────────────────────────────────────────────
646
+
647
+ app.get('/memory/export', async (req, reply) => {
648
+ const { agentId, all } = req.query as { agentId?: string; all?: string };
649
+ const includeAll = all === 'true';
650
+ const db = store.getDb();
651
+
652
+ let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
653
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
654
+ retracted, retracted_by, retracted_at, tags
655
+ FROM engrams`;
656
+ const conditions: string[] = [];
657
+ const params: string[] = [];
658
+
659
+ if (agentId) {
660
+ conditions.push('agent_id = ?');
661
+ params.push(agentId);
662
+ }
663
+ if (!includeAll) {
664
+ conditions.push('retracted = 0');
665
+ conditions.push("stage = 'active'");
666
+ }
667
+ if (conditions.length > 0) {
668
+ engramSql += ' WHERE ' + conditions.join(' AND ');
669
+ }
670
+ engramSql += ' ORDER BY created_at ASC';
671
+
672
+ const engrams = db.prepare(engramSql).all(...params) as { id: string }[];
673
+
674
+ const engramIds = new Set(engrams.map(e => e.id));
675
+ const allAssocs = db.prepare(
676
+ `SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
677
+ FROM associations`
678
+ ).all() as { from_engram_id: string; to_engram_id: string }[];
679
+ const associations = allAssocs.filter(a => engramIds.has(a.from_engram_id) && engramIds.has(a.to_engram_id));
680
+
681
+ return reply.send({
682
+ exported_at: new Date().toISOString(),
683
+ agent_id: agentId ?? null,
684
+ include_all: includeAll,
685
+ engrams_count: engrams.length,
686
+ associations_count: associations.length,
687
+ engrams,
688
+ associations,
689
+ });
690
+ });
691
+
692
+ // ─── Health ─────────────────────────────────────────────────────────────
693
+
694
+ app.get('/health', async () => {
695
+ const coordEnabled = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
696
+ const base: Record<string, unknown> = {
697
+ status: 'ok',
698
+ timestamp: new Date().toISOString(),
699
+ version: '0.7.17',
700
+ coordination: coordEnabled,
701
+ };
702
+ if (coordEnabled) {
703
+ try {
704
+ const db = deps.store.getDb();
705
+ const stats = db.prepare(`SELECT
706
+ (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
707
+ (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
708
+ (SELECT COUNT(*) FROM coord_locks) AS active_locks`).get() as { agents_alive: number; pending_tasks: number; active_locks: number };
709
+ Object.assign(base, stats);
710
+ } catch { /* tables may not exist yet */ }
711
+ }
712
+ return base;
713
+ });
714
+ }