agent-working-memory 0.9.0 → 0.10.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 (55) hide show
  1. package/dist/cli/migrate.js +29 -29
  2. package/dist/cli.js +324 -219
  3. package/dist/cli.js.map +1 -1
  4. package/dist/coordination/circuit-breaker.js +23 -23
  5. package/dist/core/salience.d.ts.map +1 -1
  6. package/dist/core/salience.js +10 -1
  7. package/dist/core/salience.js.map +1 -1
  8. package/dist/core/write-pipeline.d.ts.map +1 -1
  9. package/dist/core/write-pipeline.js +5 -1
  10. package/dist/core/write-pipeline.js.map +1 -1
  11. package/dist/storage/factory.d.ts +1 -1
  12. package/dist/storage/factory.d.ts.map +1 -1
  13. package/dist/storage/factory.js +16 -2
  14. package/dist/storage/factory.js.map +1 -1
  15. package/dist/storage/pglite-schema.js +143 -143
  16. package/dist/storage/pglite.d.ts.map +1 -1
  17. package/dist/storage/pglite.js +146 -138
  18. package/dist/storage/pglite.js.map +1 -1
  19. package/dist/storage/postgres.d.ts +228 -0
  20. package/dist/storage/postgres.d.ts.map +1 -0
  21. package/dist/storage/postgres.js +1221 -0
  22. package/dist/storage/postgres.js.map +1 -0
  23. package/package.json +3 -1
  24. package/src/api/index.ts +3 -3
  25. package/src/cli/migrate.ts +307 -307
  26. package/src/cli.ts +266 -268
  27. package/src/coordination/circuit-breaker.ts +83 -83
  28. package/src/coordination/failure-modes.ts +50 -50
  29. package/src/core/decay.ts +63 -63
  30. package/src/core/embeddings.ts +110 -110
  31. package/src/core/index.ts +5 -5
  32. package/src/core/logger.ts +36 -36
  33. package/src/core/ml-worker-entry.ts +194 -194
  34. package/src/core/ml-worker.ts +281 -281
  35. package/src/core/query-expander.ts +122 -122
  36. package/src/core/reranker.ts +119 -119
  37. package/src/core/salience.ts +10 -1
  38. package/src/core/write-pipeline.ts +5 -1
  39. package/src/engine/confidence.ts +120 -120
  40. package/src/engine/consolidation-scheduler.ts +242 -242
  41. package/src/engine/eval.ts +102 -102
  42. package/src/engine/eviction.ts +101 -101
  43. package/src/engine/index.ts +8 -8
  44. package/src/engine/retraction.ts +366 -366
  45. package/src/engine/staging.ts +74 -74
  46. package/src/storage/factory.ts +159 -147
  47. package/src/storage/index.ts +3 -3
  48. package/src/storage/pglite-schema.ts +166 -166
  49. package/src/storage/pglite.ts +1372 -1363
  50. package/src/storage/postgres.ts +1475 -0
  51. package/src/storage/store.ts +80 -80
  52. package/src/types/agent.ts +67 -67
  53. package/src/types/checkpoint.ts +46 -46
  54. package/src/types/eval.ts +100 -100
  55. package/src/types/index.ts +6 -6
package/dist/cli.js CHANGED
@@ -13,7 +13,6 @@
13
13
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
14
  import { resolve, dirname } from 'node:path';
15
15
  import { execSync } from 'node:child_process';
16
- import { randomUUID } from 'node:crypto';
17
16
  import { fileURLToPath } from 'node:url';
18
17
  const __filename = fileURLToPath(import.meta.url);
19
18
  const __dirname = dirname(__filename);
@@ -228,12 +227,38 @@ function health() {
228
227
  process.exit(1);
229
228
  }
230
229
  }
230
+ // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
231
+ //
232
+ // export/import route through openStore() so they work on ANY backend (SQLite,
233
+ // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
234
+ // (a SQLite file or PGlite dir, by shape); for a Postgres target set
235
+ // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
236
+ // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
237
+ function toISOStr(d) {
238
+ if (d == null)
239
+ return null;
240
+ return d instanceof Date ? d.toISOString() : String(d);
241
+ }
242
+ async function openCliStore(dbPath) {
243
+ // --db sets the path only when the env doesn't already select a backend/path.
244
+ if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
245
+ process.env.AWM_DB_PATH = dbPath;
246
+ }
247
+ const { openStore } = await import('./storage/factory.js');
248
+ const { store, backend } = await openStore();
249
+ return { store, backend, close: async () => { try {
250
+ await store.close?.();
251
+ }
252
+ catch { /* */ } } };
253
+ }
231
254
  // ─── EXPORT ──────────────────────────────────────
232
255
  async function exportMemories() {
233
256
  let dbPath = '';
234
257
  let agentFilter = null;
235
258
  let outputPath = null;
236
259
  let activeOnly = false;
260
+ let allStages = false;
261
+ let includeRetracted = false;
237
262
  for (let i = 1; i < args.length; i++) {
238
263
  if (args[i] === '--db' && args[i + 1])
239
264
  dbPath = args[++i];
@@ -243,93 +268,105 @@ async function exportMemories() {
243
268
  outputPath = args[++i];
244
269
  else if (args[i] === '--active-only')
245
270
  activeOnly = true;
271
+ else if (args[i] === '--all-stages')
272
+ allStages = true;
273
+ else if (args[i] === '--include-retracted')
274
+ includeRetracted = true;
246
275
  }
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);
276
+ // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
277
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
278
+ if (!usingPostgres) {
279
+ if (!dbPath) {
280
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
281
+ process.exit(1);
282
+ }
283
+ if (!existsSync(dbPath)) {
284
+ console.error(`Error: database not found: ${dbPath}`);
285
+ process.exit(1);
286
+ }
254
287
  }
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}`);
288
+ const { store, backend, close } = await openCliStore(dbPath);
289
+ try {
290
+ const agentIds = agentFilter
291
+ ? [agentFilter]
292
+ : (await store.getActiveAgents()).map((a) => a.agentId);
293
+ if (agentIds.length === 0) {
294
+ console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
295
+ }
296
+ // Default to the meaningful memory set (active stage, non-retracted). --all-stages
297
+ // widens to every stage; --include-retracted adds retracted (off with --active-only).
298
+ const stage = allStages ? undefined : 'active';
299
+ const wantRetracted = includeRetracted && !activeOnly;
300
+ const engrams = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
301
+ const memories = engrams.map((e) => ({
302
+ id: e.id,
303
+ agent_id: e.agentId,
304
+ concept: e.concept,
305
+ content: e.content,
306
+ // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
307
+ // re-embed after import) a faithful, recall-ready port when source/target embed
308
+ // models match. import skips them with --no-embeddings (then re-embed).
309
+ embedding: Array.isArray(e.embedding) ? e.embedding : null,
310
+ confidence: e.confidence,
311
+ salience: e.salience,
312
+ access_count: e.accessCount ?? 0,
313
+ last_accessed: toISOStr(e.lastAccessed),
314
+ created_at: toISOStr(e.createdAt),
315
+ stage: e.stage ?? 'active',
316
+ tags: Array.isArray(e.tags) ? e.tags : [],
317
+ memory_class: e.memoryClass ?? 'working',
318
+ memory_type: e.memoryType ?? 'unclassified',
319
+ episode_id: e.episodeId ?? null,
320
+ task_status: e.taskStatus ?? null,
321
+ task_priority: e.taskPriority ?? null,
322
+ supersedes: e.supersedes ?? null,
323
+ superseded_by: e.supersededBy ?? null,
324
+ retracted: e.retracted ? 1 : 0,
325
+ }));
326
+ const memIds = new Set(memories.map((m) => m.id));
327
+ const seen = new Set();
328
+ const associations = [];
329
+ for (const aid of agentIds) {
330
+ for (const a of (await store.getAllAssociations(aid)) ?? []) {
331
+ if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId))
332
+ continue;
333
+ const k = `${a.fromEngramId}>${a.toEngramId}`;
334
+ if (seen.has(k))
335
+ continue;
336
+ seen.add(k);
337
+ associations.push({
338
+ from_id: a.fromEngramId, to_id: a.toEngramId,
339
+ weight: a.weight, type: a.type ?? 'hebbian',
340
+ activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
341
+ });
342
+ }
343
+ }
344
+ const exportData = {
345
+ version: '0.9.2',
346
+ exported_at: new Date().toISOString(),
347
+ source_backend: backend,
348
+ agent_filter: agentFilter,
349
+ embedding_model: process.env.AWM_EMBED_MODEL ?? null,
350
+ memories,
351
+ associations,
352
+ stats: {
353
+ total_memories: memories.length,
354
+ total_associations: associations.length,
355
+ agents: [...new Set(memories.map((m) => m.agent_id))],
356
+ },
357
+ };
358
+ const json = JSON.stringify(exportData, null, 2);
359
+ if (outputPath) {
360
+ writeFileSync(outputPath, json + '\n');
361
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
362
+ }
363
+ else {
364
+ process.stdout.write(json + '\n');
365
+ }
328
366
  }
329
- else {
330
- process.stdout.write(json + '\n');
367
+ finally {
368
+ await close();
331
369
  }
332
- db.close();
333
370
  }
334
371
  // ─── IMPORT ──────────────────────────────────────
335
372
  async function importMemories() {
@@ -354,12 +391,16 @@ async function importMemories() {
354
391
  else if (!args[i].startsWith('--') && !filePath)
355
392
  filePath = args[i];
356
393
  }
394
+ // --no-embeddings: skip importing embedding vectors (use when source/target embed models
395
+ // differ → import without, then re-embed). Parsed alongside the existing flags above.
396
+ const noEmbeddings = args.includes('--no-embeddings');
357
397
  if (!filePath) {
358
398
  console.error('Error: <file> is required');
359
399
  process.exit(1);
360
400
  }
361
- if (!dbPath) {
362
- console.error('Error: --db <path> is required');
401
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
402
+ if (!dbPath && !usingPostgres) {
403
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
363
404
  process.exit(1);
364
405
  }
365
406
  if (!existsSync(filePath)) {
@@ -371,101 +412,122 @@ async function importMemories() {
371
412
  console.error('Error: invalid export file — missing memories array');
372
413
  process.exit(1);
373
414
  }
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
- const insertAssoc = db.prepare(`
415
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
416
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
417
- `);
418
- const importTx = db.transaction(() => {
419
- // Import memories
415
+ const { store, backend, close } = await openCliStore(dbPath);
416
+ try {
417
+ // Dedupe against existing memories for the TARGET agent(s).
418
+ const existingHashes = new Set();
419
+ if (dedupe) {
420
+ const targetAgents = remapAgent
421
+ ? [remapAgent]
422
+ : [...new Set(importData.memories.map((m) => m.agent_id))];
423
+ for (const aid of targetAgents) {
424
+ for (const e of (await store.getEngramsByAgent(aid, undefined, true)) ?? []) {
425
+ existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
426
+ }
427
+ }
428
+ }
429
+ const idMap = new Map(); // old export id → new store id
430
+ let imported = 0, skippedDupes = 0, skippedRetracted = 0;
431
+ // Pass 1 create engrams (createEngram mints a fresh id; we capture it for remapping).
432
+ // createdAt/accessCount normalize to import time (the contract's createEngram stamps
433
+ // them) see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
434
+ // embedding) is preserved, so recall is faithful.
420
435
  for (const mem of importData.memories) {
421
- // Skip retracted unless --include-retracted
422
436
  if (mem.retracted && !includeRetracted) {
423
437
  skippedRetracted++;
424
438
  continue;
425
439
  }
426
- // Dedupe check
427
440
  if (dedupe) {
428
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
429
- if (existingHashes.has(hash)) {
441
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
442
+ if (existingHashes.has(h)) {
430
443
  skippedDupes++;
431
444
  continue;
432
445
  }
446
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
433
447
  }
434
- const newId = randomUUID();
435
- idMap.set(mem.id, newId);
436
- const agentId = remapAgent ?? mem.agent_id;
437
- const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
438
- if (!dryRun) {
439
- 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);
448
+ // dry-run: still map the id so the association-count preview isn't always 0
449
+ if (dryRun) {
450
+ idMap.set(mem.id, mem.id);
451
+ imported++;
452
+ continue;
453
+ }
454
+ const created = await store.createEngram({
455
+ agentId: remapAgent ?? mem.agent_id,
456
+ concept: mem.concept,
457
+ content: mem.content,
458
+ tags: Array.isArray(mem.tags) ? mem.tags : [],
459
+ embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
460
+ confidence: mem.confidence ?? 0.5,
461
+ salience: mem.salience ?? 0.5,
462
+ memoryClass: mem.memory_class ?? 'working',
463
+ memoryType: mem.memory_type ?? undefined,
464
+ episodeId: mem.episode_id ?? undefined,
465
+ taskStatus: mem.task_status ?? undefined,
466
+ taskPriority: mem.task_priority ?? undefined,
467
+ });
468
+ idMap.set(mem.id, created.id);
469
+ // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
470
+ // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
471
+ // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
472
+ if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
473
+ try {
474
+ await store.updateStage(created.id, mem.stage);
475
+ }
476
+ catch { /* best-effort */ }
477
+ }
478
+ if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
479
+ try {
480
+ await store.retractEngram(created.id, mem.retracted_by ?? null);
481
+ }
482
+ catch { /* best-effort */ }
440
483
  }
441
484
  imported++;
442
485
  }
443
- // Import associations (using remapped IDs)
486
+ // Pass 2 re-link supersession with remapped ids (supersedeEngram sets both sides:
487
+ // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
488
+ if (!dryRun) {
489
+ for (const mem of importData.memories) {
490
+ const newId = idMap.get(mem.id);
491
+ if (!newId || !mem.supersedes)
492
+ continue;
493
+ const supersededNew = idMap.get(mem.supersedes);
494
+ if (supersededNew) {
495
+ try {
496
+ await store.supersedeEngram(supersededNew, newId);
497
+ }
498
+ catch { /* best-effort */ }
499
+ }
500
+ }
501
+ }
502
+ // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
444
503
  let assocImported = 0;
445
- const associations = importData.associations ?? [];
446
- for (const assoc of associations) {
447
- const fromId = idMap.get(assoc.from_id);
448
- const toId = idMap.get(assoc.to_id);
504
+ for (const a of (importData.associations ?? [])) {
505
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
449
506
  if (!fromId || !toId)
450
- continue; // skip if either memory was skipped
507
+ continue;
451
508
  if (!dryRun) {
452
- insertAssoc.run(randomUUID(), fromId, toId, assoc.weight ?? 0.5, assoc.type ?? 'hebbian', assoc.activation_count ?? 0);
509
+ try {
510
+ await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5);
511
+ }
512
+ catch { /* best-effort */ }
453
513
  }
454
514
  assocImported++;
455
515
  }
456
- return assocImported;
457
- });
458
- const assocCount = importTx();
459
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
460
- console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
461
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
462
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
463
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
464
- db.close();
516
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
517
+ console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
518
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
519
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
520
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
521
+ ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
522
+ }
523
+ finally {
524
+ await close();
525
+ }
465
526
  }
466
527
  // ─── MERGE ──────────────────────────────────────
467
528
  async function mergeMemories() {
468
529
  const Database = (await import('better-sqlite3')).default;
530
+ const { EngramStore } = await import('./storage/sqlite.js');
469
531
  const { createHash, randomUUID } = await import('node:crypto');
470
532
  let target = '';
471
533
  const sources = [];
@@ -512,27 +574,22 @@ async function mergeMemories() {
512
574
  return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
513
575
  }
514
576
  console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
515
- const targetDb = new Database(target);
516
- targetDb.pragma('journal_mode = WAL');
517
- targetDb.pragma('foreign_keys = ON');
518
- // Ensure tables exist in target
519
- targetDb.exec(`
520
- CREATE TABLE IF NOT EXISTS engrams (
521
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
522
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
523
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
524
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
525
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
526
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
527
- );
528
- CREATE TABLE IF NOT EXISTS associations (
529
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
530
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
531
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
532
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
533
- );
534
- `);
535
- // Build dedupe hash set from existing target memories
577
+ // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
578
+ // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
579
+ // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
580
+ const store = new EngramStore(target);
581
+ const targetDb = store.db; // the store's better-sqlite3 handle
582
+ const blobToArr = (b) => {
583
+ const buf = b;
584
+ return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
585
+ };
586
+ const parseJson = (s, fallback) => { try {
587
+ return s ? JSON.parse(String(s)) : fallback;
588
+ }
589
+ catch {
590
+ return fallback;
591
+ } };
592
+ // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
536
593
  const existingHashes = new Set();
537
594
  if (dedupe) {
538
595
  const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
@@ -540,33 +597,27 @@ async function mergeMemories() {
540
597
  existingHashes.add(contentHash(row.concept, row.content));
541
598
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
542
599
  }
543
- const insertEngram = targetDb.prepare(`
544
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
545
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
546
- retracted, retracted_by, retracted_at, tags)
547
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
548
- `);
549
600
  const insertAssoc = targetDb.prepare(`
550
601
  INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
551
602
  activation_count, created_at, last_activated)
552
603
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
553
604
  `);
554
605
  let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
555
- for (const sourcePath of sources) {
556
- if (!existsSync(sourcePath)) {
557
- console.error(` Source not found: ${sourcePath}`);
558
- continue;
559
- }
560
- const sourceDb = new Database(sourcePath, { readonly: true });
561
- const engrams = sourceDb.prepare(`SELECT id, agent_id, concept, content, confidence, salience, access_count,
562
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
563
- retracted, retracted_by, retracted_at, tags FROM engrams`).all();
564
- const assocs = sourceDb.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
565
- activation_count, created_at, last_activated FROM associations`).all();
566
- const idMap = new Map();
567
- const skippedIds = new Set();
568
- const result = targetDb.transaction(() => {
569
- let imported = 0, skipped = 0;
606
+ try {
607
+ for (const sourcePath of sources) {
608
+ if (!existsSync(sourcePath)) {
609
+ console.error(` Source not found: ${sourcePath}`);
610
+ continue;
611
+ }
612
+ const sourceDb = new Database(sourcePath, { readonly: true });
613
+ // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
614
+ // and createEngram fills the default.
615
+ const engrams = sourceDb.prepare('SELECT * FROM engrams').all();
616
+ const assocs = sourceDb.prepare('SELECT * FROM associations').all();
617
+ sourceDb.close(); // reads done — release the source handle before any (throwing) write work
618
+ const idMap = new Map();
619
+ const skippedIds = new Set();
620
+ let imported = 0, skipped = 0, assocImported = 0;
570
621
  for (const e of engrams) {
571
622
  const hash = contentHash(e.concept, e.content);
572
623
  if (dedupe && existingHashes.has(hash)) {
@@ -574,15 +625,63 @@ async function mergeMemories() {
574
625
  skipped++;
575
626
  continue;
576
627
  }
577
- const newId = randomUUID();
578
- idMap.set(e.id, newId);
579
628
  existingHashes.add(hash);
580
- if (!dryRun) {
581
- 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);
629
+ if (dryRun) {
630
+ idMap.set(e.id, e.id);
631
+ imported++;
632
+ continue;
633
+ }
634
+ // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
635
+ // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
636
+ const created = store.createEngram({
637
+ agentId: remapAgentId(e.agent_id),
638
+ concept: e.concept, content: e.content,
639
+ embedding: blobToArr(e.embedding),
640
+ confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
641
+ salienceFeatures: parseJson(e.salience_features, undefined),
642
+ reasonCodes: parseJson(e.reason_codes, undefined),
643
+ tags: parseJson(e.tags, []),
644
+ memoryClass: e.memory_class ?? 'working',
645
+ memoryType: e.memory_type ?? undefined,
646
+ episodeId: e.episode_id ?? undefined,
647
+ taskStatus: e.task_status ?? undefined,
648
+ taskPriority: e.task_priority ?? undefined,
649
+ blockedBy: e.blocked_by ?? undefined,
650
+ ttl: e.ttl ?? undefined,
651
+ sequence: e.sequence ?? undefined,
652
+ references: parseJson(e.references_json, undefined),
653
+ });
654
+ idMap.set(e.id, created.id);
655
+ // preserve stage + retracted (createEngram always mints active/non-retracted)
656
+ if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') {
657
+ try {
658
+ store.updateStage(created.id, e.stage);
659
+ }
660
+ catch { /* */ }
661
+ }
662
+ if (e.retracted) {
663
+ try {
664
+ store.retractEngram(created.id, e.retracted_by ?? null);
665
+ }
666
+ catch { /* */ }
582
667
  }
583
668
  imported++;
584
669
  }
585
- let assocImported = 0;
670
+ // second pass — re-link supersession with remapped ids
671
+ if (!dryRun) {
672
+ for (const e of engrams) {
673
+ const newId = idMap.get(e.id);
674
+ if (!newId || !e.supersedes)
675
+ continue;
676
+ const supNew = idMap.get(e.supersedes);
677
+ if (supNew) {
678
+ try {
679
+ store.supersedeEngram(supNew, newId);
680
+ }
681
+ catch { /* */ }
682
+ }
683
+ }
684
+ }
586
685
  for (const a of assocs) {
587
686
  if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
588
687
  continue;
@@ -591,23 +690,29 @@ async function mergeMemories() {
591
690
  if (!fromId || !toId)
592
691
  continue;
593
692
  if (!dryRun) {
594
- insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
693
+ try {
694
+ insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
695
+ }
696
+ catch { /* skip an association whose source row has an unbindable/undefined column */ }
595
697
  }
596
698
  assocImported++;
597
699
  }
598
- return { imported, skipped, assocImported };
599
- })();
600
- sourceDb.close();
601
- const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
602
- console.log(` Source: ${sourcePath}`);
603
- console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
604
- console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
605
- console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
606
- totalMemories += result.imported;
607
- totalAssociations += result.assocImported;
608
- totalSkipped += result.skipped;
609
- }
610
- targetDb.close();
700
+ const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
701
+ console.log(` Source: ${sourcePath}`);
702
+ console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
703
+ console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
704
+ console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
705
+ totalMemories += imported;
706
+ totalAssociations += assocImported;
707
+ totalSkipped += skipped;
708
+ }
709
+ }
710
+ finally {
711
+ try {
712
+ store.close();
713
+ }
714
+ catch { /* */ }
715
+ }
611
716
  console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
612
717
  if (dryRun)
613
718
  console.log('(dry run — no data written)');