@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.
package/dist/index.d.ts CHANGED
@@ -72,6 +72,46 @@ declare function createMemory(db: Database.Database, input: CreateMemoryInput):
72
72
  declare function getMemory(db: Database.Database, id: string): Memory | null;
73
73
  declare function updateMemory(db: Database.Database, id: string, input: UpdateMemoryInput): Memory | null;
74
74
  declare function deleteMemory(db: Database.Database, id: string): boolean;
75
+ interface ArchivedMemory {
76
+ id: string;
77
+ content: string;
78
+ type: string;
79
+ priority: number;
80
+ emotion_val: number;
81
+ vitality: number;
82
+ stability: number;
83
+ access_count: number;
84
+ last_accessed: string | null;
85
+ created_at: string;
86
+ updated_at: string;
87
+ archived_at: string;
88
+ archive_reason: string;
89
+ source: string | null;
90
+ agent_id: string;
91
+ hash: string | null;
92
+ emotion_tag: string | null;
93
+ source_session: string | null;
94
+ source_context: string | null;
95
+ observed_at: string | null;
96
+ }
97
+ /**
98
+ * Archive a memory to memory_archive, then delete from memories.
99
+ * If minVitality is set (default 0.1), memories below that threshold
100
+ * are directly deleted without archiving — they're decayed noise.
101
+ * Returns "archived" if actually archived, "deleted" if skipped to direct delete,
102
+ * or false if memory not found.
103
+ */
104
+ declare function archiveMemory(db: Database.Database, id: string, reason?: string, opts?: {
105
+ minVitality?: number;
106
+ }): "archived" | "deleted" | false;
107
+ declare function restoreMemory(db: Database.Database, id: string): Memory | null;
108
+ declare function listArchivedMemories(db: Database.Database, opts?: {
109
+ agent_id?: string;
110
+ limit?: number;
111
+ }): ArchivedMemory[];
112
+ declare function purgeArchive(db: Database.Database, opts?: {
113
+ agent_id?: string;
114
+ }): number;
75
115
  declare function listMemories(db: Database.Database, opts?: {
76
116
  agent_id?: string;
77
117
  type?: MemoryType;
@@ -517,6 +557,10 @@ interface ReflectInput {
517
557
  }
518
558
  declare function reflectMemories(db: Database.Database, input: ReflectInput): Promise<ReflectRunResult>;
519
559
 
560
+ interface CapacityInfo {
561
+ count: number;
562
+ limit: number | null;
563
+ }
520
564
  interface StatusResult {
521
565
  total: number;
522
566
  by_type: Record<string, number>;
@@ -525,6 +569,7 @@ interface StatusResult {
525
569
  low_vitality: number;
526
570
  feedback_events: number;
527
571
  agent_id: string;
572
+ capacity: Record<string, CapacityInfo>;
528
573
  }
529
574
  declare function getMemoryStatus(db: Database.Database, input?: {
530
575
  agent_id?: string;
@@ -764,6 +809,8 @@ interface GovernResult {
764
809
  orphanPaths: number;
765
810
  emptyMemories: number;
766
811
  evicted: number;
812
+ archived: number;
813
+ evictedByType: Record<string, number>;
767
814
  }
768
815
  interface EvictionCandidate {
769
816
  memory: Memory;
@@ -783,13 +830,28 @@ declare function computeEvictionScore(input: {
783
830
  declare function rankEvictionCandidates(db: Database.Database, opts?: {
784
831
  agent_id?: string;
785
832
  }): EvictionCandidate[];
833
+ interface TieredCapacity {
834
+ identity: number | null;
835
+ emotion: number | null;
836
+ knowledge: number | null;
837
+ event: number | null;
838
+ total: number;
839
+ }
840
+ declare function getTieredCapacity(opts?: {
841
+ maxMemories?: number;
842
+ }): TieredCapacity;
786
843
  /**
787
844
  * Run governance checks and cleanup:
788
845
  * 1. Remove orphan paths (no parent memory)
789
846
  * 2. Remove empty memories (blank content)
790
- * 3. Evict low-value memories when over capacity using eviction_score
847
+ * 3. Evict low-value memories when over capacity using tiered + global eviction
848
+ *
849
+ * Tiered capacity can be set via env vars:
850
+ * AGENT_MEMORY_MAX_IDENTITY, AGENT_MEMORY_MAX_EMOTION,
851
+ * AGENT_MEMORY_MAX_KNOWLEDGE, AGENT_MEMORY_MAX_EVENT,
852
+ * AGENT_MEMORY_MAX_MEMORIES (global cap, default 350)
791
853
  *
792
- * maxMemories can be set via AGENT_MEMORY_MAX_MEMORIES env var (default: 200).
854
+ * Evicted memories are archived to memory_archive before deletion.
793
855
  */
794
856
  declare function runGovern(db: Database.Database, opts?: {
795
857
  agent_id?: string;
@@ -843,4 +905,4 @@ declare function formatNarrativeBoot(layers: {
843
905
  */
844
906
  declare function boot(db: Database.Database, opts?: WarmBootOptions): WarmBootResult;
845
907
 
846
- export { type AgentMemoryHttpServer, type ReflectInput as AppReflectInput, type ReflectProgressEvent as AppReflectProgressEvent, type AutoIngestWatcher, type AutoIngestWatcherOptions, type BootResult, type ConflictInfo, type ConflictType, type CreateMemoryInput, type DedupScoreBreakdown, type EmbeddingProvider, type EmbeddingProviderConfig, type EmbeddingProviderKind, type EmbeddingProviderOptions, type EmbeddingStatus, type EvictionCandidate, type ExportResult, type FeedbackEventInput, type FeedbackEventRecord, type FeedbackSource, type FeedbackSummary, type GovernResult, type GuardAction, type GuardInput, type GuardResult, type HttpJobStatus, type HttpServerOptions, type HybridRecallResponse, type HybridRecallResult, type IngestExtractedItem, type IngestResult, type IngestRunOptions, type MaintenanceJob, type MaintenancePhase, type MaintenanceStatus, type Memory, type MemoryType, type MergeContext, type MergePlan, type Path, type PendingEmbeddingRecord, type Priority, type RecallInput, type ReflectCheckpoint, type ReflectOptions, type ReflectProgressEvent$1 as ReflectProgressEvent, type ReflectRunResult, type ReflectRunners, type ReflectStats, type ReflectStep, type ReindexEmbeddingsResult, type ReindexInput, type ReindexProgressEvent, type ReindexSearchResult, type RelatedLink, type RememberInput, type SearchResult, type StatusResult, type StoredEmbedding, type SurfaceInput, type SurfaceIntent, type SurfaceResponse, type SurfaceResult, type SyncInput, type SyncResult, type TidyResult, type UpdateMemoryInput, type VectorSearchResult, type WarmBootOptions, type WarmBootResult, boot, buildFtsQuery, buildMergePlan, calculateVitality, classifyIngestType, completeMaintenanceJob, computeEvictionScore, contentHash, cosineSimilarity, countMemories, createEmbeddingProvider, createHttpServer, createInitialCheckpoint, createLocalHttpEmbeddingProvider, createMaintenanceJob, createMemory, createOpenAICompatibleEmbeddingProvider, createPath, decodeVector, deleteMemory, deletePath, encodeVector, exportMemories, extractIngestItems, failMaintenanceJob, fetchRelatedLinks, findResumableMaintenanceJob, formatNarrativeBoot, formatRelativeDate, fuseHybridResults, fusionScore, getConfiguredEmbeddingProviderId, getDecayedMemories, getEmbedding, getEmbeddingProvider, getEmbeddingProviderConfigFromEnv, getEmbeddingProviderFromEnv, getFeedbackScore, getFeedbackSummary, getMaintenanceJob, getMemory, getMemoryStatus, getPath, getPathByUri, getPathsByDomain, getPathsByMemory, getPathsByPrefix, guard, healthcheckEmbeddingProvider, ingestText, isStaleContent, listMemories, listPendingEmbeddings, loadWarmBootLayers, markAllEmbeddingsPending, markEmbeddingFailed, markMemoryEmbeddingPending, parseUri, priorityPrior, rankEvictionCandidates, rebuildBm25Index, recallMemories, recallMemory, recordAccess, recordFeedbackEvent, recordPassiveFeedback, reflectMemories, reindexEmbeddings, reindexMemories, reindexMemorySearch, rememberMemory, runAutoIngestWatcher, runDecay, runGovern, runReflectOrchestrator, runTidy, searchBM25, searchByVector, slugify, splitIngestBlocks, startHttpServer, surfaceMemories, syncBatch, syncOne, tokenize, updateMaintenanceCheckpoint, updateMemory, upsertReadyEmbedding };
908
+ export { type AgentMemoryHttpServer, type ReflectInput as AppReflectInput, type ReflectProgressEvent as AppReflectProgressEvent, type ArchivedMemory, type AutoIngestWatcher, type AutoIngestWatcherOptions, type BootResult, type CapacityInfo, type ConflictInfo, type ConflictType, type CreateMemoryInput, type DedupScoreBreakdown, type EmbeddingProvider, type EmbeddingProviderConfig, type EmbeddingProviderKind, type EmbeddingProviderOptions, type EmbeddingStatus, type EvictionCandidate, type ExportResult, type FeedbackEventInput, type FeedbackEventRecord, type FeedbackSource, type FeedbackSummary, type GovernResult, type GuardAction, type GuardInput, type GuardResult, type HttpJobStatus, type HttpServerOptions, type HybridRecallResponse, type HybridRecallResult, type IngestExtractedItem, type IngestResult, type IngestRunOptions, type MaintenanceJob, type MaintenancePhase, type MaintenanceStatus, type Memory, type MemoryType, type MergeContext, type MergePlan, type Path, type PendingEmbeddingRecord, type Priority, type RecallInput, type ReflectCheckpoint, type ReflectOptions, type ReflectProgressEvent$1 as ReflectProgressEvent, type ReflectRunResult, type ReflectRunners, type ReflectStats, type ReflectStep, type ReindexEmbeddingsResult, type ReindexInput, type ReindexProgressEvent, type ReindexSearchResult, type RelatedLink, type RememberInput, type SearchResult, type StatusResult, type StoredEmbedding, type SurfaceInput, type SurfaceIntent, type SurfaceResponse, type SurfaceResult, type SyncInput, type SyncResult, type TidyResult, type TieredCapacity, type UpdateMemoryInput, type VectorSearchResult, type WarmBootOptions, type WarmBootResult, archiveMemory, boot, buildFtsQuery, buildMergePlan, calculateVitality, classifyIngestType, completeMaintenanceJob, computeEvictionScore, contentHash, cosineSimilarity, countMemories, createEmbeddingProvider, createHttpServer, createInitialCheckpoint, createLocalHttpEmbeddingProvider, createMaintenanceJob, createMemory, createOpenAICompatibleEmbeddingProvider, createPath, decodeVector, deleteMemory, deletePath, encodeVector, exportMemories, extractIngestItems, failMaintenanceJob, fetchRelatedLinks, findResumableMaintenanceJob, formatNarrativeBoot, formatRelativeDate, fuseHybridResults, fusionScore, getConfiguredEmbeddingProviderId, getDecayedMemories, getEmbedding, getEmbeddingProvider, getEmbeddingProviderConfigFromEnv, getEmbeddingProviderFromEnv, getFeedbackScore, getFeedbackSummary, getMaintenanceJob, getMemory, getMemoryStatus, getPath, getPathByUri, getPathsByDomain, getPathsByMemory, getPathsByPrefix, getTieredCapacity, guard, healthcheckEmbeddingProvider, ingestText, isStaleContent, listArchivedMemories, listMemories, listPendingEmbeddings, loadWarmBootLayers, markAllEmbeddingsPending, markEmbeddingFailed, markMemoryEmbeddingPending, parseUri, priorityPrior, purgeArchive, rankEvictionCandidates, rebuildBm25Index, recallMemories, recallMemory, recordAccess, recordFeedbackEvent, recordPassiveFeedback, reflectMemories, reindexEmbeddings, reindexMemories, reindexMemorySearch, rememberMemory, restoreMemory, runAutoIngestWatcher, runDecay, runGovern, runReflectOrchestrator, runTidy, searchBM25, searchByVector, slugify, splitIngestBlocks, startHttpServer, surfaceMemories, syncBatch, syncOne, tokenize, updateMaintenanceCheckpoint, updateMemory, upsertReadyEmbedding };
package/dist/index.js CHANGED
@@ -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 = [];
@@ -2775,13 +2967,31 @@ function rankEvictionCandidates(db, opts) {
2775
2967
  return left.memory.priority - right.memory.priority;
2776
2968
  });
2777
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
+ }
2778
2987
  function runGovern(db, opts) {
2779
2988
  const agentId = opts?.agent_id;
2780
- const envMax = Number.parseInt(process.env.AGENT_MEMORY_MAX_MEMORIES ?? "", 10);
2781
- const maxMemories = opts?.maxMemories ?? (Number.isFinite(envMax) && envMax > 0 ? envMax : 200);
2989
+ const capacity = getTieredCapacity(opts);
2782
2990
  let orphanPaths = 0;
2783
2991
  let emptyMemories = 0;
2784
2992
  let evicted = 0;
2993
+ let archived = 0;
2994
+ const evictedByType = {};
2785
2995
  const transaction = db.transaction(() => {
2786
2996
  const pathResult = agentId ? db.prepare(
2787
2997
  `DELETE FROM paths
@@ -2791,17 +3001,48 @@ function runGovern(db, opts) {
2791
3001
  orphanPaths = pathResult.changes;
2792
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();
2793
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
+ }
2794
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;
2795
- const excess = Math.max(0, total - maxMemories);
2796
- if (excess <= 0) return;
2797
- const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
2798
- for (const candidate of candidates) {
2799
- deleteMemory(db, candidate.memory.id);
2800
- 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
+ }
2801
3042
  }
2802
3043
  });
2803
3044
  transaction();
2804
- return { orphanPaths, emptyMemories, evicted };
3045
+ return { orphanPaths, emptyMemories, evicted, archived, evictedByType };
2805
3046
  }
2806
3047
 
2807
3048
  // src/sleep/jobs.ts
@@ -3042,12 +3283,21 @@ function getMemoryStatus(db, input) {
3042
3283
  const feedbackEvents = db.prepare(
3043
3284
  "SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
3044
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
+ };
3045
3294
  return {
3046
3295
  ...stats,
3047
3296
  paths: totalPaths.c,
3048
3297
  low_vitality: lowVitality.c,
3049
3298
  feedback_events: feedbackEvents.c,
3050
- agent_id: agentId
3299
+ agent_id: agentId,
3300
+ capacity
3051
3301
  };
3052
3302
  }
3053
3303
 
@@ -4001,6 +4251,7 @@ function boot(db, opts) {
4001
4251
  return result;
4002
4252
  }
4003
4253
  export {
4254
+ archiveMemory,
4004
4255
  boot,
4005
4256
  buildFtsQuery,
4006
4257
  buildMergePlan,
@@ -4048,11 +4299,13 @@ export {
4048
4299
  getPathsByDomain,
4049
4300
  getPathsByMemory,
4050
4301
  getPathsByPrefix,
4302
+ getTieredCapacity,
4051
4303
  guard,
4052
4304
  healthcheckEmbeddingProvider,
4053
4305
  ingestText,
4054
4306
  isCountRow,
4055
4307
  isStaleContent,
4308
+ listArchivedMemories,
4056
4309
  listMemories,
4057
4310
  listPendingEmbeddings,
4058
4311
  loadWarmBootLayers,
@@ -4062,6 +4315,7 @@ export {
4062
4315
  openDatabase,
4063
4316
  parseUri,
4064
4317
  priorityPrior,
4318
+ purgeArchive,
4065
4319
  rankEvictionCandidates,
4066
4320
  rebuildBm25Index,
4067
4321
  recallMemories,
@@ -4074,6 +4328,7 @@ export {
4074
4328
  reindexMemories,
4075
4329
  reindexMemorySearch,
4076
4330
  rememberMemory,
4331
+ restoreMemory,
4077
4332
  runAutoIngestWatcher,
4078
4333
  runDecay,
4079
4334
  runGovern,