agent-working-memory 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +89 -19
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +5 -1
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/api/routes.d.ts.map +1 -1
  6. package/dist/api/routes.js +2 -1
  7. package/dist/api/routes.js.map +1 -1
  8. package/dist/cli/migrate.js +29 -29
  9. package/dist/cli.js +405 -224
  10. package/dist/cli.js.map +1 -1
  11. package/dist/coordination/circuit-breaker.js +23 -23
  12. package/dist/core/salience.d.ts.map +1 -1
  13. package/dist/core/salience.js +10 -1
  14. package/dist/core/salience.js.map +1 -1
  15. package/dist/core/write-pipeline.d.ts.map +1 -1
  16. package/dist/core/write-pipeline.js +5 -1
  17. package/dist/core/write-pipeline.js.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp.js +50 -3
  21. package/dist/mcp.js.map +1 -1
  22. package/dist/onboard/index.d.ts +68 -0
  23. package/dist/onboard/index.d.ts.map +1 -0
  24. package/dist/onboard/index.js +265 -0
  25. package/dist/onboard/index.js.map +1 -0
  26. package/dist/storage/factory.d.ts +1 -1
  27. package/dist/storage/factory.d.ts.map +1 -1
  28. package/dist/storage/factory.js +16 -2
  29. package/dist/storage/factory.js.map +1 -1
  30. package/dist/storage/pglite-schema.js +143 -143
  31. package/dist/storage/pglite.d.ts.map +1 -1
  32. package/dist/storage/pglite.js +8 -0
  33. package/dist/storage/pglite.js.map +1 -1
  34. package/dist/storage/postgres.d.ts +228 -0
  35. package/dist/storage/postgres.d.ts.map +1 -0
  36. package/dist/storage/postgres.js +1221 -0
  37. package/dist/storage/postgres.js.map +1 -0
  38. package/dist/version.d.ts +2 -0
  39. package/dist/version.d.ts.map +1 -0
  40. package/dist/version.js +27 -0
  41. package/dist/version.js.map +1 -0
  42. package/package.json +11 -1
  43. package/src/adapters/common.ts +5 -1
  44. package/src/api/index.ts +3 -3
  45. package/src/api/routes.ts +2 -1
  46. package/src/cli/migrate.ts +307 -307
  47. package/src/cli.ts +342 -273
  48. package/src/coordination/circuit-breaker.ts +83 -83
  49. package/src/coordination/failure-modes.ts +50 -50
  50. package/src/core/decay.ts +63 -63
  51. package/src/core/embeddings.ts +110 -110
  52. package/src/core/index.ts +5 -5
  53. package/src/core/logger.ts +36 -36
  54. package/src/core/ml-worker-entry.ts +194 -194
  55. package/src/core/ml-worker.ts +281 -281
  56. package/src/core/query-expander.ts +122 -122
  57. package/src/core/reranker.ts +119 -119
  58. package/src/core/salience.ts +10 -1
  59. package/src/core/write-pipeline.ts +5 -1
  60. package/src/engine/confidence.ts +120 -120
  61. package/src/engine/consolidation-scheduler.ts +242 -242
  62. package/src/engine/eval.ts +102 -102
  63. package/src/engine/eviction.ts +101 -101
  64. package/src/engine/index.ts +8 -8
  65. package/src/engine/retraction.ts +366 -366
  66. package/src/engine/staging.ts +74 -74
  67. package/src/index.ts +2 -1
  68. package/src/mcp.ts +62 -3
  69. package/src/onboard/index.ts +298 -0
  70. package/src/storage/factory.ts +15 -3
  71. package/src/storage/index.ts +3 -3
  72. package/src/storage/pglite-schema.ts +166 -166
  73. package/src/storage/pglite.ts +9 -0
  74. package/src/storage/postgres.ts +1475 -0
  75. package/src/storage/store.ts +80 -80
  76. package/src/types/agent.ts +67 -67
  77. package/src/types/checkpoint.ts +46 -46
  78. package/src/types/eval.ts +100 -100
  79. package/src/types/index.ts +6 -6
  80. package/src/version.ts +26 -0
package/src/cli.ts CHANGED
@@ -13,10 +13,12 @@
13
13
  */
14
14
 
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
- import { resolve, join, dirname } from 'node:path';
16
+ import { resolve, join, dirname, basename } from 'node:path';
17
17
  import { execSync } from 'node:child_process';
18
18
  import { randomUUID } from 'node:crypto';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { VERSION } from './version.js';
21
+ import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
20
22
 
21
23
  const __filename = fileURLToPath(import.meta.url);
22
24
  const __dirname = dirname(__filename);
@@ -139,6 +141,9 @@ async function setup() {
139
141
  const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
140
142
  const hooksAction = adapter.writeHooks(ctx, skipHooks);
141
143
 
144
+ // Seed the onboarding skill so a cold store can teach the agent how to warm itself.
145
+ const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
146
+
142
147
  console.log(`
143
148
  AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
144
149
 
@@ -147,6 +152,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
147
152
  ${configAction}
148
153
  ${instructionsAction}
149
154
  ${hooksAction}
155
+ ${skillAction}
150
156
 
151
157
  Next steps:
152
158
  1. Restart ${adapter.name} to pick up the MCP server
@@ -245,6 +251,29 @@ function health() {
245
251
  }
246
252
  }
247
253
 
254
+ // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
255
+ //
256
+ // export/import route through openStore() so they work on ANY backend (SQLite,
257
+ // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
258
+ // (a SQLite file or PGlite dir, by shape); for a Postgres target set
259
+ // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
260
+ // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
261
+
262
+ function toISOStr(d: Date | string | null | undefined): string | null {
263
+ if (d == null) return null;
264
+ return d instanceof Date ? d.toISOString() : String(d);
265
+ }
266
+
267
+ async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
268
+ // --db sets the path only when the env doesn't already select a backend/path.
269
+ if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
270
+ process.env.AWM_DB_PATH = dbPath;
271
+ }
272
+ const { openStore } = await import('./storage/factory.js');
273
+ const { store, backend } = await openStore();
274
+ return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
275
+ }
276
+
248
277
  // ─── EXPORT ──────────────────────────────────────
249
278
 
250
279
  async function exportMemories() {
@@ -252,113 +281,107 @@ async function exportMemories() {
252
281
  let agentFilter: string | null = null;
253
282
  let outputPath: string | null = null;
254
283
  let activeOnly = false;
284
+ let allStages = false;
285
+ let includeRetracted = false;
255
286
 
256
287
  for (let i = 1; i < args.length; i++) {
257
288
  if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
258
289
  else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
259
290
  else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
260
291
  else if (args[i] === '--active-only') activeOnly = true;
292
+ else if (args[i] === '--all-stages') allStages = true;
293
+ else if (args[i] === '--include-retracted') includeRetracted = true;
261
294
  }
262
295
 
263
- if (!dbPath) {
264
- console.error('Error: --db <path> is required');
265
- process.exit(1);
266
- }
267
-
268
- if (!existsSync(dbPath)) {
269
- console.error(`Error: database not found: ${dbPath}`);
270
- process.exit(1);
296
+ // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
297
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
298
+ if (!usingPostgres) {
299
+ if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
300
+ if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
271
301
  }
272
302
 
273
- // Dynamic import to avoid loading better-sqlite3 for other commands
274
- const Database = (await import('better-sqlite3')).default;
275
- const db = new Database(dbPath, { readonly: true });
276
-
277
- // Build memory query
278
- let memQuery = 'SELECT * FROM engrams';
279
- const conditions: string[] = [];
280
- const params: any[] = [];
281
-
282
- if (agentFilter) {
283
- conditions.push('agent_id = ?');
284
- params.push(agentFilter);
285
- }
286
- if (activeOnly) {
287
- conditions.push('retracted = 0');
288
- }
289
-
290
- if (conditions.length > 0) {
291
- memQuery += ' WHERE ' + conditions.join(' AND ');
292
- }
293
- memQuery += ' ORDER BY created_at ASC';
294
-
295
- const rows = db.prepare(memQuery).all(...params) as any[];
296
-
297
- // Build memory objects (exclude embedding blobs)
298
- const memories = rows.map((r: any) => ({
299
- id: r.id,
300
- agent_id: r.agent_id,
301
- concept: r.concept,
302
- content: r.content,
303
- confidence: r.confidence,
304
- salience: r.salience,
305
- access_count: r.access_count,
306
- last_accessed: r.last_accessed,
307
- created_at: r.created_at,
308
- stage: r.stage,
309
- tags: r.tags ? JSON.parse(r.tags) : [],
310
- memory_class: r.memory_class ?? 'working',
311
- episode_id: r.episode_id ?? null,
312
- task_status: r.task_status ?? null,
313
- task_priority: r.task_priority ?? null,
314
- supersedes: r.supersedes ?? null,
315
- superseded_by: r.superseded_by ?? null,
316
- retracted: r.retracted ?? 0,
317
- }));
318
-
319
- // Get memory IDs for association filtering
320
- const memIds = new Set(memories.map((m: any) => m.id));
321
-
322
- // Build associations
323
- let assocQuery = 'SELECT * FROM associations';
324
- const allAssocs = db.prepare(assocQuery).all() as any[];
325
- const associations = allAssocs
326
- .filter((a: any) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
327
- .map((a: any) => ({
328
- from_id: a.from_engram_id,
329
- to_id: a.to_engram_id,
330
- weight: a.weight,
331
- type: a.type ?? 'hebbian',
332
- activation_count: a.activation_count ?? 0,
303
+ const { store, backend, close } = await openCliStore(dbPath);
304
+ try {
305
+ const agentIds: string[] = agentFilter
306
+ ? [agentFilter]
307
+ : ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
308
+ if (agentIds.length === 0) {
309
+ console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
310
+ }
311
+ // Default to the meaningful memory set (active stage, non-retracted). --all-stages
312
+ // widens to every stage; --include-retracted adds retracted (off with --active-only).
313
+ const stage = allStages ? undefined : 'active';
314
+ const wantRetracted = includeRetracted && !activeOnly;
315
+ const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
316
+
317
+ const memories = engrams.map((e: any) => ({
318
+ id: e.id,
319
+ agent_id: e.agentId,
320
+ concept: e.concept,
321
+ content: e.content,
322
+ // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
323
+ // re-embed after import) a faithful, recall-ready port when source/target embed
324
+ // models match. import skips them with --no-embeddings (then re-embed).
325
+ embedding: Array.isArray(e.embedding) ? e.embedding : null,
326
+ confidence: e.confidence,
327
+ salience: e.salience,
328
+ access_count: e.accessCount ?? 0,
329
+ last_accessed: toISOStr(e.lastAccessed),
330
+ created_at: toISOStr(e.createdAt),
331
+ stage: e.stage ?? 'active',
332
+ tags: Array.isArray(e.tags) ? e.tags : [],
333
+ memory_class: e.memoryClass ?? 'working',
334
+ memory_type: e.memoryType ?? 'unclassified',
335
+ episode_id: e.episodeId ?? null,
336
+ task_status: e.taskStatus ?? null,
337
+ task_priority: e.taskPriority ?? null,
338
+ supersedes: e.supersedes ?? null,
339
+ superseded_by: e.supersededBy ?? null,
340
+ retracted: e.retracted ? 1 : 0,
333
341
  }));
334
342
 
335
- // Collect unique agents
336
- const agents = [...new Set(memories.map((m: any) => m.agent_id))];
337
-
338
- const exportData = {
339
- version: '0.8.8',
340
- exported_at: new Date().toISOString(),
341
- source_db: dbPath,
342
- agent_filter: agentFilter,
343
- memories,
344
- associations,
345
- stats: {
346
- total_memories: memories.length,
347
- total_associations: associations.length,
348
- agents,
349
- },
350
- };
351
-
352
- const json = JSON.stringify(exportData, null, 2);
343
+ const memIds = new Set(memories.map((m) => m.id));
344
+ const seen = new Set<string>();
345
+ const associations: any[] = [];
346
+ for (const aid of agentIds) {
347
+ for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
348
+ if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
349
+ const k = `${a.fromEngramId}>${a.toEngramId}`;
350
+ if (seen.has(k)) continue;
351
+ seen.add(k);
352
+ associations.push({
353
+ from_id: a.fromEngramId, to_id: a.toEngramId,
354
+ weight: a.weight, type: a.type ?? 'hebbian',
355
+ activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
356
+ });
357
+ }
358
+ }
353
359
 
354
- if (outputPath) {
355
- writeFileSync(outputPath, json + '\n');
356
- console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath}`);
357
- } else {
358
- process.stdout.write(json + '\n');
360
+ const exportData = {
361
+ version: VERSION,
362
+ exported_at: new Date().toISOString(),
363
+ source_backend: backend,
364
+ agent_filter: agentFilter,
365
+ embedding_model: process.env.AWM_EMBED_MODEL ?? null,
366
+ memories,
367
+ associations,
368
+ stats: {
369
+ total_memories: memories.length,
370
+ total_associations: associations.length,
371
+ agents: [...new Set(memories.map((m) => m.agent_id))],
372
+ },
373
+ };
374
+
375
+ const json = JSON.stringify(exportData, null, 2);
376
+ if (outputPath) {
377
+ writeFileSync(outputPath, json + '\n');
378
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
379
+ } else {
380
+ process.stdout.write(json + '\n');
381
+ }
382
+ } finally {
383
+ await close();
359
384
  }
