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/src/cli.ts CHANGED
@@ -245,6 +245,29 @@ function health() {
245
245
  }
246
246
  }
247
247
 
248
+ // ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
249
+ //
250
+ // export/import route through openStore() so they work on ANY backend (SQLite,
251
+ // PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
252
+ // (a SQLite file or PGlite dir, by shape); for a Postgres target set
253
+ // AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
254
+ // port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
255
+
256
+ function toISOStr(d: Date | string | null | undefined): string | null {
257
+ if (d == null) return null;
258
+ return d instanceof Date ? d.toISOString() : String(d);
259
+ }
260
+
261
+ async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
262
+ // --db sets the path only when the env doesn't already select a backend/path.
263
+ if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
264
+ process.env.AWM_DB_PATH = dbPath;
265
+ }
266
+ const { openStore } = await import('./storage/factory.js');
267
+ const { store, backend } = await openStore();
268
+ return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
269
+ }
270
+
248
271
  // ─── EXPORT ──────────────────────────────────────
249
272
 
250
273
  async function exportMemories() {
@@ -252,113 +275,107 @@ async function exportMemories() {
252
275
  let agentFilter: string | null = null;
253
276
  let outputPath: string | null = null;
254
277
  let activeOnly = false;
278
+ let allStages = false;
279
+ let includeRetracted = false;
255
280
 
256
281
  for (let i = 1; i < args.length; i++) {
257
282
  if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
258
283
  else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
259
284
  else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
260
285
  else if (args[i] === '--active-only') activeOnly = true;
286
+ else if (args[i] === '--all-stages') allStages = true;
287
+ else if (args[i] === '--include-retracted') includeRetracted = true;
261
288
  }
262
289
 
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);
271
- }
272
-
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');
290
+ // --db must exist for a file/dir backend; a Postgres source is selected by env instead.
291
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
292
+ if (!usingPostgres) {
293
+ if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
294
+ if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
288
295
  }
289
296
 
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,
297
+ const { store, backend, close } = await openCliStore(dbPath);
298
+ try {
299
+ const agentIds: string[] = agentFilter
300
+ ? [agentFilter]
301
+ : ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
302
+ if (agentIds.length === 0) {
303
+ console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
304
+ }
305
+ // Default to the meaningful memory set (active stage, non-retracted). --all-stages
306
+ // widens to every stage; --include-retracted adds retracted (off with --active-only).
307
+ const stage = allStages ? undefined : 'active';
308
+ const wantRetracted = includeRetracted && !activeOnly;
309
+ const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
310
+
311
+ const memories = engrams.map((e: any) => ({
312
+ id: e.id,
313
+ agent_id: e.agentId,
314
+ concept: e.concept,
315
+ content: e.content,
316
+ // Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
317
+ // re-embed after import) → a faithful, recall-ready port when source/target embed
318
+ // models match. import skips them with --no-embeddings (then re-embed).
319
+ embedding: Array.isArray(e.embedding) ? e.embedding : null,
320
+ confidence: e.confidence,
321
+ salience: e.salience,
322
+ access_count: e.accessCount ?? 0,
323
+ last_accessed: toISOStr(e.lastAccessed),
324
+ created_at: toISOStr(e.createdAt),
325
+ stage: e.stage ?? 'active',
326
+ tags: Array.isArray(e.tags) ? e.tags : [],
327
+ memory_class: e.memoryClass ?? 'working',
328
+ memory_type: e.memoryType ?? 'unclassified',
329
+ episode_id: e.episodeId ?? null,
330
+ task_status: e.taskStatus ?? null,
331
+ task_priority: e.taskPriority ?? null,
332
+ supersedes: e.supersedes ?? null,
333
+ superseded_by: e.supersededBy ?? null,
334
+ retracted: e.retracted ? 1 : 0,
333
335
  }));
334
336
 
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);
337
+ const memIds = new Set(memories.map((m) => m.id));
338
+ const seen = new Set<string>();
339
+ const associations: any[] = [];
340
+ for (const aid of agentIds) {
341
+ for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
342
+ if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
343
+ const k = `${a.fromEngramId}>${a.toEngramId}`;
344
+ if (seen.has(k)) continue;
345
+ seen.add(k);
346
+ associations.push({
347
+ from_id: a.fromEngramId, to_id: a.toEngramId,
348
+ weight: a.weight, type: a.type ?? 'hebbian',
349
+ activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
350
+ });
351
+ }
352
+ }
353
353
 
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');
354
+ const exportData = {
355
+ version: '0.9.2',
356
+ exported_at: new Date().toISOString(),
357
+ source_backend: backend,
358
+ agent_filter: agentFilter,
359
+ embedding_model: process.env.AWM_EMBED_MODEL ?? null,
360
+ memories,
361
+ associations,
362
+ stats: {
363
+ total_memories: memories.length,
364
+ total_associations: associations.length,
365
+ agents: [...new Set(memories.map((m) => m.agent_id))],
366
+ },
367
+ };
368
+
369
+ const json = JSON.stringify(exportData, null, 2);
370
+ if (outputPath) {
371
+ writeFileSync(outputPath, json + '\n');
372
+ console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
373
+ } else {
374
+ process.stdout.write(json + '\n');
375
+ }
376
+ } finally {
377
+ await close();
359
378
  }
