@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/.github/workflows/test.yml +1 -1
- package/.pnpm-approve-builds.json +1 -0
- package/CHANGELOG.md +153 -0
- package/README.md +52 -15
- package/dist/bin/agent-memory.js +285 -27
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +77 -3
- package/dist/index.js +354 -26
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +604 -13997
- package/dist/mcp/server.js.map +1 -1
- package/docs/README-zh.md +132 -15
- package/docs/architecture.md +1 -1
- package/docs/integrations/generic.md +43 -3
- package/docs/integrations/openclaw.md +48 -8
- package/docs/migration-v3-v4.md +15 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AgentMemory
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
1447
|
-
).all(agentId,
|
|
1637
|
+
LIMIT ?`
|
|
1638
|
+
).all(agentId, sourceId, maxPerSource);
|
|
1448
1639
|
for (const link of links) {
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
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
|
|
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
|
|
2742
|
-
if (
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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,
|