agent-working-memory 0.9.1 → 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.
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,105 +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
- // 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
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.
424
435
  for (const mem of importData.memories) {
425
- // Skip retracted unless --include-retracted
426
436
  if (mem.retracted && !includeRetracted) {
427
437
  skippedRetracted++;
428
438
  continue;
429
439
  }
430
- // Dedupe check
431
440
  if (dedupe) {
432
- const hash = (mem.concept ?? '').toLowerCase().trim() + '||' + (mem.content ?? '').toLowerCase().trim();
433
- if (existingHashes.has(hash)) {
441
+ const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
442
+ if (existingHashes.has(h)) {
434
443
  skippedDupes++;
435
444
  continue;
436
445
  }
446
+ existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
437
447
  }
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);
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 */ }
444
483
  }
445
484
  imported++;
446
485
  }
447
- // 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.
448
503
  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);
504
+ for (const a of (importData.associations ?? [])) {
505
+ const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
453
506
  if (!fromId || !toId)
454
- continue; // skip if either memory was skipped
507
+ continue;
455
508
  if (!dryRun) {
456
- 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 */ }
457
513
  }
458
514
  assocImported++;
459
515
  }
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();
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
+ }
469
526
  }
470
527
  // ─── MERGE ──────────────────────────────────────
471
528
  async function mergeMemories() {
472
529
  const Database = (await import('better-sqlite3')).default;
530
+ const { EngramStore } = await import('./storage/sqlite.js');
473
531
  const { createHash, randomUUID } = await import('node:crypto');
474
532
  let target = '';
475
533
  const sources = [];
@@ -516,27 +574,22 @@ async function mergeMemories() {
516
574
  return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
517
575
  }
518
576
  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
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)
540
593
  const existingHashes = new Set();
541
594
  if (dedupe) {
542
595
  const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
@@ -544,33 +597,27 @@ async function mergeMemories() {
544
597
  existingHashes.add(contentHash(row.concept, row.content));
545
598
  console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
546
599
  }
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
600
  const insertAssoc = targetDb.prepare(`
554
601
  INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
555
602
  activation_count, created_at, last_activated)
556
603
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
557
604
  `);
558
605
  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;
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;
574
621
  for (const e of engrams) {
575
622
  const hash = contentHash(e.concept, e.content);
576
623
  if (dedupe && existingHashes.has(hash)) {
@@ -578,15 +625,63 @@ async function mergeMemories() {
578
625
  skipped++;
579
626
  continue;
580
627
  }
581
- const newId = randomUUID();
582
- idMap.set(e.id, newId);
583
628
  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);
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 { /* */ }
586
667
  }
587
668
  imported++;
588
669
  }
589
- 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
+ }
590
685
  for (const a of assocs) {
591
686
  if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
592
687
  continue;
@@ -595,23 +690,29 @@ async function mergeMemories() {
595
690
  if (!fromId || !toId)
596
691
  continue;
597
692
  if (!dryRun) {
598
- 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 */ }
599
697
  }
600
698
  assocImported++;
601
699
  }
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();
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
+ }
615
716
  console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
616
717
  if (dryRun)
617
718
  console.log('(dry run — no data written)');