@smyslenny/agent-memory 4.3.1 → 5.0.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/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 +433 -61
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +530 -80
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/agent-memory.js
CHANGED
|
@@ -95,6 +95,11 @@ function migrateDatabase(db, from, to) {
|
|
|
95
95
|
v = 6;
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
|
+
if (v === 6) {
|
|
99
|
+
migrateV6ToV7(db);
|
|
100
|
+
v = 7;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
98
103
|
throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -181,6 +186,8 @@ function inferSchemaVersion(db) {
|
|
|
181
186
|
const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
|
|
182
187
|
const hasFeedbackEvents = tableExists(db, "feedback_events");
|
|
183
188
|
const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
|
|
189
|
+
const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
190
|
+
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
|
|
184
191
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
|
|
185
192
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
|
|
186
193
|
if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
|
|
@@ -211,6 +218,10 @@ function ensureIndexes(db) {
|
|
|
211
218
|
if (tableHasColumn(db, "memories", "emotion_tag")) {
|
|
212
219
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
|
|
213
220
|
}
|
|
221
|
+
if (tableHasColumn(db, "memories", "observed_at")) {
|
|
222
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
223
|
+
}
|
|
224
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
214
225
|
if (tableExists(db, "feedback_events")) {
|
|
215
226
|
db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
|
|
216
227
|
if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
|
|
@@ -363,11 +374,40 @@ function migrateV5ToV6(db) {
|
|
|
363
374
|
throw e;
|
|
364
375
|
}
|
|
365
376
|
}
|
|
377
|
+
function migrateV6ToV7(db) {
|
|
378
|
+
const alreadyMigrated = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
|
|
379
|
+
if (alreadyMigrated) {
|
|
380
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
db.exec("BEGIN");
|
|
385
|
+
if (!tableHasColumn(db, "memories", "source_session")) {
|
|
386
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_session TEXT;");
|
|
387
|
+
}
|
|
388
|
+
if (!tableHasColumn(db, "memories", "source_context")) {
|
|
389
|
+
db.exec("ALTER TABLE memories ADD COLUMN source_context TEXT;");
|
|
390
|
+
}
|
|
391
|
+
if (!tableHasColumn(db, "memories", "observed_at")) {
|
|
392
|
+
db.exec("ALTER TABLE memories ADD COLUMN observed_at TEXT;");
|
|
393
|
+
}
|
|
394
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
|
|
395
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
|
|
396
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
|
|
397
|
+
db.exec("COMMIT");
|
|
398
|
+
} catch (e) {
|
|
399
|
+
try {
|
|
400
|
+
db.exec("ROLLBACK");
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
throw e;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
366
406
|
var SCHEMA_VERSION, SCHEMA_SQL;
|
|
367
407
|
var init_db = __esm({
|
|
368
408
|
"src/core/db.ts"() {
|
|
369
409
|
"use strict";
|
|
370
|
-
SCHEMA_VERSION =
|
|
410
|
+
SCHEMA_VERSION = 7;
|
|
371
411
|
SCHEMA_SQL = `
|
|
372
412
|
-- Memory entries
|
|
373
413
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -386,6 +426,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
386
426
|
agent_id TEXT NOT NULL DEFAULT 'default',
|
|
387
427
|
hash TEXT,
|
|
388
428
|
emotion_tag TEXT,
|
|
429
|
+
source_session TEXT,
|
|
430
|
+
source_context TEXT,
|
|
431
|
+
observed_at TEXT,
|
|
389
432
|
UNIQUE(hash, agent_id)
|
|
390
433
|
);
|
|
391
434
|
|
|
@@ -932,6 +975,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
932
975
|
const limit = opts.limit ?? 20;
|
|
933
976
|
const agentId = opts.agent_id ?? "default";
|
|
934
977
|
const minVitality = opts.min_vitality ?? 0;
|
|
978
|
+
const conditions = [
|
|
979
|
+
"e.provider_id = ?",
|
|
980
|
+
"e.status = 'ready'",
|
|
981
|
+
"e.vector IS NOT NULL",
|
|
982
|
+
"e.content_hash = m.hash",
|
|
983
|
+
"m.agent_id = ?",
|
|
984
|
+
"m.vitality >= ?"
|
|
985
|
+
];
|
|
986
|
+
const params = [opts.providerId, agentId, minVitality];
|
|
987
|
+
if (opts.after) {
|
|
988
|
+
conditions.push("m.updated_at >= ?");
|
|
989
|
+
params.push(opts.after);
|
|
990
|
+
}
|
|
991
|
+
if (opts.before) {
|
|
992
|
+
conditions.push("m.updated_at <= ?");
|
|
993
|
+
params.push(opts.before);
|
|
994
|
+
}
|
|
935
995
|
const rows = db.prepare(
|
|
936
996
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
937
997
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -939,13 +999,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
939
999
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
940
1000
|
FROM embeddings e
|
|
941
1001
|
JOIN memories m ON m.id = e.memory_id
|
|
942
|
-
WHERE
|
|
943
|
-
|
|
944
|
-
AND e.vector IS NOT NULL
|
|
945
|
-
AND e.content_hash = m.hash
|
|
946
|
-
AND m.agent_id = ?
|
|
947
|
-
AND m.vitality >= ?`
|
|
948
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1002
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1003
|
+
).all(...params);
|
|
949
1004
|
const scored = rows.map((row) => ({
|
|
950
1005
|
provider_id: row.provider_id,
|
|
951
1006
|
memory: {
|
|
@@ -1009,10 +1064,12 @@ function createMemory(db, input) {
|
|
|
1009
1064
|
}
|
|
1010
1065
|
const id = newId();
|
|
1011
1066
|
const timestamp = now();
|
|
1067
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1012
1068
|
db.prepare(
|
|
1013
1069
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1014
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1015
|
-
|
|
1070
|
+
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag,
|
|
1071
|
+
source_session, source_context, observed_at)
|
|
1072
|
+
VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1016
1073
|
).run(
|
|
1017
1074
|
id,
|
|
1018
1075
|
input.content,
|
|
@@ -1025,7 +1082,10 @@ function createMemory(db, input) {
|
|
|
1025
1082
|
input.source ?? null,
|
|
1026
1083
|
agentId,
|
|
1027
1084
|
hash,
|
|
1028
|
-
input.emotion_tag ?? null
|
|
1085
|
+
input.emotion_tag ?? null,
|
|
1086
|
+
input.source_session ?? null,
|
|
1087
|
+
sourceContext,
|
|
1088
|
+
input.observed_at ?? null
|
|
1029
1089
|
);
|
|
1030
1090
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1031
1091
|
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -1431,16 +1491,25 @@ function searchBM25(db, query, opts) {
|
|
|
1431
1491
|
const ftsQuery = buildFtsQuery(query);
|
|
1432
1492
|
if (!ftsQuery) return [];
|
|
1433
1493
|
try {
|
|
1494
|
+
const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
|
|
1495
|
+
const params = [ftsQuery, agentId, minVitality];
|
|
1496
|
+
if (opts?.after) {
|
|
1497
|
+
conditions.push("m.updated_at >= ?");
|
|
1498
|
+
params.push(opts.after);
|
|
1499
|
+
}
|
|
1500
|
+
if (opts?.before) {
|
|
1501
|
+
conditions.push("m.updated_at <= ?");
|
|
1502
|
+
params.push(opts.before);
|
|
1503
|
+
}
|
|
1504
|
+
params.push(limit);
|
|
1434
1505
|
const rows = db.prepare(
|
|
1435
1506
|
`SELECT m.*, rank AS score
|
|
1436
1507
|
FROM memories_fts f
|
|
1437
1508
|
JOIN memories m ON m.id = f.id
|
|
1438
|
-
WHERE
|
|
1439
|
-
AND m.agent_id = ?
|
|
1440
|
-
AND m.vitality >= ?
|
|
1509
|
+
WHERE ${conditions.join(" AND ")}
|
|
1441
1510
|
ORDER BY rank
|
|
1442
1511
|
LIMIT ?`
|
|
1443
|
-
).all(
|
|
1512
|
+
).all(...params);
|
|
1444
1513
|
return rows.map((row, index) => {
|
|
1445
1514
|
const { score: _score, ...memoryFields } = row;
|
|
1446
1515
|
return {
|
|
@@ -1526,16 +1595,23 @@ function priorityPrior(priority) {
|
|
|
1526
1595
|
function fusionScore(input) {
|
|
1527
1596
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1528
1597
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1598
|
+
const baseScore = lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
|
|
1599
|
+
const boost = input.recency_boost ?? 0;
|
|
1600
|
+
if (boost <= 0) return baseScore;
|
|
1601
|
+
const updatedAt = new Date(input.memory.updated_at).getTime();
|
|
1602
|
+
const daysSince = Math.max(0, (Date.now() - updatedAt) / (1e3 * 60 * 60 * 24));
|
|
1603
|
+
const recencyScore = Math.exp(-daysSince / 30);
|
|
1604
|
+
return (1 - boost) * baseScore + boost * recencyScore;
|
|
1605
|
+
}
|
|
1606
|
+
function fuseHybridResults(lexical, vector, limit, recency_boost) {
|
|
1532
1607
|
const candidates = /* @__PURE__ */ new Map();
|
|
1533
1608
|
for (const row of lexical) {
|
|
1534
1609
|
candidates.set(row.memory.id, {
|
|
1535
1610
|
memory: row.memory,
|
|
1536
1611
|
score: 0,
|
|
1537
1612
|
bm25_rank: row.rank,
|
|
1538
|
-
bm25_score: row.score
|
|
1613
|
+
bm25_score: row.score,
|
|
1614
|
+
match_type: "direct"
|
|
1539
1615
|
});
|
|
1540
1616
|
}
|
|
1541
1617
|
for (const row of vector) {
|
|
@@ -1548,13 +1624,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
1548
1624
|
memory: row.memory,
|
|
1549
1625
|
score: 0,
|
|
1550
1626
|
vector_rank: row.rank,
|
|
1551
|
-
vector_score: row.similarity
|
|
1627
|
+
vector_score: row.similarity,
|
|
1628
|
+
match_type: "direct"
|
|
1552
1629
|
});
|
|
1553
1630
|
}
|
|
1554
1631
|
}
|
|
1555
1632
|
return [...candidates.values()].map((row) => ({
|
|
1556
1633
|
...row,
|
|
1557
|
-
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
|
|
1634
|
+
score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
|
|
1558
1635
|
})).sort((left, right) => {
|
|
1559
1636
|
if (right.score !== left.score) return right.score - left.score;
|
|
1560
1637
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -1567,9 +1644,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
1567
1644
|
providerId: opts.provider.id,
|
|
1568
1645
|
agent_id: opts.agent_id,
|
|
1569
1646
|
limit: opts.limit,
|
|
1570
|
-
min_vitality: opts.min_vitality
|
|
1647
|
+
min_vitality: opts.min_vitality,
|
|
1648
|
+
after: opts.after,
|
|
1649
|
+
before: opts.before
|
|
1571
1650
|
});
|
|
1572
1651
|
}
|
|
1652
|
+
function expandRelated(db, results, agentId, maxTotal) {
|
|
1653
|
+
const existingIds = new Set(results.map((r) => r.memory.id));
|
|
1654
|
+
const related = [];
|
|
1655
|
+
for (const result of results) {
|
|
1656
|
+
const links = db.prepare(
|
|
1657
|
+
`SELECT l.target_id, l.weight, m.*
|
|
1658
|
+
FROM links l
|
|
1659
|
+
JOIN memories m ON m.id = l.target_id
|
|
1660
|
+
WHERE l.agent_id = ? AND l.source_id = ?
|
|
1661
|
+
ORDER BY l.weight DESC
|
|
1662
|
+
LIMIT 5`
|
|
1663
|
+
).all(agentId, result.memory.id);
|
|
1664
|
+
for (const link of links) {
|
|
1665
|
+
if (existingIds.has(link.target_id)) continue;
|
|
1666
|
+
existingIds.add(link.target_id);
|
|
1667
|
+
const relatedMemory = {
|
|
1668
|
+
id: link.id,
|
|
1669
|
+
content: link.content,
|
|
1670
|
+
type: link.type,
|
|
1671
|
+
priority: link.priority,
|
|
1672
|
+
emotion_val: link.emotion_val,
|
|
1673
|
+
vitality: link.vitality,
|
|
1674
|
+
stability: link.stability,
|
|
1675
|
+
access_count: link.access_count,
|
|
1676
|
+
last_accessed: link.last_accessed,
|
|
1677
|
+
created_at: link.created_at,
|
|
1678
|
+
updated_at: link.updated_at,
|
|
1679
|
+
source: link.source,
|
|
1680
|
+
agent_id: link.agent_id,
|
|
1681
|
+
hash: link.hash,
|
|
1682
|
+
emotion_tag: link.emotion_tag,
|
|
1683
|
+
source_session: link.source_session ?? null,
|
|
1684
|
+
source_context: link.source_context ?? null,
|
|
1685
|
+
observed_at: link.observed_at ?? null
|
|
1686
|
+
};
|
|
1687
|
+
related.push({
|
|
1688
|
+
memory: relatedMemory,
|
|
1689
|
+
score: result.score * link.weight * 0.6,
|
|
1690
|
+
related_source_id: result.memory.id,
|
|
1691
|
+
match_type: "related"
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const directResults = results.map((r) => ({
|
|
1696
|
+
...r,
|
|
1697
|
+
match_type: "direct"
|
|
1698
|
+
}));
|
|
1699
|
+
const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
|
|
1700
|
+
return combined;
|
|
1701
|
+
}
|
|
1573
1702
|
async function recallMemories(db, query, opts) {
|
|
1574
1703
|
const limit = opts?.limit ?? 10;
|
|
1575
1704
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -1577,10 +1706,15 @@ async function recallMemories(db, query, opts) {
|
|
|
1577
1706
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1578
1707
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1579
1708
|
const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
|
|
1709
|
+
const recencyBoost = opts?.recency_boost;
|
|
1710
|
+
const after = opts?.after;
|
|
1711
|
+
const before = opts?.before;
|
|
1580
1712
|
const lexical = searchBM25(db, query, {
|
|
1581
1713
|
agent_id: agentId,
|
|
1582
1714
|
limit: lexicalLimit,
|
|
1583
|
-
min_vitality: minVitality
|
|
1715
|
+
min_vitality: minVitality,
|
|
1716
|
+
after,
|
|
1717
|
+
before
|
|
1584
1718
|
});
|
|
1585
1719
|
let vector = [];
|
|
1586
1720
|
if (provider) {
|
|
@@ -1589,17 +1723,25 @@ async function recallMemories(db, query, opts) {
|
|
|
1589
1723
|
provider,
|
|
1590
1724
|
agent_id: agentId,
|
|
1591
1725
|
limit: vectorLimit,
|
|
1592
|
-
min_vitality: minVitality
|
|
1726
|
+
min_vitality: minVitality,
|
|
1727
|
+
after,
|
|
1728
|
+
before
|
|
1593
1729
|
});
|
|
1594
1730
|
} catch {
|
|
1595
1731
|
vector = [];
|
|
1596
1732
|
}
|
|
1597
1733
|
}
|
|
1598
1734
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1599
|
-
|
|
1735
|
+
let results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit, recencyBoost);
|
|
1736
|
+
if (opts?.related) {
|
|
1737
|
+
const maxTotal = Math.floor(limit * 1.5);
|
|
1738
|
+
results = expandRelated(db, results, agentId, maxTotal);
|
|
1739
|
+
}
|
|
1600
1740
|
if (opts?.recordAccess !== false) {
|
|
1601
1741
|
for (const row of results) {
|
|
1602
|
-
|
|
1742
|
+
if (row.match_type !== "related") {
|
|
1743
|
+
recordAccess(db, row.memory.id);
|
|
1744
|
+
}
|
|
1603
1745
|
}
|
|
1604
1746
|
}
|
|
1605
1747
|
return {
|
|
@@ -1771,6 +1913,43 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
1771
1913
|
};
|
|
1772
1914
|
}
|
|
1773
1915
|
}
|
|
1916
|
+
function recordPassiveFeedback(db, memoryIds, agentId) {
|
|
1917
|
+
if (memoryIds.length === 0) return 0;
|
|
1918
|
+
const effectiveAgentId = agentId ?? "default";
|
|
1919
|
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
1920
|
+
const placeholders = memoryIds.map(() => "?").join(",");
|
|
1921
|
+
const recentCounts = /* @__PURE__ */ new Map();
|
|
1922
|
+
try {
|
|
1923
|
+
const rows = db.prepare(
|
|
1924
|
+
`SELECT memory_id, COUNT(*) as c
|
|
1925
|
+
FROM feedback_events
|
|
1926
|
+
WHERE memory_id IN (${placeholders})
|
|
1927
|
+
AND source = 'passive'
|
|
1928
|
+
AND created_at > ?
|
|
1929
|
+
GROUP BY memory_id`
|
|
1930
|
+
).all(...memoryIds, cutoff);
|
|
1931
|
+
for (const row of rows) {
|
|
1932
|
+
recentCounts.set(row.memory_id, row.c);
|
|
1933
|
+
}
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
let recorded = 0;
|
|
1937
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1938
|
+
const insert = db.prepare(
|
|
1939
|
+
`INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
|
|
1940
|
+
VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
|
|
1941
|
+
);
|
|
1942
|
+
for (const memoryId of memoryIds) {
|
|
1943
|
+
const count = recentCounts.get(memoryId) ?? 0;
|
|
1944
|
+
if (count >= 3) continue;
|
|
1945
|
+
try {
|
|
1946
|
+
insert.run(newId(), memoryId, effectiveAgentId, timestamp);
|
|
1947
|
+
recorded++;
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return recorded;
|
|
1952
|
+
}
|
|
1774
1953
|
|
|
1775
1954
|
// src/app/surface.ts
|
|
1776
1955
|
var INTENT_PRIORS = {
|
|
@@ -1912,7 +2091,9 @@ async function surfaceMemories(db, input) {
|
|
|
1912
2091
|
searchBM25(db, trimmedQuery, {
|
|
1913
2092
|
agent_id: agentId,
|
|
1914
2093
|
limit: lexicalWindow,
|
|
1915
|
-
min_vitality: minVitality
|
|
2094
|
+
min_vitality: minVitality,
|
|
2095
|
+
after: input.after,
|
|
2096
|
+
before: input.before
|
|
1916
2097
|
}),
|
|
1917
2098
|
"queryRank"
|
|
1918
2099
|
);
|
|
@@ -1923,7 +2104,9 @@ async function surfaceMemories(db, input) {
|
|
|
1923
2104
|
searchBM25(db, trimmedTask, {
|
|
1924
2105
|
agent_id: agentId,
|
|
1925
2106
|
limit: lexicalWindow,
|
|
1926
|
-
min_vitality: minVitality
|
|
2107
|
+
min_vitality: minVitality,
|
|
2108
|
+
after: input.after,
|
|
2109
|
+
before: input.before
|
|
1927
2110
|
}),
|
|
1928
2111
|
"taskRank"
|
|
1929
2112
|
);
|
|
@@ -1934,7 +2117,9 @@ async function surfaceMemories(db, input) {
|
|
|
1934
2117
|
searchBM25(db, recentTurns.join(" "), {
|
|
1935
2118
|
agent_id: agentId,
|
|
1936
2119
|
limit: lexicalWindow,
|
|
1937
|
-
min_vitality: minVitality
|
|
2120
|
+
min_vitality: minVitality,
|
|
2121
|
+
after: input.after,
|
|
2122
|
+
before: input.before
|
|
1938
2123
|
}),
|
|
1939
2124
|
"recentRank"
|
|
1940
2125
|
);
|
|
@@ -1948,7 +2133,9 @@ async function surfaceMemories(db, input) {
|
|
|
1948
2133
|
providerId: provider.id,
|
|
1949
2134
|
agent_id: agentId,
|
|
1950
2135
|
limit: lexicalWindow,
|
|
1951
|
-
min_vitality: minVitality
|
|
2136
|
+
min_vitality: minVitality,
|
|
2137
|
+
after: input.after,
|
|
2138
|
+
before: input.before
|
|
1952
2139
|
});
|
|
1953
2140
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
1954
2141
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -2168,7 +2355,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
2168
2355
|
}
|
|
2169
2356
|
return 0.2;
|
|
2170
2357
|
}
|
|
2171
|
-
function extractObservedAt(parts, fallback) {
|
|
2358
|
+
function extractObservedAt(parts, fallback, observedAt) {
|
|
2359
|
+
if (observedAt) {
|
|
2360
|
+
const parsed = new Date(observedAt);
|
|
2361
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
2362
|
+
return parsed;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2172
2365
|
for (const part of parts) {
|
|
2173
2366
|
if (!part) continue;
|
|
2174
2367
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -2191,8 +2384,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
2191
2384
|
if (input.type !== "event") {
|
|
2192
2385
|
return 1;
|
|
2193
2386
|
}
|
|
2194
|
-
const inputTime = extractObservedAt(
|
|
2195
|
-
|
|
2387
|
+
const inputTime = extractObservedAt(
|
|
2388
|
+
[input.uri, input.source, input.content],
|
|
2389
|
+
input.now ?? null,
|
|
2390
|
+
input.observed_at ?? null
|
|
2391
|
+
);
|
|
2392
|
+
const existingTime = extractObservedAt(
|
|
2393
|
+
[candidateUri, memory.source, memory.content],
|
|
2394
|
+
memory.created_at,
|
|
2395
|
+
memory.observed_at ?? null
|
|
2396
|
+
);
|
|
2196
2397
|
if (!inputTime || !existingTime) {
|
|
2197
2398
|
return 0.5;
|
|
2198
2399
|
}
|
|
@@ -2276,6 +2477,65 @@ function fourCriterionGate(input) {
|
|
|
2276
2477
|
failedCriteria: failed
|
|
2277
2478
|
};
|
|
2278
2479
|
}
|
|
2480
|
+
var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
|
|
2481
|
+
var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
|
|
2482
|
+
var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
|
|
2483
|
+
function extractNumbers(text) {
|
|
2484
|
+
return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
|
|
2485
|
+
}
|
|
2486
|
+
function detectConflict(inputContent, candidateContent, candidateId) {
|
|
2487
|
+
let conflictScore = 0;
|
|
2488
|
+
let conflictType = "negation";
|
|
2489
|
+
const details = [];
|
|
2490
|
+
const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
|
|
2491
|
+
const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
|
|
2492
|
+
if (inputHasNegation !== candidateHasNegation) {
|
|
2493
|
+
conflictScore += 0.4;
|
|
2494
|
+
conflictType = "negation";
|
|
2495
|
+
details.push("negation mismatch");
|
|
2496
|
+
}
|
|
2497
|
+
const inputNumbers = extractNumbers(inputContent);
|
|
2498
|
+
const candidateNumbers = extractNumbers(candidateContent);
|
|
2499
|
+
if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
|
|
2500
|
+
const inputSet = new Set(inputNumbers);
|
|
2501
|
+
const candidateSet = new Set(candidateNumbers);
|
|
2502
|
+
const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
|
|
2503
|
+
const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
|
|
2504
|
+
if (hasDiff && !hasCommon) {
|
|
2505
|
+
conflictScore += 0.3;
|
|
2506
|
+
conflictType = "value";
|
|
2507
|
+
details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
|
|
2511
|
+
const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
|
|
2512
|
+
const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
|
|
2513
|
+
const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
|
|
2514
|
+
if (inputDone && candidateActive || inputActive && candidateDone) {
|
|
2515
|
+
conflictScore += 0.3;
|
|
2516
|
+
conflictType = "status";
|
|
2517
|
+
details.push("status contradiction (done vs active)");
|
|
2518
|
+
}
|
|
2519
|
+
if (conflictScore <= 0.5) return null;
|
|
2520
|
+
return {
|
|
2521
|
+
memoryId: candidateId,
|
|
2522
|
+
content: candidateContent,
|
|
2523
|
+
conflict_score: Math.min(1, conflictScore),
|
|
2524
|
+
conflict_type: conflictType,
|
|
2525
|
+
detail: details.join("; ")
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function detectConflicts(inputContent, candidates) {
|
|
2529
|
+
const conflicts = [];
|
|
2530
|
+
for (const candidate of candidates) {
|
|
2531
|
+
if (candidate.score.dedup_score < 0.6) continue;
|
|
2532
|
+
const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
|
|
2533
|
+
if (conflict) {
|
|
2534
|
+
conflicts.push(conflict);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return conflicts;
|
|
2538
|
+
}
|
|
2279
2539
|
async function guard(db, input) {
|
|
2280
2540
|
const hash = contentHash(input.content);
|
|
2281
2541
|
const agentId = input.agent_id ?? "default";
|
|
@@ -2302,18 +2562,37 @@ async function guard(db, input) {
|
|
|
2302
2562
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
2303
2563
|
}
|
|
2304
2564
|
const candidates = await recallCandidates(db, input, agentId);
|
|
2565
|
+
const candidatesList = candidates.map((c) => ({
|
|
2566
|
+
memoryId: c.result.memory.id,
|
|
2567
|
+
dedup_score: c.score.dedup_score
|
|
2568
|
+
}));
|
|
2569
|
+
const conflicts = detectConflicts(input.content, candidates);
|
|
2305
2570
|
const best = candidates[0];
|
|
2306
2571
|
if (!best) {
|
|
2307
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
2572
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
2308
2573
|
}
|
|
2309
2574
|
const score = best.score;
|
|
2310
2575
|
if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
|
|
2576
|
+
const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
|
|
2577
|
+
if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
|
|
2578
|
+
return {
|
|
2579
|
+
action: "update",
|
|
2580
|
+
reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
|
|
2581
|
+
existingId: best.result.memory.id,
|
|
2582
|
+
updatedContent: input.content,
|
|
2583
|
+
score,
|
|
2584
|
+
candidates: candidatesList,
|
|
2585
|
+
conflicts
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2311
2588
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
2312
2589
|
return {
|
|
2313
2590
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
2314
2591
|
reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
|
|
2315
2592
|
existingId: best.result.memory.id,
|
|
2316
|
-
score
|
|
2593
|
+
score,
|
|
2594
|
+
candidates: candidatesList,
|
|
2595
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2317
2596
|
};
|
|
2318
2597
|
}
|
|
2319
2598
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -2331,17 +2610,22 @@ async function guard(db, input) {
|
|
|
2331
2610
|
existingId: best.result.memory.id,
|
|
2332
2611
|
mergedContent: mergePlan.content,
|
|
2333
2612
|
mergePlan,
|
|
2334
|
-
score
|
|
2613
|
+
score,
|
|
2614
|
+
candidates: candidatesList,
|
|
2615
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2335
2616
|
};
|
|
2336
2617
|
}
|
|
2337
2618
|
return {
|
|
2338
2619
|
action: "add",
|
|
2339
2620
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
2340
|
-
score
|
|
2621
|
+
score,
|
|
2622
|
+
candidates: candidatesList,
|
|
2623
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2341
2624
|
};
|
|
2342
2625
|
}
|
|
2343
2626
|
|
|
2344
2627
|
// src/sleep/sync.ts
|
|
2628
|
+
init_db();
|
|
2345
2629
|
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
2346
2630
|
if (!uri) return;
|
|
2347
2631
|
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
@@ -2350,6 +2634,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
2350
2634
|
} catch {
|
|
2351
2635
|
}
|
|
2352
2636
|
}
|
|
2637
|
+
function createAutoLinks(db, memoryId, candidates, agentId) {
|
|
2638
|
+
if (!candidates || candidates.length === 0) return;
|
|
2639
|
+
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);
|
|
2640
|
+
if (linkCandidates.length === 0) return;
|
|
2641
|
+
const timestamp = now();
|
|
2642
|
+
const insert = db.prepare(
|
|
2643
|
+
`INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
2644
|
+
VALUES (?, ?, ?, 'related', ?, ?)`
|
|
2645
|
+
);
|
|
2646
|
+
for (const candidate of linkCandidates) {
|
|
2647
|
+
insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
|
|
2648
|
+
insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2353
2651
|
async function syncOne(db, input) {
|
|
2354
2652
|
const memInput = {
|
|
2355
2653
|
content: input.content,
|
|
@@ -2361,17 +2659,22 @@ async function syncOne(db, input) {
|
|
|
2361
2659
|
uri: input.uri,
|
|
2362
2660
|
provider: input.provider,
|
|
2363
2661
|
conservative: input.conservative,
|
|
2364
|
-
emotion_tag: input.emotion_tag
|
|
2662
|
+
emotion_tag: input.emotion_tag,
|
|
2663
|
+
source_session: input.source_session,
|
|
2664
|
+
source_context: input.source_context,
|
|
2665
|
+
observed_at: input.observed_at
|
|
2365
2666
|
};
|
|
2366
2667
|
const guardResult = await guard(db, memInput);
|
|
2668
|
+
const agentId = input.agent_id ?? "default";
|
|
2367
2669
|
switch (guardResult.action) {
|
|
2368
2670
|
case "skip":
|
|
2369
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2671
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
2370
2672
|
case "add": {
|
|
2371
2673
|
const mem = createMemory(db, memInput);
|
|
2372
2674
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
2373
2675
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
2374
|
-
|
|
2676
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
2677
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2375
2678
|
}
|
|
2376
2679
|
case "update": {
|
|
2377
2680
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -2379,7 +2682,7 @@ async function syncOne(db, input) {
|
|
|
2379
2682
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
2380
2683
|
}
|
|
2381
2684
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2382
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2685
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2383
2686
|
}
|
|
2384
2687
|
case "merge": {
|
|
2385
2688
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -2387,7 +2690,8 @@ async function syncOne(db, input) {
|
|
|
2387
2690
|
}
|
|
2388
2691
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
2389
2692
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2390
|
-
|
|
2693
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
2694
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2391
2695
|
}
|
|
2392
2696
|
}
|
|
2393
2697
|
}
|
|
@@ -2404,7 +2708,10 @@ async function rememberMemory(db, input) {
|
|
|
2404
2708
|
agent_id: input.agent_id,
|
|
2405
2709
|
provider: input.provider,
|
|
2406
2710
|
conservative: input.conservative,
|
|
2407
|
-
emotion_tag: input.emotion_tag
|
|
2711
|
+
emotion_tag: input.emotion_tag,
|
|
2712
|
+
source_session: input.source_session,
|
|
2713
|
+
source_context: input.source_context,
|
|
2714
|
+
observed_at: input.observed_at
|
|
2408
2715
|
});
|
|
2409
2716
|
}
|
|
2410
2717
|
|
|
@@ -2417,11 +2724,21 @@ async function recallMemory(db, input) {
|
|
|
2417
2724
|
lexicalLimit: input.lexicalLimit,
|
|
2418
2725
|
vectorLimit: input.vectorLimit,
|
|
2419
2726
|
provider: input.provider,
|
|
2420
|
-
recordAccess: input.recordAccess
|
|
2727
|
+
recordAccess: input.recordAccess,
|
|
2728
|
+
related: input.related,
|
|
2729
|
+
after: input.after,
|
|
2730
|
+
before: input.before,
|
|
2731
|
+
recency_boost: input.recency_boost
|
|
2421
2732
|
});
|
|
2422
2733
|
if (input.emotion_tag) {
|
|
2423
2734
|
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
2424
2735
|
}
|
|
2736
|
+
if (input.recordAccess !== false) {
|
|
2737
|
+
const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
|
|
2738
|
+
if (top3DirectIds.length > 0) {
|
|
2739
|
+
recordPassiveFeedback(db, top3DirectIds, input.agent_id);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2425
2742
|
return result;
|
|
2426
2743
|
}
|
|
2427
2744
|
|
|
@@ -2488,19 +2805,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2488
2805
|
|
|
2489
2806
|
// src/sleep/tidy.ts
|
|
2490
2807
|
init_memory();
|
|
2808
|
+
init_db();
|
|
2809
|
+
var EVENT_STALE_PATTERNS = [
|
|
2810
|
+
{ pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
|
|
2811
|
+
{ pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
2812
|
+
{ pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
2813
|
+
];
|
|
2814
|
+
var KNOWLEDGE_STALE_PATTERNS = [
|
|
2815
|
+
{ pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
|
|
2816
|
+
{ pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
|
|
2817
|
+
];
|
|
2818
|
+
function isStaleContent(content, type) {
|
|
2819
|
+
if (type === "identity" || type === "emotion") {
|
|
2820
|
+
return { stale: false, reason: "type excluded", decay_factor: 1 };
|
|
2821
|
+
}
|
|
2822
|
+
const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
|
|
2823
|
+
for (const { pattern, type: staleType, decay } of patterns) {
|
|
2824
|
+
if (pattern.test(content)) {
|
|
2825
|
+
return { stale: true, reason: staleType, decay_factor: decay };
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
|
|
2829
|
+
}
|
|
2830
|
+
function getAgeThresholdDays(staleType) {
|
|
2831
|
+
const thresholds = {
|
|
2832
|
+
in_progress: 7,
|
|
2833
|
+
pending: 14,
|
|
2834
|
+
ephemeral: 3
|
|
2835
|
+
};
|
|
2836
|
+
return thresholds[staleType] ?? 7;
|
|
2837
|
+
}
|
|
2491
2838
|
function runTidy(db, opts) {
|
|
2492
2839
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
2493
2840
|
const agentId = opts?.agent_id;
|
|
2494
2841
|
let archived = 0;
|
|
2842
|
+
let staleDecayed = 0;
|
|
2495
2843
|
const transaction = db.transaction(() => {
|
|
2496
2844
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
2497
2845
|
for (const mem of decayed) {
|
|
2498
2846
|
deleteMemory(db, mem.id);
|
|
2499
2847
|
archived += 1;
|
|
2500
2848
|
}
|
|
2849
|
+
const currentMs = Date.now();
|
|
2850
|
+
const currentTime = now();
|
|
2851
|
+
const agentCondition = agentId ? "AND agent_id = ?" : "";
|
|
2852
|
+
const agentParams = agentId ? [agentId] : [];
|
|
2853
|
+
const candidates = db.prepare(
|
|
2854
|
+
`SELECT id, content, type, created_at, updated_at, vitality
|
|
2855
|
+
FROM memories
|
|
2856
|
+
WHERE priority >= 2 AND vitality >= ?
|
|
2857
|
+
${agentCondition}`
|
|
2858
|
+
).all(threshold, ...agentParams);
|
|
2859
|
+
const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
|
|
2860
|
+
for (const mem of candidates) {
|
|
2861
|
+
const detection = isStaleContent(mem.content, mem.type);
|
|
2862
|
+
if (!detection.stale) continue;
|
|
2863
|
+
const createdMs = new Date(mem.created_at).getTime();
|
|
2864
|
+
const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
|
|
2865
|
+
const thresholdDays = getAgeThresholdDays(detection.reason);
|
|
2866
|
+
if (ageDays < thresholdDays) continue;
|
|
2867
|
+
const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
|
|
2868
|
+
if (Math.abs(newVitality - mem.vitality) > 1e-3) {
|
|
2869
|
+
updateStmt.run(newVitality, currentTime, mem.id);
|
|
2870
|
+
staleDecayed += 1;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2501
2873
|
});
|
|
2502
2874
|
transaction();
|
|
2503
|
-
return { archived, orphansCleaned: 0 };
|
|
2875
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
2504
2876
|
}
|
|
2505
2877
|
|
|
2506
2878
|
// src/sleep/govern.ts
|