@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.
@@ -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
 
@@ -692,7 +735,9 @@ function createLocalHttpEmbeddingProvider(opts) {
692
735
  };
693
736
  }
694
737
  function createGeminiEmbeddingProvider(opts) {
695
- const id = stableProviderId(`gemini:${opts.model}`, `${opts.model}|${opts.dimension}`);
738
+ const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
739
+ const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
740
+ const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
696
741
  return {
697
742
  id,
698
743
  model: opts.model,
@@ -700,7 +745,7 @@ function createGeminiEmbeddingProvider(opts) {
700
745
  async embed(texts) {
701
746
  if (texts.length === 0) return [];
702
747
  const fetchFn = getFetch(opts.fetchImpl);
703
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
748
+ const url = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
704
749
  const requests = texts.map((text) => ({
705
750
  model: `models/${opts.model}`,
706
751
  content: { parts: [{ text }] },
@@ -732,9 +777,11 @@ function createGeminiEmbeddingProvider(opts) {
732
777
  function normalizeEmbeddingBaseUrl(baseUrl) {
733
778
  return trimTrailingSlashes(baseUrl);
734
779
  }
780
+ var GEMINI_DEFAULT_BASE_URL;
735
781
  var init_embedding = __esm({
736
782
  "src/search/embedding.ts"() {
737
783
  "use strict";
784
+ GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
738
785
  }
739
786
  });
740
787
 
@@ -790,6 +837,7 @@ function createEmbeddingProvider(input, opts) {
790
837
  model: normalized.model,
791
838
  dimension: normalized.dimension,
792
839
  apiKey: normalized.apiKey,
840
+ baseUrl: normalized.baseUrl || void 0,
793
841
  fetchImpl: opts?.fetchImpl
794
842
  });
795
843
  }
@@ -927,6 +975,23 @@ function searchByVector(db, queryVector, opts) {
927
975
  const limit = opts.limit ?? 20;
928
976
  const agentId = opts.agent_id ?? "default";
929
977
  const minVitality = opts.min_vitality ?? 0;
978
+ const conditions = [
979
+ "e.provider_id = ?",
980
+ "e.status = 'ready'",
981
+ "e.vector IS NOT NULL",
982
+ "e.content_hash = m.hash",
983
+ "m.agent_id = ?",
984
+ "m.vitality >= ?"
985
+ ];
986
+ const params = [opts.providerId, agentId, minVitality];
987
+ if (opts.after) {
988
+ conditions.push("m.updated_at >= ?");
989
+ params.push(opts.after);
990
+ }
991
+ if (opts.before) {
992
+ conditions.push("m.updated_at <= ?");
993
+ params.push(opts.before);
994
+ }
930
995
  const rows = db.prepare(
931
996
  `SELECT e.provider_id, e.vector, e.content_hash,
932
997
  m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
@@ -934,13 +999,8 @@ function searchByVector(db, queryVector, opts) {
934
999
  m.updated_at, m.source, m.agent_id, m.hash
935
1000
  FROM embeddings e
936
1001
  JOIN memories m ON m.id = e.memory_id
937
- WHERE e.provider_id = ?
938
- AND e.status = 'ready'
939
- AND e.vector IS NOT NULL
940
- AND e.content_hash = m.hash
941
- AND m.agent_id = ?
942
- AND m.vitality >= ?`
943
- ).all(opts.providerId, agentId, minVitality);
1002
+ WHERE ${conditions.join(" AND ")}`
1003
+ ).all(...params);
944
1004
  const scored = rows.map((row) => ({
945
1005
  provider_id: row.provider_id,
946
1006
  memory: {
@@ -1004,10 +1064,12 @@ function createMemory(db, input) {
1004
1064
  }
1005
1065
  const id = newId();
1006
1066
  const timestamp = now();
1067
+ const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
1007
1068
  db.prepare(
1008
1069
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
1009
- access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
1010
- 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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1011
1073
  ).run(
1012
1074
  id,
1013
1075
  input.content,
@@ -1020,7 +1082,10 @@ function createMemory(db, input) {
1020
1082
  input.source ?? null,
1021
1083
  agentId,
1022
1084
  hash,
1023
- input.emotion_tag ?? null
1085
+ input.emotion_tag ?? null,
1086
+ input.source_session ?? null,
1087
+ sourceContext,
1088
+ input.observed_at ?? null
1024
1089
  );
1025
1090
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
1026
1091
  markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -1426,16 +1491,25 @@ function searchBM25(db, query, opts) {
1426
1491
  const ftsQuery = buildFtsQuery(query);
1427
1492
  if (!ftsQuery) return [];
1428
1493
  try {
1494
+ const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
1495
+ const params = [ftsQuery, agentId, minVitality];
1496
+ if (opts?.after) {
1497
+ conditions.push("m.updated_at >= ?");
1498
+ params.push(opts.after);
1499
+ }
1500
+ if (opts?.before) {
1501
+ conditions.push("m.updated_at <= ?");
1502
+ params.push(opts.before);
1503
+ }
1504
+ params.push(limit);
1429
1505
  const rows = db.prepare(
1430
1506
  `SELECT m.*, rank AS score
1431
1507
  FROM memories_fts f
1432
1508
  JOIN memories m ON m.id = f.id
1433
- WHERE memories_fts MATCH ?
1434
- AND m.agent_id = ?
1435
- AND m.vitality >= ?
1509
+ WHERE ${conditions.join(" AND ")}
1436
1510
  ORDER BY rank
1437
1511
  LIMIT ?`
1438
- ).all(ftsQuery, agentId, minVitality, limit);
1512
+ ).all(...params);
1439
1513
  return rows.map((row, index) => {
1440
1514
  const { score: _score, ...memoryFields } = row;
1441
1515
  return {
@@ -1521,16 +1595,23 @@ function priorityPrior(priority) {
1521
1595
  function fusionScore(input) {
1522
1596
  const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
1523
1597
  const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
1524
- return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
1525
- }
1526
- 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) {
1527
1607
  const candidates = /* @__PURE__ */ new Map();
1528
1608
  for (const row of lexical) {
1529
1609
  candidates.set(row.memory.id, {
1530
1610
  memory: row.memory,
1531
1611
  score: 0,
1532
1612
  bm25_rank: row.rank,
1533
- bm25_score: row.score
1613
+ bm25_score: row.score,
1614
+ match_type: "direct"
1534
1615
  });
1535
1616
  }
1536
1617
  for (const row of vector) {
@@ -1543,13 +1624,14 @@ function fuseHybridResults(lexical, vector, limit) {
1543
1624
  memory: row.memory,
1544
1625
  score: 0,
1545
1626
  vector_rank: row.rank,
1546
- vector_score: row.similarity
1627
+ vector_score: row.similarity,
1628
+ match_type: "direct"
1547
1629
  });
1548
1630
  }
1549
1631
  }
1550
1632
  return [...candidates.values()].map((row) => ({
1551
1633
  ...row,
1552
- score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
1634
+ score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
1553
1635
  })).sort((left, right) => {
1554
1636
  if (right.score !== left.score) return right.score - left.score;
1555
1637
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
@@ -1562,9 +1644,61 @@ async function searchVectorBranch(db, query, opts) {
1562
1644
  providerId: opts.provider.id,
1563
1645
  agent_id: opts.agent_id,
1564
1646
  limit: opts.limit,
1565
- min_vitality: opts.min_vitality
1647
+ min_vitality: opts.min_vitality,
1648
+ after: opts.after,
1649
+ before: opts.before
1566
1650
  });
1567
1651
  }
1652
+ function expandRelated(db, results, agentId, maxTotal) {
1653
+ const existingIds = new Set(results.map((r) => r.memory.id));
1654
+ const related = [];
1655
+ for (const result of results) {
1656
+ const links = db.prepare(
1657
+ `SELECT l.target_id, l.weight, m.*
1658
+ FROM links l
1659
+ JOIN memories m ON m.id = l.target_id
1660
+ WHERE l.agent_id = ? AND l.source_id = ?
1661
+ ORDER BY l.weight DESC
1662
+ LIMIT 5`
1663
+ ).all(agentId, result.memory.id);
1664
+ for (const link of links) {
1665
+ if (existingIds.has(link.target_id)) continue;
1666
+ existingIds.add(link.target_id);
1667
+ const relatedMemory = {
1668
+ id: link.id,
1669
+ content: link.content,
1670
+ type: link.type,
1671
+ priority: link.priority,
1672
+ emotion_val: link.emotion_val,
1673
+ vitality: link.vitality,
1674
+ stability: link.stability,
1675
+ access_count: link.access_count,
1676
+ last_accessed: link.last_accessed,
1677
+ created_at: link.created_at,
1678
+ updated_at: link.updated_at,
1679
+ source: link.source,
1680
+ agent_id: link.agent_id,
1681
+ hash: link.hash,
1682
+ emotion_tag: link.emotion_tag,
1683
+ source_session: link.source_session ?? null,
1684
+ source_context: link.source_context ?? null,
1685
+ observed_at: link.observed_at ?? null
1686
+ };
1687
+ related.push({
1688
+ memory: relatedMemory,
1689
+ score: result.score * link.weight * 0.6,
1690
+ related_source_id: result.memory.id,
1691
+ match_type: "related"
1692
+ });
1693
+ }
1694
+ }
1695
+ const directResults = results.map((r) => ({
1696
+ ...r,
1697
+ match_type: "direct"
1698
+ }));
1699
+ const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
1700
+ return combined;
1701
+ }
1568
1702
  async function recallMemories(db, query, opts) {
1569
1703
  const limit = opts?.limit ?? 10;
1570
1704
  const agentId = opts?.agent_id ?? "default";
@@ -1572,10 +1706,15 @@ async function recallMemories(db, query, opts) {
1572
1706
  const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
1573
1707
  const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
1574
1708
  const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1709
+ const recencyBoost = opts?.recency_boost;
1710
+ const after = opts?.after;
1711
+ const before = opts?.before;
1575
1712
  const lexical = searchBM25(db, query, {
1576
1713
  agent_id: agentId,
1577
1714
  limit: lexicalLimit,
1578
- min_vitality: minVitality
1715
+ min_vitality: minVitality,
1716
+ after,
1717
+ before
1579
1718
  });
1580
1719
  let vector = [];
1581
1720
  if (provider) {
@@ -1584,17 +1723,25 @@ async function recallMemories(db, query, opts) {
1584
1723
  provider,
1585
1724
  agent_id: agentId,
1586
1725
  limit: vectorLimit,
1587
- min_vitality: minVitality
1726
+ min_vitality: minVitality,
1727
+ after,
1728
+ before
1588
1729
  });
1589
1730
  } catch {
1590
1731
  vector = [];
1591
1732
  }
1592
1733
  }
1593
1734
  const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
1594
- 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
+ }
1595
1740
  if (opts?.recordAccess !== false) {
1596
1741
  for (const row of results) {
1597
- recordAccess(db, row.memory.id);
1742
+ if (row.match_type !== "related") {
1743
+ recordAccess(db, row.memory.id);
1744
+ }
1598
1745
  }
1599
1746
  }
1600
1747
  return {
@@ -1766,6 +1913,43 @@ function getFeedbackSummary(db, memoryId, agentId) {
1766
1913
  };
1767
1914
  }
1768
1915
  }
1916
+ function recordPassiveFeedback(db, memoryIds, agentId) {
1917
+ if (memoryIds.length === 0) return 0;
1918
+ const effectiveAgentId = agentId ?? "default";
1919
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
1920
+ const placeholders = memoryIds.map(() => "?").join(",");
1921
+ const recentCounts = /* @__PURE__ */ new Map();
1922
+ try {
1923
+ const rows = db.prepare(
1924
+ `SELECT memory_id, COUNT(*) as c
1925
+ FROM feedback_events
1926
+ WHERE memory_id IN (${placeholders})
1927
+ AND source = 'passive'
1928
+ AND created_at > ?
1929
+ GROUP BY memory_id`
1930
+ ).all(...memoryIds, cutoff);
1931
+ for (const row of rows) {
1932
+ recentCounts.set(row.memory_id, row.c);
1933
+ }
1934
+ } catch {
1935
+ }
1936
+ let recorded = 0;
1937
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1938
+ const insert = db.prepare(
1939
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
1940
+ VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
1941
+ );
1942
+ for (const memoryId of memoryIds) {
1943
+ const count = recentCounts.get(memoryId) ?? 0;
1944
+ if (count >= 3) continue;
1945
+ try {
1946
+ insert.run(newId(), memoryId, effectiveAgentId, timestamp);
1947
+ recorded++;
1948
+ } catch {
1949
+ }
1950
+ }
1951
+ return recorded;
1952
+ }
1769
1953
 
1770
1954
  // src/app/surface.ts
1771
1955
  var INTENT_PRIORS = {
@@ -1907,7 +2091,9 @@ async function surfaceMemories(db, input) {
1907
2091
  searchBM25(db, trimmedQuery, {
1908
2092
  agent_id: agentId,
1909
2093
  limit: lexicalWindow,
1910
- min_vitality: minVitality
2094
+ min_vitality: minVitality,
2095
+ after: input.after,
2096
+ before: input.before
1911
2097
  }),
1912
2098
  "queryRank"
1913
2099
  );
@@ -1918,7 +2104,9 @@ async function surfaceMemories(db, input) {
1918
2104
  searchBM25(db, trimmedTask, {
1919
2105
  agent_id: agentId,
1920
2106
  limit: lexicalWindow,
1921
- min_vitality: minVitality
2107
+ min_vitality: minVitality,
2108
+ after: input.after,
2109
+ before: input.before
1922
2110
  }),
1923
2111
  "taskRank"
1924
2112
  );
@@ -1929,7 +2117,9 @@ async function surfaceMemories(db, input) {
1929
2117
  searchBM25(db, recentTurns.join(" "), {
1930
2118
  agent_id: agentId,
1931
2119
  limit: lexicalWindow,
1932
- min_vitality: minVitality
2120
+ min_vitality: minVitality,
2121
+ after: input.after,
2122
+ before: input.before
1933
2123
  }),
1934
2124
  "recentRank"
1935
2125
  );
@@ -1943,7 +2133,9 @@ async function surfaceMemories(db, input) {
1943
2133
  providerId: provider.id,
1944
2134
  agent_id: agentId,
1945
2135
  limit: lexicalWindow,
1946
- min_vitality: minVitality
2136
+ min_vitality: minVitality,
2137
+ after: input.after,
2138
+ before: input.before
1947
2139
  });
1948
2140
  const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
1949
2141
  collectBranch(signals, vectorRows, "semanticRank", similarity);
@@ -2163,7 +2355,13 @@ function uriScopeMatch(inputUri, candidateUri) {
2163
2355
  }
2164
2356
  return 0.2;
2165
2357
  }
2166
- function extractObservedAt(parts, fallback) {
2358
+ function extractObservedAt(parts, fallback, observedAt) {
2359
+ if (observedAt) {
2360
+ const parsed = new Date(observedAt);
2361
+ if (!Number.isNaN(parsed.getTime())) {
2362
+ return parsed;
2363
+ }
2364
+ }
2167
2365
  for (const part of parts) {
2168
2366
  if (!part) continue;
2169
2367
  const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
@@ -2186,8 +2384,16 @@ function timeProximity(input, memory, candidateUri) {
2186
2384
  if (input.type !== "event") {
2187
2385
  return 1;
2188
2386
  }
2189
- const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
2190
- 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
+ );
2191
2397
  if (!inputTime || !existingTime) {
2192
2398
  return 0.5;
2193
2399
  }
@@ -2271,6 +2477,65 @@ function fourCriterionGate(input) {
2271
2477
  failedCriteria: failed
2272
2478
  };
2273
2479
  }
2480
+ var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
2481
+ var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
2482
+ var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
2483
+ function extractNumbers(text) {
2484
+ return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
2485
+ }
2486
+ function detectConflict(inputContent, candidateContent, candidateId) {
2487
+ let conflictScore = 0;
2488
+ let conflictType = "negation";
2489
+ const details = [];
2490
+ const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
2491
+ const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
2492
+ if (inputHasNegation !== candidateHasNegation) {
2493
+ conflictScore += 0.4;
2494
+ conflictType = "negation";
2495
+ details.push("negation mismatch");
2496
+ }
2497
+ const inputNumbers = extractNumbers(inputContent);
2498
+ const candidateNumbers = extractNumbers(candidateContent);
2499
+ if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
2500
+ const inputSet = new Set(inputNumbers);
2501
+ const candidateSet = new Set(candidateNumbers);
2502
+ const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
2503
+ const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
2504
+ if (hasDiff && !hasCommon) {
2505
+ conflictScore += 0.3;
2506
+ conflictType = "value";
2507
+ details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
2508
+ }
2509
+ }
2510
+ const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
2511
+ const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
2512
+ const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
2513
+ const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
2514
+ if (inputDone && candidateActive || inputActive && candidateDone) {
2515
+ conflictScore += 0.3;
2516
+ conflictType = "status";
2517
+ details.push("status contradiction (done vs active)");
2518
+ }
2519
+ if (conflictScore <= 0.5) return null;
2520
+ return {
2521
+ memoryId: candidateId,
2522
+ content: candidateContent,
2523
+ conflict_score: Math.min(1, conflictScore),
2524
+ conflict_type: conflictType,
2525
+ detail: details.join("; ")
2526
+ };
2527
+ }
2528
+ function detectConflicts(inputContent, candidates) {
2529
+ const conflicts = [];
2530
+ for (const candidate of candidates) {
2531
+ if (candidate.score.dedup_score < 0.6) continue;
2532
+ const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
2533
+ if (conflict) {
2534
+ conflicts.push(conflict);
2535
+ }
2536
+ }
2537
+ return conflicts;
2538
+ }
2274
2539
  async function guard(db, input) {
2275
2540
  const hash = contentHash(input.content);
2276
2541
  const agentId = input.agent_id ?? "default";
@@ -2297,18 +2562,37 @@ async function guard(db, input) {
2297
2562
  return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
2298
2563
  }
2299
2564
  const candidates = await recallCandidates(db, input, agentId);
2565
+ const candidatesList = candidates.map((c) => ({
2566
+ memoryId: c.result.memory.id,
2567
+ dedup_score: c.score.dedup_score
2568
+ }));
2569
+ const conflicts = detectConflicts(input.content, candidates);
2300
2570
  const best = candidates[0];
2301
2571
  if (!best) {
2302
- return { action: "add", reason: "No relevant semantic candidates found" };
2572
+ return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
2303
2573
  }
2304
2574
  const score = best.score;
2305
2575
  if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
2576
+ const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
2577
+ if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
2578
+ return {
2579
+ action: "update",
2580
+ reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
2581
+ existingId: best.result.memory.id,
2582
+ updatedContent: input.content,
2583
+ score,
2584
+ candidates: candidatesList,
2585
+ conflicts
2586
+ };
2587
+ }
2306
2588
  const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
2307
2589
  return {
2308
2590
  action: shouldUpdateMetadata ? "update" : "skip",
2309
2591
  reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
2310
2592
  existingId: best.result.memory.id,
2311
- score
2593
+ score,
2594
+ candidates: candidatesList,
2595
+ conflicts: conflicts.length > 0 ? conflicts : void 0
2312
2596
  };
2313
2597
  }
2314
2598
  if (score.dedup_score >= MERGE_THRESHOLD) {
@@ -2326,17 +2610,22 @@ async function guard(db, input) {
2326
2610
  existingId: best.result.memory.id,
2327
2611
  mergedContent: mergePlan.content,
2328
2612
  mergePlan,
2329
- score
2613
+ score,
2614
+ candidates: candidatesList,
2615
+ conflicts: conflicts.length > 0 ? conflicts : void 0
2330
2616
  };
2331
2617
  }
2332
2618
  return {
2333
2619
  action: "add",
2334
2620
  reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
2335
- score
2621
+ score,
2622
+ candidates: candidatesList,
2623
+ conflicts: conflicts.length > 0 ? conflicts : void 0
2336
2624
  };
2337
2625
  }
2338
2626
 
2339
2627
  // src/sleep/sync.ts
2628
+ init_db();
2340
2629
  function ensureUriPath(db, memoryId, uri, agentId) {
2341
2630
  if (!uri) return;
2342
2631
  if (getPathByUri(db, uri, agentId ?? "default")) return;
@@ -2345,6 +2634,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
2345
2634
  } catch {
2346
2635
  }
2347
2636
  }
2637
+ function createAutoLinks(db, memoryId, candidates, agentId) {
2638
+ if (!candidates || candidates.length === 0) return;
2639
+ const linkCandidates = candidates.filter((c) => c.memoryId !== memoryId && c.dedup_score >= 0.45 && c.dedup_score < 0.82).sort((a, b) => b.dedup_score - a.dedup_score).slice(0, 5);
2640
+ if (linkCandidates.length === 0) return;
2641
+ const timestamp = now();
2642
+ const insert = db.prepare(
2643
+ `INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
2644
+ VALUES (?, ?, ?, 'related', ?, ?)`
2645
+ );
2646
+ for (const candidate of linkCandidates) {
2647
+ insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
2648
+ insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
2649
+ }
2650
+ }
2348
2651
  async function syncOne(db, input) {
2349
2652
  const memInput = {
2350
2653
  content: input.content,
@@ -2356,17 +2659,22 @@ async function syncOne(db, input) {
2356
2659
  uri: input.uri,
2357
2660
  provider: input.provider,
2358
2661
  conservative: input.conservative,
2359
- emotion_tag: input.emotion_tag
2662
+ emotion_tag: input.emotion_tag,
2663
+ source_session: input.source_session,
2664
+ source_context: input.source_context,
2665
+ observed_at: input.observed_at
2360
2666
  };
2361
2667
  const guardResult = await guard(db, memInput);
2668
+ const agentId = input.agent_id ?? "default";
2362
2669
  switch (guardResult.action) {
2363
2670
  case "skip":
2364
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
2671
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
2365
2672
  case "add": {
2366
2673
  const mem = createMemory(db, memInput);
2367
2674
  if (!mem) return { action: "skipped", reason: "createMemory returned null" };
2368
2675
  ensureUriPath(db, mem.id, input.uri, input.agent_id);
2369
- 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 };
2370
2678
  }
2371
2679
  case "update": {
2372
2680
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
@@ -2374,7 +2682,7 @@ async function syncOne(db, input) {
2374
2682
  updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
2375
2683
  }
2376
2684
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
2377
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
2685
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
2378
2686
  }
2379
2687
  case "merge": {
2380
2688
  if (!guardResult.existingId || !guardResult.mergedContent) {
@@ -2382,7 +2690,8 @@ async function syncOne(db, input) {
2382
2690
  }
2383
2691
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
2384
2692
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
2385
- 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 };
2386
2695
  }
2387
2696
  }
2388
2697
  }
@@ -2399,7 +2708,10 @@ async function rememberMemory(db, input) {
2399
2708
  agent_id: input.agent_id,
2400
2709
  provider: input.provider,
2401
2710
  conservative: input.conservative,
2402
- emotion_tag: input.emotion_tag
2711
+ emotion_tag: input.emotion_tag,
2712
+ source_session: input.source_session,
2713
+ source_context: input.source_context,
2714
+ observed_at: input.observed_at
2403
2715
  });
2404
2716
  }
2405
2717
 
@@ -2412,11 +2724,21 @@ async function recallMemory(db, input) {
2412
2724
  lexicalLimit: input.lexicalLimit,
2413
2725
  vectorLimit: input.vectorLimit,
2414
2726
  provider: input.provider,
2415
- recordAccess: input.recordAccess
2727
+ recordAccess: input.recordAccess,
2728
+ related: input.related,
2729
+ after: input.after,
2730
+ before: input.before,
2731
+ recency_boost: input.recency_boost
2416
2732
  });
2417
2733
  if (input.emotion_tag) {
2418
2734
  result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
2419
2735
  }
2736
+ if (input.recordAccess !== false) {
2737
+ const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
2738
+ if (top3DirectIds.length > 0) {
2739
+ recordPassiveFeedback(db, top3DirectIds, input.agent_id);
2740
+ }
2741
+ }
2420
2742
  return result;
2421
2743
  }
2422
2744
 
@@ -2483,19 +2805,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
2483
2805
 
2484
2806
  // src/sleep/tidy.ts
2485
2807
  init_memory();
2808
+ init_db();
2809
+ var EVENT_STALE_PATTERNS = [
2810
+ { pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
2811
+ { pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
2812
+ { pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
2813
+ ];
2814
+ var KNOWLEDGE_STALE_PATTERNS = [
2815
+ { pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
2816
+ { pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
2817
+ ];
2818
+ function isStaleContent(content, type) {
2819
+ if (type === "identity" || type === "emotion") {
2820
+ return { stale: false, reason: "type excluded", decay_factor: 1 };
2821
+ }
2822
+ const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
2823
+ for (const { pattern, type: staleType, decay } of patterns) {
2824
+ if (pattern.test(content)) {
2825
+ return { stale: true, reason: staleType, decay_factor: decay };
2826
+ }
2827
+ }
2828
+ return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
2829
+ }
2830
+ function getAgeThresholdDays(staleType) {
2831
+ const thresholds = {
2832
+ in_progress: 7,
2833
+ pending: 14,
2834
+ ephemeral: 3
2835
+ };
2836
+ return thresholds[staleType] ?? 7;
2837
+ }
2486
2838
  function runTidy(db, opts) {
2487
2839
  const threshold = opts?.vitalityThreshold ?? 0.05;
2488
2840
  const agentId = opts?.agent_id;
2489
2841
  let archived = 0;
2842
+ let staleDecayed = 0;
2490
2843
  const transaction = db.transaction(() => {
2491
2844
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
2492
2845
  for (const mem of decayed) {
2493
2846
  deleteMemory(db, mem.id);
2494
2847
  archived += 1;
2495
2848
  }
2849
+ const currentMs = Date.now();
2850
+ const currentTime = now();
2851
+ const agentCondition = agentId ? "AND agent_id = ?" : "";
2852
+ const agentParams = agentId ? [agentId] : [];
2853
+ const candidates = db.prepare(
2854
+ `SELECT id, content, type, created_at, updated_at, vitality
2855
+ FROM memories
2856
+ WHERE priority >= 2 AND vitality >= ?
2857
+ ${agentCondition}`
2858
+ ).all(threshold, ...agentParams);
2859
+ const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
2860
+ for (const mem of candidates) {
2861
+ const detection = isStaleContent(mem.content, mem.type);
2862
+ if (!detection.stale) continue;
2863
+ const createdMs = new Date(mem.created_at).getTime();
2864
+ const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
2865
+ const thresholdDays = getAgeThresholdDays(detection.reason);
2866
+ if (ageDays < thresholdDays) continue;
2867
+ const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
2868
+ if (Math.abs(newVitality - mem.vitality) > 1e-3) {
2869
+ updateStmt.run(newVitality, currentTime, mem.id);
2870
+ staleDecayed += 1;
2871
+ }
2872
+ }
2496
2873
  });
2497
2874
  transaction();
2498
- return { archived, orphansCleaned: 0 };
2875
+ return { archived, orphansCleaned: 0, staleDecayed };
2499
2876
  }
2500
2877
 
2501
2878
  // src/sleep/govern.ts