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/dist/cli.js
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* awm health — check if a running server is healthy
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
|
-
import { resolve, dirname } from 'node:path';
|
|
14
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
15
15
|
import { execSync } from 'node:child_process';
|
|
16
|
-
import { randomUUID } from 'node:crypto';
|
|
17
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { VERSION } from './version.js';
|
|
18
|
+
import { runOnboard, ONBOARD_SKILL } from './onboard/index.js';
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = dirname(__filename);
|
|
20
21
|
// Load .env if present
|
|
@@ -135,6 +136,8 @@ async function setup() {
|
|
|
135
136
|
const configAction = adapter.writeMcpConfig(ctx);
|
|
136
137
|
const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
|
|
137
138
|
const hooksAction = adapter.writeHooks(ctx, skipHooks);
|
|
139
|
+
// Seed the onboarding skill so a cold store can teach the agent how to warm itself.
|
|
140
|
+
const skillAction = await seedOnboardSkill(ctx.dbPath, ctx.agentId);
|
|
138
141
|
console.log(`
|
|
139
142
|
AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
|
|
140
143
|
|
|
@@ -143,6 +146,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
|
|
|
143
146
|
${configAction}
|
|
144
147
|
${instructionsAction}
|
|
145
148
|
${hooksAction}
|
|
149
|
+
${skillAction}
|
|
146
150
|
|
|
147
151
|
Next steps:
|
|
148
152
|
1. Restart ${adapter.name} to pick up the MCP server
|
|
@@ -228,12 +232,38 @@ function health() {
|
|
|
228
232
|
process.exit(1);
|
|
229
233
|
}
|
|
230
234
|
}
|
|
235
|
+
// ─── BACKEND-AGNOSTIC STORE (export/import) ──────────────────────────────────
|
|
236
|
+
//
|
|
237
|
+
// export/import route through openStore() so they work on ANY backend (SQLite,
|
|
238
|
+
// PGlite, Postgres) — not just better-sqlite3. `--db <path>` maps to AWM_DB_PATH
|
|
239
|
+
// (a SQLite file or PGlite dir, by shape); for a Postgres target set
|
|
240
|
+
// AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL (no --db). This is what lets you
|
|
241
|
+
// port a memory store INTO managed Postgres (the SQLite-hardcoded path could not).
|
|
242
|
+
function toISOStr(d) {
|
|
243
|
+
if (d == null)
|
|
244
|
+
return null;
|
|
245
|
+
return d instanceof Date ? d.toISOString() : String(d);
|
|
246
|
+
}
|
|
247
|
+
async function openCliStore(dbPath) {
|
|
248
|
+
// --db sets the path only when the env doesn't already select a backend/path.
|
|
249
|
+
if (dbPath && !process.env.AWM_DB_PATH && (process.env.AWM_STORE_BACKEND ?? '') !== 'postgres') {
|
|
250
|
+
process.env.AWM_DB_PATH = dbPath;
|
|
251
|
+
}
|
|
252
|
+
const { openStore } = await import('./storage/factory.js');
|
|
253
|
+
const { store, backend } = await openStore();
|
|
254
|
+
return { store, backend, close: async () => { try {
|
|
255
|
+
await store.close?.();
|
|
256
|
+
}
|
|
257
|
+
catch { /* */ } } };
|
|
258
|
+
}
|
|
231
259
|
// ─── EXPORT ──────────────────────────────────────
|
|
232
260
|
async function exportMemories() {
|
|
233
261
|
let dbPath = '';
|
|
234
262
|
let agentFilter = null;
|
|
235
263
|
let outputPath = null;
|
|
236
264
|
let activeOnly = false;
|
|
265
|
+
let allStages = false;
|
|
266
|
+
let includeRetracted = false;
|
|
237
267
|
for (let i = 1; i < args.length; i++) {
|
|
238
268
|
if (args[i] === '--db' && args[i + 1])
|
|
239
269
|
dbPath = args[++i];
|
|
@@ -243,93 +273,105 @@ async function exportMemories() {
|
|
|
243
273
|
outputPath = args[++i];
|
|
244
274
|
else if (args[i] === '--active-only')
|
|
245
275
|
activeOnly = true;
|
|
276
|
+
else if (args[i] === '--all-stages')
|
|
277
|
+
allStages = true;
|
|
278
|
+
else if (args[i] === '--include-retracted')
|
|
279
|
+
includeRetracted = true;
|
|
246
280
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
// --db must exist for a file/dir backend; a Postgres source is selected by env instead.
|
|
282
|
+
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
283
|
+
if (!usingPostgres) {
|
|
284
|
+
if (!dbPath) {
|
|
285
|
+
console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
if (!existsSync(dbPath)) {
|
|
289
|
+
console.error(`Error: database not found: ${dbPath}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
254
292
|
}
|
|
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
|
-
|
|
293
|
+
const { store, backend, close } = await openCliStore(dbPath);
|
|
294
|
+
try {
|
|
295
|
+
const agentIds = agentFilter
|
|
296
|
+
? [agentFilter]
|
|
297
|
+
: (await store.getActiveAgents()).map((a) => a.agentId);
|
|
298
|
+
if (agentIds.length === 0) {
|
|
299
|
+
console.error('Warning: no agents found to export. Pass --agent <id> if the store has no tracked activity yet.');
|
|
300
|
+
}
|
|
301
|
+
// Default to the meaningful memory set (active stage, non-retracted). --all-stages
|
|
302
|
+
// widens to every stage; --include-retracted adds retracted (off with --active-only).
|
|
303
|
+
const stage = allStages ? undefined : 'active';
|
|
304
|
+
const wantRetracted = includeRetracted && !activeOnly;
|
|
305
|
+
const engrams = (await store.getEngramsByAgents(agentIds, stage, wantRetracted)) ?? [];
|
|
306
|
+
const memories = engrams.map((e) => ({
|
|
307
|
+
id: e.id,
|
|
308
|
+
agent_id: e.agentId,
|
|
309
|
+
concept: e.concept,
|
|
310
|
+
content: e.content,
|
|
311
|
+
// Embeddings ARE included now (the old SQLite-only export stripped them, forcing a
|
|
312
|
+
// re-embed after import) → a faithful, recall-ready port when source/target embed
|
|
313
|
+
// models match. import skips them with --no-embeddings (then re-embed).
|
|
314
|
+
embedding: Array.isArray(e.embedding) ? e.embedding : null,
|
|
315
|
+
confidence: e.confidence,
|
|
316
|
+
salience: e.salience,
|
|
317
|
+
access_count: e.accessCount ?? 0,
|
|
318
|
+
last_accessed: toISOStr(e.lastAccessed),
|
|
319
|
+
created_at: toISOStr(e.createdAt),
|
|
320
|
+
stage: e.stage ?? 'active',
|
|
321
|
+
tags: Array.isArray(e.tags) ? e.tags : [],
|
|
322
|
+
memory_class: e.memoryClass ?? 'working',
|
|
323
|
+
memory_type: e.memoryType ?? 'unclassified',
|
|
324
|
+
episode_id: e.episodeId ?? null,
|
|
325
|
+
task_status: e.taskStatus ?? null,
|
|
326
|
+
task_priority: e.taskPriority ?? null,
|
|
327
|
+
supersedes: e.supersedes ?? null,
|
|
328
|
+
superseded_by: e.supersededBy ?? null,
|
|
329
|
+
retracted: e.retracted ? 1 : 0,
|
|
330
|
+
}));
|
|
331
|
+
const memIds = new Set(memories.map((m) => m.id));
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
const associations = [];
|
|
334
|
+
for (const aid of agentIds) {
|
|
335
|
+
for (const a of (await store.getAllAssociations(aid)) ?? []) {
|
|
336
|
+
if (!memIds.has(a.fromEngramId) || !memIds.has(a.toEngramId))
|
|
337
|
+
continue;
|
|
338
|
+
const k = `${a.fromEngramId}>${a.toEngramId}`;
|
|
339
|
+
if (seen.has(k))
|
|
340
|
+
continue;
|
|
341
|
+
seen.add(k);
|
|
342
|
+
associations.push({
|
|
343
|
+
from_id: a.fromEngramId, to_id: a.toEngramId,
|
|
344
|
+
weight: a.weight, type: a.type ?? 'hebbian',
|
|
345
|
+
activation_count: a.activationCount ?? 0, confidence: a.confidence ?? 0.5,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const exportData = {
|
|
350
|
+
version: VERSION,
|
|
351
|
+
exported_at: new Date().toISOString(),
|
|
352
|
+
source_backend: backend,
|
|
353
|
+
agent_filter: agentFilter,
|
|
354
|
+
embedding_model: process.env.AWM_EMBED_MODEL ?? null,
|
|
355
|
+
memories,
|
|
356
|
+
associations,
|
|
357
|
+
stats: {
|
|
358
|
+
total_memories: memories.length,
|
|
359
|
+
total_associations: associations.length,
|
|
360
|
+
agents: [...new Set(memories.map((m) => m.agent_id))],
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
const json = JSON.stringify(exportData, null, 2);
|
|
364
|
+
if (outputPath) {
|
|
365
|
+
writeFileSync(outputPath, json + '\n');
|
|
366
|
+
console.error(`Exported ${memories.length} memories, ${associations.length} associations → ${outputPath} (backend: ${backend})`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
process.stdout.write(json + '\n');
|
|
370
|
+
}
|
|
328
371
|
}
|
|
329
|
-
|
|
330
|
-
|
|
372
|
+
finally {
|
|
373
|
+
await close();
|
|
331
374
|
}
|
|
332
|
-
db.close();
|
|
333
375
|
}
|
|
334
376
|
// ─── IMPORT ──────────────────────────────────────
|
|
335
377
|
async function importMemories() {
|
|
@@ -354,12 +396,16 @@ async function importMemories() {
|
|
|
354
396
|
else if (!args[i].startsWith('--') && !filePath)
|
|
355
397
|
filePath = args[i];
|
|
356
398
|
}
|
|
399
|
+
// --no-embeddings: skip importing embedding vectors (use when source/target embed models
|
|
400
|
+
// differ → import without, then re-embed). Parsed alongside the existing flags above.
|
|
401
|
+
const noEmbeddings = args.includes('--no-embeddings');
|
|
357
402
|
if (!filePath) {
|
|
358
403
|
console.error('Error: <file> is required');
|
|
359
404
|
process.exit(1);
|
|
360
405
|
}
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
const usingPostgres = (process.env.AWM_STORE_BACKEND ?? '').toLowerCase() === 'postgres';
|
|
407
|
+
if (!dbPath && !usingPostgres) {
|
|
408
|
+
console.error('Error: --db <path> is required (or set AWM_STORE_BACKEND=postgres + AWM_DATABASE_URL)');
|
|
363
409
|
process.exit(1);
|
|
364
410
|
}
|
|
365
411
|
if (!existsSync(filePath)) {
|
|
@@ -371,105 +417,122 @@ async function importMemories() {
|
|
|
371
417
|
console.error('Error: invalid export file — missing memories array');
|
|
372
418
|
process.exit(1);
|
|
373
419
|
}
|
|
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
|
|
420
|
+
const { store, backend, close } = await openCliStore(dbPath);
|
|
421
|
+
try {
|
|
422
|
+
// Dedupe against existing memories for the TARGET agent(s).
|
|
423
|
+
const existingHashes = new Set();
|
|
424
|
+
if (dedupe) {
|
|
425
|
+
const targetAgents = remapAgent
|
|
426
|
+
? [remapAgent]
|
|
427
|
+
: [...new Set(importData.memories.map((m) => m.agent_id))];
|
|
428
|
+
for (const aid of targetAgents) {
|
|
429
|
+
for (const e of (await store.getEngramsByAgent(aid, undefined, true)) ?? []) {
|
|
430
|
+
existingHashes.add(`${(e.concept ?? '').toLowerCase().trim()}||${(e.content ?? '').toLowerCase().trim()}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const idMap = new Map(); // old export id → new store id
|
|
435
|
+
let imported = 0, skippedDupes = 0, skippedRetracted = 0;
|
|
436
|
+
// Pass 1 — create engrams (createEngram mints a fresh id; we capture it for remapping).
|
|
437
|
+
// createdAt/accessCount normalize to import time (the contract's createEngram stamps
|
|
438
|
+
// them) — see CHANGELOG; everything semantic (content/tags/confidence/salience/classes/
|
|
439
|
+
// embedding) is preserved, so recall is faithful.
|
|
424
440
|
for (const mem of importData.memories) {
|
|
425
|
-
// Skip retracted unless --include-retracted
|
|
426
441
|
if (mem.retracted && !includeRetracted) {
|
|
427
442
|
skippedRetracted++;
|
|
428
443
|
continue;
|
|
429
444
|
}
|
|
430
|
-
// Dedupe check
|
|
431
445
|
if (dedupe) {
|
|
432
|
-
const
|
|
433
|
-
if (existingHashes.has(
|
|
446
|
+
const h = `${(mem.concept ?? '').toLowerCase().trim()}||${(mem.content ?? '').toLowerCase().trim()}`;
|
|
447
|
+
if (existingHashes.has(h)) {
|
|
434
448
|
skippedDupes++;
|
|
435
449
|
continue;
|
|
436
450
|
}
|
|
451
|
+
existingHashes.add(h); // also catch duplicates WITHIN this import file, not just vs the target
|
|
437
452
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
453
|
+
// dry-run: still map the id so the association-count preview isn't always 0
|
|
454
|
+
if (dryRun) {
|
|
455
|
+
idMap.set(mem.id, mem.id);
|
|
456
|
+
imported++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const created = await store.createEngram({
|
|
460
|
+
agentId: remapAgent ?? mem.agent_id,
|
|
461
|
+
concept: mem.concept,
|
|
462
|
+
content: mem.content,
|
|
463
|
+
tags: Array.isArray(mem.tags) ? mem.tags : [],
|
|
464
|
+
embedding: (!noEmbeddings && Array.isArray(mem.embedding) && mem.embedding.length > 0) ? mem.embedding : undefined,
|
|
465
|
+
confidence: mem.confidence ?? 0.5,
|
|
466
|
+
salience: mem.salience ?? 0.5,
|
|
467
|
+
memoryClass: mem.memory_class ?? 'working',
|
|
468
|
+
memoryType: mem.memory_type ?? undefined,
|
|
469
|
+
episodeId: mem.episode_id ?? undefined,
|
|
470
|
+
taskStatus: mem.task_status ?? undefined,
|
|
471
|
+
taskPriority: mem.task_priority ?? undefined,
|
|
472
|
+
});
|
|
473
|
+
idMap.set(mem.id, created.id);
|
|
474
|
+
// Restore stage + retracted status. createEngram always mints an ACTIVE, non-retracted engram, so
|
|
475
|
+
// without this an `--include-retracted` import RESURRECTS retracted memories as live, and every
|
|
476
|
+
// non-active stage (staging/consolidated/archived/fading) silently flattens to active.
|
|
477
|
+
if (typeof mem.stage === 'string' && mem.stage && mem.stage !== 'active') {
|
|
478
|
+
try {
|
|
479
|
+
await store.updateStage(created.id, mem.stage);
|
|
480
|
+
}
|
|
481
|
+
catch { /* best-effort */ }
|
|
482
|
+
}
|
|
483
|
+
if (mem.retracted) { // only reached when --include-retracted (retracted are skipped above otherwise)
|
|
484
|
+
try {
|
|
485
|
+
await store.retractEngram(created.id, mem.retracted_by ?? null);
|
|
486
|
+
}
|
|
487
|
+
catch { /* best-effort */ }
|
|
444
488
|
}
|
|
445
489
|
imported++;
|
|
446
490
|
}
|
|
447
|
-
//
|
|
491
|
+
// Pass 2 — re-link supersession with remapped ids (supersedeEngram sets both sides:
|
|
492
|
+
// old.superseded_by = new, new.supersedes = old). Skipped in dry-run.
|
|
493
|
+
if (!dryRun) {
|
|
494
|
+
for (const mem of importData.memories) {
|
|
495
|
+
const newId = idMap.get(mem.id);
|
|
496
|
+
if (!newId || !mem.supersedes)
|
|
497
|
+
continue;
|
|
498
|
+
const supersededNew = idMap.get(mem.supersedes);
|
|
499
|
+
if (supersededNew) {
|
|
500
|
+
try {
|
|
501
|
+
await store.supersedeEngram(supersededNew, newId);
|
|
502
|
+
}
|
|
503
|
+
catch { /* best-effort */ }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Pass 3 — associations, remapped; skip any whose endpoints weren't imported.
|
|
448
508
|
let assocImported = 0;
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
const fromId = idMap.get(assoc.from_id);
|
|
452
|
-
const toId = idMap.get(assoc.to_id);
|
|
509
|
+
for (const a of (importData.associations ?? [])) {
|
|
510
|
+
const fromId = idMap.get(a.from_id), toId = idMap.get(a.to_id);
|
|
453
511
|
if (!fromId || !toId)
|
|
454
|
-
continue;
|
|
512
|
+
continue;
|
|
455
513
|
if (!dryRun) {
|
|
456
|
-
|
|
514
|
+
try {
|
|
515
|
+
await store.upsertAssociation(fromId, toId, a.weight ?? 0.5, a.type ?? 'hebbian', a.confidence ?? 0.5);
|
|
516
|
+
}
|
|
517
|
+
catch { /* best-effort */ }
|
|
457
518
|
}
|
|
458
519
|
assocImported++;
|
|
459
520
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
521
|
+
const prefix = dryRun ? '[DRY RUN] Would import' : 'Imported';
|
|
522
|
+
console.log(`${prefix} ${imported} memories, ${assocImported} associations` +
|
|
523
|
+
(skippedDupes > 0 ? `, ${skippedDupes} skipped (dupes)` : '') +
|
|
524
|
+
(skippedRetracted > 0 ? `, ${skippedRetracted} skipped (retracted)` : '') +
|
|
525
|
+
(remapAgent ? ` (agent remapped to: ${remapAgent})` : '') +
|
|
526
|
+
` (backend: ${backend}${noEmbeddings ? ', embeddings skipped' : ''})`);
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
await close();
|
|
530
|
+
}
|
|
469
531
|
}
|
|
470
532
|
// ─── MERGE ──────────────────────────────────────
|
|
471
533
|
async function mergeMemories() {
|
|
472
534
|
const Database = (await import('better-sqlite3')).default;
|
|
535
|
+
const { EngramStore } = await import('./storage/sqlite.js');
|
|
473
536
|
const { createHash, randomUUID } = await import('node:crypto');
|
|
474
537
|
let target = '';
|
|
475
538
|
const sources = [];
|
|
@@ -516,27 +579,22 @@ async function mergeMemories() {
|
|
|
516
579
|
return createHash('sha256').update((concept + '\n' + content).toLowerCase().trim()).digest('hex');
|
|
517
580
|
}
|
|
518
581
|
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
|
|
582
|
+
// Open the target through the REAL store so it has the full, current schema (all columns) + the FTS
|
|
583
|
+
// triggers. The previous hand-rolled schema dropped embedding/memory_class/memory_type/supersession/
|
|
584
|
+
// task columns and never created engrams_fts — so merged rows lost vector recall, class, AND BM25.
|
|
585
|
+
const store = new EngramStore(target);
|
|
586
|
+
const targetDb = store.db; // the store's better-sqlite3 handle
|
|
587
|
+
const blobToArr = (b) => {
|
|
588
|
+
const buf = b;
|
|
589
|
+
return buf && buf.length ? Array.from(new Float32Array(buf.buffer, buf.byteOffset, Math.floor(buf.length / 4))) : undefined;
|
|
590
|
+
};
|
|
591
|
+
const parseJson = (s, fallback) => { try {
|
|
592
|
+
return s ? JSON.parse(String(s)) : fallback;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return fallback;
|
|
596
|
+
} };
|
|
597
|
+
// Build dedupe hash set from existing target memories (cross-agent read via the store's handle)
|
|
540
598
|
const existingHashes = new Set();
|
|
541
599
|
if (dedupe) {
|
|
542
600
|
const rows = targetDb.prepare('SELECT concept, content FROM engrams').all();
|
|
@@ -544,33 +602,27 @@ async function mergeMemories() {
|
|
|
544
602
|
existingHashes.add(contentHash(row.concept, row.content));
|
|
545
603
|
console.log(`Target has ${existingHashes.size} unique memories (for dedupe)\n`);
|
|
546
604
|
}
|
|
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
605
|
const insertAssoc = targetDb.prepare(`
|
|
554
606
|
INSERT OR IGNORE INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type,
|
|
555
607
|
activation_count, created_at, last_activated)
|
|
556
608
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
557
609
|
`);
|
|
558
610
|
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;
|
|
611
|
+
try {
|
|
612
|
+
for (const sourcePath of sources) {
|
|
613
|
+
if (!existsSync(sourcePath)) {
|
|
614
|
+
console.error(` Source not found: ${sourcePath}`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const sourceDb = new Database(sourcePath, { readonly: true });
|
|
618
|
+
// SELECT * is robust to older source schemas — a column a source predates just reads back undefined
|
|
619
|
+
// and createEngram fills the default.
|
|
620
|
+
const engrams = sourceDb.prepare('SELECT * FROM engrams').all();
|
|
621
|
+
const assocs = sourceDb.prepare('SELECT * FROM associations').all();
|
|
622
|
+
sourceDb.close(); // reads done — release the source handle before any (throwing) write work
|
|
623
|
+
const idMap = new Map();
|
|
624
|
+
const skippedIds = new Set();
|
|
625
|
+
let imported = 0, skipped = 0, assocImported = 0;
|
|
574
626
|
for (const e of engrams) {
|
|
575
627
|
const hash = contentHash(e.concept, e.content);
|
|
576
628
|
if (dedupe && existingHashes.has(hash)) {
|
|
@@ -578,15 +630,63 @@ async function mergeMemories() {
|
|
|
578
630
|
skipped++;
|
|
579
631
|
continue;
|
|
580
632
|
}
|
|
581
|
-
const newId = randomUUID();
|
|
582
|
-
idMap.set(e.id, newId);
|
|
583
633
|
existingHashes.add(hash);
|
|
584
|
-
if (
|
|
585
|
-
|
|
634
|
+
if (dryRun) {
|
|
635
|
+
idMap.set(e.id, e.id);
|
|
636
|
+
imported++;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
// Route each engram through the store's createEngram so EVERY column (embedding, memory_class,
|
|
640
|
+
// memory_type, task fields, sequence, references) AND the FTS index are populated correctly.
|
|
641
|
+
const created = store.createEngram({
|
|
642
|
+
agentId: remapAgentId(e.agent_id),
|
|
643
|
+
concept: e.concept, content: e.content,
|
|
644
|
+
embedding: blobToArr(e.embedding),
|
|
645
|
+
confidence: e.confidence ?? 0.5, salience: e.salience ?? 0.5,
|
|
646
|
+
salienceFeatures: parseJson(e.salience_features, undefined),
|
|
647
|
+
reasonCodes: parseJson(e.reason_codes, undefined),
|
|
648
|
+
tags: parseJson(e.tags, []),
|
|
649
|
+
memoryClass: e.memory_class ?? 'working',
|
|
650
|
+
memoryType: e.memory_type ?? undefined,
|
|
651
|
+
episodeId: e.episode_id ?? undefined,
|
|
652
|
+
taskStatus: e.task_status ?? undefined,
|
|
653
|
+
taskPriority: e.task_priority ?? undefined,
|
|
654
|
+
blockedBy: e.blocked_by ?? undefined,
|
|
655
|
+
ttl: e.ttl ?? undefined,
|
|
656
|
+
sequence: e.sequence ?? undefined,
|
|
657
|
+
references: parseJson(e.references_json, undefined),
|
|
658
|
+
});
|
|
659
|
+
idMap.set(e.id, created.id);
|
|
660
|
+
// preserve stage + retracted (createEngram always mints active/non-retracted)
|
|
661
|
+
if (typeof e.stage === 'string' && e.stage && e.stage !== 'active') {
|
|
662
|
+
try {
|
|
663
|
+
store.updateStage(created.id, e.stage);
|
|
664
|
+
}
|
|
665
|
+
catch { /* */ }
|
|
666
|
+
}
|
|
667
|
+
if (e.retracted) {
|
|
668
|
+
try {
|
|
669
|
+
store.retractEngram(created.id, e.retracted_by ?? null);
|
|
670
|
+
}
|
|
671
|
+
catch { /* */ }
|
|
586
672
|
}
|
|
587
673
|
imported++;
|
|
588
674
|
}
|
|
589
|
-
|
|
675
|
+
// second pass — re-link supersession with remapped ids
|
|
676
|
+
if (!dryRun) {
|
|
677
|
+
for (const e of engrams) {
|
|
678
|
+
const newId = idMap.get(e.id);
|
|
679
|
+
if (!newId || !e.supersedes)
|
|
680
|
+
continue;
|
|
681
|
+
const supNew = idMap.get(e.supersedes);
|
|
682
|
+
if (supNew) {
|
|
683
|
+
try {
|
|
684
|
+
store.supersedeEngram(supNew, newId);
|
|
685
|
+
}
|
|
686
|
+
catch { /* */ }
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
590
690
|
for (const a of assocs) {
|
|
591
691
|
if (skippedIds.has(a.from_engram_id) || skippedIds.has(a.to_engram_id))
|
|
592
692
|
continue;
|
|
@@ -595,23 +695,29 @@ async function mergeMemories() {
|
|
|
595
695
|
if (!fromId || !toId)
|
|
596
696
|
continue;
|
|
597
697
|
if (!dryRun) {
|
|
598
|
-
|
|
698
|
+
try {
|
|
699
|
+
insertAssoc.run(randomUUID(), fromId, toId, a.weight, a.confidence, a.type, a.activation_count, a.created_at, a.last_activated);
|
|
700
|
+
}
|
|
701
|
+
catch { /* skip an association whose source row has an unbindable/undefined column */ }
|
|
599
702
|
}
|
|
600
703
|
assocImported++;
|
|
601
704
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
705
|
+
const agentSet = new Set(engrams.map((e) => remapAgentId(e.agent_id)));
|
|
706
|
+
console.log(` Source: ${sourcePath}`);
|
|
707
|
+
console.log(` Engrams: ${engrams.length} total, ${imported} imported, ${skipped} skipped`);
|
|
708
|
+
console.log(` Associations: ${assocs.length} total, ${assocImported} imported`);
|
|
709
|
+
console.log(` Agents: ${agentSet.size} (${[...agentSet].slice(0, 5).join(', ')}${agentSet.size > 5 ? '...' : ''})\n`);
|
|
710
|
+
totalMemories += imported;
|
|
711
|
+
totalAssociations += assocImported;
|
|
712
|
+
totalSkipped += skipped;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
try {
|
|
717
|
+
store.close();
|
|
718
|
+
}
|
|
719
|
+
catch { /* */ }
|
|
720
|
+
}
|
|
615
721
|
console.log(`\nTotal: ${totalMemories} memories, ${totalAssociations} associations imported. ${totalSkipped} skipped.`);
|
|
616
722
|
if (dryRun)
|
|
617
723
|
console.log('(dry run — no data written)');
|
|
@@ -655,6 +761,78 @@ async function migrateCmd() {
|
|
|
655
761
|
process.exit(1);
|
|
656
762
|
}
|
|
657
763
|
}
|
|
764
|
+
// ─── ONBOARD ──────────────────────────────────────
|
|
765
|
+
/**
|
|
766
|
+
* Seed the onboarding skill as a canonical memory (idempotent). This is what lets
|
|
767
|
+
* a cold store teach the host agent how to warm-start itself — the agent recalls
|
|
768
|
+
* the skill and follows it. Best-effort: a seeding failure never fails `awm setup`.
|
|
769
|
+
*/
|
|
770
|
+
async function seedOnboardSkill(dbPath, agentId) {
|
|
771
|
+
try {
|
|
772
|
+
const { store, close } = await openCliStore(dbPath);
|
|
773
|
+
try {
|
|
774
|
+
const existing = await store.findActiveMatchByConcept(agentId, ONBOARD_SKILL.concept);
|
|
775
|
+
if (existing)
|
|
776
|
+
return 'Onboarding skill: already present';
|
|
777
|
+
await store.createEngram({
|
|
778
|
+
agentId, concept: ONBOARD_SKILL.concept, content: ONBOARD_SKILL.content,
|
|
779
|
+
tags: ONBOARD_SKILL.tags, confidence: 0.9, salience: 0.9, memoryClass: 'canonical',
|
|
780
|
+
});
|
|
781
|
+
return 'Onboarding skill: seeded (recall it on a cold store to warm-start)';
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
await close();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (e) {
|
|
788
|
+
return `Onboarding skill: skipped (${e?.message ?? 'store unavailable'})`;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function onboardCmd() {
|
|
792
|
+
const docs = [];
|
|
793
|
+
let repo;
|
|
794
|
+
let project = '';
|
|
795
|
+
let agentId = '';
|
|
796
|
+
let purpose;
|
|
797
|
+
let outDir = resolve(process.cwd(), '.awm');
|
|
798
|
+
for (let i = 1; i < args.length; i++) {
|
|
799
|
+
const a = args[i];
|
|
800
|
+
if (a === '--repo' && args[i + 1])
|
|
801
|
+
repo = args[++i];
|
|
802
|
+
else if (a === '--project' && args[i + 1])
|
|
803
|
+
project = args[++i];
|
|
804
|
+
else if (a === '--agent' && args[i + 1])
|
|
805
|
+
agentId = args[++i];
|
|
806
|
+
else if (a === '--purpose' && args[i + 1])
|
|
807
|
+
purpose = args[++i];
|
|
808
|
+
else if (a === '--out' && args[i + 1])
|
|
809
|
+
outDir = resolve(args[++i]);
|
|
810
|
+
else if (!a.startsWith('--'))
|
|
811
|
+
docs.push(a);
|
|
812
|
+
}
|
|
813
|
+
// Default docs to the repo (or cwd) so a bare `awm onboard --repo .` works.
|
|
814
|
+
if (docs.length === 0)
|
|
815
|
+
docs.push(repo ?? process.cwd());
|
|
816
|
+
if (!project)
|
|
817
|
+
project = basename(repo ? resolve(repo) : (docs[0] ? resolve(docs[0]) : process.cwd()));
|
|
818
|
+
if (!agentId)
|
|
819
|
+
agentId = project;
|
|
820
|
+
const { packPath, reviewPath, count } = runOnboard({ docs, repo, project, agentId, purpose, outDir });
|
|
821
|
+
console.log(`
|
|
822
|
+
AWM onboard — warm-start pack for "${project}"
|
|
823
|
+
|
|
824
|
+
Scanned: ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''}
|
|
825
|
+
Extracted: ${count} candidate memories (agent: ${agentId})
|
|
826
|
+
|
|
827
|
+
Review: ${reviewPath}
|
|
828
|
+
Pack: ${packPath}
|
|
829
|
+
|
|
830
|
+
Next:
|
|
831
|
+
1. Edit the review file / pack as needed (delete noise, answer the interview questions).
|
|
832
|
+
2. Load it: awm import ${packPath} --db <path> --dedupe
|
|
833
|
+
(embeddings backfill on the first consolidation — recall is warm immediately after)
|
|
834
|
+
`.trimEnd());
|
|
835
|
+
}
|
|
658
836
|
// ─── Dispatch ──────────────────────────────────────
|
|
659
837
|
switch (command) {
|
|
660
838
|
case 'setup':
|
|
@@ -684,6 +862,9 @@ switch (command) {
|
|
|
684
862
|
case 'migrate':
|
|
685
863
|
await migrateCmd();
|
|
686
864
|
break;
|
|
865
|
+
case 'onboard':
|
|
866
|
+
onboardCmd();
|
|
867
|
+
break;
|
|
687
868
|
case '--help':
|
|
688
869
|
case '-h':
|
|
689
870
|
case undefined:
|