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/dist/cli.js CHANGED
@@ -11,10 +11,11 @@
11
11
  * awm health — check if a running server is healthy
12
12
  */
13
13
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
- import { resolve, dirname } from 'node:path';
14
+ import { resolve, dirname, basename } from 'node:path';
15
15
  import { execSync } from 'node:child_process';
16
- import { randomUUID } from 'node:crypto';
17
16
  import { fileURLToPath } from 'node:url';
17
+ import { VERSION } from './version.js';
18
+ import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
18
19
  const __filename = fileURLToPath(import.meta.url);
19
20
  const __dirname = dirname(__filename);
20
21
  // Load .env if present
@@ -135,6 +136,8 @@ async function setup() {
135
136
  const configAction = adapter.writeMcpConfig(ctx);
136
137
  const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
137
138
  const hooksAction = adapter.writeHooks(ctx, skipHooks);
139
+ // Seed the onboarding skill so a cold store can teach the agent how to warm itself.
140
+ const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
138
141
  console.log(`
139
142
  AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
140
143
 
@@ -143,6 +146,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
143
146
  ${configAction}
144
147
  ${instructionsAction}
145
148
  ${hooksAction}
149
+ ${skillAction}
146
150
 
147
151
  Next steps:
148
152
  1. Restart ${adapter.name} to pick up the MCP server
@@ -228,12 +232,38 @@ function health() {
228
232
  process.exit(1);
229
233
  }
230
234
  }
235
+ // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
236
+ //
237
+ // export/import route through openStore() so they work on ANY backend (SQLite,
238
+ // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
239
+ // (a SQLite file or PGlite dir, by shape); for a Postgres target set
240
+ // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
241
+ // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
242
+ function toISOStr(d) {
243
+ if (d == null)
244
+ return null;
245
+ return d instanceof Date ? d.toISOString() : String(d);
246
+ }
247
+ async function openCliStore(dbPath) {
248
+ // --db sets the path only when the env doesn't already select a backend/path.
249
+ if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
250
+ process.env.AWM_DB_PATH = dbPath;
251
+ }
252
+ const { openStore } = await import('./storage/factory.js');
253
+ const { store, backend } = await openStore();
254
+ return { store, backend, close: async () => { try {
255
+ await store.close?.();
256
+ }
257
+ catch { /* */ } } };
258
+ }
231
259
  // ─── EXPORT ──────────────────────────────────────
232
260
  async function exportMemories() {
233
261
  let dbPath = '';
234
262
  let agentFilter = null;
235
263
  let outputPath = null;
236
264
  let activeOnly = false;
265
+ let allStages = false;
266
+ let includeRetracted = false;
237
267
  for (let i = 1; i < args.length; i++) {
238
268
  if (args[i] === '--db' && args[i + 1])
239
269
  dbPath = args[++i];
@@ -243,93 +273,105 @@ async function exportMemories() {
243
273
  outputPath = args[++i];
244
274
  else if (args[i] === '--active-only')
245
275
  activeOnly = true;
276
+ else if (args[i] === '--all-stages')
277
+ allStages = true;
278
+ else if (args[i] === '--include-retracted')
279
+ includeRetracted = true;
246
280
  }
247
- if (!dbPath) {
248
- console.error('Error: --db <path> is required');
249
- process.exit(1);
250
- }
251
- if (!existsSync(dbPath)) {
252
- console.error(`Error: database not found: ${dbPath}`);
253
- process.exit(1);
281
+ // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
282
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
283
+ if (!usingPostgres) {
284
+ if (!dbPath) {
285
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
286
+ process.exit(1);
287
+ }
288
+ if (!existsSync(dbPath)) {
289
+ console.error(`Error: database not found: ${dbPath}`);
290
+ process.exit(1);
291
+ }
254
292
  }
255
- // Dynamic import to avoid loading better-sqlite3 for other commands
256
- const Database = (await import('better-sqlite3')).default;
257
- const db = new Database(dbPath, { readonly: true });
258
- // Build memory query
259
- let memQuery = 'SELECT * FROM engrams';
260
- const conditions = [];
261
- const params = [];
262
- if (agentFilter) {
263
- conditions.push('agent_id = ?');
264
- params.push(agentFilter);
265
- }
266
- if (activeOnly) {
267
- conditions.push('retracted = 0');
268
- }
269
- if (conditions.length > 0) {
270
- memQuery += ' WHERE ' + conditions.join(' AND ');
271
- }
272
- memQuery += ' ORDER BY created_at ASC';
273
- const rows = db.prepare(memQuery).all(...params);
274
- // Build memory objects (exclude embedding blobs)
275
- const memories = rows.map((r) => ({
276
- id: r.id,
277
- agent_id: r.agent_id,
278
- concept: r.concept,
279
- content: r.content,
280
- confidence: r.confidence,
281
- salience: r.salience,
282
- access_count: r.access_count,
283
- last_accessed: r.last_accessed,
284
- created_at: r.created_at,
285
- stage: r.stage,
286
- tags: r.tags ? JSON.parse(r.tags) : [],
287
- memory_class: r.memory_class ?? 'working',
288
- episode_id: r.episode_id ?? null,
289
- task_status: r.task_status ?? null,
290
- task_priority: r.task_priority ?? null,
291
- supersedes: r.supersedes ?? null,
292
- superseded_by: r.superseded_by ?? null,
293
- retracted: r.retracted ?? 0,
294
- }));
295
- // Get memory IDs for association filtering
296
- const memIds = new Set(memories.map((m) => m.id));
297
- // Build associations
298
- let assocQuery = 'SELECT * FROM associations';
299
- const allAssocs = db.prepare(assocQuery).all();
300
- const associations = allAssocs
301
- .filter((a) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
302
- .map((a) => ({
303
- from_id: a.from_engram_id,
304
- to_id: a.to_engram_id,
305
- weight: a.weight,
306
- type: a.type ?? 'hebbian',
307
- activation_count: a.activation_count ?? 0,
308
- }));
309
- // Collect unique agents
310
- const agents = [...new Set(memories.map((m) => m.agent_id))];
311
- const exportData = {
312
- version: '0.8.8',
313
- exported_at: new Date().toISOString(),
314
- source_db: dbPath,
315
- agent_filter: agentFilter,
316
- memories,
317
- associations,
318
- stats: {
319
- total_memories: memories.length,
320
- total_associations: associations.length,
321
- agents,
322
- },
323
- };
324
- const json = JSON.stringify(exportData, null, 2);
325
- if (outputPath) {
326
- writeFileSync(outputPath, json + '\n');
327
- console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath}`);
293
+ const { store, backend, close } = await openCliStore(dbPath);
294
+ try {
295
+ const agentIds = agentFilter
296
+ ? [agentFilter]
297
+ : (await store.getActiveAgents()).map((a) => a.agentId);
298
+ if (agentIds.length === 0) {
299
+ console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
300
+ }
301
+ // Default to the meaningful memory set (active stage, non-retracted). --all-stages
302
+ // widens to every stage; --include-retracted adds retracted (off with --active-only).
303
+ const stage = allStages ? undefined : 'active';
304
+ const wantRetracted = includeRetracted && !activeOnly;
305
+ const engrams = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
306
+ const memories = engrams.map((e) => ({
307
+ id: e.id,
308
+ agent_id: e.agentId,
309
+ concept: e.concept,
310
+ content: e.content,
311
+ // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
312
+ // re-embed after import) a faithful, recall-ready port when source/target embed
313
+ // models match. import skips them with --no-embeddings (then re-embed).
314
+ embedding: Array.isArray(e.embedding) ? e.embedding : null,
315
+ confidence: e.confidence,
316
+ salience: e.salience,
317
+ access_count: e.accessCount ?? 0,
318
+ last_accessed: toISOStr(e.lastAccessed),
319
+ created_at: toISOStr(e.createdAt),
320
+ stage: e.stage ?? 'active',
321
+ tags: Array.isArray(e.tags) ? e.tags : [],
322
+ memory_class: e.memoryClass ?? 'working',
323
+ memory_type: e.memoryType ?? 'unclassified',
324
+ episode_id: e.episodeId ?? null,
325
+ task_status: e.taskStatus ?? null,
326
+ task_priority: e.taskPriority ?? null,
327
+ supersedes: e.supersedes ?? null,
328
+ superseded_by: e.supersededBy ?? null,
329
+ retracted: e.retracted ? 1 : 0,
330
+ }));
331
+ const memIds = new Set(memories.map((m) => m.id));
332
+ const seen = new Set();
333
+ const associations = [];
334
+ for (const aid of agentIds) {
335
+ for (const a of (await store.getAllAssociations(aid)) ?? []) {
336
+ if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId))
337
+ continue;
338
+ const k = `${a.fromEngramId}>${a.toEngramId}`;
339
+ if (seen.has(k))
340
+ continue;
341
+ seen.add(k);
342
+ associations.push({
343
+ from_id: a.fromEngramId, to_id: a.toEngramId,
344
+ weight: a.weight, type: a.type ?? 'hebbian',
345
+ activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
346
+ });
347
+ }
348
+ }
349
+ const exportData = {
350
+ version: VERSION,
351
+ exported_at: new Date().toISOString(),
352
+ source_backend: backend,
353
+ agent_filter: agentFilter,
354
+ embedding_model: process.env.AWM_EMBED_MODEL ?? null,
355
+ memories,
356
+ associations,
357
+ stats: {
358
+ total_memories: memories.length,
359
+ total_associations: associations.length,
360
+ agents: [...new Set(memories.map((m) => m.agent_id))],
361
+ },
362
+ };
363
+ const json = JSON.stringify(exportData, null, 2);
364
+ if (outputPath) {
365
+ writeFileSync(outputPath, json + '\n');
366
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
367
+ }
368
+ else {
369
+ process.stdout.write(json + '\n');
370
+ }
328
371
  }
