@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.
@@ -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
 
@@ -691,7 +734,9 @@ function createLocalHttpEmbeddingProvider(opts) {
691
734
  };
692
735
  }
693
736
  function createGeminiEmbeddingProvider(opts) {
694
- const id = stableProviderId(`gemini:${opts.model}`, `${opts.model}|${opts.dimension}`);
737
+ const baseUrl = trimTrailingSlashes(opts.baseUrl || GEMINI_DEFAULT_BASE_URL);
738
+ const descriptorInput = `${baseUrl}|${opts.model}|${opts.dimension}`;
739
+ const id = stableProviderId(`gemini:${opts.model}`, descriptorInput);
695
740
  return {
696
741
  id,
697
742
  model: opts.model,
@@ -699,7 +744,7 @@ function createGeminiEmbeddingProvider(opts) {
699
744
  async embed(texts) {
700
745
  if (texts.length === 0) return [];
701
746
  const fetchFn = getFetch(opts.fetchImpl);
702
- const url2 = `https://generativelanguage.googleapis.com/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
747
+ const url2 = `${baseUrl}/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
703
748
  const requests = texts.map((text) => ({
704
749
  model: `models/${opts.model}`,
705
750
  content: { parts: [{ text }] },
@@ -731,9 +776,11 @@ function createGeminiEmbeddingProvider(opts) {
731
776
  function normalizeEmbeddingBaseUrl(baseUrl) {
732
777
  return trimTrailingSlashes(baseUrl);
733
778
  }
779
+ var GEMINI_DEFAULT_BASE_URL;
734
780
  var init_embedding = __esm({
735
781
  "src/search/embedding.ts"() {
736
782
  "use strict";
783
+ GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
737
784
  }
738
785
  });
739
786
 
@@ -789,6 +836,7 @@ function createEmbeddingProvider(input, opts) {
789
836
  model: normalized.model,
790
837
  dimension: normalized.dimension,
791
838
  apiKey: normalized.apiKey,
839
+ baseUrl: normalized.baseUrl || void 0,
792
840
  fetchImpl: opts?.fetchImpl
793
841
  });
794
842
  }
@@ -926,6 +974,23 @@ function searchByVector(db, queryVector, opts) {
926
974
  const limit = opts.limit ?? 20;
927
975
  const agentId = opts.agent_id ?? "default";
928
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
+ }
929
994
  const rows = db.prepare(
930
995
  `SELECT e.provider_id, e.vector, e.content_hash,
931
996
  m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
@@ -933,13 +998,8 @@ function searchByVector(db, queryVector, opts) {
933
998
  m.updated_at, m.source, m.agent_id, m.hash
934
999
  FROM embeddings e
935
1000
  JOIN memories m ON m.id = e.memory_id
936
- WHERE e.provider_id = ?
937
- AND e.status = 'ready'
938
- AND e.vector IS NOT NULL
939
- AND e.content_hash = m.hash
940
- AND m.agent_id = ?
941
- AND m.vitality >= ?`
942
- ).all(opts.providerId, agentId, minVitality);
1001
+ WHERE ${conditions.join(" AND ")}`
1002
+ ).all(...params);
943
1003
  const scored = rows.map((row) => ({
944
1004
  provider_id: row.provider_id,
945
1005
  memory: {
@@ -1014,10 +1074,12 @@ function createMemory(db, input) {
1014
1074
  }
1015
1075
  const id = newId();
1016
1076
  const timestamp = now();
1077
+ const sourceContext = input.source_context ? input.source_context.slice(0, 200) : null;
1017
1078
  db.prepare(
1018
1079
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
1019
- access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
1020
- 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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1021
1083
  ).run(
1022
1084
  id,
1023
1085
  input.content,
@@ -1030,7 +1092,10 @@ function createMemory(db, input) {
1030
1092
  input.source ?? null,
1031
1093
  agentId,
1032
1094
  hash2,
1033
- input.emotion_tag ?? null
1095
+ input.emotion_tag ?? null,
1096
+ input.source_session ?? null,
1097
+ sourceContext,
1098
+ input.observed_at ?? null
1034
1099
  );
1035
1100
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
1036
1101
  markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -15124,16 +15189,25 @@ function searchBM25(db, query, opts) {
15124
15189
  const ftsQuery = buildFtsQuery(query);
15125
15190
  if (!ftsQuery) return [];
15126
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);
15127
15203
  const rows = db.prepare(
15128
15204
  `SELECT m.*, rank AS score
15129
15205
  FROM memories_fts f
15130
15206
  JOIN memories m ON m.id = f.id
15131
- WHERE memories_fts MATCH ?
15132
- AND m.agent_id = ?
15133
- AND m.vitality >= ?
15207
+ WHERE ${conditions.join(" AND ")}
15134
15208
  ORDER BY rank
15135
15209
  LIMIT ?`
15136
- ).all(ftsQuery, agentId, minVitality, limit);
15210
+ ).all(...params);
15137
15211
  return rows.map((row, index) => {
15138
15212
  const { score: _score, ...memoryFields } = row;
15139
15213
  return {
@@ -15219,16 +15293,23 @@ function priorityPrior(priority) {
15219
15293
  function fusionScore(input) {
15220
15294
  const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
15221
15295
  const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
15222
- return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
15223
- }
15224
- 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) {
15225
15305
  const candidates = /* @__PURE__ */ new Map();
15226
15306
  for (const row of lexical) {
15227
15307
  candidates.set(row.memory.id, {
15228
15308
  memory: row.memory,
15229
15309
  score: 0,
15230
15310
  bm25_rank: row.rank,
15231
- bm25_score: row.score
15311
+ bm25_score: row.score,
15312
+ match_type: "direct"
15232
15313
  });
15233
15314
  }
15234
15315
  for (const row of vector) {
@@ -15241,13 +15322,14 @@ function fuseHybridResults(lexical, vector, limit) {
15241
15322
  memory: row.memory,
15242
15323
  score: 0,
15243
15324
  vector_rank: row.rank,
15244
- vector_score: row.similarity
15325
+ vector_score: row.similarity,
15326
+ match_type: "direct"
15245
15327
  });
15246
15328
  }
15247
15329
  }
15248
15330
  return [...candidates.values()].map((row) => ({
15249
15331
  ...row,
15250
- 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 })
15251
15333
  })).sort((left, right) => {
15252
15334
  if (right.score !== left.score) return right.score - left.score;
15253
15335
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
@@ -15260,9 +15342,61 @@ async function searchVectorBranch(db, query, opts) {
15260
15342
  providerId: opts.provider.id,
15261
15343
  agent_id: opts.agent_id,
15262
15344
  limit: opts.limit,
15263
- min_vitality: opts.min_vitality
15345
+ min_vitality: opts.min_vitality,
15346
+ after: opts.after,
15347
+ before: opts.before
15264
15348
  });
15265
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
+ }
15266
15400
  async function recallMemories(db, query, opts) {
15267
15401
  const limit = opts?.limit ?? 10;
15268
15402
  const agentId = opts?.agent_id ?? "default";
@@ -15270,10 +15404,15 @@ async function recallMemories(db, query, opts) {
15270
15404
  const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
15271
15405
  const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
15272
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;
15273
15410
  const lexical = searchBM25(db, query, {
15274
15411
  agent_id: agentId,
15275
15412
  limit: lexicalLimit,
15276
- min_vitality: minVitality
15413
+ min_vitality: minVitality,
15414
+ after,
15415
+ before
15277
15416
  });
15278
15417
  let vector = [];
15279
15418
  if (provider) {
@@ -15282,17 +15421,25 @@ async function recallMemories(db, query, opts) {
15282
15421
  provider,
15283
15422
  agent_id: agentId,
15284
15423
  limit: vectorLimit,
15285
- min_vitality: minVitality
15424
+ min_vitality: minVitality,
15425
+ after,
15426
+ before
15286
15427
  });
15287
15428
  } catch {
15288
15429
  vector = [];
15289
15430
  }
15290
15431
  }
15291
15432
  const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
15292
- 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
+ }
15293
15438
  if (opts?.recordAccess !== false) {
15294
15439
  for (const row of results) {
15295
- recordAccess(db, row.memory.id);
15440
+ if (row.match_type !== "related") {
15441
+ recordAccess(db, row.memory.id);
15442
+ }
15296
15443
  }
15297
15444
  }
15298
15445
  return {
@@ -15517,7 +15664,13 @@ function uriScopeMatch(inputUri, candidateUri) {
15517
15664
  }
15518
15665
  return 0.2;
15519
15666
  }
15520
- 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
+ }
15521
15674
  for (const part of parts) {
15522
15675
  if (!part) continue;
15523
15676
  const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
@@ -15540,8 +15693,16 @@ function timeProximity(input, memory, candidateUri) {
15540
15693
  if (input.type !== "event") {
15541
15694
  return 1;
15542
15695
  }
15543
- const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
15544
- 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
+ );
15545
15706
  if (!inputTime || !existingTime) {
15546
15707
  return 0.5;
15547
15708
  }
@@ -15625,6 +15786,65 @@ function fourCriterionGate(input) {
15625
15786
  failedCriteria: failed
15626
15787
  };
15627
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
+ }
15628
15848
  async function guard(db, input) {
15629
15849
  const hash2 = contentHash(input.content);
15630
15850
  const agentId = input.agent_id ?? "default";
@@ -15651,18 +15871,37 @@ async function guard(db, input) {
15651
15871
  return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
15652
15872
  }
15653
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);
15654
15879
  const best = candidates[0];
15655
15880
  if (!best) {
15656
- return { action: "add", reason: "No relevant semantic candidates found" };
15881
+ return { action: "add", reason: "No relevant semantic candidates found", candidates: candidatesList };
15657
15882
  }
15658
15883
  const score = best.score;
15659
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
+ }
15660
15897
  const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
15661
15898
  return {
15662
15899
  action: shouldUpdateMetadata ? "update" : "skip",
15663
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)})`,
15664
15901
  existingId: best.result.memory.id,
15665
- score
15902
+ score,
15903
+ candidates: candidatesList,
15904
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15666
15905
  };
15667
15906
  }
15668
15907
  if (score.dedup_score >= MERGE_THRESHOLD) {
@@ -15680,17 +15919,22 @@ async function guard(db, input) {
15680
15919
  existingId: best.result.memory.id,
15681
15920
  mergedContent: mergePlan.content,
15682
15921
  mergePlan,
15683
- score
15922
+ score,
15923
+ candidates: candidatesList,
15924
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15684
15925
  };
15685
15926
  }
15686
15927
  return {
15687
15928
  action: "add",
15688
15929
  reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
15689
- score
15930
+ score,
15931
+ candidates: candidatesList,
15932
+ conflicts: conflicts.length > 0 ? conflicts : void 0
15690
15933
  };
15691
15934
  }
15692
15935
 
15693
15936
  // src/sleep/sync.ts
15937
+ init_db();
15694
15938
  function ensureUriPath(db, memoryId, uri, agentId) {
15695
15939
  if (!uri) return;
15696
15940
  if (getPathByUri(db, uri, agentId ?? "default")) return;
@@ -15699,6 +15943,20 @@ function ensureUriPath(db, memoryId, uri, agentId) {
15699
15943
  } catch {
15700
15944
  }
15701
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
+ }
15702
15960
  async function syncOne(db, input) {
15703
15961
  const memInput = {
15704
15962
  content: input.content,
@@ -15710,17 +15968,22 @@ async function syncOne(db, input) {
15710
15968
  uri: input.uri,
15711
15969
  provider: input.provider,
15712
15970
  conservative: input.conservative,
15713
- 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
15714
15975
  };
15715
15976
  const guardResult = await guard(db, memInput);
15977
+ const agentId = input.agent_id ?? "default";
15716
15978
  switch (guardResult.action) {
15717
15979
  case "skip":
15718
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
15980
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId, conflicts: guardResult.conflicts };
15719
15981
  case "add": {
15720
15982
  const mem = createMemory(db, memInput);
15721
15983
  if (!mem) return { action: "skipped", reason: "createMemory returned null" };
15722
15984
  ensureUriPath(db, mem.id, input.uri, input.agent_id);
15723
- 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 };
15724
15987
  }
15725
15988
  case "update": {
15726
15989
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
@@ -15728,7 +15991,7 @@ async function syncOne(db, input) {
15728
15991
  updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
15729
15992
  }
15730
15993
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
15731
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
15994
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason, conflicts: guardResult.conflicts };
15732
15995
  }
15733
15996
  case "merge": {
15734
15997
  if (!guardResult.existingId || !guardResult.mergedContent) {
@@ -15736,7 +15999,8 @@ async function syncOne(db, input) {
15736
15999
  }
15737
16000
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
15738
16001
  ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
15739
- 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 };
15740
16004
  }
15741
16005
  }
15742
16006
  }
@@ -16017,33 +16281,13 @@ async function rememberMemory(db, input) {
16017
16281
  agent_id: input.agent_id,
16018
16282
  provider: input.provider,
16019
16283
  conservative: input.conservative,
16020
- emotion_tag: input.emotion_tag
16021
- });
16022
- }
16023
-
16024
- // src/app/recall.ts
16025
- async function recallMemory(db, input) {
16026
- const result = await recallMemories(db, input.query, {
16027
- agent_id: input.agent_id,
16028
- limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
16029
- min_vitality: input.min_vitality,
16030
- lexicalLimit: input.lexicalLimit,
16031
- vectorLimit: input.vectorLimit,
16032
- provider: input.provider,
16033
- recordAccess: input.recordAccess
16284
+ emotion_tag: input.emotion_tag,
16285
+ source_session: input.source_session,
16286
+ source_context: input.source_context,
16287
+ observed_at: input.observed_at
16034
16288
  });
16035
- if (input.emotion_tag) {
16036
- result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
16037
- }
16038
- return result;
16039
16289
  }