360
-
361
- db.close();
362
385
  }
363
386
 
364
387
  // ─── IMPORT ──────────────────────────────────────
@@ -381,12 +404,17 @@ async function importMemories() {
381
404
  else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
382
405
  }
383
406
 
407
+ // --no-embeddings: skip importing embedding vectors (use when source/target embed models
408
+ // differ → import without, then re-embed). Parsed alongside the existing flags above.
409
+ const noEmbeddings = args.includes('--no-embeddings');
410
+
384
411
  if (!filePath) {
385
412
  console.error('Error: <file> is required');
386
413
  process.exit(1);
387
414
  }
388
- if (!dbPath) {
389
- console.error('Error: --db <path> is required');
415
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
416
+ if (!dbPath && !usingPostgres) {
417
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
390
418
  process.exit(1);
391
419
  }
392
420
  if (!existsSync(filePath)) {
@@ -400,134 +428,100 @@ async function importMemories() {
400
428
  process.exit(1);
401
429
  }
402
430
 
403
- const Database = (await import('better-sqlite3')).default;
404
- const db = new Database(dbPath);
405
-
406
- // Ensure tables exist in target
407
- db.exec(`
408
- CREATE TABLE IF NOT EXISTS engrams (
409
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
410
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
411
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
412
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
413
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
414
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
415
- episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
416
- memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
417
- );
418
- CREATE TABLE IF NOT EXISTS associations (
419
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
420
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
421
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
422
- created_at TEXT NOT NULL, last_activated TEXT
423
- );
424
- `);
425
-
426
- // Build dedup set if needed
427
- const existingHashes = new Set<string>();
428
- if (dedupe) {
429
- const existing = db.prepare('SELECT concept, content FROM engrams').all() as any[];
430
- for (const row of existing) {
431
- const hash = (row.concept ?? '').toLowerCase().trim() + '||' + (row.content ?? '').toLowerCase().trim();
432
- existingHashes.add(hash);
431
+ const { store, backend, close } = await openCliStore(dbPath);
432
+ try {
433
+ // Dedupe against existing memories for the TARGET agent(s).
434
+ const existingHashes = new Set<string>();
435
+ if (dedupe) {
436
+ const targetAgents = remapAgent
437
+ ? [remapAgent]
438
+ : [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
439
+ for (const aid of targetAgents) {
440
+ for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
441
+ existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
442
+ }
443
+ }
433
444
  }
434
- }
435
- const idMap = new Map<string, string>();
436
- let imported = 0;
437
- let skippedDupes = 0;
438
- let skippedRetracted = 0;
439
445
 
440
- const insertMem = db.prepare(`
441
- INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
442
- access_count, last_accessed, created_at, stage, tags, memory_class,
443
- episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
444
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
445
- `);
446
-
447
- // NOTE: associations.last_activated is NOT NULL on the engrams DB (storage/sqlite.ts),
448
- // so importing into an existing store fails if it's omitted — and because import wraps
449
- // memories+associations in ONE transaction, that rolls back the memories too (silent
450
- // "empty store"). Set it alongside created_at. (migrate/merge paths already do this.)
451
- const insertAssoc = db.prepare(`
452
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at, last_activated)
453
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
454
- `);
446
+ const idMap = new Map<string, string>(); // old export id → new store id
447
+ let imported = 0, skippedDupes = 0, skippedRetracted = 0;
455
448
 
456
- const importTx = db.transaction(() => {
457
- // Import memories
449
+ // Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
450
+ // createdAt/accessCount normalize to import time (the contract's createEngram stamps
451
+ // them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
452
+ // embedding) is preserved, so recall is faithful.
458
453
  for (const mem of importData.memories) {
459
- // Skip retracted unless --include-retracted
460
- if (mem.retracted && !includeRetracted) {
461
- skippedRetracted++;
462
- continue;
463
- }
464
-
465
- // Dedupe check
454
+ if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
466
455
  if (dedupe) {
467
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
468
- if (existingHashes.has(hash)) {
469
- skippedDupes++;
470
- continue;
471
- }
456
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
457
+ if (existingHashes.has(h)) { skippedDupes++; continue; }
458
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
472
459
  }
473
-
474
- const newId = randomUUID();
475
- idMap.set(mem.id, newId);
476
-
477
- const agentId = remapAgent ?? mem.agent_id;
478
- const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
479
-
480
- if (!dryRun) {
481
- insertMem.run(
482
- newId, agentId, mem.concept, mem.content,
483
- mem.confidence ?? 0.5, mem.salience ?? 0.5,
484
- mem.access_count ?? 0, mem.last_accessed ?? mem.created_at,
485
- mem.created_at, mem.stage ?? 'active', tags,
486
- mem.memory_class ?? 'working', mem.episode_id ?? null,
487
- mem.task_status ?? null, mem.task_priority ?? null,
488
- mem.supersedes ?? null, mem.superseded_by ?? null,
489
- mem.retracted ?? 0
490
- );
460
+ // dry-run: still map the id so the association-count preview isn't always 0
461
+ if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
462
+ const created = await store.createEngram({
463
+ agentId: remapAgent ?? mem.agent_id,
464
+ concept: mem.concept,
465
+ content: mem.content,
466
+ tags: Array.isArray(mem.tags) ? mem.tags : [],
467
+ embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
468
+ confidence: mem.confidence ?? 0.5,
469
+ salience: mem.salience ?? 0.5,
470
+ memoryClass: mem.memory_class ?? 'working',
471
+ memoryType: mem.memory_type ?? undefined,
472
+ episodeId: mem.episode_id ?? undefined,
473
+ taskStatus: mem.task_status ?? undefined,
474
+ taskPriority: mem.task_priority ?? undefined,
475
+ });
476
+ idMap.set(mem.id, created.id);
477
+ // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
478
+ // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
479
+ // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
480
+ if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
481
+ try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
482
+ }
483
+ if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
484
+ try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
491
485
  }
492
486
  imported++;
493
487
  }
494
488
 
495
- // Import associations (using remapped IDs)
496
- let assocImported = 0;
497
- const associations = importData.associations ?? [];
498
- for (const assoc of associations) {
499
- const fromId = idMap.get(assoc.from_id);
500
- const toId = idMap.get(assoc.to_id);
501
- if (!fromId || !toId) continue; // skip if either memory was skipped
502
-
503
- if (!dryRun) {
504
- insertAssoc.run(
505
- randomUUID(), fromId, toId,
506
- assoc.weight ?? 0.5, assoc.type ?? 'hebbian',
507
- assoc.activation_count ?? 0
508
- );
489
+ // Pass 2 re-link supersession with remapped ids (supersedeEngram sets both sides:
490
+ // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
491
+ if (!dryRun) {
492
+ for (const mem of importData.memories) {
493
+ const newId = idMap.get(mem.id);
494
+ if (!newId || !mem.supersedes) continue;
495
+ const supersededNew = idMap.get(mem.supersedes);
496
+ if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
509
497
  }
510
- assocImported++;
511
498
  }
512
499
 
513
- return assocImported;
514
- });
515
-
516
- const assocCount = importTx();
517
-
518
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
519
- console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
520
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
521
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
522
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
500
+ // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
501
+ let assocImported = 0;
502
+ for (const a of (importData.associations ?? [])) {
503
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
504
+ if (!fromId || !toId) continue;
505
+ if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
506
+ assocImported++;
507
+ }
523
508
 
524
- db.close();
509
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
510
+ console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
511
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
512
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
513
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
514
+ ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
515
+ } finally {
516
+ await close();
517
+ }
525
518
  }
526
519
 
527
520
  // ─── MERGE ──────────────────────────────────────
528
521
 
529
522
  async function mergeMemories() {
530
523
  const Database = (await import('better-sqlite3')).default;
524
+ const { EngramStore } = await import('./storage/sqlite.js');
531
525
  const { createHash, randomUUID } = await import('node:crypto');
532
526
 
533
527
  let target = '';
@@ -574,29 +568,19 @@ async function mergeMemories() {
574
568
 
575
569
  console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
576
570
 
577
- const targetDb = new Database(target);
578
- targetDb.pragma('journal_mode = WAL');
579
- targetDb.pragma('foreign_keys = ON');
580
-
581
- // Ensure tables exist in target
582
- targetDb.exec(`
583
- CREATE TABLE IF NOT EXISTS engrams (
584
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
585
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
586
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
587
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
588
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
589
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
590
- );
591
- CREATE TABLE IF NOT EXISTS associations (
592
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
593
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
594
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
595
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
596
- );
597
- `);
571
+ // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
572
+ // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
573
+ // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
574
+ const store: any = new EngramStore(target);
575
+ const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
576
+
577
+ const blobToArr = (b: unknown): number[] | undefined => {
578
+ const buf = b as Buffer | null | undefined;
579
+ return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
580
+ };
581
+ const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
598
582
 
599
- // Build dedupe hash set from existing target memories
583
+ // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
600
584
  const existingHashes = new Set<string>();
601
585
  if (dedupe) {
602
586
  const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
@@ -604,12 +588,6 @@ async function mergeMemories() {
604
588
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
605
589
  }
606
590
 
607
- const insertEngram = targetDb.prepare(`
608
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
609
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
610
- retracted, retracted_by, retracted_at, tags)
611
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
612
- `);
613
591
  const insertAssoc = targetDb.prepare(`
614
592
  INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
615
593
  activation_count, created_at, last_activated)
@@ -618,6 +596,7 @@ async function mergeMemories() {
618
596
 
619
597
  let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
620
598
 
599
+ try {
621
600
  for (const sourcePath of sources) {
622
601
  if (!existsSync(sourcePath)) {
623
602
  console.error(` Source not found: ${sourcePath}`);
@@ -625,63 +604,84 @@ async function mergeMemories() {
625
604
  }
626
605
 
627
606
  const sourceDb = new Database(sourcePath, { readonly: true });
628
- const engrams = sourceDb.prepare(
629
- `SELECT id, agent_id, concept, content, confidence, salience, access_count,
630
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
631
- retracted, retracted_by, retracted_at, tags FROM engrams`
632
- ).all() as any[];
633
- const assocs = sourceDb.prepare(
634
- `SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
635
- activation_count, created_at, last_activated FROM associations`
636
- ).all() as any[];
607
+ // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
608
+ // and createEngram fills the default.
609
+ const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
610
+ const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
611
+ sourceDb.close(); // reads done — release the source handle before any (throwing) write work
637
612
 
638
613
  const idMap = new Map<string, string>();
639
614
  const skippedIds = new Set<string>();
615
+ let imported = 0, skipped = 0, assocImported = 0;
640
616
 
641
- const result = targetDb.transaction(() => {
642
- let imported = 0, skipped = 0;
617
+ for (const e of engrams) {
618
+ const hash = contentHash(e.concept, e.content);
619
+ if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
620
+ existingHashes.add(hash);
621
+ if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
622
+ // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
623
+ // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
624
+ const created = store.createEngram({
625
+ agentId: remapAgentId(e.agent_id),
626
+ concept: e.concept, content: e.content,
627
+ embedding: blobToArr(e.embedding),
628
+ confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
629
+ salienceFeatures: parseJson(e.salience_features, undefined),
630
+ reasonCodes: parseJson(e.reason_codes, undefined),
631
+ tags: parseJson<string[]>(e.tags, []),
632
+ memoryClass: e.memory_class ?? 'working',
633
+ memoryType: e.memory_type ?? undefined,
634
+ episodeId: e.episode_id ?? undefined,
635
+ taskStatus: e.task_status ?? undefined,
636
+ taskPriority: e.task_priority ?? undefined,
637
+ blockedBy: e.blocked_by ?? undefined,
638
+ ttl: e.ttl ?? undefined,
639
+ sequence: e.sequence ?? undefined,
640
+ references: parseJson(e.references_json, undefined),
641
+ });
642
+ idMap.set(e.id, created.id);
643
+ // preserve stage + retracted (createEngram always mints active/non-retracted)
644
+ if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
645
+ if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
646
+ imported++;
647
+ }
648
+ // second pass — re-link supersession with remapped ids
649
+ if (!dryRun) {
643
650
  for (const e of engrams) {
644
- const hash = contentHash(e.concept, e.content);
645
- if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
646
- const newId = randomUUID();
647
- idMap.set(e.id, newId);
648
- existingHashes.add(hash);
649
- if (!dryRun) {
650
- insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence,
651
- e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features,
652
- e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
653
- }
654
- imported++;
651
+ const newId = idMap.get(e.id);
652
+ if (!newId || !e.supersedes) continue;
653
+ const supNew = idMap.get(e.supersedes);
654
+ if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
655
655
  }
656
- let assocImported = 0;
657
- for (const a of assocs) {
658
- if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
659
- const fromId = idMap.get(a.from_engram_id);
660
- const toId = idMap.get(a.to_engram_id);
661
- if (!fromId || !toId) continue;
662
- if (!dryRun) {
656
+ }
657
+ for (const a of assocs) {
658
+ if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
659
+ const fromId = idMap.get(a.from_engram_id);
660
+ const toId = idMap.get(a.to_engram_id);
661
+ if (!fromId || !toId) continue;
662
+ if (!dryRun) {
663
+ try {
663
664
  insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
664
665
  a.activation_count, a.created_at, a.last_activated);
665
- }
666
- assocImported++;
666
+ } catch { /* skip an association whose source row has an unbindable/undefined column */ }
667
667
  }
668
- return { imported, skipped, assocImported };
669
- })();
670
-
671
- sourceDb.close();
668
+ assocImported++;
669
+ }
672
670
 
673
671
  const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
674
672
  console.log(` Source: ${sourcePath}`);
675
- console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
676
- console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
673
+ console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
674
+ console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
677
675
  console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
678
676
 
679
- totalMemories += result.imported;
680
- totalAssociations += result.assocImported;
681
- totalSkipped += result.skipped;
677
+ totalMemories += imported;
678
+ totalAssociations += assocImported;
679
+ totalSkipped += skipped;
682
680
  }
683
681
 
684
- targetDb.close();
682
+ } finally {
683
+ try { store.close(); } catch { /* */ }
684
+ }
685
685
  console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
686
686
  if (dryRun) console.log('(dry run — no data written)');
687
687
  }
@@ -717,6 +717,72 @@ async function migrateCmd() {
717
717
  }
718
718
  }
719
719
 
720
+ // ─── ONBOARD ──────────────────────────────────────
721
+
722
+ /**
723
+ * Seed the onboarding skill as a canonical memory (idempotent). This is what lets
724
+ * a cold store teach the host agent how to warm-start itself — the agent recalls
725
+ * the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
726
+ */
727
+ async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
728
+ try {
729
+ const { store, close } = await openCliStore(dbPath);
730
+ try {
731
+ const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
732
+ if (existing) return 'Onboarding skill: already present';
733
+ await store.createEngram({
734
+ agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
735
+ tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
736
+ });
737
+ return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
738
+ } finally {
739
+ await close();
740
+ }
741
+ } catch (e: any) {
742
+ return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
743
+ }
744
+ }
745
+
746
+ function onboardCmd() {
747
+ const docs: string[] = [];
748
+ let repo: string | undefined;
749
+ let project = '';
750
+ let agentId = '';
751
+ let purpose: string | undefined;
752
+ let outDir = resolve(process.cwd(), '.awm');
753
+
754
+ for (let i = 1; i < args.length; i++) {
755
+ const a = args[i];
756
+ if (a === '--repo' && args[i + 1]) repo = args[++i];
757
+ else if (a === '--project' && args[i + 1]) project = args[++i];
758
+ else if (a === '--agent' && args[i + 1]) agentId = args[++i];
759
+ else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
760
+ else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
761
+ else if (!a.startsWith('--')) docs.push(a);
762
+ }
763
+
764
+ // Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
765
+ if (docs.length === 0) docs.push(repo ?? process.cwd());
766
+ if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
767
+ if (!agentId) agentId = project;
768
+
769
+ const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
770
+ console.log(`
771
+ AWM onboard — warm-start pack for "${project}"
772
+
773
+ Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
774
+ Extracted: ${count} candidate memories (agent: ${agentId})
775
+
776
+ Review: ${reviewPath}
777
+ Pack: ${packPath}
778
+
779
+ Next:
780
+ 1. Edit the review file / pack as needed (delete noise, answer the interview questions).
781
+ 2. Load it: awm import ${packPath} --db <path> --dedupe
782
+ (embeddings backfill on the first consolidation — recall is warm immediately after)
783
+ `.trimEnd());
784
+ }
785
+
720
786
  // ─── Dispatch ──────────────────────────────────────
721
787
 
722
788
  switch (command) {
@@ -747,6 +813,9 @@ switch (command) {
747
813
  case 'migrate':
748
814
  await migrateCmd();
749
815
  break;
816
+ case 'onboard':
817
+ onboardCmd();
818
+ break;
750
819
  case '--help':
751
820
  case '-h':
752
821
  case undefined: