@smyslenny/agent-memory 4.3.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,6 +94,11 @@ function migrateDatabase(db, from, to) {
94
94
  v = 6;
95
95
  continue;
96
96
  }
97
+ if (v === 6) {
98
+ migrateV6ToV7(db);
99
+ v = 7;
100
+ continue;
101
+ }
97
102
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
98
103
  }
99
104
  }
@@ -180,6 +185,8 @@ function inferSchemaVersion(db) {
180
185
  const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
181
186
  const hasFeedbackEvents = tableExists(db, "feedback_events");
182
187
  const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
188
+ const hasProvenance = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
189
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag && hasProvenance) return 7;
183
190
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
184
191
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
185
192
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
@@ -210,6 +217,10 @@ function ensureIndexes(db) {
210
217
  if (tableHasColumn(db, "memories", "emotion_tag")) {
211
218
  db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
212
219
  }
220
+ if (tableHasColumn(db, "memories", "observed_at")) {
221
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
222
+ }
223
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
213
224
  if (tableExists(db, "feedback_events")) {
214
225
  db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
215
226
  if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
@@ -362,11 +373,40 @@ function migrateV5ToV6(db) {
362
373
  throw e;
363
374
  }
364
375
  }
376
+ function migrateV6ToV7(db) {
377
+ const alreadyMigrated = tableHasColumn(db, "memories", "source_session") && tableHasColumn(db, "memories", "source_context") && tableHasColumn(db, "memories", "observed_at");
378
+ if (alreadyMigrated) {
379
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
380
+ return;
381
+ }
382
+ try {
383
+ db.exec("BEGIN");
384
+ if (!tableHasColumn(db, "memories", "source_session")) {
385
+ db.exec("ALTER TABLE memories ADD COLUMN source_session TEXT;");
386
+ }
387
+ if (!tableHasColumn(db, "memories", "source_context")) {
388
+ db.exec("ALTER TABLE memories ADD COLUMN source_context TEXT;");
389
+ }
390
+ if (!tableHasColumn(db, "memories", "observed_at")) {
391
+ db.exec("ALTER TABLE memories ADD COLUMN observed_at TEXT;");
392
+ }
393
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_observed_at ON memories(observed_at) WHERE observed_at IS NOT NULL;");
394
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);");
395
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(7));
396
+ db.exec("COMMIT");
397
+ } catch (e) {
398
+ try {
399
+ db.exec("ROLLBACK");
400
+ } catch {
401
+ }
402
+ throw e;
403
+ }
404
+ }
365
405
  var SCHEMA_VERSION, SCHEMA_SQL;
366
406
  var init_db = __esm({
367
407
  "src/core/db.ts"() {
368
408
  "use strict";
369
- SCHEMA_VERSION = 6;
409
+ SCHEMA_VERSION = 7;
370
410
  SCHEMA_SQL = `
371
411
  -- Memory entries
372
412
  CREATE TABLE IF NOT EXISTS memories (
@@ -385,6 +425,9 @@ CREATE TABLE IF NOT EXISTS memories (
385
425
  agent_id TEXT NOT NULL DEFAULT 'default',
386
426
  hash TEXT,
387
427
  emotion_tag TEXT,
428
+ source_session TEXT,
429
+ source_context TEXT,
430
+ observed_at TEXT,
388
431
  UNIQUE(hash, agent_id)
389
432
  );
390
433
 
@@ -931,6 +974,23 @@ function searchByVector(db, queryVector, opts) {
931
974
  const limit = opts.limit ?? 20;
932
975
  const agentId = opts.agent_id ?? "default";
933
976
  const minVitality = opts.min_vitality ?? 0;
977
+ const conditions = [
978
+ "e.provider_id = ?",
979
+ "e.status = 'ready'",
980
+ "e.vector IS NOT NULL",
981
+ "e.content_hash = m.hash",
982
+ "m.agent_id = ?",
983
+ "m.vitality >= ?"
984
+ ];
985
+ const params = [opts.providerId, agentId, minVitality];
986
+ if (opts.after) {
987
+ conditions.push("m.updated_at >= ?");
988
+ params.push(opts.after);
989
+ }
990
+ if (opts.before) {
991
+ conditions.push("m.updated_at <= ?");
992
+ params.push(opts.before);
993
+ }
934
994
  const rows = db.prepare(
935
995
  `SELECT e.provider_id, e.vector, e.content_hash,
936
996
  m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
@@ -938,13 +998,8 @@ function searchByVector(db, queryVector, opts) {
938
998
  m.updated_at, m.source, m.agent_id, m.hash
939
999
  FROM embeddings e
940
1000
  JOIN memories m ON m.id = e.memory_id
941
- WHERE e.provider_id = ?
942
- AND e.status = 'ready'
943
- AND e.vector IS NOT NULL
944
- AND e.content_hash = m.hash
945
- AND m.agent_id = ?
946
- AND m.vitality >= ?`
947
- ).all(opts.providerId, agentId, minVitality);
1001
+ WHERE ${conditions.join(" AND ")}`
1002
+ ).all(...params);
948
1003
  const scored = rows.map((row) => ({
949
1004
  provider_id: row.provider_id,
950
1005
  memory: {
@@ -1019,10 +1074,12 @@ function createMemory(db, input) {
1019
1074
  }
1020
1075
  const id = newId();
1021
1076
  const timestamp = now();
1077
+ const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
1022
1078
  db.prepare(
1023
1079
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
1024
- access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
1025
- VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
1080
+ access_count, created_at, updated_at, source, agent_id, hash, emotion_tag,
1081
+ source_session, source_context, observed_at)
1082
+ VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1026
1083
  ).run(
1027
1084
  id,
1028
1085
  input.content,
@@ -1035,7 +1092,10 @@ function createMemory(db, input) {
1035
1092
  input.source ?? null,
1036
1093
  agentId,
1037
1094
  hash2,
1038
- input.emotion_tag ?? null
1095
+ input.emotion_tag ?? null,
1096
+ input.source_session ?? null,
1097
+ sourceContext,
1098
+ input.observed_at ?? null
1039
1099
  );
1040
1100
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
1041
1101
  markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -15129,16 +15189,25 @@ function searchBM25(db, query, opts) {
15129
15189
  const ftsQuery = buildFtsQuery(query);
15130
15190
  if (!ftsQuery) return [];
15131
15191
  try {
15192
+ const conditions = ["memories_fts MATCH ?", "m.agent_id = ?", "m.vitality >= ?"];
15193
+ const params = [ftsQuery, agentId, minVitality];
15194
+ if (opts?.after) {
15195
+ conditions.push("m.updated_at >= ?");
15196
+ params.push(opts.after);
15197
+ }
15198
+ if (opts?.before) {
15199
+ conditions.push("m.updated_at <= ?");
15200
+ params.push(opts.before);
15201
+ }
15202
+ params.push(limit);
15132
15203
  const rows = db.prepare(
15133
15204
  `SELECT m.*, rank AS score
15134
15205
  FROM memories_fts f
15135
15206
  JOIN memories m ON m.id = f.id
15136
- WHERE memories_fts MATCH ?
15137
- AND m.agent_id = ?
15138
- AND m.vitality >= ?
15207
+ WHERE ${conditions.join(" AND ")}
15139
15208
  ORDER BY rank
15140
15209
  LIMIT ?`
15141
- ).all(ftsQuery, agentId, minVitality, limit);
15210
+ ).all(...params);
15142
15211
  return rows.map((row, index) => {
15143
15212
  const { score: _score, ...memoryFields } = row;
15144
15213
  return {
@@ -15224,16 +15293,23 @@ function priorityPrior(priority) {
15224
15293
  function fusionScore(input) {
15225
15294
  const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
15226
15295
  const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
15227
- return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
15228
- }
15229
- function fuseHybridResults(lexical, vector, limit) {
15296
+ const baseScore = lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
15297
+ const boost = input.recency_boost ?? 0;
15298
+ if (boost <= 0) return baseScore;
15299
+ const updatedAt = new Date(input.memory.updated_at).getTime();
15300
+ const daysSince = Math.max(0, (Date.now() - updatedAt) / (1e3 * 60 * 60 * 24));
15301
+ const recencyScore = Math.exp(-daysSince / 30);
15302
+ return (1 - boost) * baseScore + boost * recencyScore;
15303
+ }
15304
+ function fuseHybridResults(lexical, vector, limit, recency_boost) {
15230
15305
  const candidates = /* @__PURE__ */ new Map();
15231
15306
  for (const row of lexical) {
15232
15307
  candidates.set(row.memory.id, {
15233
15308
  memory: row.memory,
15234
15309
  score: 0,
15235
15310
  bm25_rank: row.rank,
15236
- bm25_score: row.score
15311
+ bm25_score: row.score,
15312
+ match_type: "direct"
15237
15313
  });
15238
15314
  }
15239
15315
  for (const row of vector) {
@@ -15246,13 +15322,14 @@ function fuseHybridResults(lexical, vector, limit) {
15246
15322
  memory: row.memory,
15247
15323
  score: 0,
15248
15324
  vector_rank: row.rank,
15249
- vector_score: row.similarity
15325
+ vector_score: row.similarity,
15326
+ match_type: "direct"
15250
15327
  });
15251
15328
  }
15252
15329
  }
15253
15330
  return [...candidates.values()].map((row) => ({
15254
15331
  ...row,
15255
- score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
15332
+ score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank, recency_boost })
15256
15333
  })).sort((left, right) => {
15257
15334
  if (right.score !== left.score) return right.score - left.score;
15258
15335
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
@@ -15265,9 +15342,61 @@ async function searchVectorBranch(db, query, opts) {
15265
15342
  providerId: opts.provider.id,
15266
15343
  agent_id: opts.agent_id,
15267
15344
  limit: opts.limit,
15268
- min_vitality: opts.min_vitality
15345
+ min_vitality: opts.min_vitality,
15346
+ after: opts.after,
15347
+ before: opts.before
15269
15348
  });
15270
15349
  }
15350
+ function expandRelated(db, results, agentId, maxTotal) {
15351
+ const existingIds = new Set(results.map((r) => r.memory.id));
15352
+ const related = [];
15353
+ for (const result of results) {
15354
+ const links = db.prepare(
15355
+ `SELECT l.target_id, l.weight, m.*
15356
+ FROM links l
15357
+ JOIN memories m ON m.id = l.target_id
15358
+ WHERE l.agent_id = ? AND l.source_id = ?
15359
+ ORDER BY l.weight DESC
15360
+ LIMIT 5`
15361
+ ).all(agentId, result.memory.id);
15362
+ for (const link of links) {
15363
+ if (existingIds.has(link.target_id)) continue;
15364
+ existingIds.add(link.target_id);
15365
+ const relatedMemory = {
15366
+ id: link.id,
15367
+ content: link.content,
15368
+ type: link.type,
15369
+ priority: link.priority,
15370
+ emotion_val: link.emotion_val,
15371
+ vitality: link.vitality,
15372
+ stability: link.stability,
15373
+ access_count: link.access_count,
15374
+ last_accessed: link.last_accessed,
15375
+ created_at: link.created_at,
15376
+ updated_at: link.updated_at,
15377
+ source: link.source,
15378
+ agent_id: link.agent_id,
15379
+ hash: link.hash,
15380
+ emotion_tag: link.emotion_tag,
15381
+ source_session: link.source_session ?? null,
15382
+ source_context: link.source_context ?? null,
15383
+ observed_at: link.observed_at ?? null
15384
+ };
15385
+ related.push({
15386
+ memory: relatedMemory,
15387
+ score: result.score * link.weight * 0.6,
15388
+ related_source_id: result.memory.id,
15389
+ match_type: "related"
15390
+ });
15391
+ }
15392
+ }
15393
+ const directResults = results.map((r) => ({
15394
+ ...r,
15395
+ match_type: "direct"
15396
+ }));
15397
+ const combined = [...directResults, ...related].sort((a, b) => b.score - a.score).slice(0, maxTotal);
15398
+ return combined;
15399
+ }
15271
15400
  async function recallMemories(db, query, opts) {
15272
15401
  const limit = opts?.limit ?? 10;
15273
15402
  const agentId = opts?.agent_id ?? "default";
@@ -15275,10 +15404,15 @@ async function recallMemories(db, query, opts) {
15275
15404
  const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
15276
15405
  const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
15277
15406
  const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
15407
+ const recencyBoost = opts?.recency_boost;
15408
+ const after = opts?.after;
15409
+ const before = opts?.before;
15278
15410
  const lexical = searchBM25(db, query, {
15279
15411
  agent_id: agentId,
15280
15412
  limit: lexicalLimit,
15281
- min_vitality: minVitality
15413
+ min_vitality: minVitality,
15414
+ after,
15415
+ before
15282
15416
  });
15283
15417
  let vector = [];
15284
15418
  if (provider) {
@@ -15287,17 +15421,25 @@ async function recallMemories(db, query, opts) {
15287
15421
  provider,
15288
15422
  agent_id: agentId,
15289
15423
  limit: vectorLimit,
15290
- min_vitality: minVitality
15424
+ min_vitality: minVitality,
15425
+ after,
15426
+ before
15291
15427
  });
15292
15428
  } catch {
15293
15429
  vector = [];
15294
15430
  }
15295
15431
  }
15296
15432
  const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
15297
- const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
15433
+ let results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit, recencyBoost);
15434
+ if (opts?.related) {
15435
+ const maxTotal = Math.floor(limit * 1.5);
15436
+ results = expandRelated(db, results, agentId, maxTotal);
15437
+ }
15298
15438
  if (opts?.recordAccess !== false) {
15299
15439
  for (const row of results) {
15300
- recordAccess(db, row.memory.id);
15440
+ if (row.match_type !== "related") {
15441
+ recordAccess(db, row.memory.id);
15442
+ }
15301
15443
  }
15302
15444
  }
15303
15445
  return {
@@ -15522,7 +15664,13 @@ function uriScopeMatch(inputUri, candidateUri) {
15522
15664
  }
15523
15665
  return 0.2;
15524
15666
  }
15525
- function extractObservedAt(parts, fallback) {
15667
+ function extractObservedAt(parts, fallback, observedAt) {
15668
+ if (observedAt) {
15669
+ const parsed = new Date(observedAt);
15670
+ if (!Number.isNaN(parsed.getTime())) {
15671
+ return parsed;
15672
+ }
15673
+ }
15526
15674
  for (const part of parts) {
15527
15675
  if (!part) continue;
15528
15676
  const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
@@ -15545,8 +15693,16 @@ function timeProximity(input, memory, candidateUri) {
15545
15693
  if (input.type !== "event") {
15546
15694
  return 1;
15547
15695
  }
15548
- const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
15549
- const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
15696
+ const inputTime = extractObservedAt(
15697
+ [input.uri, input.source, input.content],
15698
+ input.now ?? null,
15699
+ input.observed_at ?? null
15700
+ );
15701
+ const existingTime = extractObservedAt(
15702
+ [candidateUri, memory.source, memory.content],
15703
+ memory.created_at,
15704
+ memory.observed_at ?? null
15705
+ );
15550
15706
  if (!inputTime || !existingTime) {
15551
15707
  return 0.5;
15552
15708
  }
@@ -15630,6 +15786,65 @@ function fourCriterionGate(input) {
15630
15786
  failedCriteria: failed
15631
15787
  };
15632
15788
  }
15789
+ var NEGATION_PATTERNS = /\b(不|没|禁止|无法|取消|no|not|never|don't|doesn't|isn't|aren't|won't|can't|cannot|shouldn't)\b/i;
15790
+ var STATUS_DONE_PATTERNS = /\b(完成|已完成|已修复|已解决|已关闭|DONE|FIXED|RESOLVED|CLOSED|SHIPPED|CANCELLED|取消|放弃|已取消|已放弃|ABANDONED)\b/i;
15791
+ var STATUS_ACTIVE_PATTERNS = /\b(正在|进行中|待办|TODO|WIP|IN.?PROGRESS|PENDING|WORKING|处理中|部署中|开发中|DEPLOYING)\b/i;
15792
+ function extractNumbers(text) {
15793
+ return text.match(/\d+(?:\.\d+)+|\b\d{2,}\b/g) ?? [];
15794
+ }
15795
+ function detectConflict(inputContent, candidateContent, candidateId) {
15796
+ let conflictScore = 0;
15797
+ let conflictType = "negation";
15798
+ const details = [];
15799
+ const inputHasNegation = NEGATION_PATTERNS.test(inputContent);
15800
+ const candidateHasNegation = NEGATION_PATTERNS.test(candidateContent);
15801
+ if (inputHasNegation !== candidateHasNegation) {
15802
+ conflictScore += 0.4;
15803
+ conflictType = "negation";
15804
+ details.push("negation mismatch");
15805
+ }
15806
+ const inputNumbers = extractNumbers(inputContent);
15807
+ const candidateNumbers = extractNumbers(candidateContent);
15808
+ if (inputNumbers.length > 0 && candidateNumbers.length > 0) {
15809
+ const inputSet = new Set(inputNumbers);
15810
+ const candidateSet = new Set(candidateNumbers);
15811
+ const hasCommon = [...inputSet].some((n) => candidateSet.has(n));
15812
+ const hasDiff = [...inputSet].some((n) => !candidateSet.has(n)) || [...candidateSet].some((n) => !inputSet.has(n));
15813
+ if (hasDiff && !hasCommon) {
15814
+ conflictScore += 0.3;
15815
+ conflictType = "value";
15816
+ details.push(`value diff: [${inputNumbers.join(",")}] vs [${candidateNumbers.join(",")}]`);
15817
+ }
15818
+ }
15819
+ const inputDone = STATUS_DONE_PATTERNS.test(inputContent);
15820
+ const inputActive = STATUS_ACTIVE_PATTERNS.test(inputContent);
15821
+ const candidateDone = STATUS_DONE_PATTERNS.test(candidateContent);
15822
+ const candidateActive = STATUS_ACTIVE_PATTERNS.test(candidateContent);
15823
+ if (inputDone && candidateActive || inputActive && candidateDone) {
15824
+ conflictScore += 0.3;
15825
+ conflictType = "status";
15826
+ details.push("status contradiction (done vs active)");
15827
+ }
15828
+ if (conflictScore <= 0.5) return null;
15829
+ return {
15830
+ memoryId: candidateId,
15831
+ content: candidateContent,
15832
+ conflict_score: Math.min(1, conflictScore),
15833
+ conflict_type: conflictType,
15834
+ detail: details.join("; ")
15835
+ };
15836
+ }
15837
+ function detectConflicts(inputContent, candidates) {
15838
+ const conflicts = [];
15839
+ for (const candidate of candidates) {
15840
+ if (candidate.score.dedup_score < 0.6) continue;
15841
+ const conflict = detectConflict(inputContent, candidate.result.memory.content, candidate.result.memory.id);
15842
+ if (conflict) {
15843
+ conflicts.push(conflict);
15844
+ }
15845
+ }
15846
+ return conflicts;
15847
+ }
15633
15848
  async function guard(db, input) {
15634
15849
  const hash2 = contentHash(input.content);
15635
15850
  const agentId = input.agent_id ?? "default";
@@ -15656,18 +15871,37 @@ async function guard(db, input) {
15656
15871
  return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
15657
15872
  }
15658
15873
  const candidates = await recallCandidates(db, input, agentId);
15874
+ const candidatesList = candidates.map((c) => ({
15875
+ memoryId: c.result.memory.id,
15876
+ dedup_score: c.score.dedup_score
15877
+ }));
15878
+ const conflicts = detectConflicts(input.content, candidates);
15659
15879
  const best = candidates[0];
15660
15880
  if (!best) {
15661
- return { action: "add", reason: "No relevant semantic candidates found" };
15881
+ return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
15662
15882
  }
15663
15883
  const score = best.score;
15664
15884
  if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
15885
+ const bestConflict = conflicts.find((c) => c.memoryId === best.result.memory.id);
15886
+ if (bestConflict && (bestConflict.conflict_type === "status" || bestConflict.conflict_type === "value")) {
15887
+ return {
15888
+ action: "update",
15889
+ reason: `Conflict override: ${bestConflict.conflict_type} conflict detected despite high dedup score (${score.dedup_score.toFixed(3)})`,
15890
+ existingId: best.result.memory.id,
15891
+ updatedContent: input.content,
15892
+ score,
15893
+ candidates: candidatesList,
15894
+ conflicts
15895
+ };
15896
+ }
15665
15897
  const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
15666
15898
  return {
15667
15899
  action: shouldUpdateMetadata ? "update" : "skip",
15668
15900
  reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
15669
15901
  existingId: best.result.memory.id,
15670
- score
15902
+ score,
15903
+ candidates: candidatesList,
15904
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15671
15905
  };
15672
15906
  }
15673
15907
  if (score.dedup_score >= MERGE_THRESHOLD) {
@@ -15685,17 +15919,22 @@ async function guard(db, input) {
15685
15919
  existingId: best.result.memory.id,
15686
15920
  mergedContent: mergePlan.content,
15687
15921
  mergePlan,
15688
- score
15922
+ score,
15923
+ candidates: candidatesList,
15924
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15689
15925
  };
15690
15926
  }
15691
15927
  return {
15692
15928
  action: "add",
15693
15929
  reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
15694
- score
15930
+ score,
15931
+ candidates: candidatesList,
15932
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15695
15933
  };
15696
15934
  }
15697
15935
 
15698
15936
  // src/sleep/sync.ts
15937
+ init_db();
15699
15938
  function ensureUriPath(db, memoryId, uri, agentId) {
15700
15939
  if (!uri) return;
15701
15940
  if (getPathByUri(db, uri, agentId ?? "default")) return;
@@ -15704,6 +15943,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
15704
15943
  } catch {
15705
15944
  }
15706
15945
  }
15946
+ function createAutoLinks(db, memoryId, candidates, agentId) {
15947
+ if (!candidates || candidates.length === 0) return;
15948
+ 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);
15949
+ if (linkCandidates.length === 0) return;
15950
+ const timestamp = now();
15951
+ const insert = db.prepare(
15952
+ `INSERT OR IGNORE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
15953
+ VALUES (?, ?, ?, 'related', ?, ?)`
15954
+ );
15955
+ for (const candidate of linkCandidates) {
15956
+ insert.run(agentId, memoryId, candidate.memoryId, candidate.dedup_score, timestamp);
15957
+ insert.run(agentId, candidate.memoryId, memoryId, candidate.dedup_score, timestamp);
15958
+ }
15959
+ }
15707
15960
  async function syncOne(db, input) {
15708
15961
  const memInput = {
15709
15962
  content: input.content,
@@ -15715,17 +15968,22 @@ async function syncOne(db, input) {
15715
15968
  uri: input.uri,
15716
15969
  provider: input.provider,
15717
15970
  conservative: input.conservative,
15718
- emotion_tag: input.emotion_tag
15971
+ emotion_tag: input.emotion_tag,
15972
+ source_session: input.source_session,
15973
+ source_context: input.source_context,
15974
+ observed_at: input.observed_at
15719
15975
  };
15720
15976
  const guardResult = await guard(db, memInput);
15977
+ const agentId = input.agent_id ?? "default";
15721
15978
  switch (guardResult.action) {
15722
15979
  case "skip":
15723
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
15980
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
15724
15981
  case "add": {
15725
15982
  const mem = createMemory(db, memInput);
15726
15983
  if (!mem) return { action: "skipped", reason: "createMemory returned null" };
15727
15984
  ensureUriPath(db, mem.id, input.uri, input.agent_id);
15728
- return { action: "added", memoryId: mem.id, reason: guardResult.reason };
15985
+ createAutoLinks(db, mem.id, guardResult.candidates, agentId);
15986
+ return { action: "added", memoryId: mem.id, reason: guardResult.reason, conflicts: guardResult.conflicts };
15729
15987
  }
15730
15988
  case "update": {
15731
15989
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
@@ -15733,7 +15991,7 @@ async function syncOne(db, input) {
15733
15991
  updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
15734
15992
  }
15735
15993
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
15736
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
15994
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
15737
15995
  }
15738
15996
  case "merge": {
15739
15997
  if (!guardResult.existingId || !guardResult.mergedContent) {
@@ -15741,7 +15999,8 @@ async function syncOne(db, input) {
15741
15999
  }
15742
16000
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
15743
16001
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
15744
- return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
16002
+ createAutoLinks(db, guardResult.existingId, guardResult.candidates, agentId);
16003
+ return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
15745
16004
  }
15746
16005
  }
15747
16006
  }
@@ -15878,6 +16137,7 @@ function runAutoIngestWatcher(options) {
15878
16137
  const memoryMdPath = join(workspaceDir, "MEMORY.md");
15879
16138
  const debounceMs = options.debounceMs ?? 1200;
15880
16139
  const initialScan = options.initialScan ?? true;
16140
+ const ingestDaily = process.env.AGENT_MEMORY_AUTO_INGEST_DAILY === "1";
15881
16141
  const logger = options.logger ?? console;
15882
16142
  const timers = /* @__PURE__ */ new Map();
15883
16143
  const watchers = [];
@@ -15898,6 +16158,10 @@ function runAutoIngestWatcher(options) {
15898
16158
  const isTrackedMarkdownFile = (absPath) => {
15899
16159
  if (!absPath.endsWith(".md")) return false;
15900
16160
  if (resolve(absPath) === memoryMdPath) return true;
16161
+ if (!ingestDaily) {
16162
+ const basename = absPath.split("/").pop() ?? "";
16163
+ if (/^\d{4}-\d{2}-\d{2}\.md$/.test(basename)) return false;
16164
+ }
15901
16165
  const rel = relative(memoryDir, absPath).replace(/\\/g, "/");
15902
16166
  if (rel.startsWith("..") || rel === "") return false;
15903
16167
  return !rel.includes("/");
@@ -16022,33 +16286,13 @@ async function rememberMemory(db, input) {
16022
16286
  agent_id: input.agent_id,
16023
16287
  provider: input.provider,
16024
16288
  conservative: input.conservative,
16025
- emotion_tag: input.emotion_tag
16289
+ emotion_tag: input.emotion_tag,
16290
+ source_session: input.source_session,
16291
+ source_context: input.source_context,
16292
+ observed_at: input.observed_at
16026
16293
  });
16027
16294
  }
16028
16295
 
16029
- // src/app/recall.ts
16030
- async function recallMemory(db, input) {
16031
- const result = await recallMemories(db, input.query, {
16032
- agent_id: input.agent_id,
16033
- limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
16034
- min_vitality: input.min_vitality,
16035
- lexicalLimit: input.lexicalLimit,
16036
- vectorLimit: input.vectorLimit,
16037
- provider: input.provider,
16038
- recordAccess: input.recordAccess
16039
- });
16040
- if (input.emotion_tag) {
16041
- result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
16042
- }
16043
- return result;
16044
- }
16045
-
16046
- // src/app/surface.ts
16047
- init_memory();
16048
- init_providers();
16049
- init_tokenizer();
16050
- init_vector();
16051
-
16052
16296
  // src/app/feedback.ts
16053
16297
  init_db();
16054
16298
  function clamp012(value) {
@@ -16094,8 +16338,76 @@ function getFeedbackSummary(db, memoryId, agentId) {
16094
16338
  };
16095
16339
  }
16096
16340
  }
16341
+ function recordPassiveFeedback(db, memoryIds, agentId) {
16342
+ if (memoryIds.length === 0) return 0;
16343
+ const effectiveAgentId = agentId ?? "default";
16344
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
16345
+ const placeholders = memoryIds.map(() => "?").join(",");
16346
+ const recentCounts = /* @__PURE__ */ new Map();
16347
+ try {
16348
+ const rows = db.prepare(
16349
+ `SELECT memory_id, COUNT(*) as c
16350
+ FROM feedback_events
16351
+ WHERE memory_id IN (${placeholders})
16352
+ AND source = 'passive'
16353
+ AND created_at > ?
16354
+ GROUP BY memory_id`
16355
+ ).all(...memoryIds, cutoff);
16356
+ for (const row of rows) {
16357
+ recentCounts.set(row.memory_id, row.c);
16358
+ }
16359
+ } catch {
16360
+ }
16361
+ let recorded = 0;
16362
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
16363
+ const insert = db.prepare(
16364
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
16365
+ VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
16366
+ );
16367
+ for (const memoryId of memoryIds) {
16368
+ const count = recentCounts.get(memoryId) ?? 0;
16369
+ if (count >= 3) continue;
16370
+ try {
16371
+ insert.run(newId(), memoryId, effectiveAgentId, timestamp);
16372
+ recorded++;
16373
+ } catch {
16374
+ }
16375
+ }
16376
+ return recorded;
16377
+ }
16378
+
16379
+ // src/app/recall.ts
16380
+ async function recallMemory(db, input) {
16381
+ const result = await recallMemories(db, input.query, {
16382
+ agent_id: input.agent_id,
16383
+ limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
16384
+ min_vitality: input.min_vitality,
16385
+ lexicalLimit: input.lexicalLimit,
16386
+ vectorLimit: input.vectorLimit,
16387
+ provider: input.provider,
16388
+ recordAccess: input.recordAccess,
16389
+ related: input.related,
16390
+ after: input.after,
16391
+ before: input.before,
16392
+ recency_boost: input.recency_boost
16393
+ });
16394
+ if (input.emotion_tag) {
16395
+ result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
16396
+ }
16397
+ if (input.recordAccess !== false) {
16398
+ const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
16399
+ if (top3DirectIds.length > 0) {
16400
+ recordPassiveFeedback(db, top3DirectIds, input.agent_id);
16401
+ }
16402
+ }
16403
+ return result;
16404
+ }
16097
16405
 
16098
16406
  // src/app/surface.ts
16407
+ init_memory();
16408
+ init_providers();
16409
+ init_tokenizer();
16410
+ init_vector();
16099
16411
  var INTENT_PRIORS = {
16100
16412
  factual: {
16101
16413
  identity: 0.25,
@@ -16235,7 +16547,9 @@ async function surfaceMemories(db, input) {
16235
16547
  searchBM25(db, trimmedQuery, {
16236
16548
  agent_id: agentId,
16237
16549
  limit: lexicalWindow,
16238
- min_vitality: minVitality
16550
+ min_vitality: minVitality,
16551
+ after: input.after,
16552
+ before: input.before
16239
16553
  }),
16240
16554
  "queryRank"
16241
16555
  );
@@ -16246,7 +16560,9 @@ async function surfaceMemories(db, input) {
16246
16560
  searchBM25(db, trimmedTask, {
16247
16561
  agent_id: agentId,
16248
16562
  limit: lexicalWindow,
16249
- min_vitality: minVitality
16563
+ min_vitality: minVitality,
16564
+ after: input.after,
16565
+ before: input.before
16250
16566
  }),
16251
16567
  "taskRank"
16252
16568
  );
@@ -16257,7 +16573,9 @@ async function surfaceMemories(db, input) {
16257
16573
  searchBM25(db, recentTurns.join(" "), {
16258
16574
  agent_id: agentId,
16259
16575
  limit: lexicalWindow,
16260
- min_vitality: minVitality
16576
+ min_vitality: minVitality,
16577
+ after: input.after,
16578
+ before: input.before
16261
16579
  }),
16262
16580
  "recentRank"
16263
16581
  );
@@ -16271,7 +16589,9 @@ async function surfaceMemories(db, input) {
16271
16589
  providerId: provider.id,
16272
16590
  agent_id: agentId,
16273
16591
  limit: lexicalWindow,
16274
- min_vitality: minVitality
16592
+ min_vitality: minVitality,
16593
+ after: input.after,
16594
+ before: input.before
16275
16595
  });
16276
16596
  const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
16277
16597
  collectBranch(signals, vectorRows, "semanticRank", similarity);
@@ -16409,19 +16729,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
16409
16729
 
16410
16730
  // src/sleep/tidy.ts
16411
16731
  init_memory();
16732
+ init_db();
16733
+ var EVENT_STALE_PATTERNS = [
16734
+ { pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
16735
+ { pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
16736
+ { pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
16737
+ ];
16738
+ var KNOWLEDGE_STALE_PATTERNS = [
16739
+ { pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
16740
+ { pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
16741
+ ];
16742
+ function isStaleContent(content, type) {
16743
+ if (type === "identity" || type === "emotion") {
16744
+ return { stale: false, reason: "type excluded", decay_factor: 1 };
16745
+ }
16746
+ const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
16747
+ for (const { pattern, type: staleType, decay } of patterns) {
16748
+ if (pattern.test(content)) {
16749
+ return { stale: true, reason: staleType, decay_factor: decay };
16750
+ }
16751
+ }
16752
+ return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
16753
+ }
16754
+ function getAgeThresholdDays(staleType) {
16755
+ const thresholds = {
16756
+ in_progress: 7,
16757
+ pending: 14,
16758
+ ephemeral: 3
16759
+ };
16760
+ return thresholds[staleType] ?? 7;
16761
+ }
16412
16762
  function runTidy(db, opts) {
16413
16763
  const threshold = opts?.vitalityThreshold ?? 0.05;
16414
16764
  const agentId = opts?.agent_id;
16415
16765
  let archived = 0;
16766
+ let staleDecayed = 0;
16416
16767
  const transaction = db.transaction(() => {
16417
16768
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
16418
16769
  for (const mem of decayed) {
16419
16770
  deleteMemory(db, mem.id);
16420
16771
  archived += 1;
16421
16772
  }
16773
+ const currentMs = Date.now();
16774
+ const currentTime = now();
16775
+ const agentCondition = agentId ? "AND agent_id = ?" : "";
16776
+ const agentParams = agentId ? [agentId] : [];
16777
+ const candidates = db.prepare(
16778
+ `SELECT id, content, type, created_at, updated_at, vitality
16779
+ FROM memories
16780
+ WHERE priority >= 2 AND vitality >= ?
16781
+ ${agentCondition}`
16782
+ ).all(threshold, ...agentParams);
16783
+ const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
16784
+ for (const mem of candidates) {
16785
+ const detection = isStaleContent(mem.content, mem.type);
16786
+ if (!detection.stale) continue;
16787
+ const createdMs = new Date(mem.created_at).getTime();
16788
+ const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
16789
+ const thresholdDays = getAgeThresholdDays(detection.reason);
16790
+ if (ageDays < thresholdDays) continue;
16791
+ const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
16792
+ if (Math.abs(newVitality - mem.vitality) > 1e-3) {
16793
+ updateStmt.run(newVitality, currentTime, mem.id);
16794
+ staleDecayed += 1;
16795
+ }
16796
+ }
16422
16797
  });
16423
16798
  transaction();
16424
- return { archived, orphansCleaned: 0 };
16799
+ return { archived, orphansCleaned: 0, staleDecayed };
16425
16800
  }
16426
16801
 
16427
16802
  // src/sleep/govern.ts
@@ -16830,7 +17205,10 @@ function formatMemory(memory, score) {
16830
17205
  priority: memory.priority,
16831
17206
  vitality: memory.vitality,
16832
17207
  score,
16833
- updated_at: memory.updated_at
17208
+ updated_at: memory.updated_at,
17209
+ source_session: memory.source_session ?? void 0,
17210
+ source_context: memory.source_context ?? void 0,
17211
+ observed_at: memory.observed_at ?? void 0
16834
17212
  };
16835
17213
  }
16836
17214
  function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
@@ -16917,7 +17295,9 @@ function formatRecallPayload(result) {
16917
17295
  bm25_rank: row.bm25_rank,
16918
17296
  vector_rank: row.vector_rank,
16919
17297
  bm25_score: row.bm25_score,
16920
- vector_score: row.vector_score
17298
+ vector_score: row.vector_score,
17299
+ related_source_id: row.related_source_id,
17300
+ match_type: row.match_type
16921
17301
  }))
16922
17302
  };
16923
17303
  }
@@ -16940,7 +17320,10 @@ function formatSurfacePayload(result) {
16940
17320
  priority_prior: row.priority_prior,
16941
17321
  feedback_score: row.feedback_score,
16942
17322
  reason_codes: row.reason_codes,
16943
- updated_at: row.memory.updated_at
17323
+ updated_at: row.memory.updated_at,
17324
+ source_session: row.memory.source_session ?? void 0,
17325
+ source_context: row.memory.source_context ?? void 0,
17326
+ observed_at: row.memory.observed_at ?? void 0
16944
17327
  }))
16945
17328
  };
16946
17329
  }
@@ -16949,7 +17332,7 @@ function createMcpServer(dbPath, agentId) {
16949
17332
  const aid = agentId ?? AGENT_ID;
16950
17333
  const server = new McpServer({
16951
17334
  name: "agent-memory",
16952
- version: "4.0.0-alpha.1"
17335
+ version: "5.0.0"
16953
17336
  });
16954
17337
  server.tool(
16955
17338
  "remember",
@@ -16961,10 +17344,24 @@ function createMcpServer(dbPath, agentId) {
16961
17344
  emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
16962
17345
  source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
16963
17346
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16964
- emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)")
17347
+ emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)"),
17348
+ session_id: external_exports.string().optional().describe("Source session ID for provenance tracking"),
17349
+ context: external_exports.string().optional().describe("Trigger context for this memory (\u2264200 chars, auto-truncated)"),
17350
+ observed_at: external_exports.string().optional().describe("When the event actually happened (ISO 8601), distinct from write time")
16965
17351
  },
16966
- async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag }) => {
16967
- const result = await rememberMemory(db, { content, type, uri, emotion_val, source, agent_id: agent_id ?? aid, emotion_tag });
17352
+ async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag, session_id, context, observed_at }) => {
17353
+ const result = await rememberMemory(db, {
17354
+ content,
17355
+ type,
17356
+ uri,
17357
+ emotion_val,
17358
+ source,
17359
+ agent_id: agent_id ?? aid,
17360
+ emotion_tag,
17361
+ source_session: session_id,
17362
+ source_context: context,
17363
+ observed_at
17364
+ });
16968
17365
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16969
17366
  }
16970
17367
  );
@@ -16975,10 +17372,23 @@ function createMcpServer(dbPath, agentId) {
16975
17372
  query: external_exports.string().describe("Search query (natural language)"),
16976
17373
  limit: external_exports.number().default(10).describe("Max results to return"),
16977
17374
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16978
- emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)")
17375
+ emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)"),
17376
+ related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
17377
+ after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
17378
+ before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
17379
+ recency_boost: external_exports.number().min(0).max(1).default(0).optional().describe("Recency bias (0=none, 1=max). Higher values favor recently updated memories")
16979
17380
  },
16980
- async ({ query, limit, agent_id, emotion_tag }) => {
16981
- const result = await recallMemory(db, { query, limit, agent_id: agent_id ?? aid, emotion_tag });
17381
+ async ({ query, limit, agent_id, emotion_tag, related, after, before, recency_boost }) => {
17382
+ const result = await recallMemory(db, {
17383
+ query,
17384
+ limit,
17385
+ agent_id: agent_id ?? aid,
17386
+ emotion_tag,
17387
+ related: related ?? false,
17388
+ after,
17389
+ before,
17390
+ recency_boost: recency_boost ?? 0
17391
+ });
16982
17392
  return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
16983
17393
  }
16984
17394
  );
@@ -17168,9 +17578,13 @@ function createMcpServer(dbPath, agentId) {
17168
17578
  types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"]).describe("Optional type filter")).optional(),
17169
17579
  limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
17170
17580
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
17171
- keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted")
17581
+ keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted"),
17582
+ related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
17583
+ after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
17584
+ before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
17585
+ recency_boost: external_exports.number().min(0).max(1).default(0).optional().describe("Recency bias (0=none, 1=max)")
17172
17586
  },
17173
- async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords }) => {
17587
+ async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords, related, after, before, recency_boost }) => {
17174
17588
  const resolvedQuery = query ?? keywords?.join(" ");
17175
17589
  const result = await surfaceMemories(db, {
17176
17590
  query: resolvedQuery,
@@ -17179,7 +17593,11 @@ function createMcpServer(dbPath, agentId) {
17179
17593
  intent,
17180
17594
  types,
17181
17595
  limit: limit ?? 5,
17182
- agent_id: agent_id ?? aid
17596
+ agent_id: agent_id ?? aid,
17597
+ related: related ?? false,
17598
+ after,
17599
+ before,
17600
+ recency_boost: recency_boost ?? 0
17183
17601
  });
17184
17602
  return {
17185
17603
  content: [{
@@ -17189,6 +17607,43 @@ function createMcpServer(dbPath, agentId) {
17189
17607
  };
17190
17608
  }
17191
17609
  );
17610
+ server.tool(
17611
+ "link",
17612
+ "Manually create or remove an association between two memories.",
17613
+ {
17614
+ source_id: external_exports.string().describe("Source memory ID"),
17615
+ target_id: external_exports.string().describe("Target memory ID"),
17616
+ relation: external_exports.enum(["related", "supersedes", "contradicts"]).default("related").describe("Relation type"),
17617
+ weight: external_exports.number().min(0).max(1).default(1).optional().describe("Link weight (0-1)"),
17618
+ remove: external_exports.boolean().default(false).optional().describe("Remove the link instead of creating it"),
17619
+ agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
17620
+ },
17621
+ async ({ source_id, target_id, relation, weight, remove, agent_id }) => {
17622
+ const effectiveAgentId = agent_id ?? aid;
17623
+ if (remove) {
17624
+ const result = db.prepare(
17625
+ "DELETE FROM links WHERE agent_id = ? AND source_id = ? AND target_id = ?"
17626
+ ).run(effectiveAgentId, source_id, target_id);
17627
+ return {
17628
+ content: [{ type: "text", text: JSON.stringify({ action: "removed", changes: result.changes }) }]
17629
+ };
17630
+ }
17631
+ const timestamp = now();
17632
+ db.prepare(
17633
+ `INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
17634
+ VALUES (?, ?, ?, ?, ?, ?)`
17635
+ ).run(effectiveAgentId, source_id, target_id, relation, weight ?? 1, timestamp);
17636
+ return {
17637
+ content: [{ type: "text", text: JSON.stringify({
17638
+ action: "created",
17639
+ source_id,
17640
+ target_id,
17641
+ relation,
17642
+ weight: weight ?? 1
17643
+ }) }]
17644
+ };
17645
+ }
17646
+ );
17192
17647
  return { server, db };
17193
17648
  }
17194
17649
  async function main() {