@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/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";
|
|
@@ -676,8 +719,11 @@ function createLocalHttpEmbeddingProvider(opts) {
|
|
|
676
719
|
}
|
|
677
720
|
};
|
|
678
721
|
}
|
|
722
|
+
var GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
679
723
|
function createGeminiEmbeddingProvider(opts) {
|
|
680
|
-
const
|
|
724
|
+
const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
|
|
725
|
+
const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
|
|
726
|
+
const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
|
|
681
727
|
return {
|
|
682
728
|
id,
|
|
683
729
|
model: opts.model,
|
|
@@ -685,7 +731,7 @@ function createGeminiEmbeddingProvider(opts) {
|
|
|
685
731
|
async embed(texts) {
|
|
686
732
|
if (texts.length === 0) return [];
|
|
687
733
|
const fetchFn = getFetch(opts.fetchImpl);
|
|
688
|
-
const url =
|
|
734
|
+
const url = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
|
|
689
735
|
const requests = texts.map((text) => ({
|
|
690
736
|
model: `models/${opts.model}`,
|
|
691
737
|
content: { parts: [{ text }] },
|
|
@@ -770,6 +816,7 @@ function createEmbeddingProvider(input, opts) {
|
|
|
770
816
|
model: normalized.model,
|
|
771
817
|
dimension: normalized.dimension,
|
|
772
818
|
apiKey: normalized.apiKey,
|
|
819
|
+
baseUrl: normalized.baseUrl || void 0,
|
|
773
820
|
fetchImpl: opts?.fetchImpl
|
|
774
821
|
});
|
|
775
822
|
}
|
|
@@ -936,6 +983,23 @@ function searchByVector(db, queryVector, opts) {
|
|
|
936
983
|
const limit = opts.limit ?? 20;
|
|
937
984
|
const agentId = opts.agent_id ?? "default";
|
|
938
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
|
+
}
|
|
939
1003
|
const rows = db.prepare(
|
|
940
1004
|
`SELECT e.provider_id, e.vector, e.content_hash,
|
|
941
1005
|
m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
|
|
@@ -943,13 +1007,8 @@ function searchByVector(db, queryVector, opts) {
|
|
|
943
1007
|
m.updated_at, m.source, m.agent_id, m.hash
|
|
944
1008
|
FROM embeddings e
|
|
945
1009
|
JOIN memories m ON m.id = e.memory_id
|
|
946
|
-
WHERE
|
|
947
|
-
|
|
948
|
-
AND e.vector IS NOT NULL
|
|
949
|
-
AND e.content_hash = m.hash
|
|
950
|
-
AND m.agent_id = ?
|
|
951
|
-
AND m.vitality >= ?`
|
|
952
|
-
).all(opts.providerId, agentId, minVitality);
|
|
1010
|
+
WHERE ${conditions.join(" AND ")}`
|
|
1011
|
+
).all(...params);
|
|
953
1012
|
const scored = rows.map((row) => ({
|
|
954
1013
|
provider_id: row.provider_id,
|
|
955
1014
|
memory: {
|
|
@@ -1022,10 +1081,12 @@ function createMemory(db, input) {
|
|
|
1022
1081
|
}
|
|
1023
1082
|
const id = newId();
|
|
1024
1083
|
const timestamp = now();
|
|
1084
|
+
const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
|
|
1025
1085
|
db.prepare(
|
|
1026
1086
|
`INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
|
|
1027
|
-
access_count, created_at, updated_at, source, agent_id, hash, emotion_tag
|
|
1028
|
-
|
|
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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1029
1090
|
).run(
|
|
1030
1091
|
id,
|
|
1031
1092
|
input.content,
|
|
@@ -1038,7 +1099,10 @@ function createMemory(db, input) {
|
|
|
1038
1099
|
input.source ?? null,
|
|
1039
1100
|
agentId,
|
|
1040
1101
|
hash,
|
|
1041
|
-
input.emotion_tag ?? null
|
|
1102
|
+
input.emotion_tag ?? null,
|
|
1103
|
+
input.source_session ?? null,
|
|
1104
|
+
sourceContext,
|
|
1105
|
+
input.observed_at ?? null
|
|
1042
1106
|
);
|
|
1043
1107
|
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
|
|
1044
1108
|
markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
|
|
@@ -1213,16 +1277,25 @@ function searchBM25(db, query, opts) {
|
|
|
1213
1277
|
const ftsQuery = buildFtsQuery(query);
|
|
1214
1278
|
if (!ftsQuery) return [];
|
|
1215
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);
|
|
1216
1291
|
const rows = db.prepare(
|
|
1217
1292
|
`SELECT m.*, rank AS score
|
|
1218
1293
|
FROM memories_fts f
|
|
1219
1294
|
JOIN memories m ON m.id = f.id
|
|
1220
|
-
WHERE
|
|
1221
|
-
AND m.agent_id = ?
|
|
1222
|
-
AND m.vitality >= ?
|
|
1295
|
+
WHERE ${conditions.join(" AND ")}
|
|
1223
1296
|
ORDER BY rank
|
|
1224
1297
|
LIMIT ?`
|
|
1225
|
-
).all(
|
|
1298
|
+
).all(...params);
|
|
1226
1299
|
return rows.map((row, index) => {
|
|
1227
1300
|
const { score: _score, ...memoryFields } = row;
|
|
1228
1301
|
return {
|
|
@@ -1306,16 +1379,23 @@ function priorityPrior(priority) {
|
|
|
1306
1379
|
function fusionScore(input) {
|
|
1307
1380
|
const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
|
|
1308
1381
|
const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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) {
|
|
1312
1391
|
const candidates = /* @__PURE__ */ new Map();
|
|
1313
1392
|
for (const row of lexical) {
|
|
1314
1393
|
candidates.set(row.memory.id, {
|
|
1315
1394
|
memory: row.memory,
|
|
1316
1395
|
score: 0,
|
|
1317
1396
|
bm25_rank: row.rank,
|
|
1318
|
-
bm25_score: row.score
|
|
1397
|
+
bm25_score: row.score,
|
|
1398
|
+
match_type: "direct"
|
|
1319
1399
|
});
|
|
1320
1400
|
}
|
|
1321
1401
|
for (const row of vector) {
|
|
@@ -1328,13 +1408,14 @@ function fuseHybridResults(lexical, vector, limit) {
|
|
|
1328
1408
|
memory: row.memory,
|
|
1329
1409
|
score: 0,
|
|
1330
1410
|
vector_rank: row.rank,
|
|
1331
|
-
vector_score: row.similarity
|
|
1411
|
+
vector_score: row.similarity,
|
|
1412
|
+
match_type: "direct"
|
|
1332
1413
|
});
|
|
1333
1414
|
}
|
|
1334
1415
|
}
|
|
1335
1416
|
return [...candidates.values()].map((row) => ({
|
|
1336
1417
|
...row,
|
|
1337
|
-
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 })
|
|
1338
1419
|
})).sort((left, right) => {
|
|
1339
1420
|
if (right.score !== left.score) return right.score - left.score;
|
|
1340
1421
|
return right.memory.updated_at.localeCompare(left.memory.updated_at);
|
|
@@ -1347,9 +1428,61 @@ async function searchVectorBranch(db, query, opts) {
|
|
|
1347
1428
|
providerId: opts.provider.id,
|
|
1348
1429
|
agent_id: opts.agent_id,
|
|
1349
1430
|
limit: opts.limit,
|
|
1350
|
-
min_vitality: opts.min_vitality
|
|
1431
|
+
min_vitality: opts.min_vitality,
|
|
1432
|
+
after: opts.after,
|
|
1433
|
+
before: opts.before
|
|
1351
1434
|
});
|
|
1352
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
|
+
}
|
|
1353
1486
|
async function recallMemories(db, query, opts) {
|
|
1354
1487
|
const limit = opts?.limit ?? 10;
|
|
1355
1488
|
const agentId = opts?.agent_id ?? "default";
|
|
@@ -1357,10 +1490,15 @@ async function recallMemories(db, query, opts) {
|
|
|
1357
1490
|
const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
|
|
1358
1491
|
const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
|
|
1359
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;
|
|
1360
1496
|
const lexical = searchBM25(db, query, {
|
|
1361
1497
|
agent_id: agentId,
|
|
1362
1498
|
limit: lexicalLimit,
|
|
1363
|
-
min_vitality: minVitality
|
|
1499
|
+
min_vitality: minVitality,
|
|
1500
|
+
after,
|
|
1501
|
+
before
|
|
1364
1502
|
});
|
|
1365
1503
|
let vector = [];
|
|
1366
1504
|
if (provider) {
|
|
@@ -1369,17 +1507,25 @@ async function recallMemories(db, query, opts) {
|
|
|
1369
1507
|
provider,
|
|
1370
1508
|
agent_id: agentId,
|
|
1371
1509
|
limit: vectorLimit,
|
|
1372
|
-
min_vitality: minVitality
|
|
1510
|
+
min_vitality: minVitality,
|
|
1511
|
+
after,
|
|
1512
|
+
before
|
|
1373
1513
|
});
|
|
1374
1514
|
} catch {
|
|
1375
1515
|
vector = [];
|
|
1376
1516
|
}
|
|
1377
1517
|
}
|
|
1378
1518
|
const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
|
|
1379
|
-
|
|
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
|
+
}
|
|
1380
1524
|
if (opts?.recordAccess !== false) {
|
|
1381
1525
|
for (const row of results) {
|
|
1382
|
-
|
|
1526
|
+
if (row.match_type !== "related") {
|
|
1527
|
+
recordAccess(db, row.memory.id);
|
|
1528
|
+
}
|
|
1383
1529
|
}
|
|
1384
1530
|
}
|
|
1385
1531
|
return {
|
|
@@ -1604,7 +1750,13 @@ function uriScopeMatch(inputUri, candidateUri) {
|
|
|
1604
1750
|
}
|
|
1605
1751
|
return 0.2;
|
|
1606
1752
|
}
|
|
1607
|
-
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
|
+
}
|
|
1608
1760
|
for (const part of parts) {
|
|
1609
1761
|
if (!part) continue;
|
|
1610
1762
|
const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
|
|
@@ -1627,8 +1779,16 @@ function timeProximity(input, memory, candidateUri) {
|
|
|
1627
1779
|
if (input.type !== "event") {
|
|
1628
1780
|
return 1;
|
|
1629
1781
|
}
|
|
1630
|
-
const inputTime = extractObservedAt(
|
|
1631
|
-
|
|
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
|
+
);
|
|
1632
1792
|
if (!inputTime || !existingTime) {
|
|
1633
1793
|
return 0.5;
|
|
1634
1794
|
}
|
|
@@ -1712,6 +1872,65 @@ function fourCriterionGate(input) {
|
|
|
1712
1872
|
failedCriteria: failed
|
|
1713
1873
|
};
|
|
1714
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
|
+
}
|
|
1715
1934
|
async function guard(db, input) {
|
|
1716
1935
|
const hash = contentHash(input.content);
|
|
1717
1936
|
const agentId = input.agent_id ?? "default";
|
|
@@ -1738,18 +1957,37 @@ async function guard(db, input) {
|
|
|
1738
1957
|
return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
|
|
1739
1958
|
}
|
|
1740
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);
|
|
1741
1965
|
const best = candidates[0];
|
|
1742
1966
|
if (!best) {
|
|
1743
|
-
return { action: "add", reason: "No relevant semantic candidates found" };
|
|
1967
|
+
return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
|
|
1744
1968
|
}
|
|
1745
1969
|
const score = best.score;
|
|
1746
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
|
+
}
|
|
1747
1983
|
const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
|
|
1748
1984
|
return {
|
|
1749
1985
|
action: shouldUpdateMetadata ? "update" : "skip",
|
|
1750
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)})`,
|
|
1751
1987
|
existingId: best.result.memory.id,
|
|
1752
|
-
score
|
|
1988
|
+
score,
|
|
1989
|
+
candidates: candidatesList,
|
|
1990
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1753
1991
|
};
|
|
1754
1992
|
}
|
|
1755
1993
|
if (score.dedup_score >= MERGE_THRESHOLD) {
|
|
@@ -1767,13 +2005,17 @@ async function guard(db, input) {
|
|
|
1767
2005
|
existingId: best.result.memory.id,
|
|
1768
2006
|
mergedContent: mergePlan.content,
|
|
1769
2007
|
mergePlan,
|
|
1770
|
-
score
|
|
2008
|
+
score,
|
|
2009
|
+
candidates: candidatesList,
|
|
2010
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1771
2011
|
};
|
|
1772
2012
|
}
|
|
1773
2013
|
return {
|
|
1774
2014
|
action: "add",
|
|
1775
2015
|
reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
|
|
1776
|
-
score
|
|
2016
|
+
score,
|
|
2017
|
+
candidates: candidatesList,
|
|
2018
|
+
conflicts: conflicts.length > 0 ? conflicts : void 0
|
|
1777
2019
|
};
|
|
1778
2020
|
}
|
|
1779
2021
|
|
|
@@ -1786,6 +2028,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
|
|
|
1786
2028
|
} catch {
|
|
1787
2029
|
}
|
|
1788
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
|
+
}
|
|
1789
2045
|
async function syncOne(db, input) {
|
|
1790
2046
|
const memInput = {
|
|
1791
2047
|
content: input.content,
|
|
@@ -1797,17 +2053,22 @@ async function syncOne(db, input) {
|
|
|
1797
2053
|
uri: input.uri,
|
|
1798
2054
|
provider: input.provider,
|
|
1799
2055
|
conservative: input.conservative,
|
|
1800
|
-
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
|
|
1801
2060
|
};
|
|
1802
2061
|
const guardResult = await guard(db, memInput);
|
|
2062
|
+
const agentId = input.agent_id ?? "default";
|
|
1803
2063
|
switch (guardResult.action) {
|
|
1804
2064
|
case "skip":
|
|
1805
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
2065
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
|
|
1806
2066
|
case "add": {
|
|
1807
2067
|
const mem = createMemory(db, memInput);
|
|
1808
2068
|
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
1809
2069
|
ensureUriPath(db, mem.id, input.uri, input.agent_id);
|
|
1810
|
-
|
|
2070
|
+
createAutoLinks(db, mem.id, guardResult.candidates, agentId);
|
|
2071
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1811
2072
|
}
|
|
1812
2073
|
case "update": {
|
|
1813
2074
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
@@ -1815,7 +2076,7 @@ async function syncOne(db, input) {
|
|
|
1815
2076
|
updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
|
|
1816
2077
|
}
|
|
1817
2078
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1818
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
2079
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1819
2080
|
}
|
|
1820
2081
|
case "merge": {
|
|
1821
2082
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
@@ -1823,7 +2084,8 @@ async function syncOne(db, input) {
|
|
|
1823
2084
|
}
|
|
1824
2085
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1825
2086
|
ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
|
|
1826
|
-
|
|
2087
|
+
createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
|
|
2088
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
|
|
1827
2089
|
}
|
|
1828
2090
|
}
|
|
1829
2091
|
}
|
|
@@ -1847,27 +2109,13 @@ async function rememberMemory(db, input) {
|
|
|
1847
2109
|
agent_id: input.agent_id,
|
|
1848
2110
|
provider: input.provider,
|
|
1849
2111
|
conservative: input.conservative,
|
|
1850
|
-
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
|
|
1851
2116
|
});
|
|
1852
2117
|
}
|
|
1853
2118
|
|
|
1854
|
-
// src/app/recall.ts
|
|
1855
|
-
async function recallMemory(db, input) {
|
|
1856
|
-
const result = await recallMemories(db, input.query, {
|
|
1857
|
-
agent_id: input.agent_id,
|
|
1858
|
-
limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
|
|
1859
|
-
min_vitality: input.min_vitality,
|
|
1860
|
-
lexicalLimit: input.lexicalLimit,
|
|
1861
|
-
vectorLimit: input.vectorLimit,
|
|
1862
|
-
provider: input.provider,
|
|
1863
|
-
recordAccess: input.recordAccess
|
|
1864
|
-
});
|
|
1865
|
-
if (input.emotion_tag) {
|
|
1866
|
-
result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
|
|
1867
|
-
}
|
|
1868
|
-
return result;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
2119
|
// src/app/feedback.ts
|
|
1872
2120
|
function clamp012(value) {
|
|
1873
2121
|
if (!Number.isFinite(value)) return 0;
|
|
@@ -1947,6 +2195,70 @@ function getFeedbackSummary(db, memoryId, agentId) {
|
|
|
1947
2195
|
function getFeedbackScore(db, memoryId, agentId) {
|
|
1948
2196
|
return getFeedbackSummary(db, memoryId, agentId).score;
|
|
1949
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
|
+
}
|
|
1950
2262
|
|
|
1951
2263
|
// src/app/surface.ts
|
|
1952
2264
|
var INTENT_PRIORS = {
|
|
@@ -2088,7 +2400,9 @@ async function surfaceMemories(db, input) {
|
|
|
2088
2400
|
searchBM25(db, trimmedQuery, {
|
|
2089
2401
|
agent_id: agentId,
|
|
2090
2402
|
limit: lexicalWindow,
|
|
2091
|
-
min_vitality: minVitality
|
|
2403
|
+
min_vitality: minVitality,
|
|
2404
|
+
after: input.after,
|
|
2405
|
+
before: input.before
|
|
2092
2406
|
}),
|
|
2093
2407
|
"queryRank"
|
|
2094
2408
|
);
|
|
@@ -2099,7 +2413,9 @@ async function surfaceMemories(db, input) {
|
|
|
2099
2413
|
searchBM25(db, trimmedTask, {
|
|
2100
2414
|
agent_id: agentId,
|
|
2101
2415
|
limit: lexicalWindow,
|
|
2102
|
-
min_vitality: minVitality
|
|
2416
|
+
min_vitality: minVitality,
|
|
2417
|
+
after: input.after,
|
|
2418
|
+
before: input.before
|
|
2103
2419
|
}),
|
|
2104
2420
|
"taskRank"
|
|
2105
2421
|
);
|
|
@@ -2110,7 +2426,9 @@ async function surfaceMemories(db, input) {
|
|
|
2110
2426
|
searchBM25(db, recentTurns.join(" "), {
|
|
2111
2427
|
agent_id: agentId,
|
|
2112
2428
|
limit: lexicalWindow,
|
|
2113
|
-
min_vitality: minVitality
|
|
2429
|
+
min_vitality: minVitality,
|
|
2430
|
+
after: input.after,
|
|
2431
|
+
before: input.before
|
|
2114
2432
|
}),
|
|
2115
2433
|
"recentRank"
|
|
2116
2434
|
);
|
|
@@ -2124,7 +2442,9 @@ async function surfaceMemories(db, input) {
|
|
|
2124
2442
|
providerId: provider.id,
|
|
2125
2443
|
agent_id: agentId,
|
|
2126
2444
|
limit: lexicalWindow,
|
|
2127
|
-
min_vitality: minVitality
|
|
2445
|
+
min_vitality: minVitality,
|
|
2446
|
+
after: input.after,
|
|
2447
|
+
before: input.before
|
|
2128
2448
|
});
|
|
2129
2449
|
const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
|
|
2130
2450
|
collectBranch(signals, vectorRows, "semanticRank", similarity);
|
|
@@ -2260,19 +2580,73 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2260
2580
|
}
|
|
2261
2581
|
|
|
2262
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
|
+
}
|
|
2263
2612
|
function runTidy(db, opts) {
|
|
2264
2613
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
2265
2614
|
const agentId = opts?.agent_id;
|
|
2266
2615
|
let archived = 0;
|
|
2616
|
+
let staleDecayed = 0;
|
|
2267
2617
|
const transaction = db.transaction(() => {
|
|
2268
2618
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
2269
2619
|
for (const mem of decayed) {
|
|
2270
2620
|
deleteMemory(db, mem.id);
|
|
2271
2621
|
archived += 1;
|
|
2272
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
|
+
}
|
|
2273
2647
|
});
|
|
2274
2648
|
transaction();
|
|
2275
|
-
return { archived, orphansCleaned: 0 };
|
|
2649
|
+
return { archived, orphansCleaned: 0, staleDecayed };
|
|
2276
2650
|
}
|
|
2277
2651
|
|
|
2278
2652
|
// src/sleep/govern.ts
|
|
@@ -3600,6 +3974,7 @@ export {
|
|
|
3600
3974
|
healthcheckEmbeddingProvider,
|
|
3601
3975
|
ingestText,
|
|
3602
3976
|
isCountRow,
|
|
3977
|
+
isStaleContent,
|
|
3603
3978
|
listMemories,
|
|
3604
3979
|
listPendingEmbeddings,
|
|
3605
3980
|
loadWarmBootLayers,
|
|
@@ -3615,6 +3990,7 @@ export {
|
|
|
3615
3990
|
recallMemory,
|
|
3616
3991
|
recordAccess,
|
|
3617
3992
|
recordFeedbackEvent,
|
|
3993
|
+
recordPassiveFeedback,
|
|
3618
3994
|
reflectMemories,
|
|
3619
3995
|
reindexEmbeddings,
|
|
3620
3996
|
reindexMemories,
|