@smyslenny/agent-memory 5.0.1 → 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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // AgentMemory v2 — Sleep-cycle memory for AI agents
1
+ // AgentMemory — Sleep-cycle memory for AI agents
2
2
 
3
3
  // src/core/memory.ts
4
4
  import { createHash as createHash2 } from "crypto";
@@ -6,7 +6,7 @@ import { createHash as createHash2 } from "crypto";
6
6
  // src/core/db.ts
7
7
  import Database from "better-sqlite3";
8
8
  import { randomUUID } from "crypto";
9
- var SCHEMA_VERSION = 7;
9
+ var SCHEMA_VERSION = 8;
10
10
  var SCHEMA_SQL = `
11
11
  -- Memory entries
12
12
  CREATE TABLE IF NOT EXISTS memories (
@@ -112,6 +112,30 @@ CREATE TABLE IF NOT EXISTS schema_meta (
112
112
  value TEXT NOT NULL
113
113
  );
114
114
 
115
+ -- Memory archive (eviction archive)
116
+ CREATE TABLE IF NOT EXISTS memory_archive (
117
+ id TEXT PRIMARY KEY,
118
+ content TEXT NOT NULL,
119
+ type TEXT NOT NULL,
120
+ priority INTEGER NOT NULL,
121
+ emotion_val REAL NOT NULL DEFAULT 0.0,
122
+ vitality REAL NOT NULL DEFAULT 0.0,
123
+ stability REAL NOT NULL DEFAULT 1.0,
124
+ access_count INTEGER NOT NULL DEFAULT 0,
125
+ last_accessed TEXT,
126
+ created_at TEXT NOT NULL,
127
+ updated_at TEXT NOT NULL,
128
+ archived_at TEXT NOT NULL,
129
+ archive_reason TEXT NOT NULL DEFAULT 'eviction',
130
+ source TEXT,
131
+ agent_id TEXT NOT NULL DEFAULT 'default',
132
+ hash TEXT,
133
+ emotion_tag TEXT,
134
+ source_session TEXT,
135
+ source_context TEXT,
136
+ observed_at TEXT
137
+ );
138
+
115
139
  -- Indexes for common queries
116
140
  CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
117
141
  CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
@@ -122,6 +146,9 @@ CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
122
146
  CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
123
147
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
124
148
  CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
149
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);
150
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);
151
+ CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);
125
152
  `;
126
153
  function isCountRow(row) {
127
154
  return row !== null && typeof row === "object" && "c" in row && typeof row.c === "number";
@@ -213,6 +240,11 @@ function migrateDatabase(db, from, to) {
213
240
  v = 7;
214
241
  continue;
215
242
  }
243
+ if (v === 7) {
244
+ migrateV7ToV8(db);
245
+ v = 8;
246
+ continue;
247
+ }
216
248
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
217
249
  }
218
250
  }
@@ -300,6 +332,8 @@ function inferSchemaVersion(db) {
300
332
  const hasFeedbackEvents = tableExists(db, "feedback_events");
301
333
  const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
302
334
  const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
335
+ const hasMemoryArchive = tableExists(db, "memory_archive");
336
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance && hasMemoryArchive) return 8;
303
337
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
304
338
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
305
339
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
@@ -341,6 +375,11 @@ function ensureIndexes(db) {
341
375
  db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
342
376
  }
343
377
  }
378
+ if (tableExists(db, "memory_archive")) {
379
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);");
380
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);");
381
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);");
382
+ }
344
383
  }
345
384
  function ensureFeedbackEventSchema(db) {
346
385
  if (!tableExists(db, "feedback_events")) return;
@@ -516,6 +555,50 @@ function migrateV6ToV7(db) {
516
555
  throw e;
517
556
  }
518
557
  }
558
+ function migrateV7ToV8(db) {
559
+ if (tableExists(db, "memory_archive")) {
560
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(8));
561
+ return;
562
+ }
563
+ try {
564
+ db.exec("BEGIN");
565
+ db.exec(`
566
+ CREATE TABLE IF NOT EXISTS memory_archive (
567
+ id TEXT PRIMARY KEY,
568
+ content TEXT NOT NULL,
569
+ type TEXT NOT NULL,
570
+ priority INTEGER NOT NULL,
571
+ emotion_val REAL NOT NULL DEFAULT 0.0,
572
+ vitality REAL NOT NULL DEFAULT 0.0,
573
+ stability REAL NOT NULL DEFAULT 1.0,
574
+ access_count INTEGER NOT NULL DEFAULT 0,
575
+ last_accessed TEXT,
576
+ created_at TEXT NOT NULL,
577
+ updated_at TEXT NOT NULL,
578
+ archived_at TEXT NOT NULL,
579
+ archive_reason TEXT NOT NULL DEFAULT 'eviction',
580
+ source TEXT,
581
+ agent_id TEXT NOT NULL DEFAULT 'default',
582
+ hash TEXT,
583
+ emotion_tag TEXT,
584
+ source_session TEXT,
585
+ source_context TEXT,
586
+ observed_at TEXT
587
+ );
588
+ `);
589
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_agent ON memory_archive(agent_id);");
590
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_type ON memory_archive(type);");
591
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_archive_archived_at ON memory_archive(archived_at);");
592
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(8));
593
+ db.exec("COMMIT");
594
+ } catch (e) {
595
+ try {
596
+ db.exec("ROLLBACK");
597
+ } catch {
598
+ }
599
+ throw e;
600
+ }
601
+ }
519
602
 
520
603
  // src/search/tokenizer.ts
521
604
  import { readFileSync } from "fs";
@@ -1169,9 +1252,118 @@ function updateMemory(db, id, input) {
1169
1252
  }
1170
1253
  function deleteMemory(db, id) {
1171
1254
  db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
1255
+ try {
1256
+ db.prepare("DELETE FROM embeddings WHERE memory_id = ?").run(id);
1257
+ } catch {
1258
+ }
1172
1259
  const result = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
1173
1260
  return result.changes > 0;
1174
1261
  }
1262
+ function archiveMemory(db, id, reason, opts) {
1263
+ const mem = getMemory(db, id);
1264
+ if (!mem) return false;
1265
+ const minVitality = opts?.minVitality ?? 0.1;
1266
+ if (mem.vitality < minVitality) {
1267
+ deleteMemory(db, id);
1268
+ return "deleted";
1269
+ }
1270
+ const archivedAt = now();
1271
+ const archiveReason = reason ?? "eviction";
1272
+ db.prepare(
1273
+ `INSERT OR REPLACE INTO memory_archive
1274
+ (id, content, type, priority, emotion_val, vitality, stability, access_count,
1275
+ last_accessed, created_at, updated_at, archived_at, archive_reason,
1276
+ source, agent_id, hash, emotion_tag, source_session, source_context, observed_at)
1277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1278
+ ).run(
1279
+ mem.id,
1280
+ mem.content,
1281
+ mem.type,
1282
+ mem.priority,
1283
+ mem.emotion_val,
1284
+ mem.vitality,
1285
+ mem.stability,
1286
+ mem.access_count,
1287
+ mem.last_accessed,
1288
+ mem.created_at,
1289
+ mem.updated_at,
1290
+ archivedAt,
1291
+ archiveReason,
1292
+ mem.source,
1293
+ mem.agent_id,
1294
+ mem.hash,
1295
+ mem.emotion_tag,
1296
+ mem.source_session,
1297
+ mem.source_context,
1298
+ mem.observed_at
1299
+ );
1300
+ deleteMemory(db, id);
1301
+ return "archived";
1302
+ }
1303
+ function restoreMemory(db, id) {
1304
+ const archived = db.prepare("SELECT * FROM memory_archive WHERE id = ?").get(id);
1305
+ if (!archived) return null;
1306
+ db.prepare(
1307
+ `INSERT INTO memories
1308
+ (id, content, type, priority, emotion_val, vitality, stability, access_count,
1309
+ last_accessed, created_at, updated_at, source, agent_id, hash, emotion_tag,
1310
+ source_session, source_context, observed_at)
1311
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1312
+ ).run(
1313
+ archived.id,
1314
+ archived.content,
1315
+ archived.type,
1316
+ archived.priority,
1317
+ archived.emotion_val,
1318
+ archived.vitality,
1319
+ archived.stability,
1320
+ archived.access_count,
1321
+ archived.last_accessed,
1322
+ archived.created_at,
1323
+ now(),
1324
+ // updated_at = restore time
1325
+ archived.source,
1326
+ archived.agent_id,
1327
+ archived.hash,
1328
+ archived.emotion_tag,
1329
+ archived.source_session,
1330
+ archived.source_context,
1331
+ archived.observed_at
1332
+ );
1333
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
1334
+ archived.id,
1335
+ tokenizeForIndex(archived.content)
1336
+ );
1337
+ if (archived.hash) {
1338
+ const providerId = getConfiguredEmbeddingProviderId();
1339
+ if (providerId) {
1340
+ try {
1341
+ markMemoryEmbeddingPending(db, archived.id, providerId, archived.hash);
1342
+ } catch {
1343
+ }
1344
+ }
1345
+ }
1346
+ db.prepare("DELETE FROM memory_archive WHERE id = ?").run(id);
1347
+ return getMemory(db, archived.id);
1348
+ }
1349
+ function listArchivedMemories(db, opts) {
1350
+ const agentId = opts?.agent_id;
1351
+ const limit = opts?.limit ?? 20;
1352
+ if (agentId) {
1353
+ return db.prepare(
1354
+ "SELECT * FROM memory_archive WHERE agent_id = ? ORDER BY archived_at DESC LIMIT ?"
1355
+ ).all(agentId, limit);
1356
+ }
1357
+ return db.prepare(
1358
+ "SELECT * FROM memory_archive ORDER BY archived_at DESC LIMIT ?"
1359
+ ).all(limit);
1360
+ }
1361
+ function purgeArchive(db, opts) {
1362
+ if (opts?.agent_id) {
1363
+ return db.prepare("DELETE FROM memory_archive WHERE agent_id = ?").run(opts.agent_id).changes;
1364
+ }
1365
+ return db.prepare("DELETE FROM memory_archive").run().changes;
1366
+ }
1175
1367
  function listMemories(db, opts) {
1176
1368
  const conditions = [];
1177
1369
  const params = [];
@@ -1433,21 +1625,20 @@ async function searchVectorBranch(db, query, opts) {
1433
1625
  before: opts.before
1434
1626
  });
