@smyslenny/agent-memory 4.3.1 → 5.0.1
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/bin/agent-memory.js +417 -45
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +108 -44
- package/dist/index.js +438 -61
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +535 -80
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
9
|
+
var SCHEMA_VERSION = 7;
|
|
10
10
|
var SCHEMA_SQL = `
|
|
11
11
|
-- Memory entries
|
|
12
12
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -25,6 +25,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
25
25
|
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
26
26
|
hash TEXT,
|
|
27
27
|
emotion_tag TEXT,
|
|
28
|
+
source_session TEXT,
|
|
29
|
+
source_context TEXT,
|
|
30
|
+
observed_at TEXT,
|
|
28
31
|
UNIQUE(hash, agent_id)
|
|
29
32
|
);
|
|
30
33
|
|
|
@@ -205,6 +208,11 @@ function migrateDatabase(db, from, to) {
|
|
|
205
208
|
v = 6;
|
|
206
209
|
continue;
|
|
207
210
|
}
|
|
211
|
+
if (v === 6) {
|
|
212
|
+
migrateV6ToV7(db);
|
|
213
|
+
v = 7;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
208
216
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
209
217
|
}
|
|
210
218
|
}
|
|
@@ -291,6 +299,8 @@ function inferSchemaVersion(db) {
|
|
|
291
299
|
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
292
300
|
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
293
301
|
const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
|
|
302
|
+
const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
303
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
|
|
294
304
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
|
|
295
305
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
296
306
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
@@ -321,6 +331,10 @@ function ensureIndexes(db) {
|
|
|
321
331
|
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
322
332
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
323
333
|
}
|
|
334
|
+
if (tableHasColumn(db, "memories", "observed_at")) {
|
|
335
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
336
|
+
}
|
|
337
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
324
338
|
if (tableExists(db, "feedback_events")) {
|
|
325
339
|
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
326
340
|
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
@@ -473,6 +487,35 @@ function migrateV5ToV6(db) {
|
|
|
473
487
|
throw e;
|
|
474
488
|
}
|
|
475
489
|
}
|
|
490
|
+
function migrateV6ToV7(db) {
|
|
491
|
+
const alreadyMigrated = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
492
|
+
if (alreadyMigrated) {
|
|
493
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
db.exec("BEGIN");
|
|
498
|
+
if (!tableHasColumn(db, "memories", "source_session")) {
|
|
499
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_session TEXT;");
|
|
500
|
+
}
|
|
501
|
+
if (!tableHasColumn(db, "memories", "source_context")) {
|
|
502
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_context TEXT;");
|
|
503
|
+
}
|
|
504
|
+
if (!tableHasColumn(db, "memories", "observed_at")) {
|
|
505
|
+
db.exec("ALTER TABLE memories ADD COLUMN observed_at TEXT;");
|
|
506
|
+
}
|
|
507
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
508
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
509
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
510
|
+
db.exec("COMMIT");
|
|
511
|
+
} catch (e) {
|
|
512
|
+
try {
|
|
513
|
+
db.exec("ROLLBACK");
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
throw e;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
476
519
|
|
|
477
520
|
// src/search/tokenizer.ts
|
|
478
521
|
import { readFileSync } from "fs";
|
|
@@ -940,6 +983,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
940
983
|
const limit = opts.limit ?? 20;
|
|
941
984
|
const agentId = opts.agent_id ?? "default";
|
|
942
985
|
const minVitality = opts.min_vitality ?? 0;
|
|
986
|
+
const conditions = [
|
|
987
|
+
"e.provider_id = ?",
|
|
988
|
+
"e.status = 'ready'",
|
|
989
|
+
"e.vector IS NOT NULL",
|
|
990
|
+
"e.content_hash = m.hash",
|
|
991
|
+
"m.agent_id = ?",
|
|
992
|
+
"m.vitality >= ?"
|
|
993
|
+
];
|
|
994
|
+
const params = [opts.providerId, agentId, minVitality];
|
|
995
|
+
if (opts.after) {
|
|
996
|
+
conditions.push("m.updated_at >= ?");
|
|
997
|
+
params.push(opts.after);
|
|
998
|
+
}
|
|
999
|
+
if (opts.before) {
|
|
1000
|
+
conditions.push("m.updated_at <= ?");
|
|
1001
|
+
params.push(opts.before);
|
|
1002
|
+
}
|
|
943
1003
|
const rows = db.prepare(
|
|
944
1004
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
945
1005
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -947,13 +1007,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
947
1007
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
948
1008
|
FROM embeddings e
|
|
949
1009
|
JOIN memories m ON m.id = e.memory_id
|
|
950
|
-
WHERE
|
|
951
|
-
|
|
952
|
-
AND e.vector IS NOT NULL
|
|
953
|
-
AND e.content_hash = m.hash
|
|
954
|
-
AND m.agent_id = ?
|
|
955
|
-
AND m.vitality >= ?`
|
|
956
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1010
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1011
|
+
).all(...params);
|
|
957
1012
|
const scored = rows.map((row) => ({
|
|
958
1013
|
provider_id: row.provider_id,
|
|
959
1014
|
memory: {
|
|
@@ -1026,10 +1081,12 @@ function createMemory(db, input) {
|
|
|
1026
1081
|
}
|
|
1027
1082
|
const id = newId();
|
|
1028
1083
|
const timestamp = now();
|
|
1084
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1029
1085
|
db.prepare(
|
|
1030
1086
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1031
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1032
|
-
|
|
1087
|
+
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag,
|
|
1088
|
+
source_session, source_context, observed_at)
|
|
1089
|
+
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1033
1090
|
).run(
|
|
1034
1091
|
id,
|
|
1035
1092
|
input.content,
|
|
@@ -1042,7 +1099,10 @@ function createMemory(db, input) {
|
|
|
1042
1099
|
input.source ?? null,
|
|
1043
1100
|
agentId,
|
|
1044
1101
|
hash,
|
|
1045
|
-
input.emotion_tag ?? null
|
|
1102
|
+
input.emotion_tag ?? null,
|
|
1103
|
+
input.source_session ?? null,
|
|
1104
|
+
sourceContext,
|
|
1105
|
+
input.observed_at ?? null
|
|
1046
1106
|
);
|
|
1047
1107
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1048
1108
|
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -1217,16 +1277,25 @@ function searchBM25(db, query, opts) {
|
|
|
1217
1277
|
const ftsQuery = buildFtsQuery(query);
|
|
1218
1278
|
if (!ftsQuery) return [];
|
|
1219
1279
|
try {
|
|
1280
|
+
const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
|
|
1281
|
+
const params = [ftsQuery, agentId, minVitality];
|
|
1282
|
+
if (opts?.after) {
|
|
1283
|
+
conditions.push("m.updated_at >= ?");
|
|
1284
|
+
params.push(opts.after);
|
|
1285
|
+
}
|
|
1286
|
+
if (opts?.before) {
|
|
1287
|
+
conditions.push("m.updated_at <= ?");
|
|
1288
|
+
params.push(opts.before);
|
|
1289
|
+
}
|
|
1290
|
+
params.push(limit);
|
|
1220
1291
|
const rows = db.prepare(
|
|
1221
1292
|
`SELECT m.*, rank AS score
|
|
1222
1293
|
FROM memories_fts f
|
|
1223
1294
|
JOIN memories m ON m.id = f.id
|
|
1224
|
-
WHERE
|
|
1225
|
-
AND m.agent_id = ?
|
|
1226
|
-
AND m.vitality >= ?
|
|
1295
|
+
WHERE ${conditions.join(" AND ")}
|
|
1227
1296
|
ORDER BY rank
|
|
1228
1297
|
LIMIT ?`
|
|
1229
|
-
).all(
|
|
1298
|
+
).all(...params);
|
|
1230
1299
|
return rows.map((row, index) => {
|
|
1231
1300
|
const { score: _score, ...memoryFields } = row;
|
|
1232
1301
|
return {
|
|
@@ -1310,16 +1379,23 @@ function priorityPrior(priority) {
|
|
|
1310
1379
|
function fusionScore(input) {
|
|
1311
1380
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1312
1381
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1382
|
+
const baseScore = lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
1383
|
+
const boost = input.recency_boost ?? 0;
|
|
1384
|
+
if (boost <= 0) return baseScore;
|
|
1385
|
+
const updatedAt = new Date(input.memory.updated_at).getTime();
|
|
1386
|
+
const daysSince = Math.max(0, (Date.now() - updatedAt) / (1e3 * 60 * 60 * 24));
|
|
1387
|
+
const recencyScore = Math.exp(-daysSince / 30);
|
|
1388
|
+
return (1 - boost) * baseScore + boost * recencyScore;
|
|
1389
|
+
}
|
|
1390
|
+
function fuseHybridResults(lexical, vector, limit, recency_boost) {
|
|
1316
1391
|
const candidates = /* @__PURE__ */ new Map();
|
|
1317
1392
|
for (const row of lexical) {
|
|
1318
1393
|
candidates.set(row.memory.id, {
|
|
1319
1394
|
memory: row.memory,
|
|
1320
1395
|
score: 0,
|
|
1321
1396
|
bm25_rank: row.rank,
|
|
1322
|
-
bm25_score: row.score
|
|
1397
|
+
bm25_score: row.score,
|
|
1398
|
+
match_type: "direct"
|
|
1323
1399
|
});
|
|
1324
1400
|
}
|
|
1325
1401
|
for (const row of vector) {
|
|
@@ -1332,13 +1408,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
1332
1408
|
memory: row.memory,
|
|
1333
1409
|
score: 0,
|
|
1334
1410
|
vector_rank: row.rank,
|
|
1335
|
-
vector_score: row.similarity
|
|
1411
|
+
vector_score: row.similarity,
|
|
1412
|
+
match_type: "direct"
|
|
1336
1413
|
});
|
|
1337
1414
|
}
|
|
1338
1415
|
}
|
|
1339
1416
|
return [...candidates.values()].map((row) => ({
|
|
1340
1417
|
...row,
|
|
1341
|
-
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
1418
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
|
|
1342
1419
|
})).sort((left, right) => {
|
|
1343
1420
|
if (right.score !== left.score) return right.score - left.score;
|
|
1344
1421
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -1351,9 +1428,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
1351
1428
|
providerId: opts.provider.id,
|
|
1352
1429
|
agent_id: opts.agent_id,
|
|
1353
1430
|
limit: opts.limit,
|
|
1354
|
-
min_vitality: opts.min_vitality
|
|
1431
|
+
min_vitality: opts.min_vitality,
|
|
1432
|
+
after: opts.after,
|
|
1433
|
+
before: opts.before
|
|
1355
1434
|
});
|
|
1356
1435
|
}
|
|
1436
|
+
function expandRelated(db, results, agentId, maxTotal) {
|
|
1437
|
+
const existingIds = new Set(results.map((r) => r.memory.id));
|
|
1438
|
+
const related = [];
|
|
1439
|
+
for (const result of results) {
|
|
1440
|
+
const links = db.prepare(
|
|
1441
|
+
`SELECT l.target_id, l.weight, m.*
|
|
1442
|
+
FROM links l
|
|
1443
|
+
JOIN memories m ON m.id = l.target_id
|
|
1444
|
+
WHERE l.agent_id = ? AND l.source_id = ?
|
|
1445
|
+
ORDER BY l.weight DESC
|
|
1446
|
+
LIMIT 5`
|
|
1447
|
+
).all(agentId, result.memory.id);
|
|
1448
|
+
for (const link of links) {
|
|
1449
|
+
if (existingIds.has(link.target_id)) continue;
|
|
1450
|
+
existingIds.add(link.target_id);
|
|
1451
|
+
const relatedMemory = {
|
|
1452
|
+
id: link.id,
|
|
1453
|
+
content: link.content,
|
|
1454
|
+
type: link.type,
|
|
1455
|
+
priority: link.priority,
|
|
1456
|
+
emotion_val: link.emotion_val,
|
|
1457
|
+
vitality: link.vitality,
|
|
1458
|
+
stability: link.stability,
|
|
1459
|
+
access_count: link.access_count,
|
|
1460
|
+
last_accessed: link.last_accessed,
|
|
1461
|
+
created_at: link.created_at,
|
|
1462
|
+
updated_at: link.updated_at,
|
|
1463
|
+
source: link.source,
|
|
1464
|
+
agent_id: link.agent_id,
|
|
1465
|
+
hash: link.hash,
|
|
1466
|
+
emotion_tag: link.emotion_tag,
|
|
1467
|
+
source_session: link.source_session ?? null,
|
|
1468
|
+
source_context: link.source_context ?? null,
|
|
1469
|
+
observed_at: link.observed_at ?? null
|
|
1470
|
+
};
|
|
1471
|
+
related.push({
|
|
1472
|
+
memory: relatedMemory,
|
|
1473
|
+
score: result.score * link.weight * 0.6,
|
|
1474
|
+
related_source_id: result.memory.id,
|
|
1475
|
+
match_type: "related"
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const directResults = results.map((r) => ({
|
|
1480
|
+
...r,
|
|
1481
|
+
match_type: "direct"
|
|
1482
|
+
}));
|
|
1483
|
+
const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
|
|
1484
|
+
return combined;
|
|
1485
|
+
}
|
|
1357
1486
|
async function recallMemories(db, query, opts) {
|
|
1358
1487
|
const limit = opts?.limit ?? 10;
|
|
1359
1488
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -1361,10 +1490,15 @@ async function recallMemories(db, query, opts) {
|
|
|
1361
1490
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1362
1491
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1363
1492
|
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1493
|
+
const recencyBoost = opts?.recency_boost;
|
|
1494
|
+
const after = opts?.after;
|
|
1495
|
+
const before = opts?.before;
|
|
1364
1496
|
const lexical = searchBM25(db, query, {
|
|
1365
1497
|
agent_id: agentId,
|
|
1366
1498
|
limit: lexicalLimit,
|
|
1367
|
-
min_vitality: minVitality
|
|
1499
|
+
min_vitality: minVitality,
|
|
1500
|
+
after,
|
|
1501
|
+
before
|
|
1368
1502
|
});
|
|
1369
1503
|
let vector = [];
|
|
1370
1504
|
if (provider) {
|
|
@@ -1373,17 +1507,25 @@ async function recallMemories(db, query, opts) {
|
|
|
1373
1507
|
provider,
|
|
1374
1508
|
agent_id: agentId,
|
|
1375
1509
|
limit: vectorLimit,
|
|
1376
|
-
min_vitality: minVitality
|
|
1510
|
+
min_vitality: minVitality,
|
|
1511
|
+
after,
|
|
1512
|
+
before
|
|
1377
1513
|
});
|
|
1378
1514
|
} catch {
|
|
1379
1515
|
vector = [];
|
|
1380
1516
|
}
|
|
1381
1517
|
}
|
|
1382
1518
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1383
|
-
|
|
1519
|
+
let results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit, recencyBoost);
|
|
1520
|
+
if (opts?.related) {
|
|
1521
|
+
const maxTotal = Math.floor(limit * 1.5);
|
|
1522
|
+
results = expandRelated(db, results, agentId, maxTotal);
|
|
1523
|
+
}
|
|
1384
1524
|
if (opts?.recordAccess !== false) {
|
|
1385
1525
|
for (const row of results) {
|
|
1386
|
-
|
|
1526
|
+
if (row.match_type !== "related") {
|
|
1527
|
+
recordAccess(db, row.memory.id);
|
|
1528
|
+
}
|
|
1387
1529
|
}
|
|
1388
1530
|
}
|
|
1389
1531
|
return {
|
|
@@ -1608,7 +1750,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
1608
1750
|
}
|
|
1609
1751
|
return 0.2;
|
|
1610
1752
|
}
|
|
1611
|
-
function extractObservedAt(parts, fallback) {
|
|
1753
|
+
function extractObservedAt(parts, fallback, observedAt) {
|
|
1754
|
+
if (observedAt) {
|
|
1755
|
+
const parsed = new Date(observedAt);
|
|
1756
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1757
|
+
return parsed;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1612
1760
|
for (const part of parts) {
|
|
1613
1761
|
if (!part) continue;
|
|
1614
1762
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -1631,8 +1779,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
1631
1779
|
if (input.type !== "event") {
|
|
1632
1780
|
return 1;
|
|
1633
1781
|
}
|
|
1634
|
-
const inputTime = extractObservedAt(
|
|
1635
|
-
|
|
1782
|
+
const inputTime = extractObservedAt(
|
|
1783
|
+
[input.uri, input.source, input.content],
|
|
1784
|
+
input.now ?? null,
|
|
1785
|
+
input.observed_at ?? null
|
|
1786
|
+
);
|
|
1787
|
+
const existingTime = extractObservedAt(
|
|
1788
|
+
[candidateUri, memory.source, memory.content],
|
|
1789
|
+
memory.created_at,
|
|
1790
|
+
memory.observed_at ?? null
|
|
1791
|
+
);
|
|
1636
1792
|
if (!inputTime || !existingTime) {
|
|
1637
1793
|
return 0.5;
|
|
1638
1794
|
}
|
|
@@ -1716,6 +1872,65 @@ function fourCriterionGate(input) {
|
|
|
1716
1872
|
failedCriteria: failed
|
|
1717
1873
|
};
|
|
1718
1874
|
}
|
|
1875
|
+
var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
|
|
1876
|
+
var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
|
|
1877
|
+
var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
|
|
1878
|
+
function extractNumbers(text) {
|
|
1879
|
+
return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
|
|
1880
|
+
}
|
|
1881
|
+
function detectConflict(inputContent, candidateContent, candidateId) {
|
|
1882
|
+
let conflictScore = 0;
|
|
1883
|
+
let conflictType = "negation";
|
|
1884
|
+
const details = [];
|
|
1885
|
+
const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
|
|
1886
|
+
const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
|
|
1887
|
+
if (inputHasNegation !== candidateHasNegation) {
|
|
1888
|
+
conflictScore += 0.4;
|
|
1889
|
+
conflictType = "negation";
|
|
1890
|
+
details.push("negation mismatch");
|
|
1891
|
+
}
|
|
1892
|
+
const inputNumbers = extractNumbers(inputContent);
|
|
1893
|
+
const candidateNumbers = extractNumbers(candidateContent);
|
|
1894
|
+
if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
|
|
1895
|
+
const inputSet = new Set(inputNumbers);
|
|
1896
|
+
const candidateSet = new Set(candidateNumbers);
|
|
1897
|
+
const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
|
|
1898
|
+
const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
|
|
1899
|
+
if (hasDiff && !hasCommon) {
|
|
1900
|
+
conflictScore += 0.3;
|
|
1901
|
+
conflictType = "value";
|
|
1902
|
+
details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
|
|
1906
|
+
const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
|
|
1907
|
+
const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
|
|
1908
|
+
const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
|
|
1909
|
+
if (inputDone && candidateActive || inputActive && candidateDone) {
|
|
1910
|
+
conflictScore += 0.3;
|
|
1911
|
+
conflictType = "status";
|
|
1912
|
+
details.push("status contradiction (done vs active)");
|
|
1913
|
+
}
|
|
1914
|
+
if (conflictScore <= 0.5) return null;
|
|
1915
|
+
return {
|
|
1916
|
+
memoryId: candidateId,
|
|
1917
|
+
content: candidateContent,
|
|
1918
|
+
conflict_score: Math.min(1, conflictScore),
|
|
1919
|
+
conflict_type: conflictType,
|
|
1920
|
+
detail: details.join("; ")
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function detectConflicts(inputContent, candidates) {
|
|
1924
|
+
const conflicts = [];
|
|
1925
|
+
for (const candidate of candidates) {
|
|
1926
|
+
if (candidate.score.dedup_score < 0.6) continue;
|
|
1927
|
+
const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
|
|
1928
|
+
if (conflict) {
|
|
1929
|
+
conflicts.push(conflict);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return conflicts;
|
|
1933
|
+
}
|
|
1719
1934
|
async function guard(db, input) {
|
|
1720
1935
|
const hash = contentHash(input.content);
|
|
1721
1936
|
const agentId = input.agent_id ?? "default";
|
|
@@ -1742,18 +1957,37 @@ async function guard(db, input) {
|
|
|
1742
1957
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
1743
1958
|
}
|
|
1744
1959
|
const candidates = await recallCandidates(db, input, agentId);
|
|
1960
|
+
const candidatesList = candidates.map((c) => ({
|
|
1961
|
+
memoryId: c.result.memory.id,
|
|
1962
|
+
dedup_score: c.score.dedup_score
|
|
1963
|
+
}));
|
|
1964
|
+
const conflicts = detectConflicts(input.content, candidates);
|
|
1745
1965
|
const best = candidates[0];
|
|
1746
1966
|
if (!best) {
|
|
1747
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
1967
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
1748
1968
|
}
|
|
1749
1969
|
const score = best.score;
|
|
1750
1970
|
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
1971
|
+
const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
|
|
1972
|
+
if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
|
|
1973
|
+
return {
|
|
1974
|
+
action: "update",
|
|
1975
|
+
reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
|
|
1976
|
+
existingId: best.result.memory.id,
|
|
1977
|
+
updatedContent: input.content,
|
|
1978
|
+
score,
|
|
1979
|
+
candidates: candidatesList,
|
|
1980
|
+
conflicts
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1751
1983
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
1752
1984
|
return {
|
|
1753
1985
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
1754
1986
|
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
1755
1987
|
existingId: best.result.memory.id,
|
|
1756
|
-
score
|
|
1988
|
+
score,
|
|
1989
|
+
candidates: candidatesList,
|
|
1990
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1757
1991
|
};
|
|
1758
1992
|
}
|
|
1759
1993
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -1771,13 +2005,17 @@ async function guard(db, input) {
|
|
|
1771
2005
|
existingId: best.result.memory.id,
|
|
1772
2006
|
mergedContent: mergePlan.content,
|
|
1773
2007
|
mergePlan,
|
|
1774
|
-
score
|
|
2008
|
+
score,
|
|
2009
|
+
candidates: candidatesList,
|
|
2010
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1775
2011
|
};
|
|
1776
2012
|
}
|
|
1777
2013
|
return {
|
|
1778
2014
|
action: "add",
|
|
1779
2015
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
1780
|
-
score
|
|
2016
|
+
score,
|
|
2017
|
+
candidates: candidatesList,
|
|
2018
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1781
2019
|
};
|
|
1782
2020
|
}
|
|
1783
2021
|
|
|
@@ -1790,6 +2028,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
1790
2028
|
} catch {
|
|
1791
2029
|
}
|
|
1792
2030
|
}
|
|
2031
|
+
function createAutoLinks(db, memoryId, candidates, agentId) {
|
|
2032
|
+
if (!candidates || candidates.length === 0) return;
|
|
2033
|
+
const linkCandidates = candidates.filter((c) => c.memoryId !== memoryId && c.dedup_score >= 0.45 && c.dedup_score < 0.82).sort((a, b) => b.dedup_score - a.dedup_score).slice(0, 5);
|
|
2034
|
+
if (linkCandidates.length === 0) return;
|
|
2035
|
+
const timestamp = now();
|
|
2036
|
+
const insert = db.prepare(
|
|
2037
|
+
`INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
2038
|
+
VALUES (?, ?, ?, 'related', ?, ?)`
|
|
2039
|
+
);
|
|
2040
|
+
for (const candidate of linkCandidates) {
|
|
2041
|
+
insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
|
|
2042
|
+
insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1793
2045
|
async function syncOne(db, input) {
|
|
1794
2046
|
const memInput = {
|
|
1795
2047
|
content: input.content,
|
|
@@ -1801,17 +2053,22 @@ async function syncOne(db, input) {
|
|
|
1801
2053
|
uri: input.uri,
|
|
1802
2054
|
provider: input.provider,
|
|
1803
2055
|
conservative: input.conservative,
|
|
1804
|
-
emotion_tag: input.emotion_tag
|
|
2056
|
+
emotion_tag: input.emotion_tag,
|
|
2057
|
+
source_session: input.source_session,
|
|
2058
|
+
source_context: input.source_context,
|
|
2059
|
+
observed_at: input.observed_at
|
|
1805
2060
|
};
|
|
1806
2061
|
const guardResult = await guard(db, memInput);
|
|
2062
|
+
const agentId = input.agent_id ?? "default";
|
|
1807
2063
|
switch (guardResult.action) {
|
|
1808
2064
|
case "skip":
|
|
1809
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2065
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
1810
2066
|
case "add": {
|
|
1811
2067
|
const mem = createMemory(db, memInput);
|
|
1812
2068
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
1813
2069
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
1814
|
-
|
|
2070
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
2071
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1815
2072
|
}
|
|
1816
2073
|
case "update": {
|
|
1817
2074
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -1819,7 +2076,7 @@ async function syncOne(db, input) {
|
|
|
1819
2076
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
1820
2077
|
}
|
|
1821
2078
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1822
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2079
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1823
2080
|
}
|
|
1824
2081
|
case "merge": {
|
|
1825
2082
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -1827,7 +2084,8 @@ async function syncOne(db, input) {
|
|
|
1827
2084
|
}
|
|
1828
2085
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1829
2086
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1830
|
-
|
|
2087
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
2088
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1831
2089
|
}
|
|
1832
2090
|
}
|
|
1833
2091
|
}
|
|
@@ -1851,27 +2109,13 @@ async function rememberMemory(db, input) {
|
|
|
1851
2109
|
agent_id: input.agent_id,
|
|
1852
2110
|
provider: input.provider,
|
|
1853
2111
|
conservative: input.conservative,
|
|
1854
|
-
emotion_tag: input.emotion_tag
|
|
2112
|
+
emotion_tag: input.emotion_tag,
|
|
2113
|
+
source_session: input.source_session,
|
|
2114
|
+
source_context: input.source_context,
|
|
2115
|
+
observed_at: input.observed_at
|
|
1855
2116
|
});
|
|
1856
2117
|
}
|
|
1857
2118
|
|
|
1858
|
-
// src/app/recall.ts
|
|
1859
|
-
async function recallMemory(db, input) {
|
|
1860
|
-
const result = await recallMemories(db, input.query, {
|
|
1861
|
-
agent_id: input.agent_id,
|
|
1862
|
-
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
1863
|
-
min_vitality: input.min_vitality,
|
|
1864
|
-
lexicalLimit: input.lexicalLimit,
|
|
1865
|
-
vectorLimit: input.vectorLimit,
|
|
1866
|
-
provider: input.provider,
|
|
1867
|
-
recordAccess: input.recordAccess
|
|
1868
|
-
});
|
|
1869
|
-
if (input.emotion_tag) {
|
|
1870
|
-
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
1871
|
-
}
|
|
1872
|
-
return result;
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
2119
|
// src/app/feedback.ts
|
|
1876
2120
|
function clamp012(value) {
|
|
1877
2121
|
if (!Number.isFinite(value)) return 0;
|
|
@@ -1951,6 +2195,70 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
1951
2195
|
function getFeedbackScore(db, memoryId, agentId) {
|
|
1952
2196
|
return getFeedbackSummary(db, memoryId, agentId).score;
|
|
1953
2197
|
}
|
|
2198
|
+
function recordPassiveFeedback(db, memoryIds, agentId) {
|
|
2199
|
+
if (memoryIds.length === 0) return 0;
|
|
2200
|
+
const effectiveAgentId = agentId ?? "default";
|
|
2201
|
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
2202
|
+
const placeholders = memoryIds.map(() => "?").join(",");
|
|
2203
|
+
const recentCounts = /* @__PURE__ */ new Map();
|
|
2204
|
+
try {
|
|
2205
|
+
const rows = db.prepare(
|
|
2206
|
+
`SELECT memory_id, COUNT(*) as c
|
|
2207
|
+
FROM feedback_events
|
|
2208
|
+
WHERE memory_id IN (${placeholders})
|
|
2209
|
+
AND source = 'passive'
|
|
2210
|
+
AND created_at > ?
|
|
2211
|
+
GROUP BY memory_id`
|
|
2212
|
+
).all(...memoryIds, cutoff);
|
|
2213
|
+
for (const row of rows) {
|
|
2214
|
+
recentCounts.set(row.memory_id, row.c);
|
|
2215
|
+
}
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
let recorded = 0;
|
|
2219
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2220
|
+
const insert = db.prepare(
|
|
2221
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
2222
|
+
VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
|
|
2223
|
+
);
|
|
2224
|
+
for (const memoryId of memoryIds) {
|
|
2225
|
+
const count = recentCounts.get(memoryId) ?? 0;
|
|
2226
|
+
if (count >= 3) continue;
|
|
2227
|
+
try {
|
|
2228
|
+
insert.run(newId(), memoryId, effectiveAgentId, timestamp);
|
|
2229
|
+
recorded++;
|
|
2230
|
+
} catch {
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return recorded;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// src/app/recall.ts
|
|
2237
|
+
async function recallMemory(db, input) {
|
|
2238
|
+
const result = await recallMemories(db, input.query, {
|
|
2239
|
+
agent_id: input.agent_id,
|
|
2240
|
+
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
2241
|
+
min_vitality: input.min_vitality,
|
|
2242
|
+
lexicalLimit: input.lexicalLimit,
|
|
2243
|
+
vectorLimit: input.vectorLimit,
|
|
2244
|
+
provider: input.provider,
|
|
2245
|
+
recordAccess: input.recordAccess,
|
|
2246
|
+
related: input.related,
|
|
2247
|
+
after: input.after,
|
|
2248
|
+
before: input.before,
|
|
2249
|
+
recency_boost: input.recency_boost
|
|
2250
|
+
});
|
|
2251
|
+
if (input.emotion_tag) {
|
|
2252
|
+
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
2253
|
+
}
|
|
2254
|
+
if (input.recordAccess !== false) {
|
|
2255
|
+
const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
|
|
2256
|
+
if (top3DirectIds.length > 0) {
|
|
2257
|
+
recordPassiveFeedback(db, top3DirectIds, input.agent_id);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return result;
|
|
2261
|
+
}
|
|
1954
2262
|
|
|
1955
2263
|
// src/app/surface.ts
|
|
1956
2264
|
var INTENT_PRIORS = {
|
|
@@ -2092,7 +2400,9 @@ async function surfaceMemories(db, input) {
|
|
|
2092
2400
|
searchBM25(db, trimmedQuery, {
|
|
2093
2401
|
agent_id: agentId,
|
|
2094
2402
|
limit: lexicalWindow,
|
|
2095
|
-
min_vitality: minVitality
|
|
2403
|
+
min_vitality: minVitality,
|
|
2404
|
+
after: input.after,
|
|
2405
|
+
before: input.before
|
|
2096
2406
|
}),
|
|
2097
2407
|
"queryRank"
|
|
2098
2408
|
);
|
|
@@ -2103,7 +2413,9 @@ async function surfaceMemories(db, input) {
|
|
|
2103
2413
|
searchBM25(db, trimmedTask, {
|
|
2104
2414
|
agent_id: agentId,
|
|
2105
2415
|
limit: lexicalWindow,
|
|
2106
|
-
min_vitality: minVitality
|
|
2416
|
+
min_vitality: minVitality,
|
|
2417
|
+
after: input.after,
|
|
2418
|
+
before: input.before
|
|
2107
2419
|
}),
|
|
2108
2420
|
"taskRank"
|
|
2109
2421
|
);
|
|
@@ -2114,7 +2426,9 @@ async function surfaceMemories(db, input) {
|
|
|
2114
2426
|
searchBM25(db, recentTurns.join(" "), {
|
|
2115
2427
|
agent_id: agentId,
|
|
2116
2428
|
limit: lexicalWindow,
|
|
2117
|
-
min_vitality: minVitality
|
|
2429
|
+
min_vitality: minVitality,
|
|
2430
|
+
after: input.after,
|
|
2431
|
+
before: input.before
|
|
2118
2432
|
}),
|
|
2119
2433
|
"recentRank"
|
|
2120
2434
|
);
|
|
@@ -2128,7 +2442,9 @@ async function surfaceMemories(db, input) {
|
|
|
2128
2442
|
providerId: provider.id,
|
|
2129
2443
|
agent_id: agentId,
|
|
2130
2444
|
limit: lexicalWindow,
|
|
2131
|
-
min_vitality: minVitality
|
|
2445
|
+
min_vitality: minVitality,
|
|
2446
|
+
after: input.after,
|
|
2447
|
+
before: input.before
|
|
2132
2448
|
});
|
|
2133
2449
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
2134
2450
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -2264,19 +2580,73 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2264
2580
|
}
|
|
2265
2581
|
|
|
2266
2582
|
// src/sleep/tidy.ts
|
|
2583
|
+
var EVENT_STALE_PATTERNS = [
|
|
2584
|
+
{ pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
|
|
2585
|
+
{ pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
2586
|
+
{ pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
2587
|
+
];
|
|
2588
|
+
var KNOWLEDGE_STALE_PATTERNS = [
|
|
2589
|
+
{ pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
2590
|
+
{ pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
2591
|
+
];
|
|
2592
|
+
function isStaleContent(content, type) {
|
|
2593
|
+
if (type === "identity" || type === "emotion") {
|
|
2594
|
+
return { stale: false, reason: "type excluded", decay_factor: 1 };
|
|
2595
|
+
}
|
|
2596
|
+
const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
|
|
2597
|
+
for (const { pattern, type: staleType, decay } of patterns) {
|
|
2598
|
+
if (pattern.test(content)) {
|
|
2599
|
+
return { stale: true, reason: staleType, decay_factor: decay };
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
|
|
2603
|
+
}
|
|
2604
|
+
function getAgeThresholdDays(staleType) {
|
|
2605
|
+
const thresholds = {
|
|
2606
|
+
in_progress: 7,
|
|
2607
|
+
pending: 14,
|
|
2608
|
+
ephemeral: 3
|
|
2609
|
+
};
|
|
2610
|
+
return thresholds[staleType] ?? 7;
|
|
2611
|
+
}
|
|
2267
2612
|
function runTidy(db, opts) {
|
|
2268
2613
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
2269
2614
|
const agentId = opts?.agent_id;
|
|
2270
2615
|
let archived = 0;
|
|
2616
|
+
let staleDecayed = 0;
|
|
2271
2617
|
const transaction = db.transaction(() => {
|
|
2272
2618
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
2273
2619
|
for (const mem of decayed) {
|
|
2274
2620
|
deleteMemory(db, mem.id);
|
|
2275
2621
|
archived += 1;
|
|
2276
2622
|
}
|
|
2623
|
+
const currentMs = Date.now();
|
|
2624
|
+
const currentTime = now();
|
|
2625
|
+
const agentCondition = agentId ? "AND agent_id = ?" : "";
|
|
2626
|
+
const agentParams = agentId ? [agentId] : [];
|
|
2627
|
+
const candidates = db.prepare(
|
|
2628
|
+
`SELECT id, content, type, created_at, updated_at, vitality
|
|
2629
|
+
FROM memories
|
|
2630
|
+
WHERE priority >= 2 AND vitality >= ?
|
|
2631
|
+
${agentCondition}`
|
|
2632
|
+
).all(threshold, ...agentParams);
|
|
2633
|
+
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
2634
|
+
for (const mem of candidates) {
|
|
2635
|
+
const detection = isStaleContent(mem.content, mem.type);
|
|
2636
|
+
if (!detection.stale) continue;
|
|
2637
|
+
const createdMs = new Date(mem.created_at).getTime();
|
|
2638
|
+
const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
|
|
2639
|
+
const thresholdDays = getAgeThresholdDays(detection.reason);
|
|
2640
|
+
if (ageDays < thresholdDays) continue;
|
|
2641
|
+
const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
|
|
2642
|
+
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
2643
|
+
updateStmt.run(newVitality, currentTime, mem.id);
|
|
2644
|
+
staleDecayed += 1;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2277
2647
|
});
|
|
2278
2648
|
transaction();
|
|
2279
|
-
return { archived, orphansCleaned: 0 };
|
|
2649
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
2280
2650
|
}
|
|
2281
2651
|
|
|
2282
2652
|
// src/sleep/govern.ts
|
|
@@ -3304,6 +3674,7 @@ function runAutoIngestWatcher(options) {
|
|
|
3304
3674
|
const memoryMdPath = join2(workspaceDir, "MEMORY.md");
|
|
3305
3675
|
const debounceMs = options.debounceMs ?? 1200;
|
|
3306
3676
|
const initialScan = options.initialScan ?? true;
|
|
3677
|
+
const ingestDaily = process.env.AGENT_MEMORY_AUTO_INGEST_DAILY === "1";
|
|
3307
3678
|
const logger = options.logger ?? console;
|
|
3308
3679
|
const timers = /* @__PURE__ */ new Map();
|
|
3309
3680
|
const watchers = [];
|
|
@@ -3324,6 +3695,10 @@ function runAutoIngestWatcher(options) {
|
|
|
3324
3695
|
const isTrackedMarkdownFile = (absPath) => {
|
|
3325
3696
|
if (!absPath.endsWith(".md")) return false;
|
|
3326
3697
|
if (resolve(absPath) === memoryMdPath) return true;
|
|
3698
|
+
if (!ingestDaily) {
|
|
3699
|
+
const basename = absPath.split("/").pop() ?? "";
|
|
3700
|
+
if (/^\d{4}-\d{2}-\d{2}\.md$/.test(basename)) return false;
|
|
3701
|
+
}
|
|
3327
3702
|
const rel = relative(memoryDir, absPath).replace(/\\/g, "/");
|
|
3328
3703
|
if (rel.startsWith("..") || rel === "") return false;
|
|
3329
3704
|
return !rel.includes("/");
|
|
@@ -3604,6 +3979,7 @@ export {
|
|
|
3604
3979
|
healthcheckEmbeddingProvider,
|
|
3605
3980
|
ingestText,
|
|
3606
3981
|
isCountRow,
|
|
3982
|
+
isStaleContent,
|
|
3607
3983
|
listMemories,
|
|
3608
3984
|
listPendingEmbeddings,
|
|
3609
3985
|
loadWarmBootLayers,
|
|
@@ -3619,6 +3995,7 @@ export {
|
|
|
3619
3995
|
recallMemory,
|
|
3620
3996
|
recordAccess,
|
|
3621
3997
|
recordFeedbackEvent,
|
|
3998
|
+
recordPassiveFeedback,
|
|
3622
3999
|
reflectMemories,
|
|
3623
4000
|
reindexEmbeddings,
|
|
3624
4001
|
reindexMemories,
|