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.
- package/dist/cli/migrate.js +29 -29
- package/dist/cli.js +324 -219
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.js +23 -23
- 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-schema.js +143 -143
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +146 -138
- 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/api/index.ts +3 -3
- package/src/cli/migrate.ts +307 -307
- package/src/cli.ts +266 -268
- package/src/coordination/circuit-breaker.ts +83 -83
- package/src/coordination/failure-modes.ts +50 -50
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -110
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -194
- package/src/core/ml-worker.ts +281 -281
- package/src/core/query-expander.ts +122 -122
- package/src/core/reranker.ts +119 -119
- package/src/core/salience.ts +10 -1
- package/src/core/write-pipeline.ts +5 -1
- package/src/engine/confidence.ts +120 -120
- package/src/engine/consolidation-scheduler.ts +242 -242
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -366
- package/src/engine/staging.ts +74 -74
- package/src/storage/factory.ts +159 -147
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -166
- package/src/storage/pglite.ts +1372 -1363
- package/src/storage/postgres.ts +1475 -0
- package/src/storage/store.ts +80 -80
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { resolve, dirname } from 'node:path';
|
|
15
15
|
import { execSync } from 'node:child_process';
|
|
16
|
-
import { randomUUID } from 'node:crypto';
|
|
17
16
|
import { fileURLToPath } from 'node:url';
|
|
18
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
18
|
const __dirname = dirname(__filename);
|
|
@@ -228,12 +227,38 @@ function health() {
|
|
|
228
227
|
process.exit(1);
|
|
229
228
|
}
|
|
230
229
|
}
|
|
230
|
+
// ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
|
|
231
|
+
//
|
|
232
|
+
// export/import route through openStore() so they work on ANY backend (SQLite,
|
|
233
|
+
// PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
|
|
234
|
+
// (a SQLite file or PGlite dir, by shape); for a Postgres target set
|
|
235
|
+
// AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
|
|
236
|
+
// port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
|
|
237
|
+
function toISOStr(d) {
|
|
238
|
+
if (d == null)
|
|
239
|
+
return null;
|
|
240
|
+
return d instanceof Date ? d.toISOString() : String(d);
|
|
241
|
+
}
|
|
242
|
+
async function openCliStore(dbPath) {
|
|
243
|
+
// --db sets the path only when the env doesn't already select a backend/path.
|
|
244
|
+
if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
|
|
245
|
+
process.env.AWM_DB_PATH = dbPath;
|
|
246
|
+
}
|
|
247
|
+
const { openStore } = await import('./storage/factory.js');
|
|
248
|
+
const { store, backend } = await openStore();
|
|
249
|
+
return { store, backend, close: async () => { try {
|
|
250
|
+
await store.close?.();
|
|
251
|
+
}
|
|
252
|
+
catch { /* */ } } };
|
|
253
|
+
}
|
|
231
254
|
// ─── EXPORT ──────────────────────────────────────
|
|
232
255
|
async function exportMemories() {
|
|
233
256
|
let dbPath = '';
|
|
234
257
|
let agentFilter = null;
|
|
235
258
|
let outputPath = null;
|
|
236
259
|
let activeOnly = false;
|
|
260
|
+
let allStages = false;
|
|
261
|
+
let includeRetracted = false;
|
|
237
262
|
for (let i = 1; i < args.length; i++) {
|
|
238
263
|
if (args[i] === '--db' && args[i + 1])
|
|
239
264
|
dbPath = args[++i];
|
|
@@ -243,93 +268,105 @@ async function exportMemories() {
|
|
|
243
268
|
outputPath = args[++i];
|
|
244
269
|
else if (args[i] === '--active-only')
|
|
245
270
|
activeOnly = true;
|
|
271
|
+
else if (args[i] === '--all-stages')
|
|
272
|
+
allStages = true;
|
|
273
|
+
else if (args[i] === '--include-retracted')
|
|
274
|
+
includeRetracted = true;
|
|
246
275
|
}
|
|
247
|
-
|
|
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,101 +412,122 @@ async function importMemories() {
|
|
|
371
412
|
console.error('Error: invalid export file — missing memories array');
|
|
372
413
|
process.exit(1);
|
|
373
414
|
}
|
|
374
|
-
const
|
|
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
|
-
const insertAssoc = db.prepare(`
|
|
415
|
-
INSERT INTO associations (id, from_engram_id, to_engram_id, weight, type, activation_count, created_at)
|
|
416
|
-
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
417
|
-
`);
|
|
418
|
-
const importTx = db.transaction(() => {
|
|
419
|
-
// Import memories
|
|
415
|
+
const { store, backend, close } = await openCliStore(dbPath);
|
|
416
|
+
try {
|
|
417
|
+
// Dedupe against existing memories for the TARGET agent(s).
|
|
418
|
+
const existingHashes = new Set();
|
|
419
|
+
if (dedupe) {
|
|
420
|
+
const targetAgents = remapAgent
|
|
421
|
+
? [remapAgent]
|
|
422
|
+
: [...new Set(importData.memories.map((m) => m.agent_id))];
|
|
423
|
+
for (const aid of targetAgents) {
|
|
424
|
+
for (const e of (await store.getEngramsByAgent(aid, undefined, true)) ?? []) {
|
|
425
|
+
existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const idMap = new Map(); // old export id → new store id
|
|
430
|
+
let imported = 0, skippedDupes = 0, skippedRetracted = 0;
|
|
431
|
+
// Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
|
|
432
|
+
// createdAt/accessCount normalize to import time (the contract's createEngram stamps
|
|
433
|
+
// them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
|
|
434
|
+
// embedding) is preserved, so recall is faithful.
|
|
420
435
|
for (const mem of importData.memories) {
|
|
421
|
-
// Skip retracted unless --include-retracted
|
|
422
436
|
if (mem.retracted && !includeRetracted) {
|
|
423
437
|
skippedRetracted++;
|
|
424
438
|
continue;
|
|
425
439
|
}
|
|
426
|
-
// Dedupe check
|
|
427
440
|
if (dedupe) {
|
|
428
|
-
const
|
|
429
|
-
if (existingHashes.has(
|
|
441
|
+
const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
|
|
442
|
+
if (existingHashes.has(h)) {
|
|
430
443
|
skippedDupes++;
|
|
431
444
|
continue;
|
|
432
445
|
}
|
|
446
|
+
existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
|
|
433
447
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
448
|
+
// dry-run: still map the id so the association-count preview isn't always 0
|
|
449
|
+
if (dryRun) {
|
|
450
|
+
idMap.set(mem.id, mem.id);
|
|
451
|
+
imported++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const created = await store.createEngram({
|
|
455
|
+
agentId: remapAgent ?? mem.agent_id,
|
|
456
|
+
concept: mem.concept,
|
|
457
|
+
content: mem.content,
|
|
458
|
+
tags: Array.isArray(mem.tags) ? mem.tags : [],
|
|
459
|
+
embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
|
|
460
|
+
confidence: mem.confidence ?? 0.5,
|
|
461
|
+
salience: mem.salience ?? 0.5,
|
|
462
|
+
memoryClass: mem.memory_class ?? 'working',
|
|
463
|
+
memoryType: mem.memory_type ?? undefined,
|
|
464
|
+
episodeId: mem.episode_id ?? undefined,
|
|
465
|
+
taskStatus: mem.task_status ?? undefined,
|
|
466
|
+
taskPriority: mem.task_priority ?? undefined,
|
|
467
|
+
});
|
|
468
|
+
idMap.set(mem.id, created.id);
|
|
469
|
+
// Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
|
|
470
|
+
// without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
|
|
471
|
+
// non-active stage (staging/consolidated/archived/fading) silently flattens to active.
|
|
472
|
+
if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
|
|
473
|
+
try {
|
|
474
|
+
await store.updateStage(created.id, mem.stage);
|
|
475
|
+
}
|
|
476
|
+
catch { /* best-effort */ }
|
|
477
|
+
}
|
|
478
|
+
if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
|
|
479
|
+
try {
|
|
480
|
+
await store.retractEngram(created.id, mem.retracted_by ?? null);
|
|
481
|
+
}
|
|
482
|
+
catch { /* best-effort */ }
|
|
440
483
|
}
|
|
441
484
|
imported++;
|
|
442
485
|
}
|
|
443
|
-
//
|
|
486
|
+
// Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
|
|
487
|
+
// old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
|
|
488
|
+
if (!dryRun) {
|
|
489
|
+
for (const mem of importData.memories) {
|
|
490
|
+
const newId = idMap.get(mem.id);
|
|
491
|
+
if (!newId || !mem.supersedes)
|
|
492
|
+
continue;
|
|
493
|
+
const supersededNew = idMap.get(mem.supersedes);
|
|
494
|
+
if (supersededNew) {
|
|
495
|
+
try {
|
|
496
|
+
await store.supersedeEngram(supersededNew, newId);
|
|
497
|
+
}
|
|
498
|
+
catch { /* best-effort */ }
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
|
|
444
503
|
let assocImported = 0;
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const fromId = idMap.get(assoc.from_id);
|
|
448
|
-
const toId = idMap.get(assoc.to_id);
|
|
504
|
+
for (const a of (importData.associations ?? [])) {
|
|
505
|
+
const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
|
|
449
506
|
if (!fromId || !toId)
|
|
450
|
-
continue;
|
|
507
|
+
continue;
|
|
451
508
|
if (!dryRun) {
|
|
452
|
-
|
|
509
|
+
try {
|
|
510
|
+
await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5);
|
|
511
|
+
}
|
|
512
|
+
catch { /* best-effort */ }
|
|
453
513
|
}
|
|
454
514
|
assocImported++;
|
|
455
515
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
516
|
+
const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
|
|
517
|
+
console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
|
|
518
|
+
(skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
|
|
519
|
+
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
520
|
+
(remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
|
|
521
|
+
` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
await close();
|
|
525
|
+
}
|
|
465
526
|
}
|
|
466
527
|
// ─── MERGE ──────────────────────────────────────
|
|
467
528
|
async function mergeMemories() {
|
|
468
529
|
const Database = (await import('better-sqlite3')).default;
|
|
530
|
+
const { EngramStore } = await import('./storage/sqlite.js');
|
|
469
531
|
const { createHash, randomUUID } = await import('node:crypto');
|
|
470
532
|
let target = '';
|
|
471
533
|
const sources = [];
|
|
@@ -512,27 +574,22 @@ async function mergeMemories() {
|
|
|
512
574
|
return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
|
|
513
575
|
}
|
|
514
576
|
console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
targetDb.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
type TEXT NOT NULL DEFAULT 'hebbian', activation_count INTEGER NOT NULL DEFAULT 0,
|
|
532
|
-
created_at TEXT NOT NULL, last_activated TEXT NOT NULL
|
|
533
|
-
);
|
|
534
|
-
`);
|
|
535
|
-
// Build dedupe hash set from existing target memories
|
|
577
|
+
// Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
|
|
578
|
+
// triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
|
|
579
|
+
// task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
|
|
580
|
+
const store = new EngramStore(target);
|
|
581
|
+
const targetDb = store.db; // the store's better-sqlite3 handle
|
|
582
|
+
const blobToArr = (b) => {
|
|
583
|
+
const buf = b;
|
|
584
|
+
return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
|
|
585
|
+
};
|
|
586
|
+
const parseJson = (s, fallback) => { try {
|
|
587
|
+
return s ? JSON.parse(String(s)) : fallback;
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return fallback;
|
|
591
|
+
} };
|
|
592
|
+
// Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
|
|
536
593
|
const existingHashes = new Set();
|
|
537
594
|
if (dedupe) {
|
|
538
595
|
const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
|
|
@@ -540,33 +597,27 @@ async function mergeMemories() {
|
|
|
540
597
|
existingHashes.add(contentHash(row.concept, row.content));
|
|
541
598
|
console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
|
|
542
599
|
}
|
|
543
|
-
const insertEngram = targetDb.prepare(`
|
|
544
|
-
INSERT OR IGNORE INTO engrams (id, agent_id, concept, content, confidence, salience, access_count,
|
|
545
|
-
last_accessed, created_at, salience_features, reason_codes, stage, ttl,
|
|
546
|
-
retracted, retracted_by, retracted_at, tags)
|
|
547
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
548
|
-
`);
|
|
549
600
|
const insertAssoc = targetDb.prepare(`
|
|
550
601
|
INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
551
602
|
activation_count, created_at, last_activated)
|
|
552
603
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
553
604
|
`);
|
|
554
605
|
let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
let imported = 0, skipped = 0;
|
|
606
|
+
try {
|
|
607
|
+
for (const sourcePath of sources) {
|
|
608
|
+
if (!existsSync(sourcePath)) {
|
|
609
|
+
console.error(` Source not found: ${sourcePath}`);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const sourceDb = new Database(sourcePath, { readonly: true });
|
|
613
|
+
// SELECT * is robust to older source schemas — a column a source predates just reads back undefined
|
|
614
|
+
// and createEngram fills the default.
|
|
615
|
+
const engrams = sourceDb.prepare('SELECT * FROM engrams').all();
|
|
616
|
+
const assocs = sourceDb.prepare('SELECT * FROM associations').all();
|
|
617
|
+
sourceDb.close(); // reads done — release the source handle before any (throwing) write work
|
|
618
|
+
const idMap = new Map();
|
|
619
|
+
const skippedIds = new Set();
|
|
620
|
+
let imported = 0, skipped = 0, assocImported = 0;
|
|
570
621
|
for (const e of engrams) {
|
|
571
622
|
const hash = contentHash(e.concept, e.content);
|
|
572
623
|
if (dedupe && existingHashes.has(hash)) {
|
|
@@ -574,15 +625,63 @@ async function mergeMemories() {
|
|
|
574
625
|
skipped++;
|
|
575
626
|
continue;
|
|
576
627
|
}
|
|
577
|
-
const newId = randomUUID();
|
|
578
|
-
idMap.set(e.id, newId);
|
|
579
628
|
existingHashes.add(hash);
|
|
580
|
-
if (
|
|
581
|
-
|
|
629
|
+
if (dryRun) {
|
|
630
|
+
idMap.set(e.id, e.id);
|
|
631
|
+
imported++;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
// Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
|
|
635
|
+
// memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
|
|
636
|
+
const created = store.createEngram({
|
|
637
|
+
agentId: remapAgentId(e.agent_id),
|
|
638
|
+
concept: e.concept, content: e.content,
|
|
639
|
+
embedding: blobToArr(e.embedding),
|
|
640
|
+
confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
|
|
641
|
+
salienceFeatures: parseJson(e.salience_features, undefined),
|
|
642
|
+
reasonCodes: parseJson(e.reason_codes, undefined),
|
|
643
|
+
tags: parseJson(e.tags, []),
|
|
644
|
+
memoryClass: e.memory_class ?? 'working',
|
|
645
|
+
memoryType: e.memory_type ?? undefined,
|
|
646
|
+
episodeId: e.episode_id ?? undefined,
|
|
647
|
+
taskStatus: e.task_status ?? undefined,
|
|
648
|
+
taskPriority: e.task_priority ?? undefined,
|
|
649
|
+
blockedBy: e.blocked_by ?? undefined,
|
|
650
|
+
ttl: e.ttl ?? undefined,
|
|
651
|
+
sequence: e.sequence ?? undefined,
|
|
652
|
+
references: parseJson(e.references_json, undefined),
|
|
653
|
+
});
|
|
654
|
+
idMap.set(e.id, created.id);
|
|
655
|
+
// preserve stage + retracted (createEngram always mints active/non-retracted)
|
|
656
|
+
if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') {
|
|
657
|
+
try {
|
|
658
|
+
store.updateStage(created.id, e.stage);
|
|
659
|
+
}
|
|
660
|
+
catch { /* */ }
|
|
661
|
+
}
|
|
662
|
+
if (e.retracted) {
|
|
663
|
+
try {
|
|
664
|
+
store.retractEngram(created.id, e.retracted_by ?? null);
|
|
665
|
+
}
|
|
666
|
+
catch { /* */ }
|
|
582
667
|
}
|
|
583
668
|
imported++;
|
|
584
669
|
}
|
|
585
|
-
|
|
670
|
+
// second pass — re-link supersession with remapped ids
|
|
671
|
+
if (!dryRun) {
|
|
672
|
+
for (const e of engrams) {
|
|
673
|
+
const newId = idMap.get(e.id);
|
|
674
|
+
if (!newId || !e.supersedes)
|
|
675
|
+
continue;
|
|
676
|
+
const supNew = idMap.get(e.supersedes);
|
|
677
|
+
if (supNew) {
|
|
678
|
+
try {
|
|
679
|
+
store.supersedeEngram(supNew, newId);
|
|
680
|
+
}
|
|
681
|
+
catch { /* */ }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
586
685
|
for (const a of assocs) {
|
|
587
686
|
if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
|
|
588
687
|
continue;
|
|
@@ -591,23 +690,29 @@ async function mergeMemories() {
|
|
|
591
690
|
if (!fromId || !toId)
|
|
592
691
|
continue;
|
|
593
692
|
if (!dryRun) {
|
|
594
|
-
|
|
693
|
+
try {
|
|
694
|
+
insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
|
|
695
|
+
}
|
|
696
|
+
catch { /* skip an association whose source row has an unbindable/undefined column */ }
|
|
595
697
|
}
|
|
596
698
|
assocImported++;
|
|
597
699
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
700
|
+
const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
|
|
701
|
+
console.log(` Source: ${sourcePath}`);
|
|
702
|
+
console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
|
|
703
|
+
console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
|
|
704
|
+
console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
|
|
705
|
+
totalMemories += imported;
|
|
706
|
+
totalAssociations += assocImported;
|
|
707
|
+
totalSkipped += skipped;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
try {
|
|
712
|
+
store.close();
|
|
713
|
+
}
|
|
714
|
+
catch { /* */ }
|
|
715
|
+
}
|
|
611
716
|
console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
|
|
612
717
|
if (dryRun)
|
|
613
718
|
console.log('(dry run — no data written)');
|