1435
1627
  }
1436
- function expandRelated(db, results, agentId, maxTotal) {
1437
- const existingIds = new Set(results.map((r) => r.memory.id));
1628
+ function fetchRelatedLinks(db, sourceIds, agentId, excludeIds, maxPerSource = 5) {
1438
1629
  const related = [];
1439
- for (const result of results) {
1630
+ for (const sourceId of sourceIds) {
1440
1631
  const links = db.prepare(
1441
1632
  `SELECT l.target_id, l.weight, m.*
1442
1633
  FROM links l
1443
1634
  JOIN memories m ON m.id = l.target_id
1444
1635
  WHERE l.agent_id = ? AND l.source_id = ?
1445
1636
  ORDER BY l.weight DESC
1446
- LIMIT 5`
1447
- ).all(agentId, result.memory.id);
1637
+ LIMIT ?`
1638
+ ).all(agentId, sourceId, maxPerSource);
1448
1639
  for (const link of links) {
1449
- if (existingIds.has(link.target_id)) continue;
1450
- existingIds.add(link.target_id);
1640
+ if (excludeIds.has(link.target_id)) continue;
1641
+ excludeIds.add(link.target_id);
1451
1642
  const relatedMemory = {
1452
1643
  id: link.id,
1453
1644
  content: link.content,
@@ -1470,12 +1661,30 @@ function expandRelated(db, results, agentId, maxTotal) {
1470
1661
  };
1471
1662
  related.push({
1472
1663
  memory: relatedMemory,
1473
- score: result.score * link.weight * 0.6,
1474
- related_source_id: result.memory.id,
1475
- match_type: "related"
1664
+ sourceId,
1665
+ weight: link.weight
1476
1666
  });
1477
1667
  }
1478
1668
  }
1669
+ return related;
1670
+ }
1671
+ function expandRelated(db, results, agentId, maxTotal) {
1672
+ const existingIds = new Set(results.map((r) => r.memory.id));
1673
+ const links = fetchRelatedLinks(
1674
+ db,
1675
+ results.map((r) => r.memory.id),
1676
+ agentId,
1677
+ existingIds
1678
+ );
1679
+ const related = links.map((link) => {
1680
+ const sourceResult = results.find((r) => r.memory.id === link.sourceId);
1681
+ return {
1682
+ memory: link.memory,
1683
+ score: (sourceResult?.score ?? 0) * link.weight * 0.6,
1684
+ related_source_id: link.sourceId,
1685
+ match_type: "related"
1686
+ };
1687
+ });
1479
1688
  const directResults = results.map((r) => ({
1480
1689
  ...r,
1481
1690
  match_type: "direct"
@@ -2501,7 +2710,8 @@ async function surfaceMemories(db, input) {
2501
2710
  }),
2502
2711
  lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
2503
2712
  semantic_rank: signal.semanticRank,
2504
- semantic_similarity: signal.semanticSimilarity
2713
+ semantic_similarity: signal.semanticSimilarity,
2714
+ match_type: "direct"
2505
2715
  };
2506
2716
  }).sort((left, right) => {
2507
2717
  if (right.score !== left.score) return right.score - left.score;
@@ -2510,6 +2720,42 @@ async function surfaceMemories(db, input) {
2510
2720
  if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
2511
2721
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
2512
2722
  }).slice(0, limit);
2723
+ if (input.related) {
2724
+ const existingIds = new Set(results.map((r) => r.memory.id));
2725
+ const links = fetchRelatedLinks(
2726
+ db,
2727
+ results.map((r) => r.memory.id),
2728
+ agentId,
2729
+ existingIds
2730
+ );
2731
+ const relatedResults = links.map((link) => {
2732
+ const sourceResult = results.find((r) => r.memory.id === link.sourceId);
2733
+ const feedbackSummary = getFeedbackSummary(db, link.memory.id, agentId);
2734
+ return {
2735
+ memory: link.memory,
2736
+ score: (sourceResult?.score ?? 0) * link.weight * 0.6,
2737
+ semantic_score: 0,
2738
+ lexical_score: 0,
2739
+ task_match: 0,
2740
+ vitality: link.memory.vitality,
2741
+ priority_prior: priorityPrior(link.memory.priority),
2742
+ feedback_score: feedbackSummary.score,
2743
+ feedback_summary: feedbackSummary,
2744
+ reason_codes: [`type:${link.memory.type}`, "related"],
2745
+ related_source_id: link.sourceId,
2746
+ match_type: "related"
2747
+ };
2748
+ });
2749
+ const maxTotal = Math.floor(limit * 1.5);
2750
+ const combined = [...results, ...relatedResults].sort((a, b) => b.score - a.score).slice(0, maxTotal);
2751
+ return {
2752
+ count: combined.length,
2753
+ query: trimmedQuery,
2754
+ task: trimmedTask,
2755
+ intent: input.intent,
2756
+ results: combined
2757
+ };
2758
+ }
2513
2759
  return {
2514
2760
  count: results.length,
2515
2761
  query: trimmedQuery,
@@ -2721,13 +2967,31 @@ function rankEvictionCandidates(db, opts) {
2721
2967
  return left.memory.priority - right.memory.priority;
2722
2968
  });
2723
2969
  }
2970
+ function parseEnvInt(envKey) {
2971
+ const raw = process.env[envKey];
2972
+ if (raw === void 0 || raw === "") return null;
2973
+ const n = Number.parseInt(raw, 10);
2974
+ return Number.isFinite(n) && n > 0 ? n : null;
2975
+ }
2976
+ function getTieredCapacity(opts) {
2977
+ const envMax = parseEnvInt("AGENT_MEMORY_MAX_MEMORIES");
2978
+ return {
2979
+ identity: parseEnvInt("AGENT_MEMORY_MAX_IDENTITY"),
2980
+ // default: null (unlimited)
2981
+ emotion: parseEnvInt("AGENT_MEMORY_MAX_EMOTION") ?? 50,
2982
+ knowledge: parseEnvInt("AGENT_MEMORY_MAX_KNOWLEDGE") ?? 250,
2983
+ event: parseEnvInt("AGENT_MEMORY_MAX_EVENT") ?? 50,
2984
+ total: opts?.maxMemories ?? (envMax ?? 350)
2985
+ };
2986
+ }
2724
2987
  function runGovern(db, opts) {
2725
2988
  const agentId = opts?.agent_id;
2726
- const envMax = Number.parseInt(process.env.AGENT_MEMORY_MAX_MEMORIES ?? "", 10);
2727
- const maxMemories = opts?.maxMemories ?? (Number.isFinite(envMax) && envMax > 0 ? envMax : 200);
2989
+ const capacity = getTieredCapacity(opts);
2728
2990
  let orphanPaths = 0;
2729
2991
  let emptyMemories = 0;
2730
2992
  let evicted = 0;
2993
+ let archived = 0;
2994
+ const evictedByType = {};
2731
2995
  const transaction = db.transaction(() => {
2732
2996
  const pathResult = agentId ? db.prepare(
2733
2997
  `DELETE FROM paths
@@ -2737,17 +3001,48 @@ function runGovern(db, opts) {
2737
3001
  orphanPaths = pathResult.changes;
2738
3002
  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();
2739
3003
  emptyMemories = emptyResult.changes;
3004
+ const typeLimits = [
3005
+ { type: "identity", limit: capacity.identity },
3006
+ { type: "emotion", limit: capacity.emotion },
3007
+ { type: "knowledge", limit: capacity.knowledge },
3008
+ { type: "event", limit: capacity.event }
3009
+ ];
3010
+ const allCandidates = rankEvictionCandidates(db, { agent_id: agentId });
3011
+ const evictedIds = /* @__PURE__ */ new Set();
3012
+ for (const { type, limit } of typeLimits) {
3013
+ if (limit === null) continue;
3014
+ const typeCount = db.prepare(
3015
+ agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ? AND type = ?" : "SELECT COUNT(*) as c FROM memories WHERE type = ?"
3016
+ ).get(...agentId ? [agentId, type] : [type]).c;
3017
+ const excess = Math.max(0, typeCount - limit);
3018
+ if (excess <= 0) continue;
3019
+ const typeCandidates = allCandidates.filter((c) => c.memory.type === type && !evictedIds.has(c.memory.id));
3020
+ const toEvict = typeCandidates.slice(0, excess);
3021
+ for (const candidate of toEvict) {
3022
+ const result = archiveMemory(db, candidate.memory.id, "eviction");
3023
+ evictedIds.add(candidate.memory.id);
3024
+ evicted += 1;
3025
+ if (result === "archived") archived += 1;
3026
+ evictedByType[type] = (evictedByType[type] ?? 0) + 1;
3027
+ }
3028
+ }
2740
3029
  const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
2741
- const excess = Math.max(0, total - maxMemories);
2742
- if (excess <= 0) return;
2743
- const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
2744
- for (const candidate of candidates) {
2745
- deleteMemory(db, candidate.memory.id);
2746
- evicted += 1;
3030
+ const globalExcess = Math.max(0, total - capacity.total);
3031
+ if (globalExcess > 0) {
3032
+ const globalCandidates = allCandidates.filter((c) => !evictedIds.has(c.memory.id));
3033
+ const toEvict = globalCandidates.slice(0, globalExcess);
3034
+ for (const candidate of toEvict) {
3035
+ const result = archiveMemory(db, candidate.memory.id, "eviction");
3036
+ evictedIds.add(candidate.memory.id);
3037
+ evicted += 1;
3038
+ if (result === "archived") archived += 1;
3039
+ const t = candidate.memory.type;
3040
+ evictedByType[t] = (evictedByType[t] ?? 0) + 1;
3041
+ }
2747
3042
  }
2748
3043
  });
2749
3044
  transaction();
2750
- return { orphanPaths, emptyMemories, evicted };
3045
+ return { orphanPaths, emptyMemories, evicted, archived, evictedByType };
2751
3046
  }
2752
3047
 
2753
3048
  // src/sleep/jobs.ts
@@ -2988,12 +3283,21 @@ function getMemoryStatus(db, input) {
2988
3283
  const feedbackEvents = db.prepare(
2989
3284
  "SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
2990
3285
  ).get(agentId);
3286
+ const tiered = getTieredCapacity();
3287
+ const capacity = {
3288
+ identity: { count: stats.by_type.identity ?? 0, limit: tiered.identity },
3289
+ emotion: { count: stats.by_type.emotion ?? 0, limit: tiered.emotion },
3290
+ knowledge: { count: stats.by_type.knowledge ?? 0, limit: tiered.knowledge },
3291
+ event: { count: stats.by_type.event ?? 0, limit: tiered.event },
3292
+ total: { count: stats.total, limit: tiered.total }
3293
+ };
2991
3294
  return {
2992
3295
  ...stats,
2993
3296
  paths: totalPaths.c,
2994
3297
  low_vitality: lowVitality.c,
2995
3298
  feedback_events: feedbackEvents.c,
2996
- agent_id: agentId
3299
+ agent_id: agentId,
3300
+ capacity
2997
3301
  };
2998
3302
  }
2999
3303
 
@@ -3129,6 +3433,8 @@ function formatRecallResponse(result) {
3129
3433
  vector_rank: row.vector_rank,
3130
3434
  bm25_score: row.bm25_score,
3131
3435
  vector_score: row.vector_score,
3436
+ related_source_id: row.related_source_id,
3437
+ match_type: row.match_type,
3132
3438
  updated_at: row.memory.updated_at
3133
3439
  }))
3134
3440
  };
