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/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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
433
|
-
if (existingHashes.has(
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
//
|
|
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
|
|
450
|
-
|
|
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;
|
|
507
|
+
continue;
|
|
455
508
|
if (!dryRun) {
|
|
456
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
targetDb.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 (
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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)');
|