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 +324 -223
- package/dist/cli.js.map +1 -1
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +10 -1
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +5 -1
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/storage/factory.d.ts +1 -1
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +16 -2
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +8 -0
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +228 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +1221 -0
- package/dist/storage/postgres.js.map +1 -0
- package/package.json +3 -1
- package/src/cli.ts +266 -272
- package/src/core/salience.ts +10 -1
- package/src/core/write-pipeline.ts +5 -1
- package/src/storage/factory.ts +15 -3
- package/src/storage/pglite.ts +9 -0
- package/src/storage/postgres.ts +1475 -0
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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,134 +422,100 @@ async function importMemories() {
|
|
|
400
422
|
process.exit(1);
|
|
401
423
|
}
|
|
402
424
|
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
// memories+associations in ONE transaction, that rolls back the memories too (silent
|
|
450
|
-
// "empty store"). Set it alongside created_at. (migrate/merge paths already do this.)
|
|
451
|
-
const insertAssoc = db.prepare(`
|
|
452
|
-
INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at, last_activated)
|
|
453
|
-
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
454
|
-
`);
|
|
440
|
+
const idMap = new Map<string, string>(); // old export id → new store id
|
|
441
|
+
let imported = 0, skippedDupes = 0, skippedRetracted = 0;
|
|
455
442
|
|
|
456
|
-
|
|
457
|
-
//
|
|
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.
|
|
458
447
|
for (const mem of importData.memories) {
|
|
459
|
-
|
|
460
|
-
if (mem.retracted && !includeRetracted) {
|
|
461
|
-
skippedRetracted++;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Dedupe check
|
|
448
|
+
if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
|
|
466
449
|
if (dedupe) {
|
|
467
|
-
const
|
|
468
|
-
if (existingHashes.has(
|
|
469
|
-
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
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
|
|
472
453
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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 */ }
|
|
491
479
|
}
|
|
492
480
|
imported++;
|
|
493
481
|
}
|
|
494
482
|
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (!dryRun) {
|
|
504
|
-
insertAssoc.run(
|
|
505
|
-
randomUUID(), fromId, toId,
|
|
506
|
-
assoc.weight ?? 0.5, assoc.type ?? 'hebbian',
|
|
507
|
-
assoc.activation_count ?? 0
|
|
508
|
-
);
|
|
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 */ } }
|
|
509
491
|
}
|
|
510
|
-
assocImported++;
|
|
511
492
|
}
|
|
512
493
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
522
|
-
(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
|
+
}
|
|
523
502
|
|
|
524
|
-
|
|
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
|
+
}
|
|
525
512
|
}
|
|
526
513
|
|
|
527
514
|
// ─── MERGE ──────────────────────────────────────
|
|
528
515
|
|
|
529
516
|
async function mergeMemories() {
|
|
530
517
|
const Database = (await import('better-sqlite3')).default;
|
|
518
|
+
const { EngramStore } = await import('./storage/sqlite.js');
|
|
531
519
|
const { createHash, randomUUID } = await import('node:crypto');
|
|
532
520
|
|
|
533
521
|
let target = '';
|
|
@@ -574,29 +562,19 @@ async function mergeMemories() {
|
|
|
574
562
|
|
|
575
563
|
console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
|
|
576
564
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
stage TEXT NOT NULL DEFAULT 'active', ttl INTEGER, retracted INTEGER NOT NULL DEFAULT 0,
|
|
589
|
-
retracted_by TEXT, retracted_at TEXT, tags TEXT NOT NULL DEFAULT '[]'
|
|
590
|
-
);
|
|
591
|
-
CREATE TABLE IF NOT EXISTS associations (
|
|
592
|
-
id TEXT PRIMARY KEY, from_engram_id TEXT NOT NULL, to_engram_id TEXT NOT NULL,
|
|
593
|
-
weight REAL NOT NULL DEFAULT 0.1, confidence REAL NOT NULL DEFAULT 0.5,
|
|
594
|
-
type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
|
|
595
|
-
created_at TEXT NOT NULL, last_activated TEXT NOT NULL
|
|
596
|
-
);
|
|
597
|
-
`);
|
|
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; } };
|
|
598
576
|
|
|
599
|
-
// 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)
|
|
600
578
|
const existingHashes = new Set<string>();
|
|
601
579
|
if (dedupe) {
|
|
602
580
|
const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
|
|
@@ -604,12 +582,6 @@ async function mergeMemories() {
|
|
|
604
582
|
console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
|
|
605
583
|
}
|
|
606
584
|
|
|
607
|
-
const insertEngram = targetDb.prepare(`
|
|
608
|
-
INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
|
|
609
|
-
last_accessed, created_at, salience_features, reason_codes, stage, ttl,
|
|
610
|
-
retracted, retracted_by, retracted_at, tags)
|
|
611
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
612
|
-
`);
|
|
613
585
|
const insertAssoc = targetDb.prepare(`
|
|
614
586
|
INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
615
587
|
activation_count, created_at, last_activated)
|
|
@@ -618,6 +590,7 @@ async function mergeMemories() {
|
|
|
618
590
|
|
|
619
591
|
let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
|
|
620
592
|
|
|
593
|
+
try {
|
|
621
594
|
for (const sourcePath of sources) {
|
|
622
595
|
if (!existsSync(sourcePath)) {
|
|
623
596
|
console.error(` Source not found: ${sourcePath}`);
|
|
@@ -625,63 +598,84 @@ async function mergeMemories() {
|
|
|
625
598
|
}
|
|
626
599
|
|
|
627
600
|
const sourceDb = new Database(sourcePath, { readonly: true });
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const assocs = sourceDb.prepare(
|
|
634
|
-
`SELECT id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
635
|
-
activation_count, created_at, last_activated FROM associations`
|
|
636
|
-
).all() as any[];
|
|
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
|
|
637
606
|
|
|
638
607
|
const idMap = new Map<string, string>();
|
|
639
608
|
const skippedIds = new Set<string>();
|
|
609
|
+
let imported = 0, skipped = 0, assocImported = 0;
|
|
640
610
|
|
|
641
|
-
const
|
|
642
|
-
|
|
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) {
|
|
643
644
|
for (const e of engrams) {
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
existingHashes.add(hash);
|
|
649
|
-
if (!dryRun) {
|
|
650
|
-
insertEngram.run(newId, remapAgentId(e.agent_id), e.concept, e.content, e.confidence,
|
|
651
|
-
e.salience, e.access_count, e.last_accessed, e.created_at, e.salience_features,
|
|
652
|
-
e.reason_codes, e.stage, e.ttl, e.retracted, e.retracted_by, e.retracted_at, e.tags);
|
|
653
|
-
}
|
|
654
|
-
imported++;
|
|
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 { /* */ } }
|
|
655
649
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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 {
|
|
663
658
|
insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
|
|
664
659
|
a.activation_count, a.created_at, a.last_activated);
|
|
665
|
-
}
|
|
666
|
-
assocImported++;
|
|
660
|
+
} catch { /* skip an association whose source row has an unbindable/undefined column */ }
|
|
667
661
|
}
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
sourceDb.close();
|
|
662
|
+
assocImported++;
|
|
663
|
+
}
|
|
672
664
|
|
|
673
665
|
const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
|
|
674
666
|
console.log(` Source: ${sourcePath}`);
|
|
675
|
-
console.log(` Engrams: ${engrams.length} total, ${
|
|
676
|
-
console.log(` Associations: ${assocs.length} total, ${
|
|
667
|
+
console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
|
|
668
|
+
console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
|
|
677
669
|
console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
|
|
678
670
|
|
|
679
|
-
totalMemories +=
|
|
680
|
-
totalAssociations +=
|
|
681
|
-
totalSkipped +=
|
|
671
|
+
totalMemories += imported;
|
|
672
|
+
totalAssociations += assocImported;
|
|
673
|
+
totalSkipped += skipped;
|
|
682
674
|
}
|
|
683
675
|
|
|
684
|
-
|
|
676
|
+
} finally {
|
|
677
|
+
try { store.close(); } catch { /* */ }
|
|
678
|
+
}
|
|
685
679
|
console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
|
|
686
680
|
if (dryRun) console.log('(dry run — no data written)');
|
|
687
681
|
}
|