@@ -3153,6 +3459,8 @@ function formatSurfaceResponse(result) {
3153
3459
  feedback_score: row.feedback_score,
3154
3460
  feedback_summary: row.feedback_summary,
3155
3461
  reason_codes: row.reason_codes,
3462
+ related_source_id: row.related_source_id,
3463
+ match_type: row.match_type,
3156
3464
  updated_at: row.memory.updated_at
3157
3465
  }))
3158
3466
  };
@@ -3322,7 +3630,11 @@ function createHttpServer(options) {
3322
3630
  emotion_val: asNumber(body.emotion_val),
3323
3631
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3324
3632
  conservative: asBoolean(body.conservative),
3325
- provider: options?.provider
3633
+ provider: options?.provider,
3634
+ emotion_tag: asString(body.emotion_tag),
3635
+ source_session: asString(body.source_session),
3636
+ source_context: asString(body.source_context),
3637
+ observed_at: asString(body.observed_at)
3326
3638
  });
3327
3639
  sendJson(res, 200, result);
3328
3640
  return;
@@ -3337,7 +3649,12 @@ function createHttpServer(options) {
3337
3649
  query,
3338
3650
  limit: asNumber(body.limit),
3339
3651
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3340
- provider: options?.provider
3652
+ provider: options?.provider,
3653
+ related: asBoolean(body.related),
3654
+ after: asString(body.after),
3655
+ before: asString(body.before),
3656
+ recency_boost: asNumber(body.recency_boost),
3657
+ emotion_tag: asString(body.emotion_tag)
3341
3658
  });