16040
16290
 
16041
- // src/app/surface.ts
16042
- init_memory();
16043
- init_providers();
16044
- init_tokenizer();
16045
- init_vector();
16046
-
16047
16291
  // src/app/feedback.ts
16048
16292
  init_db();
16049
16293
  function clamp012(value) {
@@ -16089,8 +16333,76 @@ function getFeedbackSummary(db, memoryId, agentId) {
16089
16333
  };
16090
16334
  }
16091
16335
  }
16336
+ function recordPassiveFeedback(db, memoryIds, agentId) {
16337
+ if (memoryIds.length === 0) return 0;
16338
+ const effectiveAgentId = agentId ?? "default";
16339
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
16340
+ const placeholders = memoryIds.map(() => "?").join(",");
16341
+ const recentCounts = /* @__PURE__ */ new Map();
16342
+ try {
16343
+ const rows = db.prepare(
16344
+ `SELECT memory_id, COUNT(*) as c
16345
+ FROM feedback_events
16346
+ WHERE memory_id IN (${placeholders})
16347
+ AND source = 'passive'
16348
+ AND created_at > ?
16349
+ GROUP BY memory_id`
16350
+ ).all(...memoryIds, cutoff);
16351
+ for (const row of rows) {
16352
+ recentCounts.set(row.memory_id, row.c);
16353
+ }
16354
+ } catch {
16355
+ }
16356
+ let recorded = 0;
16357
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
16358
+ const insert = db.prepare(
16359
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
16360
+ VALUES (?, ?, 'passive', 1, ?, 'passive:useful', 0.7, ?)`
16361
+ );
16362
+ for (const memoryId of memoryIds) {
16363
+ const count = recentCounts.get(memoryId) ?? 0;
16364
+ if (count >= 3) continue;
16365
+ try {
16366
+ insert.run(newId(), memoryId, effectiveAgentId, timestamp);
16367
+ recorded++;
16368
+ } catch {
16369
+ }
16370
+ }
16371
+ return recorded;
16372
+ }
16373
+
16374
+ // src/app/recall.ts
16375
+ async function recallMemory(db, input) {
16376
+ const result = await recallMemories(db, input.query, {
16377
+ agent_id: input.agent_id,
16378
+ limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
16379
+ min_vitality: input.min_vitality,
16380
+ lexicalLimit: input.lexicalLimit,
16381
+ vectorLimit: input.vectorLimit,
16382
+ provider: input.provider,
16383
+ recordAccess: input.recordAccess,
16384
+ related: input.related,
16385
+ after: input.after,
16386
+ before: input.before,
16387
+ recency_boost: input.recency_boost
16388
+ });
16389
+ if (input.emotion_tag) {
16390
+ result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
16391
+ }
16392
+ if (input.recordAccess !== false) {
16393
+ const top3DirectIds = result.results.filter((r) => r.match_type !== "related").slice(0, 3).map((r) => r.memory.id);
16394
+ if (top3DirectIds.length > 0) {
16395
+ recordPassiveFeedback(db, top3DirectIds, input.agent_id);
16396
+ }
16397
+ }
16398
+ return result;
16399
+ }
16092
16400
 
16093
16401
  // src/app/surface.ts
16402
+ init_memory();
16403
+ init_providers();
16404
+ init_tokenizer();
16405
+ init_vector();
16094
16406
  var INTENT_PRIORS = {
16095
16407
  factual: {
16096
16408
  identity: 0.25,
@@ -16230,7 +16542,9 @@ async function surfaceMemories(db, input) {
16230
16542
  searchBM25(db, trimmedQuery, {
16231
16543
  agent_id: agentId,
16232
16544
  limit: lexicalWindow,
16233
- min_vitality: minVitality
16545
+ min_vitality: minVitality,
16546
+ after: input.after,
16547
+ before: input.before
16234
16548
  }),
16235
16549
  "queryRank"
16236
16550
  );
@@ -16241,7 +16555,9 @@ async function surfaceMemories(db, input) {
16241
16555
  searchBM25(db, trimmedTask, {
16242
16556
  agent_id: agentId,
16243
16557
  limit: lexicalWindow,
16244
- min_vitality: minVitality
16558
+ min_vitality: minVitality,
16559
+ after: input.after,
16560
+ before: input.before
16245
16561
  }),
16246
16562
  "taskRank"
16247
16563
  );
@@ -16252,7 +16568,9 @@ async function surfaceMemories(db, input) {
16252
16568
  searchBM25(db, recentTurns.join(" "), {
16253
16569
  agent_id: agentId,
16254
16570
  limit: lexicalWindow,
16255
- min_vitality: minVitality
16571
+ min_vitality: minVitality,
16572
+ after: input.after,
16573
+ before: input.before
16256
16574
  }),
16257
16575
  "recentRank"
16258
16576
  );
@@ -16266,7 +16584,9 @@ async function surfaceMemories(db, input) {
16266
16584
  providerId: provider.id,
16267
16585
  agent_id: agentId,
16268
16586
  limit: lexicalWindow,
16269
- min_vitality: minVitality
16587
+ min_vitality: minVitality,
16588
+ after: input.after,
16589
+ before: input.before
16270
16590
  });
16271
16591
  const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
16272
16592
  collectBranch(signals, vectorRows, "semanticRank", similarity);
@@ -16404,19 +16724,74 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
16404
16724
 
16405
16725
  // src/sleep/tidy.ts
16406
16726
  init_memory();
16727
+ init_db();
16728
+ var EVENT_STALE_PATTERNS = [
16729
+ { pattern: /正在|进行中|部署中|处理中|in progress|deploying|working on/i, type: "in_progress", decay: 0.3, maxAgeDays: 7 },
16730
+ { pattern: /待办|TODO|等.*回复|等.*确认|需要.*确认/i, type: "pending", decay: 0.5, maxAgeDays: 14 },
16731
+ { pattern: /刚才|刚刚|just now|a moment ago/i, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
16732
+ ];
16733
+ var KNOWLEDGE_STALE_PATTERNS = [
16734
+ { pattern: /^(TODO|WIP|FIXME|待办|进行中)[::]/im, type: "pending", decay: 0.5, maxAgeDays: 14 },
16735
+ { pattern: /^(刚才|刚刚)/m, type: "ephemeral", decay: 0.2, maxAgeDays: 3 }
16736
+ ];
16737
+ function isStaleContent(content, type) {
16738
+ if (type === "identity" || type === "emotion") {
16739
+ return { stale: false, reason: "type excluded", decay_factor: 1 };
16740
+ }
16741
+ const patterns = type === "event" ? EVENT_STALE_PATTERNS : KNOWLEDGE_STALE_PATTERNS;
16742
+ for (const { pattern, type: staleType, decay } of patterns) {
16743
+ if (pattern.test(content)) {
16744
+ return { stale: true, reason: staleType, decay_factor: decay };
16745
+ }
16746
+ }
16747
+ return { stale: false, reason: "no stale patterns matched", decay_factor: 1 };
16748
+ }
16749
+ function getAgeThresholdDays(staleType) {
16750
+ const thresholds = {
16751
+ in_progress: 7,
16752
+ pending: 14,
16753
+ ephemeral: 3
16754
+ };
16755
+ return thresholds[staleType] ?? 7;
16756
+ }
16407
16757
  function runTidy(db, opts) {
16408
16758
  const threshold = opts?.vitalityThreshold ?? 0.05;
16409
16759
  const agentId = opts?.agent_id;
16410
16760
  let archived = 0;
16761
+ let staleDecayed = 0;
16411
16762
  const transaction = db.transaction(() => {
16412
16763
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
16413
16764
  for (const mem of decayed) {
16414
16765
  deleteMemory(db, mem.id);
16415
16766
  archived += 1;
16416
16767
  }
16768
+ const currentMs = Date.now();
16769
+ const currentTime = now();
16770
+ const agentCondition = agentId ? "AND agent_id = ?" : "";
16771
+ const agentParams = agentId ? [agentId] : [];
16772
+ const candidates = db.prepare(
16773
+ `SELECT id, content, type, created_at, updated_at, vitality
16774
+ FROM memories
16775
+ WHERE priority >= 2 AND vitality >= ?
16776
+ ${agentCondition}`
16777
+ ).all(threshold, ...agentParams);
16778
+ const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
16779
+ for (const mem of candidates) {
16780
+ const detection = isStaleContent(mem.content, mem.type);
16781
+ if (!detection.stale) continue;
16782
+ const createdMs = new Date(mem.created_at).getTime();
16783
+ const ageDays = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
16784
+ const thresholdDays = getAgeThresholdDays(detection.reason);
16785
+ if (ageDays < thresholdDays) continue;
16786
+ const newVitality = Math.max(0, mem.vitality * detection.decay_factor);
16787
+ if (Math.abs(newVitality - mem.vitality) > 1e-3) {
16788
+ updateStmt.run(newVitality, currentTime, mem.id);
16789
+ staleDecayed += 1;
16790
+ }
16791
+ }
16417
16792
  });
16418
16793
  transaction();
16419
- return { archived, orphansCleaned: 0 };
16794
+ return { archived, orphansCleaned: 0, staleDecayed };
16420
16795
  }
16421
16796
 
16422
16797
  // src/sleep/govern.ts
@@ -16825,7 +17200,10 @@ function formatMemory(memory, score) {
16825
17200
  priority: memory.priority,
16826
17201
  vitality: memory.vitality,
16827
17202
  score,
16828
- updated_at: memory.updated_at
17203
+ updated_at: memory.updated_at,
17204
+ source_session: memory.source_session ?? void 0,
17205
+ source_context: memory.source_context ?? void 0,
17206
+ observed_at: memory.observed_at ?? void 0
16829
17207
  };
16830
17208
  }
16831
17209
  function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
@@ -16912,7 +17290,9 @@ function formatRecallPayload(result) {
16912
17290
  bm25_rank: row.bm25_rank,
16913
17291
  vector_rank: row.vector_rank,
16914
17292
  bm25_score: row.bm25_score,
16915
- vector_score: row.vector_score
17293
+ vector_score: row.vector_score,
17294
+ related_source_id: row.related_source_id,
17295
+ match_type: row.match_type
16916
17296
  }))
16917
17297
  };
16918
17298
  }
@@ -16935,7 +17315,10 @@ function formatSurfacePayload(result) {
16935
17315
  priority_prior: row.priority_prior,
16936
17316
  feedback_score: row.feedback_score,
16937
17317
  reason_codes: row.reason_codes,
16938
- updated_at: row.memory.updated_at
17318
+ updated_at: row.memory.updated_at,
17319
+ source_session: row.memory.source_session ?? void 0,
17320
+ source_context: row.memory.source_context ?? void 0,
17321
+ observed_at: row.memory.observed_at ?? void 0
16939
17322
  }))
16940
17323
  };
16941
17324
  }
@@ -16944,7 +17327,7 @@ function createMcpServer(dbPath, agentId) {
16944
17327
  const aid = agentId ?? AGENT_ID;
16945
17328
  const server = new McpServer({
16946
17329
  name: "agent-memory",
16947
- version: "4.0.0-alpha.1"
17330
+ version: "5.0.0"
16948
17331
  });
16949
17332
  server.tool(
16950
17333
  "remember",
@@ -16956,10 +17339,24 @@ function createMcpServer(dbPath, agentId) {
16956
17339
  emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
16957
17340
  source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
16958
17341
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16959
- emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)")
17342
+ emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)"),
17343
+ session_id: external_exports.string().optional().describe("Source session ID for provenance tracking"),
17344
+ context: external_exports.string().optional().describe("Trigger context for this memory (\u2264200 chars, auto-truncated)"),
17345
+ observed_at: external_exports.string().optional().describe("When the event actually happened (ISO 8601), distinct from write time")
16960
17346
  },
16961
- async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag }) => {
16962
- const result = await rememberMemory(db, { content, type, uri, emotion_val, source, agent_id: agent_id ?? aid, emotion_tag });
17347
+ async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag, session_id, context, observed_at }) => {
17348
+ const result = await rememberMemory(db, {
17349
+ content,
17350
+ type,
17351
+ uri,
17352
+ emotion_val,
17353
+ source,
17354
+ agent_id: agent_id ?? aid,
17355
+ emotion_tag,
17356
+ source_session: session_id,
17357
+ source_context: context,
17358
+ observed_at
17359
+ });
16963
17360
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16964
17361
  }
16965
17362
  );
@@ -16970,10 +17367,23 @@ function createMcpServer(dbPath, agentId) {
16970
17367
  query: external_exports.string().describe("Search query (natural language)"),
16971
17368
  limit: external_exports.number().default(10).describe("Max results to return"),
16972
17369
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16973
- emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)")
17370
+ emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)"),
17371
+ related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
17372
+ after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
17373
+ before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
17374
+ 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")
16974
17375
  },
16975
- async ({ query, limit, agent_id, emotion_tag }) => {
16976
- const result = await recallMemory(db, { query, limit, agent_id: agent_id ?? aid, emotion_tag });
17376
+ async ({ query, limit, agent_id, emotion_tag, related, after, before, recency_boost }) => {
17377
+ const result = await recallMemory(db, {
17378
+ query,
17379
+ limit,
17380
+ agent_id: agent_id ?? aid,
17381
+ emotion_tag,
17382
+ related: related ?? false,
17383
+ after,
17384
+ before,
17385
+ recency_boost: recency_boost ?? 0
17386
+ });
16977
17387
  return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
16978
17388
  }
16979
17389
  );
@@ -17163,9 +17573,13 @@ function createMcpServer(dbPath, agentId) {
17163
17573
  types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"]).describe("Optional type filter")).optional(),
17164
17574
  limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
17165
17575
  agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
17166
- keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted")
17576
+ keywords: external_exports.array(external_exports.string()).optional().describe("Deprecated alias: joined into query when query is omitted"),
17577
+ related: external_exports.boolean().default(false).optional().describe("Expand results with related memories from the links table"),
17578
+ after: external_exports.string().optional().describe("Only return memories updated after this ISO 8601 timestamp"),
17579
+ before: external_exports.string().optional().describe("Only return memories updated before this ISO 8601 timestamp"),
17580
+ recency_boost: external_exports.number().min(0).max(1).default(0).optional().describe("Recency bias (0=none, 1=max)")
17167
17581
  },
17168
- async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords }) => {
17582
+ async ({ query, task, recent_turns, intent, types, limit, agent_id, keywords, related, after, before, recency_boost }) => {
17169
17583
  const resolvedQuery = query ?? keywords?.join(" ");
17170
17584
  const result = await surfaceMemories(db, {
17171
17585
  query: resolvedQuery,
@@ -17174,7 +17588,11 @@ function createMcpServer(dbPath, agentId) {
17174
17588
  intent,
17175
17589
  types,
17176
17590
  limit: limit ?? 5,
17177
- agent_id: agent_id ?? aid
17591
+ agent_id: agent_id ?? aid,
17592
+ related: related ?? false,
17593
+ after,
17594
+ before,
17595
+ recency_boost: recency_boost ?? 0
17178
17596
  });
17179
17597
  return {
17180
17598
  content: [{
@@ -17184,6 +17602,43 @@ function createMcpServer(dbPath, agentId) {
17184
17602
  };
17185
17603
  }
17186
17604
  );
17605
+ server.tool(
17606
+ "link",
17607
+ "Manually create or remove an association between two memories.",
17608
+ {
17609
+ source_id: external_exports.string().describe("Source memory ID"),
17610
+ target_id: external_exports.string().describe("Target memory ID"),
17611
+ relation: external_exports.enum(["related", "supersedes", "contradicts"]).default("related").describe("Relation type"),
17612
+ weight: external_exports.number().min(0).max(1).default(1).optional().describe("Link weight (0-1)"),
17613
+ remove: external_exports.boolean().default(false).optional().describe("Remove the link instead of creating it"),
17614
+ agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
17615
+ },
17616
+ async ({ source_id, target_id, relation, weight, remove, agent_id }) => {
17617
+ const effectiveAgentId = agent_id ?? aid;
17618
+ if (remove) {
17619
+ const result = db.prepare(
17620
+ "DELETE FROM links WHERE agent_id = ? AND source_id = ? AND target_id = ?"
17621
+ ).run(effectiveAgentId, source_id, target_id);
17622
+ return {
17623
+ content: [{ type: "text", text: JSON.stringify({ action: "removed", changes: result.changes }) }]
17624
+ };
17625
+ }
17626
+ const timestamp = now();
17627
+ db.prepare(
17628
+ `INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
17629
+ VALUES (?, ?, ?, ?, ?, ?)`
17630
+ ).run(effectiveAgentId, source_id, target_id, relation, weight ?? 1, timestamp);
17631
+ return {
17632
+ content: [{ type: "text", text: JSON.stringify({
17633
+ action: "created",
17634
+ source_id,
17635
+ target_id,
17636
+ relation,
17637
+ weight: weight ?? 1
17638
+ }) }]
17639
+ };
17640
+ }
17641
+ );
17187
17642
  return { server, db };
17188
17643
  }
17189
17644
  async function main() {