@smyslenny/agent-memory 5.0.2 → 5.1.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.
@@ -99,6 +99,11 @@ function migrateDatabase(db, from, to) {
99
99
  v = 7;
100
100
  continue;
101
101
  }
102
+ if (v === 7) {
103
+ migrateV7ToV8(db);
104
+ v = 8;
105
+ continue;
106
+ }
102
107
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
103
108
  }
104
109
  }
@@ -186,6 +191,8 @@ function inferSchemaVersion(db) {
186
191
  const hasFeedbackEvents = tableExists(db, "feedback_events");
187
192
  const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
188
193
  const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
194
+ const hasMemoryArchive = tableExists(db, "memory_archive");
195
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance && hasMemoryArchive) return 8;
189
196
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
190
197
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
191
198
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
@@ -227,6 +234,11 @@ function ensureIndexes(db) {
227
234
  db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
228
235
  }
229
236
  }
237
+ if (tableExists(db, "memory_archive")) {
238
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);");
239
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);");
240
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);");
241
+ }
230
242
  }
231
243
  function ensureFeedbackEventSchema(db) {
232
244
  if (!tableExists(db, "feedback_events")) return;
@@ -402,11 +414,55 @@ function migrateV6ToV7(db) {
402
414
  throw e;
403
415
  }
404
416
  }
417
+ function migrateV7ToV8(db) {
418
+ if (tableExists(db, "memory_archive")) {
419
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(8));
420
+ return;
421
+ }
422
+ try {
423
+ db.exec("BEGIN");
424
+ db.exec(`
425
+ CREATE TABLE IF NOT EXISTS memory_archive (
426
+ id TEXT PRIMARY KEY,
427
+ content TEXT NOT NULL,
428
+ type TEXT NOT NULL,
429
+ priority INTEGER NOT NULL,
430
+ emotion_val REAL NOT NULL DEFAULT 0.0,
431
+ vitality REAL NOT NULL DEFAULT 0.0,
432
+ stability REAL NOT NULL DEFAULT 1.0,
433
+ access_count INTEGER NOT NULL DEFAULT 0,
434
+ last_accessed TEXT,
435
+ created_at TEXT NOT NULL,
436
+ updated_at TEXT NOT NULL,
437
+ archived_at TEXT NOT NULL,
438
+ archive_reason TEXT NOT NULL DEFAULT 'eviction',
439
+ source TEXT,
440
+ agent_id TEXT NOT NULL DEFAULT 'default',
441
+ hash TEXT,
442
+ emotion_tag TEXT,
443
+ source_session TEXT,
444
+ source_context TEXT,
445
+ observed_at TEXT
446
+ );
447
+ `);
448
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);");
449
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);");
450
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);");
451
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(8));
452
+ db.exec("COMMIT");
453
+ } catch (e) {
454
+ try {
455
+ db.exec("ROLLBACK");
456
+ } catch {
457
+ }
458
+ throw e;
459
+ }
460
+ }
405
461
  var SCHEMA_VERSION, SCHEMA_SQL;
406
462
  var init_db = __esm({
407
463
  "src/core/db.ts"() {
408
464
  "use strict";
409
- SCHEMA_VERSION = 7;
465
+ SCHEMA_VERSION = 8;
410
466
  SCHEMA_SQL = `
411
467
  -- Memory entries
412
468
  CREATE TABLE IF NOT EXISTS memories (
@@ -512,6 +568,30 @@ CREATE TABLE IF NOT EXISTS schema_meta (
512
568
  value TEXT NOT NULL
513
569
  );
514
570
 
571
+ -- Memory archive (eviction archive)
572
+ CREATE TABLE IF NOT EXISTS memory_archive (
573
+ id TEXT PRIMARY KEY,
574
+ content TEXT NOT NULL,
575
+ type TEXT NOT NULL,
576
+ priority INTEGER NOT NULL,
577
+ emotion_val REAL NOT NULL DEFAULT 0.0,
578
+ vitality REAL NOT NULL DEFAULT 0.0,
579
+ stability REAL NOT NULL DEFAULT 1.0,
580
+ access_count INTEGER NOT NULL DEFAULT 0,
581
+ last_accessed TEXT,
582
+ created_at TEXT NOT NULL,
583
+ updated_at TEXT NOT NULL,
584
+ archived_at TEXT NOT NULL,
585
+ archive_reason TEXT NOT NULL DEFAULT 'eviction',
586
+ source TEXT,
587
+ agent_id TEXT NOT NULL DEFAULT 'default',
588
+ hash TEXT,
589
+ emotion_tag TEXT,
590
+ source_session TEXT,
591
+ source_context TEXT,
592
+ observed_at TEXT
593
+ );
594
+
515
595
  -- Indexes for common queries
516
596
  CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
517
597
  CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
@@ -522,6 +602,9 @@ CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
522
602
  CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
523
603
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
524
604
  CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
605
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);
606
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);
607
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);
525
608
  `;
526
609
  }
527
610
  });
@@ -1037,13 +1120,17 @@ var init_vector = __esm({
1037
1120
  // src/core/memory.ts
1038
1121
  var memory_exports = {};
1039
1122
  __export(memory_exports, {
1123
+ archiveMemory: () => archiveMemory,
1040
1124
  contentHash: () => contentHash,
1041
1125
  countMemories: () => countMemories,
1042
1126
  createMemory: () => createMemory,
1043
1127
  deleteMemory: () => deleteMemory,
1044
1128
  getMemory: () => getMemory,
1129
+ listArchivedMemories: () => listArchivedMemories,
1045
1130
  listMemories: () => listMemories,
1131
+ purgeArchive: () => purgeArchive,
1046
1132
  recordAccess: () => recordAccess,
1133
+ restoreMemory: () => restoreMemory,
1047
1134
  updateMemory: () => updateMemory
1048
1135
  });
1049
1136
  import { createHash as createHash2 } from "crypto";
@@ -1162,9 +1249,118 @@ function updateMemory(db, id, input) {
1162
1249
  }
1163
1250
  function deleteMemory(db, id) {
1164
1251
  db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
1252
+ try {
1253
+ db.prepare("DELETE FROM embeddings WHERE memory_id = ?").run(id);
1254
+ } catch {
1255
+ }
1165
1256
  const result = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
1166
1257
  return result.changes > 0;
1167
1258
  }
1259
+ function archiveMemory(db, id, reason, opts) {
1260
+ const mem = getMemory(db, id);
1261
+ if (!mem) return false;
1262
+ const minVitality = opts?.minVitality ?? 0.1;
1263
+ if (mem.vitality < minVitality) {
1264
+ deleteMemory(db, id);
1265
+ return "deleted";
1266
+ }
1267
+ const archivedAt = now();
1268
+ const archiveReason = reason ?? "eviction";
1269
+ db.prepare(
1270
+ `INSERT OR REPLACE INTO memory_archive
1271
+ (id, content, type, priority, emotion_val, vitality, stability, access_count,
1272
+ last_accessed, created_at, updated_at, archived_at, archive_reason,
1273
+ source, agent_id, hash, emotion_tag, source_session, source_context, observed_at)
1274
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1275
+ ).run(
1276
+ mem.id,
1277
+ mem.content,
1278
+ mem.type,
1279
+ mem.priority,
1280
+ mem.emotion_val,
1281
+ mem.vitality,
1282
+ mem.stability,
1283
+ mem.access_count,
1284
+ mem.last_accessed,
1285
+ mem.created_at,
1286
+ mem.updated_at,
1287
+ archivedAt,
1288
+ archiveReason,
1289
+ mem.source,
1290
+ mem.agent_id,
1291
+ mem.hash,
1292
+ mem.emotion_tag,
1293
+ mem.source_session,
1294
+ mem.source_context,
1295
+ mem.observed_at
1296
+ );
1297
+ deleteMemory(db, id);
1298
+ return "archived";
1299
+ }
1300
+ function restoreMemory(db, id) {
1301
+ const archived = db.prepare("SELECT * FROM memory_archive WHERE id = ?").get(id);
1302
+ if (!archived) return null;
1303
+ db.prepare(
1304
+ `INSERT INTO memories
1305
+ (id, content, type, priority, emotion_val, vitality, stability, access_count,
1306
+ last_accessed, created_at, updated_at, source, agent_id, hash, emotion_tag,
1307
+ source_session, source_context, observed_at)
1308
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1309
+ ).run(
1310
+ archived.id,
1311
+ archived.content,
1312
+ archived.type,
1313
+ archived.priority,
1314
+ archived.emotion_val,
1315
+ archived.vitality,
1316
+ archived.stability,
1317
+ archived.access_count,
1318
+ archived.last_accessed,
1319
+ archived.created_at,
1320
+ now(),
1321
+ // updated_at = restore time
1322
+ archived.source,
1323
+ archived.agent_id,
1324
+ archived.hash,
1325
+ archived.emotion_tag,
1326
+ archived.source_session,
1327
+ archived.source_context,
1328
+ archived.observed_at
1329
+ );
1330
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
1331
+ archived.id,
1332
+ tokenizeForIndex(archived.content)
1333
+ );
1334
+ if (archived.hash) {
1335
+ const providerId = getConfiguredEmbeddingProviderId();
1336
+ if (providerId) {
1337
+ try {
1338
+ markMemoryEmbeddingPending(db, archived.id, providerId, archived.hash);
1339
+ } catch {
1340
+ }
1341
+ }
1342
+ }
1343
+ db.prepare("DELETE FROM memory_archive WHERE id = ?").run(id);
1344
+ return getMemory(db, archived.id);
1345
+ }
1346
+ function listArchivedMemories(db, opts) {
1347
+ const agentId = opts?.agent_id;
1348
+ const limit = opts?.limit ?? 20;
1349
+ if (agentId) {
1350
+ return db.prepare(
1351
+ "SELECT * FROM memory_archive WHERE agent_id = ? ORDER BY archived_at DESC LIMIT ?"
1352
+ ).all(agentId, limit);
1353
+ }
1354
+ return db.prepare(
1355
+ "SELECT * FROM memory_archive ORDER BY archived_at DESC LIMIT ?"
1356
+ ).all(limit);
1357
+ }
1358
+ function purgeArchive(db, opts) {
1359
+ if (opts?.agent_id) {
1360
+ return db.prepare("DELETE FROM memory_archive WHERE agent_id = ?").run(opts.agent_id).changes;
1361
+ }
1362
+ return db.prepare("DELETE FROM memory_archive").run().changes;
1363
+ }
1168
1364
  function listMemories(db, opts) {
1169
1365
  const conditions = [];
1170
1366
  const params = [];
@@ -3158,13 +3354,31 @@ function rankEvictionCandidates(db, opts) {
3158
3354
  return left.memory.priority - right.memory.priority;
3159
3355
  });
3160
3356
  }
3357
+ function parseEnvInt(envKey) {
3358
+ const raw = process.env[envKey];
3359
+ if (raw === void 0 || raw === "") return null;
3360
+ const n = Number.parseInt(raw, 10);
3361
+ return Number.isFinite(n) && n > 0 ? n : null;
3362
+ }
3363
+ function getTieredCapacity(opts) {
3364
+ const envMax = parseEnvInt("AGENT_MEMORY_MAX_MEMORIES");
3365
+ return {
3366
+ identity: parseEnvInt("AGENT_MEMORY_MAX_IDENTITY"),
3367
+ // default: null (unlimited)
3368
+ emotion: parseEnvInt("AGENT_MEMORY_MAX_EMOTION") ?? 50,
3369
+ knowledge: parseEnvInt("AGENT_MEMORY_MAX_KNOWLEDGE") ?? 250,
3370
+ event: parseEnvInt("AGENT_MEMORY_MAX_EVENT") ?? 50,
3371
+ total: opts?.maxMemories ?? (envMax ?? 350)
3372
+ };
3373
+ }
3161
3374
  function runGovern(db, opts) {
3162
3375
  const agentId = opts?.agent_id;
3163
- const envMax = Number.parseInt(process.env.AGENT_MEMORY_MAX_MEMORIES ?? "", 10);
3164
- const maxMemories = opts?.maxMemories ?? (Number.isFinite(envMax) && envMax > 0 ? envMax : 200);
3376
+ const capacity = getTieredCapacity(opts);
3165
3377
  let orphanPaths = 0;
3166
3378
  let emptyMemories = 0;
3167
3379
  let evicted = 0;
3380
+ let archived = 0;
3381
+ const evictedByType = {};
3168
3382
  const transaction = db.transaction(() => {
3169
3383
  const pathResult = agentId ? db.prepare(
3170
3384
  `DELETE FROM paths
@@ -3174,17 +3388,48 @@ function runGovern(db, opts) {
3174
3388
  orphanPaths = pathResult.changes;
3175
3389
  const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
3176
3390
  emptyMemories = emptyResult.changes;
3391
+ const typeLimits = [
3392
+ { type: "identity", limit: capacity.identity },
3393
+ { type: "emotion", limit: capacity.emotion },
3394
+ { type: "knowledge", limit: capacity.knowledge },
3395
+ { type: "event", limit: capacity.event }
3396
+ ];
3397
+ const allCandidates = rankEvictionCandidates(db, { agent_id: agentId });
3398
+ const evictedIds = /* @__PURE__ */ new Set();
3399
+ for (const { type, limit } of typeLimits) {
3400
+ if (limit === null) continue;
3401
+ const typeCount = db.prepare(
3402
+ agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ? AND type = ?" : "SELECT COUNT(*) as c FROM memories WHERE type = ?"
3403
+ ).get(...agentId ? [agentId, type] : [type]).c;
3404
+ const excess = Math.max(0, typeCount - limit);
3405
+ if (excess <= 0) continue;
3406
+ const typeCandidates = allCandidates.filter((c) => c.memory.type === type && !evictedIds.has(c.memory.id));
3407
+ const toEvict = typeCandidates.slice(0, excess);
3408
+ for (const candidate of toEvict) {
3409
+ const result = archiveMemory(db, candidate.memory.id, "eviction");
3410
+ evictedIds.add(candidate.memory.id);
3411
+ evicted += 1;
3412
+ if (result === "archived") archived += 1;
3413
+ evictedByType[type] = (evictedByType[type] ?? 0) + 1;
3414
+ }
3415
+ }
3177
3416
  const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
3178
- const excess = Math.max(0, total - maxMemories);
3179
- if (excess <= 0) return;
3180
- const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
3181
- for (const candidate of candidates) {
3182
- deleteMemory(db, candidate.memory.id);
3183
- evicted += 1;
3417
+ const globalExcess = Math.max(0, total - capacity.total);
3418
+ if (globalExcess > 0) {
3419
+ const globalCandidates = allCandidates.filter((c) => !evictedIds.has(c.memory.id));
3420
+ const toEvict = globalCandidates.slice(0, globalExcess);
3421
+ for (const candidate of toEvict) {
3422
+ const result = archiveMemory(db, candidate.memory.id, "eviction");
3423
+ evictedIds.add(candidate.memory.id);
3424
+ evicted += 1;
3425
+ if (result === "archived") archived += 1;
3426
+ const t = candidate.memory.type;
3427
+ evictedByType[t] = (evictedByType[t] ?? 0) + 1;
3428
+ }
3184
3429
  }
3185
3430
  });
3186
3431
  transaction();
3187
- return { orphanPaths, emptyMemories, evicted };
3432
+ return { orphanPaths, emptyMemories, evicted, archived, evictedByType };
3188
3433
  }
3189
3434
 
3190
3435
  // src/sleep/jobs.ts
@@ -3427,12 +3672,21 @@ function getMemoryStatus(db, input) {
3427
3672
  const feedbackEvents = db.prepare(
3428
3673
  "SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
3429
3674
  ).get(agentId);
3675
+ const tiered = getTieredCapacity();
3676
+ const capacity = {
3677
+ identity: { count: stats.by_type.identity ?? 0, limit: tiered.identity },
3678
+ emotion: { count: stats.by_type.emotion ?? 0, limit: tiered.emotion },
3679
+ knowledge: { count: stats.by_type.knowledge ?? 0, limit: tiered.knowledge },
3680
+ event: { count: stats.by_type.event ?? 0, limit: tiered.event },
3681
+ total: { count: stats.total, limit: tiered.total }
3682
+ };
3430
3683
  return {
3431
3684
  ...stats,
3432
3685
  paths: totalPaths.c,
3433
3686
  low_vitality: lowVitality.c,
3434
3687
  feedback_events: feedbackEvents.c,
3435
- agent_id: agentId
3688
+ agent_id: agentId,
3689
+ capacity
3436
3690
  };
3437
3691
  }
3438
3692
 
@@ -3617,7 +3871,7 @@ function createMcpServer(dbPath, agentId) {
3617
3871
  const aid = agentId ?? AGENT_ID;
3618
3872
  const server = new McpServer({
3619
3873
  name: "agent-memory",
3620
- version: "5.0.0"
3874
+ version: "5.1.0"
3621
3875
  });
3622
3876
  server.tool(
3623
3877
  "remember",
@@ -3768,8 +4022,8 @@ function createMcpServer(dbPath, agentId) {
3768
4022
  return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
3769
4023
  }
3770
4024
  if (hard) {
3771
- const { deleteMemory: deleteMemory2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
3772
- deleteMemory2(db, id);
4025
+ const { deleteMemory: deleteMemory3 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
4026
+ deleteMemory3(db, id);
3773
4027
  return { content: [{ type: "text", text: JSON.stringify({ action: "deleted", id }) }] };
3774
4028
  }
3775
4029
  updateMemory(db, id, { vitality: Math.max(0, memory.vitality * 0.1) });
@@ -3929,6 +4183,74 @@ function createMcpServer(dbPath, agentId) {
3929
4183
  };
3930
4184
  }
3931
4185
  );
4186
+ server.tool(
4187
+ "archive",
4188
+ "Manage archived memories (evicted by governance). List, restore, or purge archived memories.",
4189
+ {
4190
+ action: z.enum(["list", "restore", "purge"]).describe("list: view archived memories; restore: recover one by id; purge: permanently delete all archived"),
4191
+ id: z.string().optional().describe("Memory ID to restore (required for restore action)"),
4192
+ agent_id: z.string().optional().describe("Override agent scope (defaults to current agent)"),
4193
+ limit: z.number().min(1).max(100).default(20).optional().describe("Max results for list (default 20)")
4194
+ },
4195
+ async ({ action, id, agent_id, limit }) => {
4196
+ const effectiveAgentId = agent_id ?? aid;
4197
+ if (action === "list") {
4198
+ const archived = listArchivedMemories(db, { agent_id: effectiveAgentId, limit: limit ?? 20 });
4199
+ return {
4200
+ content: [{
4201
+ type: "text",
4202
+ text: JSON.stringify({
4203
+ action: "list",
4204
+ count: archived.length,
4205
+ memories: archived.map((m) => ({
4206
+ id: m.id,
4207
+ content: m.content.slice(0, 200),
4208
+ type: m.type,
4209
+ priority: m.priority,
4210
+ vitality: m.vitality,
4211
+ archived_at: m.archived_at,
4212
+ archive_reason: m.archive_reason
4213
+ }))
4214
+ }, null, 2)
4215
+ }]
4216
+ };
4217
+ }
4218
+ if (action === "restore") {
4219
+ if (!id) {
4220
+ return { content: [{ type: "text", text: '{"error": "id is required for restore action"}' }] };
4221
+ }
4222
+ const restored = restoreMemory(db, id);
4223
+ if (!restored) {
4224
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Archived memory not found", id }) }] };
4225
+ }
4226
+ return {
4227
+ content: [{
4228
+ type: "text",
4229
+ text: JSON.stringify({
4230
+ action: "restored",
4231
+ memory: {
4232
+ id: restored.id,
4233
+ content: restored.content,
4234
+ type: restored.type,
4235
+ priority: restored.priority,
4236
+ vitality: restored.vitality
4237
+ }
4238
+ }, null, 2)
4239
+ }]
4240
+ };
4241
+ }
4242
+ if (action === "purge") {
4243
+ const purged = purgeArchive(db, { agent_id: effectiveAgentId });
4244
+ return {
4245
+ content: [{
4246
+ type: "text",
4247
+ text: JSON.stringify({ action: "purged", deleted: purged }, null, 2)
4248
+ }]
4249
+ };
4250
+ }
4251
+ return { content: [{ type: "text", text: '{"error": "Unknown action"}' }] };
4252
+ }
4253
+ );
3932
4254
  return { server, db };
3933
4255
  }
3934
4256
  async function main() {