3342
3659
  sendJson(res, 200, formatRecallResponse(result));
3343
3660
  return;
@@ -3357,7 +3674,12 @@ function createHttpServer(options) {
3357
3674
  types,
3358
3675
  limit: asNumber(body.limit),
3359
3676
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3360
- provider: options?.provider
3677
+ provider: options?.provider,
3678
+ related: asBoolean(body.related),
3679
+ after: asString(body.after),
3680
+ before: asString(body.before),
3681
+ recency_boost: asNumber(body.recency_boost),
3682
+ emotion_tag: asString(body.emotion_tag)
3361
3683
  });
3362
3684
  sendJson(res, 200, formatSurfaceResponse(result));
3363
3685
  return;
@@ -3929,6 +4251,7 @@ function boot(db, opts) {
3929
4251
  return result;
3930
4252
  }
3931
4253
  export {
4254
+ archiveMemory,
3932
4255
  boot,
3933
4256
  buildFtsQuery,
3934
4257
  buildMergePlan,
@@ -3954,6 +4277,7 @@ export {
3954
4277
  exportMemories,
3955
4278
  extractIngestItems,
3956
4279
  failMaintenanceJob,
4280
+ fetchRelatedLinks,
3957
4281
  findResumableMaintenanceJob,
3958
4282
  formatNarrativeBoot,
3959
4283
  formatRelativeDate,
@@ -3975,11 +4299,13 @@ export {
3975
4299
  getPathsByDomain,
3976
4300
  getPathsByMemory,
3977
4301
  getPathsByPrefix,
4302
+ getTieredCapacity,
3978
4303
  guard,
3979
4304
  healthcheckEmbeddingProvider,
3980
4305
  ingestText,
3981
4306
  isCountRow,
3982
4307
  isStaleContent,
4308
+ listArchivedMemories,
3983
4309
  listMemories,
3984
4310
  listPendingEmbeddings,
3985
4311
  loadWarmBootLayers,
@@ -3989,6 +4315,7 @@ export {
3989
4315
  openDatabase,
3990
4316
  parseUri,
3991
4317
  priorityPrior,
4318
+ purgeArchive,
3992
4319
  rankEvictionCandidates,
3993
4320
  rebuildBm25Index,
3994
4321
  recallMemories,
@@ -4001,6 +4328,7 @@ export {
4001
4328
  reindexMemories,
4002
4329
  reindexMemorySearch,
4003
4330
  rememberMemory,
4331
+ restoreMemory,
4004
4332
  runAutoIngestWatcher,
4005
4333
  runDecay,
4006
4334
  runGovern,