@smyslenny/agent-memory 4.3.0 → 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 +424 -47
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +108 -44
- package/dist/index.js +439 -63
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +537 -82
- 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
|
|
|
@@ -692,7 +735,9 @@ function createLocalHttpEmbeddingProvider(opts) {
|
|
|
692
735
|
};
|
|
693
736
|
}
|
|
694
737
|
function createGeminiEmbeddingProvider(opts) {
|
|
695
|
-
const
|
|
738
|
+
const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
|
|
739
|
+
const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
|
|
740
|
+
const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
|
|
696
741
|
return {
|
|
697
742
|
id,
|
|
698
743
|
model: opts.model,
|
|
@@ -700,7 +745,7 @@ function createGeminiEmbeddingProvider(opts) {
|
|
|
700
745
|
async embed(texts) {
|
|
701
746
|
if (texts.length === 0) return [];
|
|
702
747
|
const fetchFn = getFetch(opts.fetchImpl);
|
|
703
|
-
const url =
|
|
748
|
+
const url = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
|
|
704
749
|
const requests = texts.map((text) => ({
|
|
705
750
|
model: `models/${opts.model}`,
|
|
706
751
|
content: { parts: [{ text }] },
|
|
@@ -732,9 +777,11 @@ function createGeminiEmbeddingProvider(opts) {
|
|
|
732
777
|
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
733
778
|
return trimTrailingSlashes(baseUrl);
|
|
734
779
|
}
|
|
780
|
+
var GEMINI_DEFAULT_BASE_URL;
|
|
735
781
|
var init_embedding = __esm({
|
|
736
782
|
"src/search/embedding.ts"() {
|
|
737
783
|
"use strict";
|
|
784
|
+
GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
738
785
|
}
|
|
739
786
|
});
|
|
740
787
|
|
|
@@ -790,6 +837,7 @@ function createEmbeddingProvider(input, opts) {
|
|
|
790
837
|
model: normalized.model,
|
|
791
838
|
dimension: normalized.dimension,
|
|
792
839
|
apiKey: normalized.apiKey,
|
|
840
|
+
baseUrl: normalized.baseUrl || void 0,
|
|
793
841
|
fetchImpl: opts?.fetchImpl
|
|
794
842
|
});
|
|
795
843
|
}
|
|
@@ -927,6 +975,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
927
975
|
const limit = opts.limit ?? 20;
|
|
928
976
|
const agentId = opts.agent_id ?? "default";
|
|
929
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
|
+
}
|
|
930
995
|
const rows = db.prepare(
|
|
931
996
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
932
997
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -934,13 +999,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
934
999
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
935
1000
|
FROM embeddings e
|
|
936
1001
|
JOIN memories m ON m.id = e.memory_id
|
|
937
|
-
WHERE
|
|
938
|
-
|
|
939
|
-
AND e.vector IS NOT NULL
|
|
940
|
-
AND e.content_hash = m.hash
|
|
941
|
-
AND m.agent_id = ?
|
|
942
|
-
AND m.vitality >= ?`
|
|
943
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1002
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1003
|
+
).all(...params);
|
|
944
1004
|
const scored = rows.map((row) => ({
|
|
945
1005
|
provider_id: row.provider_id,
|
|
946
1006
|
memory: {
|
|
@@ -1004,10 +1064,12 @@ function createMemory(db, input) {
|
|
|
1004
1064
|
}
|
|
1005
1065
|
const id = newId();
|
|
1006
1066
|
const timestamp = now();
|
|
1067
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1007
1068
|
db.prepare(
|
|
1008
1069
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1009
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1010
|
-
|
|
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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1011
1073
|
).run(
|
|
1012
1074
|
id,
|
|
1013
1075
|
input.content,
|
|
@@ -1020,7 +1082,10 @@ function createMemory(db, input) {
|
|
|
1020
1082
|
input.source ?? null,
|
|
1021
1083
|
agentId,
|
|
1022
1084
|
hash,
|
|
1023
|
-
input.emotion_tag ?? null
|
|
1085
|
+
input.emotion_tag ?? null,
|
|
1086
|
+
input.source_session ?? null,
|
|
1087
|
+
sourceContext,
|
|
1088
|
+
input.observed_at ?? null
|
|
1024
1089
|
);
|
|
1025
1090
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1026
1091
|
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -1426,16 +1491,25 @@ function searchBM25(db, query, opts) {
|
|
|
1426
1491
|
const ftsQuery = buildFtsQuery(query);
|
|
1427
1492
|
if (!ftsQuery) return [];
|
|
1428
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);
|
|
1429
1505
|
const rows = db.prepare(
|
|
1430
1506
|
`SELECT m.*, rank AS score
|
|
1431
1507
|
FROM memories_fts f
|
|
1432
1508
|
JOIN memories m ON m.id = f.id
|
|
1433
|
-
WHERE
|
|
1434
|
-
AND m.agent_id = ?
|
|
1435
|
-
AND m.vitality >= ?
|
|
1509
|
+
WHERE ${conditions.join(" AND ")}
|
|
1436
1510
|
ORDER BY rank
|
|
1437
1511
|
LIMIT ?`
|
|
1438
|
-
).all(
|
|
1512
|
+
).all(...params);
|
|
1439
1513
|
return rows.map((row, index) => {
|
|
1440
1514
|
const { score: _score, ...memoryFields } = row;
|
|
1441
1515
|
return {
|
|
@@ -1521,16 +1595,23 @@ function priorityPrior(priority) {
|
|
|
1521
1595
|
function fusionScore(input) {
|
|
1522
1596
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1523
1597
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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) {
|
|
1527
1607
|
const candidates = /* @__PURE__ */ new Map();
|
|
1528
1608
|
for (const row of lexical) {
|
|
1529
1609
|
candidates.set(row.memory.id, {
|
|
1530
1610
|
memory: row.memory,
|
|
1531
1611
|
score: 0,
|
|
1532
1612
|
bm25_rank: row.rank,
|
|
1533
|
-
bm25_score: row.score
|
|
1613
|
+
bm25_score: row.score,
|
|
1614
|
+
match_type: "direct"
|
|
1534
1615
|
});
|
|
1535
1616
|
}
|
|
1536
1617
|
for (const row of vector) {
|
|
@@ -1543,13 +1624,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
1543
1624
|
memory: row.memory,
|
|
1544
1625
|
score: 0,
|
|
1545
1626
|
vector_rank: row.rank,
|
|
1546
|
-
vector_score: row.similarity
|
|
1627
|
+
vector_score: row.similarity,
|
|
1628
|
+
match_type: "direct"
|
|
1547
1629
|
});
|
|
1548
1630
|
}
|
|
1549
1631
|
}
|
|
1550
1632
|
return [...candidates.values()].map((row) => ({
|
|
1551
1633
|
...row,
|
|
1552
|
-
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 })
|
|
1553
1635
|
})).sort((left, right) => {
|
|
1554
1636
|
if (right.score !== left.score) return right.score - left.score;
|
|
1555
1637
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -1562,9 +1644,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
1562
1644
|
providerId: opts.provider.id,
|
|
1563
1645
|
agent_id: opts.agent_id,
|
|
1564
1646
|
limit: opts.limit,
|
|
1565
|
-
min_vitality: opts.min_vitality
|
|
1647
|
+
min_vitality: opts.min_vitality,
|
|
1648
|
+
after: opts.after,
|
|
1649
|
+
before: opts.before
|
|
1566
1650
|
});
|
|
1567
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
|
+
}
|
|
1568
1702
|
async function recallMemories(db, query, opts) {
|
|
1569
1703
|
const limit = opts?.limit ?? 10;
|
|
1570
1704
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -1572,10 +1706,15 @@ async function recallMemories(db, query, opts) {
|
|
|
1572
1706
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1573
1707
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1574
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;
|
|
1575
1712
|
const lexical = searchBM25(db, query, {
|
|
1576
1713
|
agent_id: agentId,
|
|
1577
1714
|
limit: lexicalLimit,
|
|
1578
|
-
min_vitality: minVitality
|
|
1715
|
+
min_vitality: minVitality,
|
|
1716
|
+
after,
|
|
1717
|
+
before
|
|
1579
1718
|
});
|
|
1580
1719
|
let vector = [];
|
|
1581
1720
|
if (provider) {
|
|
@@ -1584,17 +1723,25 @@ async function recallMemories(db, query, opts) {
|
|
|
1584
1723
|
provider,
|
|
1585
1724
|
agent_id: agentId,
|
|
1586
1725
|
limit: vectorLimit,
|
|
1587
|
-
min_vitality: minVitality
|
|
1726
|
+
min_vitality: minVitality,
|
|
1727
|
+
after,
|
|
1728
|
+
before
|
|
1588
1729
|
});
|
|
1589
1730
|
} catch {
|
|
1590
1731
|
vector = [];
|
|
1591
1732
|
}
|
|
1592
1733
|
}
|
|
1593
1734
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1594
|
-
|
|
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
|
+
}
|
|
1595
1740
|
if (opts?.recordAccess !== false) {
|
|
1596
1741
|
for (const row of results) {
|
|
1597
|
-
|
|
1742
|
+
if (row.match_type !== "related") {
|
|
1743
|
+
recordAccess(db, row.memory.id);
|
|
1744
|
+
}
|
|
1598
1745
|
}
|
|
1599
1746
|
}
|
|
1600
1747
|
return {
|
|
@@ -1766,6 +1913,43 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
1766
1913
|
};
|
|
1767
1914
|
}
|
|
1768
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
|
+
}
|
|
1769
1953
|
|
|
1770
1954
|
// src/app/surface.ts
|
|
1771
1955
|
var INTENT_PRIORS = {
|
|
@@ -1907,7 +2091,9 @@ async function surfaceMemories(db, input) {
|
|
|
1907
2091
|
searchBM25(db, trimmedQuery, {
|
|
1908
2092
|
agent_id: agentId,
|
|
1909
2093
|
limit: lexicalWindow,
|
|
1910
|
-
min_vitality: minVitality
|
|
2094
|
+
min_vitality: minVitality,
|
|
2095
|
+
after: input.after,
|
|
2096
|
+
before: input.before
|
|
1911
2097
|
}),
|
|
1912
2098
|
"queryRank"
|
|
1913
2099
|
);
|
|
@@ -1918,7 +2104,9 @@ async function surfaceMemories(db, input) {
|
|
|
1918
2104
|
searchBM25(db, trimmedTask, {
|
|
1919
2105
|
agent_id: agentId,
|
|
1920
2106
|
limit: lexicalWindow,
|
|
1921
|
-
min_vitality: minVitality
|
|
2107
|
+
min_vitality: minVitality,
|
|
2108
|
+
after: input.after,
|
|
2109
|
+
before: input.before
|
|
1922
2110
|
}),
|
|
1923
2111
|
"taskRank"
|
|
1924
2112
|
);
|
|
@@ -1929,7 +2117,9 @@ async function surfaceMemories(db, input) {
|
|
|
1929
2117
|
searchBM25(db, recentTurns.join(" "), {
|
|
1930
2118
|
agent_id: agentId,
|
|
1931
2119
|
limit: lexicalWindow,
|
|
1932
|
-
min_vitality: minVitality
|
|
2120
|
+
min_vitality: minVitality,
|
|
2121
|
+
after: input.after,
|
|
2122
|
+
before: input.before
|
|
1933
2123
|
}),
|
|
1934
2124
|
"recentRank"
|
|
1935
2125
|
);
|
|
@@ -1943,7 +2133,9 @@ async function surfaceMemories(db, input) {
|
|
|
1943
2133
|
providerId: provider.id,
|
|
1944
2134
|
agent_id: agentId,
|
|
1945
2135
|
limit: lexicalWindow,
|
|
1946
|
-
min_vitality: minVitality
|
|
2136
|
+
min_vitality: minVitality,
|
|
2137
|
+
after: input.after,
|
|
2138
|
+
before: input.before
|
|
1947
2139
|
});
|
|
1948
2140
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
1949
2141
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -2163,7 +2355,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
2163
2355
|
}
|
|
2164
2356
|
return 0.2;
|
|
2165
2357
|
}
|
|
2166
|
-
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
|
+
}
|
|
2167
2365
|
for (const part of parts) {
|
|
2168
2366
|
if (!part) continue;
|
|
2169
2367
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -2186,8 +2384,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
2186
2384
|
if (input.type !== "event") {
|
|
2187
2385
|
return 1;
|
|
2188
2386
|
}
|
|
2189
|
-
const inputTime = extractObservedAt(
|
|
2190
|
-
|
|
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
|
+
);
|
|
2191
2397
|
if (!inputTime || !existingTime) {
|
|
2192
2398
|
return 0.5;
|
|
2193
2399
|
}
|
|
@@ -2271,6 +2477,65 @@ function fourCriterionGate(input) {
|
|
|
2271
2477
|
failedCriteria: failed
|
|
2272
2478
|
};
|
|
2273
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
|
+
}
|
|
2274
2539
|
async function guard(db, input) {
|
|
2275
2540
|
const hash = contentHash(input.content);
|
|
2276
2541
|
const agentId = input.agent_id ?? "default";
|
|
@@ -2297,18 +2562,37 @@ async function guard(db, input) {
|
|
|
2297
2562
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
2298
2563
|
}
|
|
2299
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);
|
|
2300
2570
|
const best = candidates[0];
|
|
2301
2571
|
if (!best) {
|
|
2302
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
2572
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
2303
2573
|
}
|
|
2304
2574
|
const score = best.score;
|
|
2305
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
|
+
}
|
|
2306
2588
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
2307
2589
|
return {
|
|
2308
2590
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
2309
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)})`,
|
|
2310
2592
|
existingId: best.result.memory.id,
|
|
2311
|
-
score
|
|
2593
|
+
score,
|
|
2594
|
+
candidates: candidatesList,
|
|
2595
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2312
2596
|
};
|
|
2313
2597
|
}
|
|
2314
2598
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -2326,17 +2610,22 @@ async function guard(db, input) {
|
|
|
2326
2610
|
existingId: best.result.memory.id,
|
|
2327
2611
|
mergedContent: mergePlan.content,
|
|
2328
2612
|
mergePlan,
|
|
2329
|
-
score
|
|
2613
|
+
score,
|
|
2614
|
+
candidates: candidatesList,
|
|
2615
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2330
2616
|
};
|
|
2331
2617
|
}
|
|
2332
2618
|
return {
|
|
2333
2619
|
action: "add",
|
|
2334
2620
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
2335
|
-
score
|
|
2621
|
+
score,
|
|
2622
|
+
candidates: candidatesList,
|
|
2623
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
2336
2624
|
};
|
|
2337
2625
|
}
|
|
2338
2626
|
|
|
2339
2627
|
// src/sleep/sync.ts
|
|
2628
|
+
init_db();
|
|
2340
2629
|
function ensureUriPath(db, memoryId, uri, agentId) {
|
|
2341
2630
|
if (!uri) return;
|
|
2342
2631
|
if (getPathByUri(db, uri, agentId ?? "default")) return;
|
|
@@ -2345,6 +2634,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
2345
2634
|
} catch {
|
|
2346
2635
|
}
|
|
2347
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
|
+
}
|
|
2348
2651
|
async function syncOne(db, input) {
|
|
2349
2652
|
const memInput = {
|
|
2350
2653
|
content: input.content,
|
|
@@ -2356,17 +2659,22 @@ async function syncOne(db, input) {
|
|
|
2356
2659
|
uri: input.uri,
|
|
2357
2660
|
provider: input.provider,
|
|
2358
2661
|
conservative: input.conservative,
|
|
2359
|
-
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
|
|
2360
2666
|
};
|
|
2361
2667
|
const guardResult = await guard(db, memInput);
|
|
2668
|
+
const agentId = input.agent_id ?? "default";
|
|
2362
2669
|
switch (guardResult.action) {
|
|
2363
2670
|
case "skip":
|
|
2364
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2671
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
2365
2672
|
case "add": {
|
|
2366
2673
|
const mem = createMemory(db, memInput);
|
|
2367
2674
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
2368
2675
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
2369
|
-
|
|
2676
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
2677
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2370
2678
|
}
|
|
2371
2679
|
case "update": {
|
|
2372
2680
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -2374,7 +2682,7 @@ async function syncOne(db, input) {
|
|
|
2374
2682
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
2375
2683
|
}
|
|
2376
2684
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2377
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2685
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2378
2686
|
}
|
|
2379
2687
|
case "merge": {
|
|
2380
2688
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -2382,7 +2690,8 @@ async function syncOne(db, input) {
|
|
|
2382
2690
|
}
|
|
2383
2691
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
2384
2692
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
2385
|
-
|
|
2693
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
2694
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
2386
2695
|
}
|
|
2387
2696
|
}
|
|
2388
2697
|
}
|
|
@@ -2399,7 +2708,10 @@ async function rememberMemory(db, input) {
|
|
|
2399
2708
|
agent_id: input.agent_id,
|
|
2400
2709
|
provider: input.provider,
|
|
2401
2710
|
conservative: input.conservative,
|
|
2402
|
-
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
|
|
2403
2715
|
});
|
|
2404
2716
|
}
|
|
2405
2717
|
|
|
@@ -2412,11 +2724,21 @@ async function recallMemory(db, input) {
|
|
|
2412
2724
|
lexicalLimit: input.lexicalLimit,
|
|
2413
2725
|
vectorLimit: input.vectorLimit,
|
|
2414
2726
|
provider: input.provider,
|
|
2415
|
-
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
|
|
2416
2732
|
});
|
|
2417
2733
|
if (input.emotion_tag) {
|
|
2418
2734
|
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
2419
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
|
+
}
|
|
2420
2742
|
return result;
|
|
2421
2743
|
}
|
|
2422
2744
|
|
|
@@ -2483,19 +2805,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2483
2805
|
|
|
2484
2806
|
// src/sleep/tidy.ts
|
|
2485
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
|
+
}
|
|
2486
2838
|
function runTidy(db, opts) {
|
|
2487
2839
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
2488
2840
|
const agentId = opts?.agent_id;
|
|
2489
2841
|
let archived = 0;
|
|
2842
|
+
let staleDecayed = 0;
|
|
2490
2843
|
const transaction = db.transaction(() => {
|
|
2491
2844
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
2492
2845
|
for (const mem of decayed) {
|
|
2493
2846
|
deleteMemory(db, mem.id);
|
|
2494
2847
|
archived += 1;
|
|
2495
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
|
+
}
|
|
2496
2873
|
});
|
|
2497
2874
|
transaction();
|
|
2498
|
-
return { archived, orphansCleaned: 0 };
|
|
2875
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
2499
2876
|
}
|
|
2500
2877
|
|
|
2501
2878
|
// src/sleep/govern.ts
|