329
- else {
330
- process.stdout.write(json + '\n');
372
+ finally {
373
+ await close();
331
374
  }
332
- db.close();
333
375
  }
334
376
  // ─── IMPORT ──────────────────────────────────────
335
377
  async function importMemories() {
@@ -354,12 +396,16 @@ async function importMemories() {
354
396
  else if (!args[i].startsWith('--') && !filePath)
355
397
  filePath = args[i];
356
398
  }
399
+ // --no-embeddings: skip importing embedding vectors (use when source/target embed models
400
+ // differ → import without, then re-embed). Parsed alongside the existing flags above.
401
+ const noEmbeddings = args.includes('--no-embeddings');
357
402
  if (!filePath) {
358
403
  console.error('Error: <file> is required');
359
404
  process.exit(1);
360
405
  }
361
- if (!dbPath) {
362
- console.error('Error: --db <path> is required');
406
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
407
+ if (!dbPath && !usingPostgres) {
408
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
363
409
  process.exit(1);
364
410
  }
365
411
  if (!existsSync(filePath)) {
@@ -371,105 +417,122 @@ async function importMemories() {
371
417
  console.error('Error: invalid export file — missing memories array');
372
418
  process.exit(1);
373
419
  }
374
- const Database = (await import('better-sqlite3')).default;
375
- const db = new Database(dbPath);
376
- // Ensure tables exist in target
377
- db.exec(`
378
- CREATE TABLE IF NOT EXISTS engrams (
379
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
380
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
381
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
382
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
383
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
384
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]',
385
- episode_id TEXT, task_status TEXT, task_priority TEXT, blocked_by TEXT,
386
- memory_class TEXT NOT NULL DEFAULT 'working', superseded_by TEXT, supersedes TEXT
387
- );
388
- CREATE TABLE IF NOT EXISTS associations (
389
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
390
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
391
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
392
- created_at TEXT NOT NULL, last_activated TEXT
393
- );
394
- `);
395
- // Build dedup set if needed
396
- const existingHashes = new Set();
397
- if (dedupe) {
398
- const existing = db.prepare('SELECT concept, content FROM engrams').all();
399
- for (const row of existing) {
400
- const hash = (row.concept ?? '').toLowerCase().trim() + '||' + (row.content ?? '').toLowerCase().trim();
401
- existingHashes.add(hash);
402
- }
403
- }
404
- const idMap = new Map();
405
- let imported = 0;
406
- let skippedDupes = 0;
407
- let skippedRetracted = 0;
408
- const insertMem = db.prepare(`
409
- INSERT INTO engrams (id, agent_id, concept, content, confidence, salience,
410
- access_count, last_accessed, created_at, stage, tags, memory_class,
411
- episode_id, task_status, task_priority, supersedes, superseded_by, retracted)
412
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
413
- `);
414
- // NOTE: associations.last_activated is NOT NULL on the engrams DB (storage/sqlite.ts),
415
- // so importing into an existing store fails if it's omitted — and because import wraps
416
- // memories+associations in ONE transaction, that rolls back the memories too (silent
417
- // "empty store"). Set it alongside created_at. (migrate/merge paths already do this.)
418
- const insertAssoc = db.prepare(`
419
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at, last_activated)
420
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
421
- `);
422
- const importTx = db.transaction(() => {
423
- // Import memories
420
+ const { store, backend, close } = await openCliStore(dbPath);
421
+ try {
422
+ // Dedupe against existing memories for the TARGET agent(s).
423
+ const existingHashes = new Set();
424
+ if (dedupe) {
425
+ const targetAgents = remapAgent
426
+ ? [remapAgent]
427
+ : [...new Set(importData.memories.map((m) => m.agent_id))];
428
+ for (const aid of targetAgents) {
429
+ for (const e of (await store.getEngramsByAgent(aid, undefined, true)) ?? []) {
430
+ existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
431
+ }
432
+ }
433
+ }
434
+ const idMap = new Map(); // old export id → new store id
435
+ let imported = 0, skippedDupes = 0, skippedRetracted = 0;
436
+ // Pass 1 create engrams (createEngram mints a fresh id; we capture it for remapping).
437
+ // createdAt/accessCount normalize to import time (the contract's createEngram stamps
438
+ // them) see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
439
+ // embedding) is preserved, so recall is faithful.
424
440
  for (const mem of importData.memories) {
425
- // Skip retracted unless --include-retracted
426
441
  if (mem.retracted && !includeRetracted) {
427
442
  skippedRetracted++;
428
443
  continue;
429
444
  }
430
- // Dedupe check
431
445
  if (dedupe) {
432
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
433
- if (existingHashes.has(hash)) {
446
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
447
+ if (existingHashes.has(h)) {
434
448
  skippedDupes++;
435
449
  continue;
436
450
  }
451
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
437
452
  }
438
- const newId = randomUUID();
439
- idMap.set(mem.id, newId);
440
- const agentId = remapAgent ?? mem.agent_id;
441
- const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
442
- if (!dryRun) {
443
- insertMem.run(newId, agentId, mem.concept, mem.content, mem.confidence ?? 0.5, mem.salience ?? 0.5, mem.access_count ?? 0, mem.last_accessed ?? mem.created_at, mem.created_at, mem.stage ?? 'active', tags, mem.memory_class ?? 'working', mem.episode_id ?? null, mem.task_status ?? null, mem.task_priority ?? null, mem.supersedes ?? null, mem.superseded_by ?? null, mem.retracted ?? 0);
453
+ // dry-run: still map the id so the association-count preview isn't always 0
454
+ if (dryRun) {
455
+ idMap.set(mem.id, mem.id);
456
+ imported++;
457
+ continue;
458
+ }
459
+ const created = await store.createEngram({
460
+ agentId: remapAgent ?? mem.agent_id,
461
+ concept: mem.concept,
462
+ content: mem.content,
463
+ tags: Array.isArray(mem.tags) ? mem.tags : [],
464
+ embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
465
+ confidence: mem.confidence ?? 0.5,
466
+ salience: mem.salience ?? 0.5,
467
+ memoryClass: mem.memory_class ?? 'working',
468
+ memoryType: mem.memory_type ?? undefined,
469
+ episodeId: mem.episode_id ?? undefined,
470
+ taskStatus: mem.task_status ?? undefined,
471
+ taskPriority: mem.task_priority ?? undefined,
472
+ });
473
+ idMap.set(mem.id, created.id);
474
+ // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
475
+ // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
476
+ // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
477
+ if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
478
+ try {
479
+ await store.updateStage(created.id, mem.stage);
480
+ }
481
+ catch { /* best-effort */ }
482
+ }
483
+ if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
484
+ try {
485
+ await store.retractEngram(created.id, mem.retracted_by ?? null);
486
+ }
487
+ catch { /* best-effort */ }
444
488
  }
445
489
  imported++;
446
490
  }
447
- // Import associations (using remapped IDs)
491
+ // Pass 2 re-link supersession with remapped ids (supersedeEngram sets both sides:
492
+ // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
493
+ if (!dryRun) {
494
+ for (const mem of importData.memories) {
495
+ const newId = idMap.get(mem.id);
496
+ if (!newId || !mem.supersedes)
497
+ continue;
498
+ const supersededNew = idMap.get(mem.supersedes);
499
+ if (supersededNew) {
500
+ try {
501
+ await store.supersedeEngram(supersededNew, newId);
502
+ }
503
+ catch { /* best-effort */ }
504
+ }
505
+ }
506
+ }
507
+ // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
448
508
  let assocImported = 0;
449
- const associations = importData.associations ?? [];
450
- for (const assoc of associations) {
451
- const fromId = idMap.get(assoc.from_id);
452
- const toId = idMap.get(assoc.to_id);
509
+ for (const a of (importData.associations ?? [])) {
510
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
453
511
  if (!fromId || !toId)
454
- continue; // skip if either memory was skipped
512
+ continue;
455
513
  if (!dryRun) {
456
- insertAssoc.run(randomUUID(), fromId, toId, assoc.weight ?? 0.5, assoc.type ?? 'hebbian', assoc.activation_count ?? 0);
514
+ try {
515
+ await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5);
516
+ }
517
+ catch { /* best-effort */ }
457
518
  }
458
519
  assocImported++;
459
520
  }
460
- return assocImported;
461
- });
462
- const assocCount = importTx();
463
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
464
- console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
465
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
466
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
467
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
468
- db.close();
521
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
522
+ console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
523
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
524
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
525
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
526
+ ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
527
+ }
528
+ finally {
529
+ await close();
530
+ }
469
531
  }
470
532
  // ─── MERGE ──────────────────────────────────────
471
533
  async function mergeMemories() {
472
534
  const Database = (await import('better-sqlite3')).default;
535
+ const { EngramStore } = await import('./storage/sqlite.js');
473
536
  const { createHash, randomUUID } = await import('node:crypto');
474
537
  let target = '';
475
538
  const sources = [];
@@ -516,27 +579,22 @@ async function mergeMemories() {
516
579
  return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
517
580
  }
518
581
  console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
519
- const targetDb = new Database(target);
520
- targetDb.pragma('journal_mode = WAL');
521
- targetDb.pragma('foreign_keys = ON');
522
- // Ensure tables exist in target
523
- targetDb.exec(`
524
- CREATE TABLE IF NOT EXISTS engrams (
525
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
526
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
527
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
528
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
529
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
530
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
531
- );
532
- CREATE TABLE IF NOT EXISTS associations (
533
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
534
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
535
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
536
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
537
- );
538
- `);
539
- // Build dedupe hash set from existing target memories
582
+ // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
583
+ // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
584
+ // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
585
+ const store = new EngramStore(target);
586
+ const targetDb = store.db; // the store's better-sqlite3 handle
587
+ const blobToArr = (b) => {
588
+ const buf = b;
589
+ return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
590
+ };
591
+ const parseJson = (s, fallback) => { try {
592
+ return s ? JSON.parse(String(s)) : fallback;
593
+ }
594
+ catch {
595
+ return fallback;
596
+ } };
597
+ // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
540
598
  const existingHashes = new Set();
541
599
  if (dedupe) {
542
600
  const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
@@ -544,33 +602,27 @@ async function mergeMemories() {
544
602
  existingHashes.add(contentHash(row.concept, row.content));
545
603
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
546
604
  }
547
- const insertEngram = targetDb.prepare(`
548
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
549
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
550
- retracted, retracted_by, retracted_at, tags)
551
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
552
- `);
553
605
  const insertAssoc = targetDb.prepare(`
554
606
  INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
555
607
  activation_count, created_at, last_activated)
556
608
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
557
609
  `);
558
610
  let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
559
- for (const sourcePath of sources) {
560
- if (!existsSync(sourcePath)) {
561
- console.error(` Source not found: ${sourcePath}`);
562
- continue;
563
- }
564
- const sourceDb = new Database(sourcePath, { readonly: true });
565
- const engrams = sourceDb.prepare(`SELECT id, agent_id, concept, content, confidence, salience, access_count,
566
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
567
- retracted, retracted_by, retracted_at, tags FROM engrams`).all();
568
- const assocs = sourceDb.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
569
- activation_count, created_at, last_activated FROM associations`).all();
570
- const idMap = new Map();
571
- const skippedIds = new Set();
572
- const result = targetDb.transaction(() => {
573
- let imported = 0, skipped = 0;
611
+ try {
612
+ for (const sourcePath of sources) {
613
+ if (!existsSync(sourcePath)) {
614
+ console.error(` Source not found: ${sourcePath}`);
615
+ continue;
616
+ }
617
+ const sourceDb = new Database(sourcePath, { readonly: true });
618
+ // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
619
+ // and createEngram fills the default.
620
+ const engrams = sourceDb.prepare('SELECT * FROM engrams').all();
621
+ const assocs = sourceDb.prepare('SELECT * FROM associations').all();
622
+ sourceDb.close(); // reads done — release the source handle before any (throwing) write work
623
+ const idMap = new Map();
624
+ const skippedIds = new Set();
625
+ let imported = 0, skipped = 0, assocImported = 0;
574
626
  for (const e of engrams) {
575
627
  const hash = contentHash(e.concept, e.content);
576
628
  if (dedupe && existingHashes.has(hash)) {
@@ -578,15 +630,63 @@ async function mergeMemories() {
578
630
  skipped++;
579
631
  continue;
580
632
  }
581
- const newId = randomUUID();
582
- idMap.set(e.id, newId);
583
633
  existingHashes.add(hash);
584
- if (!dryRun) {
585
- insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence, e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features, e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
634
+ if (dryRun) {
635
+ idMap.set(e.id, e.id);
636
+ imported++;
637
+ continue;
638
+ }
639
+ // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
640
+ // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
641
+ const created = store.createEngram({
642
+ agentId: remapAgentId(e.agent_id),
643
+ concept: e.concept, content: e.content,
644
+ embedding: blobToArr(e.embedding),
645
+ confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
646
+ salienceFeatures: parseJson(e.salience_features, undefined),
647
+ reasonCodes: parseJson(e.reason_codes, undefined),
648
+ tags: parseJson(e.tags, []),
649
+ memoryClass: e.memory_class ?? 'working',
650
+ memoryType: e.memory_type ?? undefined,
651
+ episodeId: e.episode_id ?? undefined,
652
+ taskStatus: e.task_status ?? undefined,
653
+ taskPriority: e.task_priority ?? undefined,
654
+ blockedBy: e.blocked_by ?? undefined,
655
+ ttl: e.ttl ?? undefined,
656
+ sequence: e.sequence ?? undefined,
657
+ references: parseJson(e.references_json, undefined),
658
+ });
659
+ idMap.set(e.id, created.id);
660
+ // preserve stage + retracted (createEngram always mints active/non-retracted)
661
+ if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') {
662
+ try {
663
+ store.updateStage(created.id, e.stage);
664
+ }
665
+ catch { /* */ }
666
+ }
667
+ if (e.retracted) {
668
+ try {
669
+ store.retractEngram(created.id, e.retracted_by ?? null);
670
+ }
671
+ catch { /* */ }
586
672
  }
587
673
  imported++;
588
674
  }
589
- let assocImported = 0;
675
+ // second pass — re-link supersession with remapped ids
676
+ if (!dryRun) {
677
+ for (const e of engrams) {
678
+ const newId = idMap.get(e.id);
679
+ if (!newId || !e.supersedes)
680
+ continue;
681
+ const supNew = idMap.get(e.supersedes);
682
+ if (supNew) {
683
+ try {
684
+ store.supersedeEngram(supNew, newId);
685
+ }
686
+ catch { /* */ }
687
+ }
688
+ }
689
+ }
590
690
  for (const a of assocs) {
591
691
  if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
592
692
  continue;
@@ -595,23 +695,29 @@ async function mergeMemories() {
595
695
  if (!fromId || !toId)
596
696
  continue;
597
697
  if (!dryRun) {
598
- insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
698
+ try {
699
+ insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
700
+ }
701
+ catch { /* skip an association whose source row has an unbindable/undefined column */ }
599
702
  }
600
703
  assocImported++;
601
704
  }
602
- return { imported, skipped, assocImported };
603
- })();
604
- sourceDb.close();
605
- const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
606
- console.log(` Source: ${sourcePath}`);
607
- console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
608
- console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
609
- console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
610
- totalMemories += result.imported;
611
- totalAssociations += result.assocImported;
612
- totalSkipped += result.skipped;
613
- }
614
- targetDb.close();
705
+ const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
706
+ console.log(` Source: ${sourcePath}`);
707
+ console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
708
+ console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
709
+ console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
710
+ totalMemories += imported;
711
+ totalAssociations += assocImported;
712
+ totalSkipped += skipped;
713
+ }
714
+ }
715
+ finally {
716
+ try {
717
+ store.close();
718
+ }
719
+ catch { /* */ }
720
+ }
615
721
  console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
616
722
  if (dryRun)
617
723
  console.log('(dry run — no data written)');
@@ -655,6 +761,78 @@ async function migrateCmd() {
655
761
  process.exit(1);
656
762
  }
657
763
  }
764
+ // ─── ONBOARD ──────────────────────────────────────
765
+ /**
766
+ * Seed the onboarding skill as a canonical memory (idempotent). This is what lets
767
+ * a cold store teach the host agent how to warm-start itself — the agent recalls
768
+ * the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
769
+ */
770
+ async function seedOnboardSkill(dbPath, agentId) {
771
+ try {
772
+ const { store, close } = await openCliStore(dbPath);
773
+ try {
774
+ const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
775
+ if (existing)
776
+ return 'Onboarding skill: already present';
777
+ await store.createEngram({
778
+ agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
779
+ tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
780
+ });
781
+ return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
782
+ }
783
+ finally {
784
+ await close();
785
+ }
786
+ }
787
+ catch (e) {
788
+ return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
789
+ }
790
+ }
791
+ function onboardCmd() {
792
+ const docs = [];
793
+ let repo;
794
+ let project = '';
795
+ let agentId = '';
796
+ let purpose;
797
+ let outDir = resolve(process.cwd(), '.awm');
798
+ for (let i = 1; i < args.length; i++) {
799
+ const a = args[i];
800
+ if (a === '--repo' && args[i + 1])
801
+ repo = args[++i];
802
+ else if (a === '--project' && args[i + 1])
803
+ project = args[++i];
804
+ else if (a === '--agent' && args[i + 1])
805
+ agentId = args[++i];
806
+ else if (a === '--purpose' && args[i + 1])
807
+ purpose = args[++i];
808
+ else if (a === '--out' && args[i + 1])
809
+ outDir = resolve(args[++i]);
810
+ else if (!a.startsWith('--'))
811
+ docs.push(a);
812
+ }
813
+ // Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
814
+ if (docs.length === 0)
815
+ docs.push(repo ?? process.cwd());
816
+ if (!project)
817
+ project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
818
+ if (!agentId)
819
+ agentId = project;
820
+ const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
821
+ console.log(`
822
+ AWM onboard — warm-start pack for "${project}"
823
+
824
+ Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
825
+ Extracted: ${count} candidate memories (agent: ${agentId})
826
+
827
+ Review: ${reviewPath}
828
+ Pack: ${packPath}
829
+
830
+ Next:
831
+ 1. Edit the review file / pack as needed (delete noise, answer the interview questions).
832
+ 2. Load it: awm import ${packPath} --db <path> --dedupe
833
+ (embeddings backfill on the first consolidation — recall is warm immediately after)
834
+ `.trimEnd());
835
+ }
658
836
  // ─── Dispatch ──────────────────────────────────────
659
837
  switch (command) {
660
838
  case 'setup':
@@ -684,6 +862,9 @@ switch (command) {
684
862
  case 'migrate':
685
863
  await migrateCmd();
686
864
  break;
865
+ case 'onboard':
866
+ onboardCmd();
867
+ break;
687
868
  case '--help':
688
869
  case '-h':
689
870
  case undefined: