@smyslenny/agent-memory 4.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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";
@@ -676,8 +719,11 @@ function createLocalHttpEmbeddingProvider(opts) {
676
719
  }
677
720
  };
678
721
  }
722
+ var GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
679
723
  function createGeminiEmbeddingProvider(opts) {
680
- const id = stableProviderId(`gemini:${opts.model}`, `${opts.model}|${opts.dimension}`);
724
+ const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
725
+ const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
726
+ const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
681
727
  return {
682
728
  id,
683
729
  model: opts.model,
@@ -685,7 +731,7 @@ function createGeminiEmbeddingProvider(opts) {
685
731
  async embed(texts) {
686
732
  if (texts.length === 0) return [];
687
733
  const fetchFn = getFetch(opts.fetchImpl);
688
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
734
+ const url = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
689
735
  const requests = texts.map((text) => ({
690
736
  model: `models/${opts.model}`,
691
737
  content: { parts: [{ text }] },
@@ -770,6 +816,7 @@ function createEmbeddingProvider(input, opts) {
770
816
  model: normalized.model,
771
817
  dimension: normalized.dimension,
772
818
  apiKey: normalized.apiKey,
819
+ baseUrl: normalized.baseUrl || void 0,
773
820
  fetchImpl: opts?.fetchImpl
774
821
  });
775
822
  }
@@ -936,6 +983,23 @@ function searchByVector(db, queryVector, opts) {
936
983
  const limit = opts.limit ?? 20;
937
984
  const agentId = opts.agent_id ?? "default";
938
985
  const minVitality = opts.min_vitality ?? 0;
986
+ const conditions = [
987
+ "e.provider_id = ?",
988
+ "e.status = 'ready'",
989
+ "e.vector IS NOT NULL",
990
+ "e.content_hash = m.hash",
991
+ "m.agent_id = ?",
992
+ "m.vitality >= ?"
993
+ ];
994
+ const params = [opts.providerId, agentId, minVitality];
995
+ if (opts.after) {
996
+ conditions.push("m.updated_at >= ?");
997
+ params.push(opts.after);
998
+ }
999
+ if (opts.before) {
1000
+ conditions.push("m.updated_at <= ?");
1001
+ params.push(opts.before);
1002
+ }
939
1003
  const rows = db.prepare(
940
1004
  `SELECT e.provider_id, e.vector, e.content_hash,
941
1005
  m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
@@ -943,13 +1007,8 @@ function searchByVector(db, queryVector, opts) {
943
1007
  m.updated_at, m.source, m.agent_id, m.hash
944
1008
  FROM embeddings e
945
1009
  JOIN memories m ON m.id = e.memory_id
946
- WHERE e.provider_id = ?
947
- AND e.status = 'ready'
948
- AND e.vector IS NOT NULL
949
- AND e.content_hash = m.hash
950
- AND m.agent_id = ?
951
- AND m.vitality >= ?`
952
- ).all(opts.providerId, agentId, minVitality);
1010
+ WHERE ${conditions.join(" AND ")}`
1011
+ ).all(...params);
953
1012
  const scored = rows.map((row) => ({
954
1013
  provider_id: row.provider_id,
955
1014
  memory: {
@@ -1022,10 +1081,12 @@ function createMemory(db, input) {
1022
1081
  }
1023
1082
  const id = newId();
1024
1083
  const timestamp = now();
1084
+ const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
1025
1085
  db.prepare(
1026
1086
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
1027
- access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
1028
- 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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1029
1090
  ).run(
1030
1091
  id,
1031
1092
  input.content,
@@ -1038,7 +1099,10 @@ function createMemory(db, input) {
1038
1099
  input.source ?? null,
1039
1100
  agentId,
1040
1101
  hash,
1041
- input.emotion_tag ?? null
1102
+ input.emotion_tag ?? null,
1103
+ input.source_session ?? null,
1104
+ sourceContext,
1105
+ input.observed_at ?? null
1042
1106
  );
1043
1107
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
1044
1108
  markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -1213,16 +1277,25 @@ function searchBM25(db, query, opts) {
1213
1277
  const ftsQuery = buildFtsQuery(query);
1214
1278
  if (!ftsQuery) return [];
1215
1279
  try {
1280
+ const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
1281
+ const params = [ftsQuery, agentId, minVitality];
1282
+ if (opts?.after) {
1283
+ conditions.push("m.updated_at >= ?");
1284
+ params.push(opts.after);
1285
+ }
1286
+ if (opts?.before) {
1287
+ conditions.push("m.updated_at <= ?");
1288
+ params.push(opts.before);
1289
+ }
1290
+ params.push(limit);
1216
1291
  const rows = db.prepare(
1217
1292
  `SELECT m.*, rank AS score
1218
1293
  FROM memories_fts f
1219
1294
  JOIN memories m ON m.id = f.id
1220
- WHERE memories_fts MATCH ?
1221
- AND m.agent_id = ?
1222
- AND m.vitality >= ?
1295
+ WHERE ${conditions.join(" AND ")}
1223
1296
  ORDER BY rank
1224
1297
  LIMIT ?`
1225
- ).all(ftsQuery, agentId, minVitality, limit);
1298
+ ).all(...params);
1226
1299
  return rows.map((row, index) => {
1227
1300
  const { score: _score, ...memoryFields } = row;
1228
1301
  return {
@@ -1306,16 +1379,23 @@ function priorityPrior(priority) {
1306
1379
  function fusionScore(input) {
1307
1380
  const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
1308
1381
  const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
1309
- return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
1310
- }
1311
- 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) {
1312
1391
  const candidates = /* @__PURE__ */ new Map();
1313
1392
  for (const row of lexical) {
1314
1393
  candidates.set(row.memory.id, {
1315
1394
  memory: row.memory,
1316
1395
  score: 0,
1317
1396
  bm25_rank: row.rank,
1318
- bm25_score: row.score
1397
+ bm25_score: row.score,
1398
+ match_type: "direct"
1319
1399
  });
1320
1400
  }
1321
1401
  for (const row of vector) {
@@ -1328,13 +1408,14 @@ function fuseHybridResults(lexical, vector, limit) {
1328
1408
  memory: row.memory,
1329
1409
  score: 0,
1330
1410
  vector_rank: row.rank,
1331
- vector_score: row.similarity
1411
+ vector_score: row.similarity,
1412
+ match_type: "direct"
1332
1413
  });
1333
1414
  }
1334
1415
  }
1335
1416
  return [...candidates.values()].map((row) => ({
1336
1417
  ...row,
1337
- score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
1418
+ score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
1338
1419
  })).sort((left, right) => {
1339
1420
  if (right.score !== left.score) return right.score - left.score;
1340
1421
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
@@ -1347,9 +1428,61 @@ async function searchVectorBranch(db, query, opts) {
1347
1428
  providerId: opts.provider.id,
1348
1429
  agent_id: opts.agent_id,
1349
1430
  limit: opts.limit,
1350
- min_vitality: opts.min_vitality
1431
+ min_vitality: opts.min_vitality,
1432
+ after: opts.after,
1433
+ before: opts.before
1351
1434
  });
1352
1435
  }
1436
+ function expandRelated(db, results, agentId, maxTotal) {
1437
+ const existingIds = new Set(results.map((r) => r.memory.id));
1438
+ const related = [];
1439
+ for (const result of results) {
1440
+ const links = db.prepare(
1441
+ `SELECT l.target_id, l.weight, m.*
1442
+ FROM links l
1443
+ JOIN memories m ON m.id = l.target_id
1444
+ WHERE l.agent_id = ? AND l.source_id = ?
1445
+ ORDER BY l.weight DESC
1446
+ LIMIT 5`
1447
+ ).all(agentId, result.memory.id);
1448
+ for (const link of links) {
1449
+ if (existingIds.has(link.target_id)) continue;
1450
+ existingIds.add(link.target_id);
1451
+ const relatedMemory = {
1452
+ id: link.id,
1453
+ content: link.content,
1454
+ type: link.type,
1455
+ priority: link.priority,
1456
+ emotion_val: link.emotion_val,
1457
+ vitality: link.vitality,
1458
+ stability: link.stability,
1459
+ access_count: link.access_count,
1460
+ last_accessed: link.last_accessed,
1461
+ created_at: link.created_at,
1462
+ updated_at: link.updated_at,
1463
+ source: link.source,
1464
+ agent_id: link.agent_id,
1465
+ hash: link.hash,
1466
+ emotion_tag: link.emotion_tag,
1467
+ source_session: link.source_session ?? null,
1468
+ source_context: link.source_context ?? null,
1469
+ observed_at: link.observed_at ?? null
1470
+ };
1471
+ related.push({
1472
+ memory: relatedMemory,
1473
+ score: result.score * link.weight * 0.6,
1474
+ related_source_id: result.memory.id,
1475
+ match_type: "related"
1476
+ });
1477
+ }
1478
+ }
1479
+ const directResults = results.map((r) => ({
1480
+ ...r,
1481
+ match_type: "direct"
1482
+ }));
1483
+ const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
1484
+ return combined;
1485
+ }
1353
1486
  async function recallMemories(db, query, opts) {
1354
1487
  const limit = opts?.limit ?? 10;
1355
1488
  const agentId = opts?.agent_id ?? "default";
@@ -1357,10 +1490,15 @@ async function recallMemories(db, query, opts) {
1357
1490
  const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
1358
1491
  const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
1359
1492
  const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1493
+ const recencyBoost = opts?.recency_boost;
1494
+ const after = opts?.after;
1495
+ const before = opts?.before;
1360
1496
  const lexical = searchBM25(db, query, {
1361
1497
  agent_id: agentId,
1362
1498
  limit: lexicalLimit,
1363
- min_vitality: minVitality
1499
+ min_vitality: minVitality,
1500
+ after,
1501
+ before
1364
1502
  });
1365
1503
  let vector = [];
1366
1504
  if (provider) {
@@ -1369,17 +1507,25 @@ async function recallMemories(db, query, opts) {
1369
1507
  provider,
1370
1508
  agent_id: agentId,
1371
1509
  limit: vectorLimit,
1372
- min_vitality: minVitality
1510
+ min_vitality: minVitality,
1511
+ after,
1512
+ before
1373
1513
  });
1374
1514
  } catch {
1375
1515
  vector = [];
1376
1516
  }
1377
1517
  }
1378
1518
  const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
1379
- 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
+ }
1380
1524
  if (opts?.recordAccess !== false) {
1381
1525
  for (const row of results) {
1382
- recordAccess(db, row.memory.id);
1526
+ if (row.match_type !== "related") {
1527
+ recordAccess(db, row.memory.id);
1528
+ }
1383
1529
  }
1384
1530
  }
1385
1531
  return {
@@ -1604,7 +1750,13 @@ function uriScopeMatch(inputUri, candidateUri) {
1604
1750
  }
1605
1751
  return 0.2;
1606
1752
  }
1607
- function extractObservedAt(parts, fallback) {
1753
+ function extractObservedAt(parts, fallback, observedAt) {
1754
+ if (observedAt) {
1755
+ const parsed = new Date(observedAt);
1756
+ if (!Number.isNaN(parsed.getTime())) {
1757
+ return parsed;
1758
+ }
1759
+ }
1608
1760
  for (const part of parts) {
1609
1761
  if (!part) continue;
1610
1762
  const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
@@ -1627,8 +1779,16 @@ function timeProximity(input, memory, candidateUri) {
1627
1779
  if (input.type !== "event") {
1628
1780
  return 1;
1629
1781
  }
1630
- const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
1631
- 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
+ );
1632
1792
  if (!inputTime || !existingTime) {
1633
1793
  return 0.5;
1634
1794
  }
@@ -1712,6 +1872,65 @@ function fourCriterionGate(input) {
1712
1872
  failedCriteria: failed
1713
1873
  };
1714
1874
  }
1875
+ var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
1876
+ var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
1877
+ var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
1878
+ function extractNumbers(text) {
1879
+ return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
1880
+ }
1881
+ function detectConflict(inputContent, candidateContent, candidateId) {
1882
+ let conflictScore = 0;
1883
+ let conflictType = "negation";
1884
+ const details = [];
1885
+ const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
1886
+ const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
1887
+ if (inputHasNegation !== candidateHasNegation) {
1888
+ conflictScore += 0.4;
1889
+ conflictType = "negation";
1890
+ details.push("negation mismatch");
1891
+ }
1892
+ const inputNumbers = extractNumbers(inputContent);
1893
+ const candidateNumbers = extractNumbers(candidateContent);
1894
+ if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
1895
+ const inputSet = new Set(inputNumbers);
1896
+ const candidateSet = new Set(candidateNumbers);
1897
+ const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
1898
+ const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
1899
+ if (hasDiff && !hasCommon) {
1900
+ conflictScore += 0.3;
1901
+ conflictType = "value";
1902
+ details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
1903
+ }
1904
+ }
1905
+ const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
1906
+ const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
1907
+ const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
1908
+ const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
1909
+ if (inputDone && candidateActive || inputActive && candidateDone) {
1910
+ conflictScore += 0.3;
1911
+ conflictType = "status";
1912
+ details.push("status contradiction (done vs active)");
1913
+ }
1914
+ if (conflictScore <= 0.5) return null;
1915
+ return {
1916
+ memoryId: candidateId,
1917
+ content: candidateContent,
1918
+ conflict_score: Math.min(1, conflictScore),
1919
+ conflict_type: conflictType,
1920
+ detail: details.join("; ")
1921
+ };
1922
+ }
1923
+ function detectConflicts(inputContent, candidates) {
1924
+ const conflicts = [];
1925
+ for (const candidate of candidates) {
1926
+ if (candidate.score.dedup_score < 0.6) continue;
1927
+ const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
1928
+ if (conflict) {
1929
+ conflicts.push(conflict);
1930
+ }
1931
+ }
1932
+ return conflicts;
1933
+ }
1715
1934
  async function guard(db, input) {
1716
1935
  const hash = contentHash(input.content);
1717
1936
  const agentId = input.agent_id ?? "default";
@@ -1738,18 +1957,37 @@ async function guard(db, input) {
1738
1957
  return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
1739
1958
  }
1740
1959
  const candidates = await recallCandidates(db, input, agentId);
1960
+ const candidatesList = candidates.map((c) => ({
1961
+ memoryId: c.result.memory.id,
1962
+ dedup_score: c.score.dedup_score
1963
+ }));
1964
+ const conflicts = detectConflicts(input.content, candidates);
1741
1965
  const best = candidates[0];
1742
1966
  if (!best) {
1743
- return { action: "add", reason: "No relevant semantic candidates found" };
1967
+ return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
1744
1968
  }
1745
1969
  const score = best.score;
1746
1970
  if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
1971
+ const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
1972
+ if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
1973
+ return {
1974
+ action: "update",
1975
+ reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
1976
+ existingId: best.result.memory.id,
1977
+ updatedContent: input.content,
1978
+ score,
1979
+ candidates: candidatesList,
1980
+ conflicts
1981
+ };
1982
+ }
1747
1983
  const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
1748
1984
  return {
1749
1985
  action: shouldUpdateMetadata ? "update" : "skip",
1750
1986
  reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
1751
1987
  existingId: best.result.memory.id,
1752
- score
1988
+ score,
1989
+ candidates: candidatesList,
1990
+ conflicts: conflicts.length > 0 ? conflicts : void 0
1753
1991
  };
1754
1992
  }
1755
1993
  if (score.dedup_score >= MERGE_THRESHOLD) {
@@ -1767,13 +2005,17 @@ async function guard(db, input) {
1767
2005
  existingId: best.result.memory.id,
1768
2006
  mergedContent: mergePlan.content,
1769
2007
  mergePlan,
1770
- score
2008
+ score,
2009
+ candidates: candidatesList,
2010
+ conflicts: conflicts.length > 0 ? conflicts : void 0
1771
2011
  };
1772
2012
  }
1773
2013
  return {
1774
2014
  action: "add",
1775
2015
  reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
1776
- score
2016
+ score,
2017
+ candidates: candidatesList,
2018
+ conflicts: conflicts.length > 0 ? conflicts : void 0
1777
2019
  };
1778
2020
  }
1779
2021
 
@@ -1786,6 +2028,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
1786
2028
  } catch {
1787
2029
  }
1788
2030
  }
2031
+ function createAutoLinks(db, memoryId, candidates, agentId) {
2032
+ if (!candidates || candidates.length === 0) return;
2033
+ const linkCandidates = candidates.filter((c) => c.memoryId !== memoryId && c.dedup_score >= 0.45 && c.dedup_score < 0.82).sort((a, b) => b.dedup_score - a.dedup_score).slice(0, 5);
2034
+ if (linkCandidates.length === 0) return;
2035
+ const timestamp = now();
2036
+ const insert = db.prepare(
2037
+ `INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
2038
+ VALUES (?, ?, ?, 'related', ?, ?)`
2039
+ );
2040
+ for (const candidate of linkCandidates) {
2041
+ insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
2042
+ insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
2043
+ }
2044
+ }
1789
2045
  async function syncOne(db, input) {
1790
2046
  const memInput = {
1791
2047
  content: input.content,
@@ -1797,17 +2053,22 @@ async function syncOne(db, input) {
1797
2053
  uri: input.uri,
1798
2054
  provider: input.provider,
1799
2055
  conservative: input.conservative,
1800
- emotion_tag: input.emotion_tag
2056
+ emotion_tag: input.emotion_tag,
2057
+ source_session: input.source_session,
2058
+ source_context: input.source_context,
2059
+ observed_at: input.observed_at
1801
2060
  };
1802
2061
  const guardResult = await guard(db, memInput);
2062
+ const agentId = input.agent_id ?? "default";
1803
2063
  switch (guardResult.action) {
1804
2064
  case "skip":
1805
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
2065
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
1806
2066
  case "add": {
1807
2067
  const mem = createMemory(db, memInput);
1808
2068
  if (!mem) return { action: "skipped", reason: "createMemory returned null" };
1809
2069
  ensureUriPath(db, mem.id, input.uri, input.agent_id);
1810
- 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 };
1811
2072
  }
1812
2073
  case "update": {
1813
2074
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
@@ -1815,7 +2076,7 @@ async function syncOne(db, input) {
1815
2076
  updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
1816
2077
  }
1817
2078
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1818
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
2079
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
1819
2080
  }
1820
2081
  case "merge": {
1821
2082
  if (!guardResult.existingId || !guardResult.mergedContent) {
@@ -1823,7 +2084,8 @@ async function syncOne(db, input) {
1823
2084
  }
1824
2085
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1825
2086
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1826
- 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 };
1827
2089
  }
1828
2090
  }
1829
2091
  }
@@ -1847,27 +2109,13 @@ async function rememberMemory(db, input) {
1847
2109
  agent_id: input.agent_id,
1848
2110
  provider: input.provider,
1849
2111
  conservative: input.conservative,
1850
- emotion_tag: input.emotion_tag
2112
+ emotion_tag: input.emotion_tag,
2113
+ source_session: input.source_session,
2114
+ source_context: input.source_context,
2115
+ observed_at: input.observed_at
1851
2116
  });
1852
2117
  }
1853
2118
 
1854
- // src/app/recall.ts
1855
- async function recallMemory(db, input) {
1856
- const result = await recallMemories(db, input.query, {
1857
- agent_id: input.agent_id,
1858
- limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
1859
- min_vitality: input.min_vitality,
1860
- lexicalLimit: input.lexicalLimit,
1861
- vectorLimit: input.vectorLimit,
1862
- provider: input.provider,
1863
- recordAccess: input.recordAccess
1864
- });
1865
- if (input.emotion_tag) {
1866
- result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
1867
- }
1868
- return result;
1869
- }
1870
-
1871
2119
  // src/app/feedback.ts
1872
2120
  function clamp012(value) {
1873
2121
  if (!Number.isFinite(value)) return 0;
@@ -1947,6 +2195,70 @@ function getFeedbackSummary(db, memoryId, agentId) {
1947
2195
  function getFeedbackScore(db, memoryId, agentId) {
1948
2196
  return getFeedbackSummary(db, memoryId, agentId).score;
1949
2197
  }
2198
+ function recordPassiveFeedback(db, memoryIds, agentId) {
2199
+ if (memoryIds.length === 0) return 0;
2200
+ const effectiveAgentId = agentId ?? "default";
2201
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
2202
+ const placeholders = memoryIds.map(() => "?").join(",");
2203
+ const recentCounts = /* @__PURE__ */ new Map();
2204
+ try {
2205
+ const rows = db.prepare(
2206
+ `SELECT memory_id, COUNT(*) as c
2207
+ FROM feedback_events
2208
+ WHERE memory_id IN (${placeholders})
2209
+ AND source = 'passive'
2210
+ AND created_at > ?
2211
+ GROUP BY memory_id`
2212
+ ).all(...memoryIds, cutoff);
2213
+ for (const row of rows) {
2214
+ recentCounts.set(row.memory_id, row.c);
2215
+ }
2216
+ } catch {
2217
+ }
2218
+ let recorded = 0;
2219
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2220
+ const insert = db.prepare(
2221
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
2222
+ VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
2223
+ );
2224
+ for (const memoryId of memoryIds) {
2225
+ const count = recentCounts.get(memoryId) ?? 0;
2226
+ if (count >= 3) continue;
2227
+ try {
2228
+ insert.run(newId(), memoryId, effectiveAgentId, timestamp);
2229
+ recorded++;
2230
+ } catch {
2231
+ }
2232
+ }
2233
+ return recorded;
2234
+ }
2235
+
2236
+ // src/app/recall.ts
2237
+ async function recallMemory(db, input) {
2238
+ const result = await recallMemories(db, input.query, {
2239
+ agent_id: input.agent_id,
2240
+ limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
2241
+ min_vitality: input.min_vitality,
2242
+ lexicalLimit: input.lexicalLimit,
2243
+ vectorLimit: input.vectorLimit,
2244
+ provider: input.provider,
2245
+ recordAccess: input.recordAccess,
2246
+ related: input.related,
2247
+ after: input.after,
2248
+ before: input.before,
2249
+ recency_boost: input.recency_boost
2250
+ });
2251
+ if (input.emotion_tag) {
2252
+ result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
2253
+ }
2254
+ if (input.recordAccess !== false) {
2255
+ const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
2256
+ if (top3DirectIds.length > 0) {
2257
+ recordPassiveFeedback(db, top3DirectIds, input.agent_id);
2258
+ }
2259
+ }
2260
+ return result;
2261
+ }
1950
2262
 
1951
2263
  // src/app/surface.ts
1952
2264
  var INTENT_PRIORS = {
@@ -2088,7 +2400,9 @@ async function surfaceMemories(db, input) {
2088
2400
  searchBM25(db, trimmedQuery, {
2089
2401
  agent_id: agentId,
2090
2402
  limit: lexicalWindow,
2091
- min_vitality: minVitality
2403
+ min_vitality: minVitality,
2404
+ after: input.after,
2405
+ before: input.before
2092
2406
  }),
2093
2407
  "queryRank"
2094
2408
  );
@@ -2099,7 +2413,9 @@ async function surfaceMemories(db, input) {
2099
2413
  searchBM25(db, trimmedTask, {
2100
2414
  agent_id: agentId,
2101
2415
  limit: lexicalWindow,
2102
- min_vitality: minVitality
2416
+ min_vitality: minVitality,
2417
+ after: input.after,
2418
+ before: input.before
2103
2419
  }),
2104
2420
  "taskRank"
2105
2421
  );
@@ -2110,7 +2426,9 @@ async function surfaceMemories(db, input) {
2110
2426
  searchBM25(db, recentTurns.join(" "), {
2111
2427
  agent_id: agentId,
2112
2428
  limit: lexicalWindow,
2113
- min_vitality: minVitality
2429
+ min_vitality: minVitality,
2430
+ after: input.after,
2431
+ before: input.before
2114
2432
  }),
2115
2433
  "recentRank"
2116
2434
  );
@@ -2124,7 +2442,9 @@ async function surfaceMemories(db, input) {
2124
2442
  providerId: provider.id,
2125
2443
  agent_id: agentId,
2126
2444
  limit: lexicalWindow,
2127
- min_vitality: minVitality
2445
+ min_vitality: minVitality,
2446
+ after: input.after,
2447
+ before: input.before
2128
2448
  });
2129
2449
  const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
2130
2450
  collectBranch(signals, vectorRows, "semanticRank", similarity);
@@ -2260,19 +2580,73 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
2260
2580
  }
2261
2581
 
2262
2582
  // src/sleep/tidy.ts
2583
+ var EVENT_STALE_PATTERNS = [
2584
+ { pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
2585
+ { pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
2586
+ { pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
2587
+ ];
2588
+ var KNOWLEDGE_STALE_PATTERNS = [
2589
+ { pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
2590
+ { pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
2591
+ ];
2592
+ function isStaleContent(content, type) {
2593
+ if (type === "identity" || type === "emotion") {
2594
+ return { stale: false, reason: "type excluded", decay_factor: 1 };
2595
+ }
2596
+ const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
2597
+ for (const { pattern, type: staleType, decay } of patterns) {
2598
+ if (pattern.test(content)) {
2599
+ return { stale: true, reason: staleType, decay_factor: decay };
2600
+ }
2601
+ }
2602
+ return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
2603
+ }
2604
+ function getAgeThresholdDays(staleType) {
2605
+ const thresholds = {
2606
+ in_progress: 7,
2607
+ pending: 14,
2608
+ ephemeral: 3
2609
+ };
2610
+ return thresholds[staleType] ?? 7;
2611
+ }
2263
2612
  function runTidy(db, opts) {
2264
2613
  const threshold = opts?.vitalityThreshold ?? 0.05;
2265
2614
  const agentId = opts?.agent_id;
2266
2615
  let archived = 0;
2616
+ let staleDecayed = 0;
2267
2617
  const transaction = db.transaction(() => {
2268
2618
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
2269
2619
  for (const mem of decayed) {
2270
2620
  deleteMemory(db, mem.id);
2271
2621
  archived += 1;
2272
2622
  }
2623
+ const currentMs = Date.now();
2624
+ const currentTime = now();
2625
+ const agentCondition = agentId ? "AND agent_id = ?" : "";
2626
+ const agentParams = agentId ? [agentId] : [];
2627
+ const candidates = db.prepare(
2628
+ `SELECT id, content, type, created_at, updated_at, vitality
2629
+ FROM memories
2630
+ WHERE priority >= 2 AND vitality >= ?
2631
+ ${agentCondition}`
2632
+ ).all(threshold, ...agentParams);
2633
+ const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
2634
+ for (const mem of candidates) {
2635
+ const detection = isStaleContent(mem.content, mem.type);
2636
+ if (!detection.stale) continue;
2637
+ const createdMs = new Date(mem.created_at).getTime();
2638
+ const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
2639
+ const thresholdDays = getAgeThresholdDays(detection.reason);
2640
+ if (ageDays < thresholdDays) continue;
2641
+ const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
2642
+ if (Math.abs(newVitality - mem.vitality) > 1e-3) {
2643
+ updateStmt.run(newVitality, currentTime, mem.id);
2644
+ staleDecayed += 1;
2645
+ }
2646
+ }
2273
2647
  });
2274
2648
  transaction();
2275
- return { archived, orphansCleaned: 0 };
2649
+ return { archived, orphansCleaned: 0, staleDecayed };
2276
2650
  }
2277
2651
 
2278
2652
  // src/sleep/govern.ts
@@ -3600,6 +3974,7 @@ export {
3600
3974
  healthcheckEmbeddingProvider,
3601
3975
  ingestText,
3602
3976
  isCountRow,
3977
+ isStaleContent,
3603
3978
  listMemories,
3604
3979
  listPendingEmbeddings,
3605
3980
  loadWarmBootLayers,
@@ -3615,6 +3990,7 @@ export {
3615
3990
  recallMemory,
3616
3991
  recordAccess,
3617
3992
  recordFeedbackEvent,
3993
+ recordPassiveFeedback,
3618
3994
  reflectMemories,
3619
3995
  reindexEmbeddings,
3620
3996
  reindexMemories,