360
-
361
- db.close();
362
379
  }
363
380
 
364
381
  // ─── IMPORT ──────────────────────────────────────
@@ -381,12 +398,17 @@ async function importMemories() {
381
398
  else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
382
399
  }
383
400
 
401
+ // --no-embeddings: skip importing embedding vectors (use when source/target embed models
402
+ // differ → import without, then re-embed). Parsed alongside the existing flags above.
403
+ const noEmbeddings = args.includes('--no-embeddings');
404
+
384
405
  if (!filePath) {
385
406
  console.error('Error: <file> is required');
386
407
  process.exit(1);
387
408
  }
388
- if (!dbPath) {
389
- console.error('Error: --db <path> is required');
409
+ const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
410
+ if (!dbPath && !usingPostgres) {
411
+ console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
390
412
  process.exit(1);
391
413
  }
392
414
  if (!existsSync(filePath)) {
@@ -400,130 +422,100 @@ async function importMemories() {
400
422
  process.exit(1);
401
423
  }
402
424
 
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);
425
+ const { store, backend, close } = await openCliStore(dbPath);
426
+ try {
427
+ // Dedupe against existing memories for the TARGET agent(s).
428
+ const existingHashes = new Set<string>();
429
+ if (dedupe) {
430
+ const targetAgents = remapAgent
431
+ ? [remapAgent]
432
+ : [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
433
+ for (const aid of targetAgents) {
434
+ for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
435
+ existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
436
+ }
437
+ }
433
438
  }
434
- }
435
- const idMap = new Map<string, string>();
436
- let imported = 0;
437
- let skippedDupes = 0;
438
- let skippedRetracted = 0;
439
-
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
439
 
447
- const insertAssoc = db.prepare(`
448
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
449
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
450
- `);
440
+ const idMap = new Map<string, string>(); // old export id → new store id
441
+ let imported = 0, skippedDupes = 0, skippedRetracted = 0;
451
442
 
452
- const importTx = db.transaction(() => {
453
- // Import memories
443
+ // Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
444
+ // createdAt/accessCount normalize to import time (the contract's createEngram stamps
445
+ // them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
446
+ // embedding) is preserved, so recall is faithful.
454
447
  for (const mem of importData.memories) {
455
- // Skip retracted unless --include-retracted
456
- if (mem.retracted && !includeRetracted) {
457
- skippedRetracted++;
458
- continue;
459
- }
460
-
461
- // Dedupe check
448
+ if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
462
449
  if (dedupe) {
463
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
464
- if (existingHashes.has(hash)) {
465
- skippedDupes++;
466
- continue;
467
- }
450
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
451
+ if (existingHashes.has(h)) { skippedDupes++; continue; }
452
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
468
453
  }
469
-
470
- const newId = randomUUID();
471
- idMap.set(mem.id, newId);
472
-
473
- const agentId = remapAgent ?? mem.agent_id;
474
- const tags = Array.isArray(mem.tags) ? JSON.stringify(mem.tags) : (mem.tags ?? '[]');
475
-
476
- if (!dryRun) {
477
- insertMem.run(
478
- newId, agentId, mem.concept, mem.content,
479
- mem.confidence ?? 0.5, mem.salience ?? 0.5,
480
- mem.access_count ?? 0, mem.last_accessed ?? mem.created_at,
481
- mem.created_at, mem.stage ?? 'active', tags,
482
- mem.memory_class ?? 'working', mem.episode_id ?? null,
483
- mem.task_status ?? null, mem.task_priority ?? null,
484
- mem.supersedes ?? null, mem.superseded_by ?? null,
485
- mem.retracted ?? 0
486
- );
454
+ // dry-run: still map the id so the association-count preview isn't always 0
455
+ if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
456
+ const created = await store.createEngram({
457
+ agentId: remapAgent ?? mem.agent_id,
458
+ concept: mem.concept,
459
+ content: mem.content,
460
+ tags: Array.isArray(mem.tags) ? mem.tags : [],
461
+ embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
462
+ confidence: mem.confidence ?? 0.5,
463
+ salience: mem.salience ?? 0.5,
464
+ memoryClass: mem.memory_class ?? 'working',
465
+ memoryType: mem.memory_type ?? undefined,
466
+ episodeId: mem.episode_id ?? undefined,
467
+ taskStatus: mem.task_status ?? undefined,
468
+ taskPriority: mem.task_priority ?? undefined,
469
+ });
470
+ idMap.set(mem.id, created.id);
471
+ // Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
472
+ // without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
473
+ // non-active stage (staging/consolidated/archived/fading) silently flattens to active.
474
+ if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
475
+ try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
476
+ }
477
+ if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
478
+ try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
487
479
  }
488
480
  imported++;
489
481
  }
490
482
 
491
- // Import associations (using remapped IDs)
492
- let assocImported = 0;
493
- const associations = importData.associations ?? [];
494
- for (const assoc of associations) {
495
- const fromId = idMap.get(assoc.from_id);
496
- const toId = idMap.get(assoc.to_id);
497
- if (!fromId || !toId) continue; // skip if either memory was skipped
498
-
499
- if (!dryRun) {
500
- insertAssoc.run(
501
- randomUUID(), fromId, toId,
502
- assoc.weight ?? 0.5, assoc.type ?? 'hebbian',
503
- assoc.activation_count ?? 0
504
- );
483
+ // Pass 2 re-link supersession with remapped ids (supersedeEngram sets both sides:
484
+ // old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
485
+ if (!dryRun) {
486
+ for (const mem of importData.memories) {
487
+ const newId = idMap.get(mem.id);
488
+ if (!newId || !mem.supersedes) continue;
489
+ const supersededNew = idMap.get(mem.supersedes);
490
+ if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
505
491
  }
506
- assocImported++;
507
492
  }
508
493
 
509
- return assocImported;
510
- });
511
-
512
- const assocCount = importTx();
513
-
514
- const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
515
- console.log(`${prefix} ${imported} memories, ${assocCount} associations` +
516
- (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
517
- (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
518
- (remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
494
+ // Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
495
+ let assocImported = 0;
496
+ for (const a of (importData.associations ?? [])) {
497
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
498
+ if (!fromId || !toId) continue;
499
+ if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
500
+ assocImported++;
501
+ }
519
502
 
520
- db.close();
503
+ const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
504
+ console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
505
+ (skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
506
+ (skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
507
+ (remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
508
+ ` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
509
+ } finally {
510
+ await close();
511
+ }
521
512
  }
522
513
 
523
514
  // ─── MERGE ──────────────────────────────────────
524
515
 
525
516
  async function mergeMemories() {
526
517
  const Database = (await import('better-sqlite3')).default;
518
+ const { EngramStore } = await import('./storage/sqlite.js');
527
519
  const { createHash, randomUUID } = await import('node:crypto');
528
520
 
529
521
  let target = '';
@@ -570,29 +562,19 @@ async function mergeMemories() {
570
562
 
571
563
  console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
572
564
 
573
- const targetDb = new Database(target);
574
- targetDb.pragma('journal_mode = WAL');
575
- targetDb.pragma('foreign_keys = ON');
576
-
577
- // Ensure tables exist in target
578
- targetDb.exec(`
579
- CREATE TABLE IF NOT EXISTS engrams (
580
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, concept TEXT NOT NULL, content TEXT NOT NULL,
581
- embedding BLOB, confidence REAL NOT NULL DEFAULT 0.5, salience REAL NOT NULL DEFAULT 0.5,
582
- access_count INTEGER NOT NULL DEFAULT 0, last_accessed TEXT NOT NULL, created_at TEXT NOT NULL,
583
- salience_features TEXT NOT NULL DEFAULT '{}', reason_codes TEXT NOT NULL DEFAULT '[]',
584
- stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
585
- retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
586
- );
587
- CREATE TABLE IF NOT EXISTS associations (
588
- id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
589
- weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
590
- type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
591
- created_at TEXT NOT NULL, last_activated TEXT NOT NULL
592
- );
593
- `);
565
+ // Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
566
+ // triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
567
+ // task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
568
+ const store: any = new EngramStore(target);
569
+ const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
570
+
571
+ const blobToArr = (b: unknown): number[] | undefined => {
572
+ const buf = b as Buffer | null | undefined;
573
+ return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
574
+ };
575
+ const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
594
576
 
595
- // Build dedupe hash set from existing target memories
577
+ // Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
596
578
  const existingHashes = new Set<string>();
597
579
  if (dedupe) {
598
580
  const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
@@ -600,12 +582,6 @@ async function mergeMemories() {
600
582
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
601
583
  }
602
584
 
603
- const insertEngram = targetDb.prepare(`
604
- INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
605
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
606
- retracted, retracted_by, retracted_at, tags)
607
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
608
- `);
609
585
  const insertAssoc = targetDb.prepare(`
610
586
  INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
611
587
  activation_count, created_at, last_activated)
@@ -614,6 +590,7 @@ async function mergeMemories() {
614
590
 
615
591
  let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
616
592
 
593
+ try {
617
594
  for (const sourcePath of sources) {
618
595
  if (!existsSync(sourcePath)) {
619
596
  console.error(` Source not found: ${sourcePath}`);
@@ -621,63 +598,84 @@ async function mergeMemories() {
621
598
  }
622
599
 
623
600
  const sourceDb = new Database(sourcePath, { readonly: true });
624
- const engrams = sourceDb.prepare(
625
- `SELECT id, agent_id, concept, content, confidence, salience, access_count,
626
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
627
- retracted, retracted_by, retracted_at, tags FROM engrams`
628
- ).all() as any[];
629
- const assocs = sourceDb.prepare(
630
- `SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
631
- activation_count, created_at, last_activated FROM associations`
632
- ).all() as any[];
601
+ // SELECT * is robust to older source schemas — a column a source predates just reads back undefined
602
+ // and createEngram fills the default.
603
+ const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
604
+ const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
605
+ sourceDb.close(); // reads done — release the source handle before any (throwing) write work
633
606
 
634
607
  const idMap = new Map<string, string>();
635
608
  const skippedIds = new Set<string>();
609
+ let imported = 0, skipped = 0, assocImported = 0;
636
610
 
637
- const result = targetDb.transaction(() => {
638
- let imported = 0, skipped = 0;
611
+ for (const e of engrams) {
612
+ const hash = contentHash(e.concept, e.content);
613
+ if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
614
+ existingHashes.add(hash);
615
+ if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
616
+ // Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
617
+ // memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
618
+ const created = store.createEngram({
619
+ agentId: remapAgentId(e.agent_id),
620
+ concept: e.concept, content: e.content,
621
+ embedding: blobToArr(e.embedding),
622
+ confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
623
+ salienceFeatures: parseJson(e.salience_features, undefined),
624
+ reasonCodes: parseJson(e.reason_codes, undefined),
625
+ tags: parseJson<string[]>(e.tags, []),
626
+ memoryClass: e.memory_class ?? 'working',
627
+ memoryType: e.memory_type ?? undefined,
628
+ episodeId: e.episode_id ?? undefined,
629
+ taskStatus: e.task_status ?? undefined,
630
+ taskPriority: e.task_priority ?? undefined,
631
+ blockedBy: e.blocked_by ?? undefined,
632
+ ttl: e.ttl ?? undefined,
633
+ sequence: e.sequence ?? undefined,
634
+ references: parseJson(e.references_json, undefined),
635
+ });
636
+ idMap.set(e.id, created.id);
637
+ // preserve stage + retracted (createEngram always mints active/non-retracted)
638
+ if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
639
+ if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
640
+ imported++;
641
+ }
642
+ // second pass — re-link supersession with remapped ids
643
+ if (!dryRun) {
639
644
  for (const e of engrams) {
640
- const hash = contentHash(e.concept, e.content);
641
- if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
642
- const newId = randomUUID();
643
- idMap.set(e.id, newId);
644
- existingHashes.add(hash);
645
- if (!dryRun) {
646
- insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence,
647
- e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features,
648
- e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
649
- }
650
- imported++;
645
+ const newId = idMap.get(e.id);
646
+ if (!newId || !e.supersedes) continue;
647
+ const supNew = idMap.get(e.supersedes);
648
+ if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
651
649
  }
652
- let assocImported = 0;
653
- for (const a of assocs) {
654
- if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
655
- const fromId = idMap.get(a.from_engram_id);
656
- const toId = idMap.get(a.to_engram_id);
657
- if (!fromId || !toId) continue;
658
- if (!dryRun) {
650
+ }
651
+ for (const a of assocs) {
652
+ if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
653
+ const fromId = idMap.get(a.from_engram_id);
654
+ const toId = idMap.get(a.to_engram_id);
655
+ if (!fromId || !toId) continue;
656
+ if (!dryRun) {
657
+ try {
659
658
  insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
660
659
  a.activation_count, a.created_at, a.last_activated);
661
- }
662
- assocImported++;
660
+ } catch { /* skip an association whose source row has an unbindable/undefined column */ }
663
661
  }
664
- return { imported, skipped, assocImported };
665
- })();
666
-
667
- sourceDb.close();
662
+ assocImported++;
663
+ }
668
664
 
669
665
  const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
670
666
  console.log(` Source: ${sourcePath}`);
671
- console.log(` Engrams: ${engrams.length} total, ${result.imported} imported, ${result.skipped} skipped`);
672
- console.log(` Associations: ${assocs.length} total, ${result.assocImported} imported`);
667
+ console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
668
+ console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
673
669
  console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
674
670
 
675
- totalMemories += result.imported;
676
- totalAssociations += result.assocImported;
677
- totalSkipped += result.skipped;
671
+ totalMemories += imported;
672
+ totalAssociations += assocImported;
673
+ totalSkipped += skipped;
678
674
  }
679
675
 
680
- targetDb.close();
676
+ } finally {
677
+ try { store.close(); } catch { /* */ }
678
+ }
681
679
  console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
682
680
  if (dryRun) console.log('(dry run — no data written)');
683
681
  }