@smyslenny/agent-memory 4.3.1 → 5.0.0

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