agent-working-memory 0.9.1 → 0.11.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/README.md +89 -19
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +5 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +2 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.js +29 -29
- package/dist/cli.js +405 -224
- 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/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +50 -3
- package/dist/mcp.js.map +1 -1
- package/dist/onboard/index.d.ts +68 -0
- package/dist/onboard/index.d.ts.map +1 -0
- package/dist/onboard/index.js +265 -0
- package/dist/onboard/index.js.map +1 -0
- 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 +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/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +27 -0
- package/dist/version.js.map +1 -0
- package/package.json +11 -1
- package/src/adapters/common.ts +5 -1
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +2 -1
- package/src/cli/migrate.ts +307 -307
- package/src/cli.ts +342 -273
- 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/index.ts +2 -1
- package/src/mcp.ts +62 -3
- package/src/onboard/index.ts +298 -0
- package/src/storage/factory.ts +15 -3
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -166
- package/src/storage/pglite.ts +9 -0
- 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/src/version.ts +26 -0
package/src/cli.ts
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
16
|
-
import { resolve, join, dirname } from 'node:path';
|
|
16
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
17
17
|
import { execSync } from 'node:child_process';
|
|
18
18
|
import { randomUUID } from 'node:crypto';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { VERSION } from './version.js';
|
|
21
|
+
import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
|
|
20
22
|
|
|
21
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
24
|
const __dirname = dirname(__filename);
|
|
@@ -139,6 +141,9 @@ async function setup() {
|
|
|
139
141
|
const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
|
|
140
142
|
const hooksAction = adapter.writeHooks(ctx, skipHooks);
|
|
141
143
|
|
|
144
|
+
// Seed the onboarding skill so a cold store can teach the agent how to warm itself.
|
|
145
|
+
const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
|
|
146
|
+
|
|
142
147
|
console.log(`
|
|
143
148
|
AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
|
|
144
149
|
|
|
@@ -147,6 +152,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
|
|
|
147
152
|
${configAction}
|
|
148
153
|
${instructionsAction}
|
|
149
154
|
${hooksAction}
|
|
155
|
+
${skillAction}
|
|
150
156
|
|
|
151
157
|
Next steps:
|
|
152
158
|
1. Restart ${adapter.name} to pick up the MCP server
|
|
@@ -245,6 +251,29 @@ function health() {
|
|
|
245
251
|
}
|
|
246
252
|
}
|
|
247
253
|
|
|
254
|
+
// ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
|
|
255
|
+
//
|
|
256
|
+
// export/import route through openStore() so they work on ANY backend (SQLite,
|
|
257
|
+
// PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
|
|
258
|
+
// (a SQLite file or PGlite dir, by shape); for a Postgres target set
|
|
259
|
+
// AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
|
|
260
|
+
// port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
|
|
261
|
+
|
|
262
|
+
function toISOStr(d: Date | string | null | undefined): string | null {
|
|
263
|
+
if (d == null) return null;
|
|
264
|
+
return d instanceof Date ? d.toISOString() : String(d);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function openCliStore(dbPath?: string): Promise<{ store: any; backend: string; close: () => Promise<void> }> {
|
|
268
|
+
// --db sets the path only when the env doesn't already select a backend/path.
|
|
269
|
+
if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
|
|
270
|
+
process.env.AWM_DB_PATH = dbPath;
|
|
271
|
+
}
|
|
272
|
+
const { openStore } = await import('./storage/factory.js');
|
|
273
|
+
const { store, backend } = await openStore();
|
|
274
|
+
return { store, backend, close: async () => { try { await store.close?.(); } catch { /* */ } } };
|
|
275
|
+
}
|
|
276
|
+
|
|
248
277
|
// ─── EXPORT ──────────────────────────────────────
|
|
249
278
|
|
|
250
279
|
async function exportMemories() {
|
|
@@ -252,113 +281,107 @@ async function exportMemories() {
|
|
|
252
281
|
let agentFilter: string | null = null;
|
|
253
282
|
let outputPath: string | null = null;
|
|
254
283
|
let activeOnly = false;
|
|
284
|
+
let allStages = false;
|
|
285
|
+
let includeRetracted = false;
|
|
255
286
|
|
|
256
287
|
for (let i = 1; i < args.length; i++) {
|
|
257
288
|
if (args[i] === '--db' && args[i + 1]) dbPath = args[++i];
|
|
258
289
|
else if (args[i] === '--agent' && args[i + 1]) agentFilter = args[++i];
|
|
259
290
|
else if (args[i] === '--output' && args[i + 1]) outputPath = args[++i];
|
|
260
291
|
else if (args[i] === '--active-only') activeOnly = true;
|
|
292
|
+
else if (args[i] === '--all-stages') allStages = true;
|
|
293
|
+
else if (args[i] === '--include-retracted') includeRetracted = true;
|
|
261
294
|
}
|
|
262
295
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!existsSync(dbPath)) {
|
|
269
|
-
console.error(`Error: database not found: ${dbPath}`);
|
|
270
|
-
process.exit(1);
|
|
296
|
+
// --db must exist for a file/dir backend; a Postgres source is selected by env instead.
|
|
297
|
+
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
298
|
+
if (!usingPostgres) {
|
|
299
|
+
if (!dbPath) { console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)'); process.exit(1); }
|
|
300
|
+
if (!existsSync(dbPath)) { console.error(`Error: database not found: ${dbPath}`); process.exit(1); }
|
|
271
301
|
}
|
|
272
302
|
|
|
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
|
-
episode_id: r.episode_id ?? null,
|
|
312
|
-
task_status: r.task_status ?? null,
|
|
313
|
-
task_priority: r.task_priority ?? null,
|
|
314
|
-
supersedes: r.supersedes ?? null,
|
|
315
|
-
superseded_by: r.superseded_by ?? null,
|
|
316
|
-
retracted: r.retracted ?? 0,
|
|
317
|
-
}));
|
|
318
|
-
|
|
319
|
-
// Get memory IDs for association filtering
|
|
320
|
-
const memIds = new Set(memories.map((m: any) => m.id));
|
|
321
|
-
|
|
322
|
-
// Build associations
|
|
323
|
-
let assocQuery = 'SELECT * FROM associations';
|
|
324
|
-
const allAssocs = db.prepare(assocQuery).all() as any[];
|
|
325
|
-
const associations = allAssocs
|
|
326
|
-
.filter((a: any) => memIds.has(a.from_engram_id) && memIds.has(a.to_engram_id))
|
|
327
|
-
.map((a: any) => ({
|
|
328
|
-
from_id: a.from_engram_id,
|
|
329
|
-
to_id: a.to_engram_id,
|
|
330
|
-
weight: a.weight,
|
|
331
|
-
type: a.type ?? 'hebbian',
|
|
332
|
-
activation_count: a.activation_count ?? 0,
|
|
303
|
+
const { store, backend, close } = await openCliStore(dbPath);
|
|
304
|
+
try {
|
|
305
|
+
const agentIds: string[] = agentFilter
|
|
306
|
+
? [agentFilter]
|
|
307
|
+
: ((await store.getActiveAgents()) as any[]).map((a) => a.agentId);
|
|
308
|
+
if (agentIds.length === 0) {
|
|
309
|
+
console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
|
|
310
|
+
}
|
|
311
|
+
// Default to the meaningful memory set (active stage, non-retracted). --all-stages
|
|
312
|
+
// widens to every stage; --include-retracted adds retracted (off with --active-only).
|
|
313
|
+
const stage = allStages ? undefined : 'active';
|
|
314
|
+
const wantRetracted = includeRetracted && !activeOnly;
|
|
315
|
+
const engrams: any[] = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
|
|
316
|
+
|
|
317
|
+
const memories = engrams.map((e: any) => ({
|
|
318
|
+
id: e.id,
|
|
319
|
+
agent_id: e.agentId,
|
|
320
|
+
concept: e.concept,
|
|
321
|
+
content: e.content,
|
|
322
|
+
// Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
|
|
323
|
+
// re-embed after import) → a faithful, recall-ready port when source/target embed
|
|
324
|
+
// models match. import skips them with --no-embeddings (then re-embed).
|
|
325
|
+
embedding: Array.isArray(e.embedding) ? e.embedding : null,
|
|
326
|
+
confidence: e.confidence,
|
|
327
|
+
salience: e.salience,
|
|
328
|
+
access_count: e.accessCount ?? 0,
|
|
329
|
+
last_accessed: toISOStr(e.lastAccessed),
|
|
330
|
+
created_at: toISOStr(e.createdAt),
|
|
331
|
+
stage: e.stage ?? 'active',
|
|
332
|
+
tags: Array.isArray(e.tags) ? e.tags : [],
|
|
333
|
+
memory_class: e.memoryClass ?? 'working',
|
|
334
|
+
memory_type: e.memoryType ?? 'unclassified',
|
|
335
|
+
episode_id: e.episodeId ?? null,
|
|
336
|
+
task_status: e.taskStatus ?? null,
|
|
337
|
+
task_priority: e.taskPriority ?? null,
|
|
338
|
+
supersedes: e.supersedes ?? null,
|
|
339
|
+
superseded_by: e.supersededBy ?? null,
|
|
340
|
+
retracted: e.retracted ? 1 : 0,
|
|
333
341
|
}));
|
|
334
342
|
|
|
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);
|
|
343
|
+
const memIds = new Set(memories.map((m) => m.id));
|
|
344
|
+
const seen = new Set<string>();
|
|
345
|
+
const associations: any[] = [];
|
|
346
|
+
for (const aid of agentIds) {
|
|
347
|
+
for (const a of ((await store.getAllAssociations(aid)) as any[]) ?? []) {
|
|
348
|
+
if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId)) continue;
|
|
349
|
+
const k = `${a.fromEngramId}>${a.toEngramId}`;
|
|
350
|
+
if (seen.has(k)) continue;
|
|
351
|
+
seen.add(k);
|
|
352
|
+
associations.push({
|
|
353
|
+
from_id: a.fromEngramId, to_id: a.toEngramId,
|
|
354
|
+
weight: a.weight, type: a.type ?? 'hebbian',
|
|
355
|
+
activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
353
359
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
const exportData = {
|
|
361
|
+
version: VERSION,
|
|
362
|
+
exported_at: new Date().toISOString(),
|
|
363
|
+
source_backend: backend,
|
|
364
|
+
agent_filter: agentFilter,
|
|
365
|
+
embedding_model: process.env.AWM_EMBED_MODEL ?? null,
|
|
366
|
+
memories,
|
|
367
|
+
associations,
|
|
368
|
+
stats: {
|
|
369
|
+
total_memories: memories.length,
|
|
370
|
+
total_associations: associations.length,
|
|
371
|
+
agents: [...new Set(memories.map((m) => m.agent_id))],
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const json = JSON.stringify(exportData, null, 2);
|
|
376
|
+
if (outputPath) {
|
|
377
|
+
writeFileSync(outputPath, json + '\n');
|
|
378
|
+
console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
|
|
379
|
+
} else {
|
|
380
|
+
process.stdout.write(json + '\n');
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
await close();
|
|
359
384
|
}
|
|
360
|
-
|
|
361
|
-
db.close();
|
|
362
385
|
}
|
|
363
386
|
|
|
364
387
|
// ─── IMPORT ──────────────────────────────────────
|
|
@@ -381,12 +404,17 @@ async function importMemories() {
|
|
|
381
404
|
else if (!args[i].startsWith('--') && !filePath) filePath = args[i];
|
|
382
405
|
}
|
|
383
406
|
|
|
407
|
+
// --no-embeddings: skip importing embedding vectors (use when source/target embed models
|
|
408
|
+
// differ → import without, then re-embed). Parsed alongside the existing flags above.
|
|
409
|
+
const noEmbeddings = args.includes('--no-embeddings');
|
|
410
|
+
|
|
384
411
|
if (!filePath) {
|
|
385
412
|
console.error('Error: <file> is required');
|
|
386
413
|
process.exit(1);
|
|
387
414
|
}
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
416
|
+
if (!dbPath && !usingPostgres) {
|
|
417
|
+
console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
|
|
390
418
|
process.exit(1);
|
|
391
419
|
}
|
|
392
420
|
if (!existsSync(filePath)) {
|
|
@@ -400,134 +428,100 @@ async function importMemories() {
|
|
|
400
428
|
process.exit(1);
|
|
401
429
|
}
|
|
402
430
|
|
|
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);
|
|
431
|
+
const { store, backend, close } = await openCliStore(dbPath);
|
|
432
|
+
try {
|
|
433
|
+
// Dedupe against existing memories for the TARGET agent(s).
|
|
434
|
+
const existingHashes = new Set<string>();
|
|
435
|
+
if (dedupe) {
|
|
436
|
+
const targetAgents = remapAgent
|
|
437
|
+
? [remapAgent]
|
|
438
|
+
: [...new Set(importData.memories.map((m: any) => m.agent_id))] as string[];
|
|
439
|
+
for (const aid of targetAgents) {
|
|
440
|
+
for (const e of ((await store.getEngramsByAgent(aid, undefined, true)) as any[]) ?? []) {
|
|
441
|
+
existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
433
444
|
}
|
|
434
|
-
}
|
|
435
|
-
const idMap = new Map<string, string>();
|
|
436
|
-
let imported = 0;
|
|
437
|
-
let skippedDupes = 0;
|
|
438
|
-
let skippedRetracted = 0;
|
|
439
445
|
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
447
|
-
// NOTE: associations.last_activated is NOT NULL on the engrams DB (storage/sqlite.ts),
|
|
448
|
-
// so importing into an existing store fails if it's omitted — and because import wraps
|
|
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
|
-
`);
|
|
446
|
+
const idMap = new Map<string, string>(); // old export id → new store id
|
|
447
|
+
let imported = 0, skippedDupes = 0, skippedRetracted = 0;
|
|
455
448
|
|
|
456
|
-
|
|
457
|
-
//
|
|
449
|
+
// Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
|
|
450
|
+
// createdAt/accessCount normalize to import time (the contract's createEngram stamps
|
|
451
|
+
// them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
|
|
452
|
+
// embedding) is preserved, so recall is faithful.
|
|
458
453
|
for (const mem of importData.memories) {
|
|
459
|
-
|
|
460
|
-
if (mem.retracted && !includeRetracted) {
|
|
461
|
-
skippedRetracted++;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Dedupe check
|
|
454
|
+
if (mem.retracted && !includeRetracted) { skippedRetracted++; continue; }
|
|
466
455
|
if (dedupe) {
|
|
467
|
-
const
|
|
468
|
-
if (existingHashes.has(
|
|
469
|
-
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
456
|
+
const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
|
|
457
|
+
if (existingHashes.has(h)) { skippedDupes++; continue; }
|
|
458
|
+
existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
|
|
472
459
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
460
|
+
// dry-run: still map the id so the association-count preview isn't always 0
|
|
461
|
+
if (dryRun) { idMap.set(mem.id, mem.id); imported++; continue; }
|
|
462
|
+
const created = await store.createEngram({
|
|
463
|
+
agentId: remapAgent ?? mem.agent_id,
|
|
464
|
+
concept: mem.concept,
|
|
465
|
+
content: mem.content,
|
|
466
|
+
tags: Array.isArray(mem.tags) ? mem.tags : [],
|
|
467
|
+
embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
|
|
468
|
+
confidence: mem.confidence ?? 0.5,
|
|
469
|
+
salience: mem.salience ?? 0.5,
|
|
470
|
+
memoryClass: mem.memory_class ?? 'working',
|
|
471
|
+
memoryType: mem.memory_type ?? undefined,
|
|
472
|
+
episodeId: mem.episode_id ?? undefined,
|
|
473
|
+
taskStatus: mem.task_status ?? undefined,
|
|
474
|
+
taskPriority: mem.task_priority ?? undefined,
|
|
475
|
+
});
|
|
476
|
+
idMap.set(mem.id, created.id);
|
|
477
|
+
// Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
|
|
478
|
+
// without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
|
|
479
|
+
// non-active stage (staging/consolidated/archived/fading) silently flattens to active.
|
|
480
|
+
if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
|
|
481
|
+
try { await store.updateStage(created.id, mem.stage); } catch { /* best-effort */ }
|
|
482
|
+
}
|
|
483
|
+
if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
|
|
484
|
+
try { await store.retractEngram(created.id, (mem as { retracted_by?: string }).retracted_by ?? null); } catch { /* best-effort */ }
|
|
491
485
|
}
|
|
492
486
|
imported++;
|
|
493
487
|
}
|
|
494
488
|
|
|
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
|
-
);
|
|
489
|
+
// Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
|
|
490
|
+
// old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
|
|
491
|
+
if (!dryRun) {
|
|
492
|
+
for (const mem of importData.memories) {
|
|
493
|
+
const newId = idMap.get(mem.id);
|
|
494
|
+
if (!newId || !mem.supersedes) continue;
|
|
495
|
+
const supersededNew = idMap.get(mem.supersedes);
|
|
496
|
+
if (supersededNew) { try { await store.supersedeEngram(supersededNew, newId); } catch { /* best-effort */ } }
|
|
509
497
|
}
|
|
510
|
-
assocImported++;
|
|
511
498
|
}
|
|
512
499
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
522
|
-
(remapAgent ? ` (agent remapped to: ${remapAgent})` : ''));
|
|
500
|
+
// Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
|
|
501
|
+
let assocImported = 0;
|
|
502
|
+
for (const a of (importData.associations ?? [])) {
|
|
503
|
+
const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
|
|
504
|
+
if (!fromId || !toId) continue;
|
|
505
|
+
if (!dryRun) { try { await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5); } catch { /* best-effort */ } }
|
|
506
|
+
assocImported++;
|
|
507
|
+
}
|
|
523
508
|
|
|
524
|
-
|
|
509
|
+
const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
|
|
510
|
+
console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
|
|
511
|
+
(skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
|
|
512
|
+
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
513
|
+
(remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
|
|
514
|
+
` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
|
|
515
|
+
} finally {
|
|
516
|
+
await close();
|
|
517
|
+
}
|
|
525
518
|
}
|
|
526
519
|
|
|
527
520
|
// ─── MERGE ──────────────────────────────────────
|
|
528
521
|
|
|
529
522
|
async function mergeMemories() {
|
|
530
523
|
const Database = (await import('better-sqlite3')).default;
|
|
524
|
+
const { EngramStore } = await import('./storage/sqlite.js');
|
|
531
525
|
const { createHash, randomUUID } = await import('node:crypto');
|
|
532
526
|
|
|
533
527
|
let target = '';
|
|
@@ -574,29 +568,19 @@ async function mergeMemories() {
|
|
|
574
568
|
|
|
575
569
|
console.log(`Target: ${target}${dryRun ? ' (DRY RUN)' : ''}`);
|
|
576
570
|
|
|
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
|
-
`);
|
|
571
|
+
// Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
|
|
572
|
+
// triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
|
|
573
|
+
// task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
|
|
574
|
+
const store: any = new EngramStore(target);
|
|
575
|
+
const targetDb = store.db as import('better-sqlite3').Database; // the store's better-sqlite3 handle
|
|
576
|
+
|
|
577
|
+
const blobToArr = (b: unknown): number[] | undefined => {
|
|
578
|
+
const buf = b as Buffer | null | undefined;
|
|
579
|
+
return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
|
|
580
|
+
};
|
|
581
|
+
const parseJson = <T>(s: unknown, fallback: T): T => { try { return s ? JSON.parse(String(s)) as T : fallback; } catch { return fallback; } };
|
|
598
582
|
|
|
599
|
-
// Build dedupe hash set from existing target memories
|
|
583
|
+
// Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
|
|
600
584
|
const existingHashes = new Set<string>();
|
|
601
585
|
if (dedupe) {
|
|
602
586
|
const rows = targetDb.prepare('SELECT concept, content FROM engrams').all() as { concept: string; content: string }[];
|
|
@@ -604,12 +588,6 @@ async function mergeMemories() {
|
|
|
604
588
|
console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
|
|
605
589
|
}
|
|
606
590
|
|
|
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
591
|
const insertAssoc = targetDb.prepare(`
|
|
614
592
|
INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
615
593
|
activation_count, created_at, last_activated)
|
|
@@ -618,6 +596,7 @@ async function mergeMemories() {
|
|
|
618
596
|
|
|
619
597
|
let totalMemories = 0, totalAssociations = 0, totalSkipped = 0;
|
|
620
598
|
|
|
599
|
+
try {
|
|
621
600
|
for (const sourcePath of sources) {
|
|
622
601
|
if (!existsSync(sourcePath)) {
|
|
623
602
|
console.error(` Source not found: ${sourcePath}`);
|
|
@@ -625,63 +604,84 @@ async function mergeMemories() {
|
|
|
625
604
|
}
|
|
626
605
|
|
|
627
606
|
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[];
|
|
607
|
+
// SELECT * is robust to older source schemas — a column a source predates just reads back undefined
|
|
608
|
+
// and createEngram fills the default.
|
|
609
|
+
const engrams = sourceDb.prepare('SELECT * FROM engrams').all() as any[];
|
|
610
|
+
const assocs = sourceDb.prepare('SELECT * FROM associations').all() as any[];
|
|
611
|
+
sourceDb.close(); // reads done — release the source handle before any (throwing) write work
|
|
637
612
|
|
|
638
613
|
const idMap = new Map<string, string>();
|
|
639
614
|
const skippedIds = new Set<string>();
|
|
615
|
+
let imported = 0, skipped = 0, assocImported = 0;
|
|
640
616
|
|
|
641
|
-
const
|
|
642
|
-
|
|
617
|
+
for (const e of engrams) {
|
|
618
|
+
const hash = contentHash(e.concept, e.content);
|
|
619
|
+
if (dedupe && existingHashes.has(hash)) { skippedIds.add(e.id); skipped++; continue; }
|
|
620
|
+
existingHashes.add(hash);
|
|
621
|
+
if (dryRun) { idMap.set(e.id, e.id); imported++; continue; }
|
|
622
|
+
// Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
|
|
623
|
+
// memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
|
|
624
|
+
const created = store.createEngram({
|
|
625
|
+
agentId: remapAgentId(e.agent_id),
|
|
626
|
+
concept: e.concept, content: e.content,
|
|
627
|
+
embedding: blobToArr(e.embedding),
|
|
628
|
+
confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
|
|
629
|
+
salienceFeatures: parseJson(e.salience_features, undefined),
|
|
630
|
+
reasonCodes: parseJson(e.reason_codes, undefined),
|
|
631
|
+
tags: parseJson<string[]>(e.tags, []),
|
|
632
|
+
memoryClass: e.memory_class ?? 'working',
|
|
633
|
+
memoryType: e.memory_type ?? undefined,
|
|
634
|
+
episodeId: e.episode_id ?? undefined,
|
|
635
|
+
taskStatus: e.task_status ?? undefined,
|
|
636
|
+
taskPriority: e.task_priority ?? undefined,
|
|
637
|
+
blockedBy: e.blocked_by ?? undefined,
|
|
638
|
+
ttl: e.ttl ?? undefined,
|
|
639
|
+
sequence: e.sequence ?? undefined,
|
|
640
|
+
references: parseJson(e.references_json, undefined),
|
|
641
|
+
});
|
|
642
|
+
idMap.set(e.id, created.id);
|
|
643
|
+
// preserve stage + retracted (createEngram always mints active/non-retracted)
|
|
644
|
+
if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') { try { store.updateStage(created.id, e.stage); } catch { /* */ } }
|
|
645
|
+
if (e.retracted) { try { store.retractEngram(created.id, e.retracted_by ?? null); } catch { /* */ } }
|
|
646
|
+
imported++;
|
|
647
|
+
}
|
|
648
|
+
// second pass — re-link supersession with remapped ids
|
|
649
|
+
if (!dryRun) {
|
|
643
650
|
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++;
|
|
651
|
+
const newId = idMap.get(e.id);
|
|
652
|
+
if (!newId || !e.supersedes) continue;
|
|
653
|
+
const supNew = idMap.get(e.supersedes);
|
|
654
|
+
if (supNew) { try { store.supersedeEngram(supNew, newId); } catch { /* */ } }
|
|
655
655
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
656
|
+
}
|
|
657
|
+
for (const a of assocs) {
|
|
658
|
+
if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id)) continue;
|
|
659
|
+
const fromId = idMap.get(a.from_engram_id);
|
|
660
|
+
const toId = idMap.get(a.to_engram_id);
|
|
661
|
+
if (!fromId || !toId) continue;
|
|
662
|
+
if (!dryRun) {
|
|
663
|
+
try {
|
|
663
664
|
insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type,
|
|
664
665
|
a.activation_count, a.created_at, a.last_activated);
|
|
665
|
-
}
|
|
666
|
-
assocImported++;
|
|
666
|
+
} catch { /* skip an association whose source row has an unbindable/undefined column */ }
|
|
667
667
|
}
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
sourceDb.close();
|
|
668
|
+
assocImported++;
|
|
669
|
+
}
|
|
672
670
|
|
|
673
671
|
const agentSet = new Set(engrams.map((e: any) => remapAgentId(e.agent_id)));
|
|
674
672
|
console.log(` Source: ${sourcePath}`);
|
|
675
|
-
console.log(` Engrams: ${engrams.length} total, ${
|
|
676
|
-
console.log(` Associations: ${assocs.length} total, ${
|
|
673
|
+
console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
|
|
674
|
+
console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
|
|
677
675
|
console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
|
|
678
676
|
|
|
679
|
-
totalMemories +=
|
|
680
|
-
totalAssociations +=
|
|
681
|
-
totalSkipped +=
|
|
677
|
+
totalMemories += imported;
|
|
678
|
+
totalAssociations += assocImported;
|
|
679
|
+
totalSkipped += skipped;
|
|
682
680
|
}
|
|
683
681
|
|
|
684
|
-
|
|
682
|
+
} finally {
|
|
683
|
+
try { store.close(); } catch { /* */ }
|
|
684
|
+
}
|
|
685
685
|
console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
|
|
686
686
|
if (dryRun) console.log('(dry run — no data written)');
|
|
687
687
|
}
|
|
@@ -717,6 +717,72 @@ async function migrateCmd() {
|
|
|
717
717
|
}
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
// ─── ONBOARD ──────────────────────────────────────
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Seed the onboarding skill as a canonical memory (idempotent). This is what lets
|
|
724
|
+
* a cold store teach the host agent how to warm-start itself — the agent recalls
|
|
725
|
+
* the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
|
|
726
|
+
*/
|
|
727
|
+
async function seedOnboardSkill(dbPath: string, agentId: string): Promise<string> {
|
|
728
|
+
try {
|
|
729
|
+
const { store, close } = await openCliStore(dbPath);
|
|
730
|
+
try {
|
|
731
|
+
const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
|
|
732
|
+
if (existing) return 'Onboarding skill: already present';
|
|
733
|
+
await store.createEngram({
|
|
734
|
+
agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
|
|
735
|
+
tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
|
|
736
|
+
});
|
|
737
|
+
return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
|
|
738
|
+
} finally {
|
|
739
|
+
await close();
|
|
740
|
+
}
|
|
741
|
+
} catch (e: any) {
|
|
742
|
+
return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function onboardCmd() {
|
|
747
|
+
const docs: string[] = [];
|
|
748
|
+
let repo: string | undefined;
|
|
749
|
+
let project = '';
|
|
750
|
+
let agentId = '';
|
|
751
|
+
let purpose: string | undefined;
|
|
752
|
+
let outDir = resolve(process.cwd(), '.awm');
|
|
753
|
+
|
|
754
|
+
for (let i = 1; i < args.length; i++) {
|
|
755
|
+
const a = args[i];
|
|
756
|
+
if (a === '--repo' && args[i + 1]) repo = args[++i];
|
|
757
|
+
else if (a === '--project' && args[i + 1]) project = args[++i];
|
|
758
|
+
else if (a === '--agent' && args[i + 1]) agentId = args[++i];
|
|
759
|
+
else if (a === '--purpose' && args[i + 1]) purpose = args[++i];
|
|
760
|
+
else if (a === '--out' && args[i + 1]) outDir = resolve(args[++i]);
|
|
761
|
+
else if (!a.startsWith('--')) docs.push(a);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
|
|
765
|
+
if (docs.length === 0) docs.push(repo ?? process.cwd());
|
|
766
|
+
if (!project) project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
|
|
767
|
+
if (!agentId) agentId = project;
|
|
768
|
+
|
|
769
|
+
const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
|
|
770
|
+
console.log(`
|
|
771
|
+
AWM onboard — warm-start pack for "${project}"
|
|
772
|
+
|
|
773
|
+
Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
|
|
774
|
+
Extracted: ${count} candidate memories (agent: ${agentId})
|
|
775
|
+
|
|
776
|
+
Review: ${reviewPath}
|
|
777
|
+
Pack: ${packPath}
|
|
778
|
+
|
|
779
|
+
Next:
|
|
780
|
+
1. Edit the review file / pack as needed (delete noise, answer the interview questions).
|
|
781
|
+
2. Load it: awm import ${packPath} --db <path> --dedupe
|
|
782
|
+
(embeddings backfill on the first consolidation — recall is warm immediately after)
|
|
783
|
+
`.trimEnd());
|
|
784
|
+
}
|
|
785
|
+
|
|
720
786
|
// ─── Dispatch ──────────────────────────────────────
|
|
721
787
|
|
|
722
788
|
switch (command) {
|
|
@@ -747,6 +813,9 @@ switch (command) {
|
|
|
747
813
|
case 'migrate':
|
|
748
814
|
await migrateCmd();
|
|
749
815
|
break;
|
|
816
|
+
case 'onboard':
|
|
817
|
+
onboardCmd();
|
|
818
|
+
break;
|
|
750
819
|
case '--help':
|
|
751
820
|
case '-h':
|
|
752
821
|
case undefined:
|