@smyslenny/agent-memory 4.3.1 → 5.0.1

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