@smyslenny/agent-memory 2.1.0 → 3.1.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.
@@ -14366,241 +14366,6 @@ function getPathsByPrefix(db, prefix, agent_id = "default") {
14366
14366
  return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri LIKE ? ORDER BY uri").all(agent_id, `${prefix}%`);
14367
14367
  }
14368
14368
 
14369
- // src/core/link.ts
14370
- init_db();
14371
- function createLink(db, sourceId, targetId, relation, weight = 1, agent_id) {
14372
- const sourceAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id;
14373
- const targetAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(targetId)?.agent_id;
14374
- if (!sourceAgent) throw new Error(`Source memory not found: ${sourceId}`);
14375
- if (!targetAgent) throw new Error(`Target memory not found: ${targetId}`);
14376
- if (sourceAgent !== targetAgent) throw new Error("Cross-agent links are not allowed");
14377
- if (agent_id && agent_id !== sourceAgent) throw new Error("Agent mismatch for link");
14378
- const agentId = agent_id ?? sourceAgent;
14379
- db.prepare(
14380
- `INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
14381
- VALUES (?, ?, ?, ?, ?, ?)`
14382
- ).run(agentId, sourceId, targetId, relation, weight, now());
14383
- return { agent_id: agentId, source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
14384
- }
14385
- function getLinks(db, memoryId, agent_id) {
14386
- const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id ?? "default";
14387
- return db.prepare("SELECT * FROM links WHERE agent_id = ? AND (source_id = ? OR target_id = ?)").all(agentId, memoryId, memoryId);
14388
- }
14389
- function traverse(db, startId, maxHops = 2, agent_id) {
14390
- const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(startId)?.agent_id ?? "default";
14391
- const visited = /* @__PURE__ */ new Set();
14392
- const results = [];
14393
- const queue = [
14394
- { id: startId, hop: 0, relation: "self" }
14395
- ];
14396
- while (queue.length > 0) {
14397
- const current = queue.shift();
14398
- if (visited.has(current.id)) continue;
14399
- visited.add(current.id);
14400
- if (current.hop > 0) {
14401
- results.push(current);
14402
- }
14403
- if (current.hop < maxHops) {
14404
- const links = db.prepare("SELECT target_id, relation FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, current.id);
14405
- for (const link of links) {
14406
- if (!visited.has(link.target_id)) {
14407
- queue.push({
14408
- id: link.target_id,
14409
- hop: current.hop + 1,
14410
- relation: link.relation
14411
- });
14412
- }
14413
- }
14414
- const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE agent_id = ? AND target_id = ?").all(agentId, current.id);
14415
- for (const link of reverseLinks) {
14416
- if (!visited.has(link.source_id)) {
14417
- queue.push({
14418
- id: link.source_id,
14419
- hop: current.hop + 1,
14420
- relation: link.relation
14421
- });
14422
- }
14423
- }
14424
- }
14425
- }
14426
- return results;
14427
- }
14428
-
14429
- // src/core/snapshot.ts
14430
- init_db();
14431
- init_tokenizer();
14432
- function createSnapshot(db, memoryId, action, changedBy) {
14433
- const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
14434
- if (!memory) throw new Error(`Memory not found: ${memoryId}`);
14435
- const id = newId();
14436
- db.prepare(
14437
- `INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
14438
- VALUES (?, ?, ?, ?, ?, ?)`
14439
- ).run(id, memoryId, memory.content, changedBy ?? null, action, now());
14440
- return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
14441
- }
14442
- function getSnapshots(db, memoryId) {
14443
- return db.prepare("SELECT * FROM snapshots WHERE memory_id = ? ORDER BY created_at DESC").all(memoryId);
14444
- }
14445
- function getSnapshot(db, id) {
14446
- return db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id) ?? null;
14447
- }
14448
- function rollback(db, snapshotId) {
14449
- const snapshot = getSnapshot(db, snapshotId);
14450
- if (!snapshot) return false;
14451
- createSnapshot(db, snapshot.memory_id, "update", "rollback");
14452
- db.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?").run(
14453
- snapshot.content,
14454
- now(),
14455
- snapshot.memory_id
14456
- );
14457
- db.prepare("DELETE FROM memories_fts WHERE id = ?").run(snapshot.memory_id);
14458
- db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
14459
- snapshot.memory_id,
14460
- tokenizeForIndex(snapshot.content)
14461
- );
14462
- return true;
14463
- }
14464
-
14465
- // src/search/intent.ts
14466
- init_tokenizer();
14467
- var INTENT_PATTERNS = {
14468
- factual: [
14469
- // English
14470
- /^(what|who|where|which|how much|how many)\b/i,
14471
- /\b(name|address|number|password|config|setting)\b/i,
14472
- // Chinese - questions about facts
14473
- /是(什么|谁|哪|啥)/,
14474
- /叫(什么|啥)/,
14475
- /(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
14476
- /(多少|几个|哪个|哪些|哪里)/,
14477
- // Chinese - lookup patterns
14478
- /(查一下|找一下|看看|搜一下)/,
14479
- /(.+)是什么$/
14480
- ],
14481
- temporal: [
14482
- // English
14483
- /^(when|what time|how long)\b/i,
14484
- /\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
14485
- /\b(first|latest|newest|oldest|previous|next)\b/i,
14486
- // Chinese - time expressions
14487
- /什么时候/,
14488
- /(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
14489
- /(几月|几号|几点|多久|多长时间)/,
14490
- /(上次|下次|第一次|最后一次|那天|那时)/,
14491
- // Date patterns
14492
- /\d{4}[-/.]\d{1,2}/,
14493
- /\d{1,2}月\d{1,2}[日号]/,
14494
- // Chinese - temporal context
14495
- /(历史|记录|日志|以来|至今|期间)/
14496
- ],
14497
- causal: [
14498
- // English
14499
- /^(why|how come|what caused)\b/i,
14500
- /\b(because|due to|reason|cause|result)\b/i,
14501
- // Chinese - causal questions
14502
- /为(什么|啥|何)/,
14503
- /(原因|导致|造成|引起|因为|所以|结果)/,
14504
- /(怎么回事|怎么了|咋回事|咋了)/,
14505
- /(为啥|凭啥|凭什么)/,
14506
- // Chinese - problem/diagnosis
14507
- /(出(了|了什么)?问题|报错|失败|出错|bug)/
14508
- ],
14509
- exploratory: [
14510
- // English
14511
- /^(how|tell me about|explain|describe|show me)\b/i,
14512
- /^(what do you think|what about|any)\b/i,
14513
- /\b(overview|summary|list|compare)\b/i,
14514
- // Chinese - exploratory
14515
- /(怎么样|怎样|如何)/,
14516
- /(介绍|说说|讲讲|聊聊|谈谈)/,
14517
- /(有哪些|有什么|有没有)/,
14518
- /(关于|对于|至于|关联)/,
14519
- /(总结|概括|梳理|回顾|盘点)/,
14520
- // Chinese - opinion/analysis
14521
- /(看法|想法|意见|建议|评价|感觉|觉得)/,
14522
- /(对比|比较|区别|差异|优缺点)/
14523
- ]
14524
- };
14525
- var CN_STRUCTURE_BOOSTS = {
14526
- factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
14527
- temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
14528
- causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
14529
- exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
14530
- };
14531
- function classifyIntent(query) {
14532
- const scores = {
14533
- factual: 0,
14534
- exploratory: 0,
14535
- temporal: 0,
14536
- causal: 0
14537
- };
14538
- for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
14539
- for (const pattern of patterns) {
14540
- if (pattern.test(query)) {
14541
- scores[intent] += 1;
14542
- }
14543
- }
14544
- }
14545
- for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
14546
- for (const pattern of patterns) {
14547
- if (pattern.test(query)) {
14548
- scores[intent] += 0.5;
14549
- }
14550
- }
14551
- }
14552
- const tokens = tokenize(query);
14553
- const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
14554
- if (totalPatternScore === 0 && tokens.length <= 3) {
14555
- scores.factual += 1;
14556
- }
14557
- let maxIntent = "factual";
14558
- let maxScore = 0;
14559
- for (const [intent, score] of Object.entries(scores)) {
14560
- if (score > maxScore) {
14561
- maxScore = score;
14562
- maxIntent = intent;
14563
- }
14564
- }
14565
- const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
14566
- const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
14567
- return { intent: maxIntent, confidence };
14568
- }
14569
- function getStrategy(intent) {
14570
- switch (intent) {
14571
- case "factual":
14572
- return { boostRecent: false, boostPriority: true, limit: 5 };
14573
- case "temporal":
14574
- return { boostRecent: true, boostPriority: false, limit: 10 };
14575
- case "causal":
14576
- return { boostRecent: false, boostPriority: false, limit: 10 };
14577
- case "exploratory":
14578
- return { boostRecent: false, boostPriority: false, limit: 15 };
14579
- }
14580
- }
14581
-
14582
- // src/search/rerank.ts
14583
- function rerank(results, opts) {
14584
- const now2 = Date.now();
14585
- const scored = results.map((r) => {
14586
- let finalScore = r.score;
14587
- if (opts.boostPriority) {
14588
- const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
14589
- finalScore *= priorityMultiplier;
14590
- }
14591
- if (opts.boostRecent && r.memory.updated_at) {
14592
- const age = now2 - new Date(r.memory.updated_at).getTime();
14593
- const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
14594
- const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
14595
- finalScore *= recencyBoost;
14596
- }
14597
- finalScore *= Math.max(0.1, r.memory.vitality);
14598
- return { ...r, score: finalScore };
14599
- });
14600
- scored.sort((a, b) => b.score - a.score);
14601
- return scored.slice(0, opts.limit);
14602
- }
14603
-
14604
14369
  // src/search/bm25.ts
14605
14370
  init_tokenizer();
14606
14371
  function searchBM25(db, query, opts) {
@@ -14653,227 +14418,6 @@ function buildFtsQuery(text) {
14653
14418
  return tokens.map((w) => `"${w}"`).join(" OR ");
14654
14419
  }
14655
14420
 
14656
- // src/search/embeddings.ts
14657
- init_db();
14658
- function encodeEmbedding(vector) {
14659
- const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
14660
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
14661
- }
14662
- function decodeEmbedding(buf) {
14663
- const copy = Buffer.from(buf);
14664
- return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
14665
- }
14666
- function upsertEmbedding(db, input) {
14667
- const ts = now();
14668
- const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
14669
- const blob = encodeEmbedding(vec);
14670
- db.prepare(
14671
- `INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
14672
- VALUES (?, ?, ?, ?, ?, ?, ?)
14673
- ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
14674
- dim = excluded.dim,
14675
- vector = excluded.vector,
14676
- updated_at = excluded.updated_at`
14677
- ).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
14678
- }
14679
- function listEmbeddings(db, agent_id, model) {
14680
- const rows = db.prepare(
14681
- "SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
14682
- ).all(agent_id, model);
14683
- return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
14684
- }
14685
-
14686
- // src/search/hybrid.ts
14687
- function cosine(a, b) {
14688
- const n = Math.min(a.length, b.length);
14689
- let dot = 0;
14690
- let na = 0;
14691
- let nb = 0;
14692
- for (let i = 0; i < n; i++) {
14693
- const x = a[i];
14694
- const y = b[i];
14695
- dot += x * y;
14696
- na += x * x;
14697
- nb += y * y;
14698
- }
14699
- if (na === 0 || nb === 0) return 0;
14700
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
14701
- }
14702
- function rrfScore(rank, k) {
14703
- return 1 / (k + rank);
14704
- }
14705
- function fuseRrf(lists, k) {
14706
- const out = /* @__PURE__ */ new Map();
14707
- for (const list of lists) {
14708
- for (let i = 0; i < list.items.length; i++) {
14709
- const it = list.items[i];
14710
- const rank = i + 1;
14711
- const add = rrfScore(rank, k);
14712
- const prev = out.get(it.id);
14713
- if (!prev) out.set(it.id, { score: add, sources: [list.name] });
14714
- else {
14715
- prev.score += add;
14716
- if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
14717
- }
14718
- }
14719
- }
14720
- return out;
14721
- }
14722
- function fetchMemories(db, ids, agentId) {
14723
- if (ids.length === 0) return [];
14724
- const placeholders = ids.map(() => "?").join(", ");
14725
- const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
14726
- const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
14727
- return rows;
14728
- }
14729
- async function searchHybrid(db, query, opts) {
14730
- const agentId = opts?.agent_id ?? "default";
14731
- const limit = opts?.limit ?? 10;
14732
- const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
14733
- const semanticCandidates = opts?.semanticCandidates ?? 50;
14734
- const rrfK = opts?.rrfK ?? 60;
14735
- const bm25 = searchBM25(db, query, {
14736
- agent_id: agentId,
14737
- limit: limit * bm25Mult
14738
- });
14739
- const provider = opts?.embeddingProvider ?? null;
14740
- const model = opts?.embeddingModel ?? provider?.model;
14741
- if (!provider || !model) {
14742
- return bm25.slice(0, limit);
14743
- }
14744
- const qVec = Float32Array.from(await provider.embed(query));
14745
- const embeddings = listEmbeddings(db, agentId, model);
14746
- const scored = [];
14747
- for (const e of embeddings) {
14748
- scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
14749
- }
14750
- scored.sort((a, b) => b.score - a.score);
14751
- const semanticTop = scored.slice(0, semanticCandidates);
14752
- const fused = fuseRrf(
14753
- [
14754
- { name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
14755
- { name: "semantic", items: semanticTop }
14756
- ],
14757
- rrfK
14758
- );
14759
- const ids = [...fused.keys()];
14760
- const memories = fetchMemories(db, ids, agentId);
14761
- const byId = new Map(memories.map((m) => [m.id, m]));
14762
- const out = [];
14763
- for (const [id, meta3] of fused) {
14764
- const mem = byId.get(id);
14765
- if (!mem) continue;
14766
- out.push({
14767
- memory: mem,
14768
- score: meta3.score,
14769
- matchReason: meta3.sources.sort().join("+")
14770
- });
14771
- }
14772
- out.sort((a, b) => b.score - a.score);
14773
- return out.slice(0, limit);
14774
- }
14775
-
14776
- // src/search/providers.ts
14777
- function getEmbeddingProviderFromEnv() {
14778
- const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
14779
- if (provider === "none" || provider === "off" || provider === "false") return null;
14780
- if (provider === "openai") {
14781
- const apiKey = process.env.OPENAI_API_KEY;
14782
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
14783
- const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
14784
- if (!apiKey) return null;
14785
- return createOpenAIProvider({ apiKey, model, baseUrl });
14786
- }
14787
- if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
14788
- const apiKey = process.env.DASHSCOPE_API_KEY;
14789
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
14790
- const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
14791
- if (!apiKey) return null;
14792
- return createDashScopeProvider({ apiKey, model, baseUrl });
14793
- }
14794
- return null;
14795
- }
14796
- function authHeader(apiKey) {
14797
- return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
14798
- }
14799
- function normalizeEmbedding(e) {
14800
- if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
14801
- if (e.length === 0) throw new Error("Invalid embedding: empty");
14802
- return e.map((x) => {
14803
- if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
14804
- return x;
14805
- });
14806
- }
14807
- function createOpenAIProvider(opts) {
14808
- const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
14809
- return {
14810
- id: "openai",
14811
- model: opts.model,
14812
- async embed(text) {
14813
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
14814
- method: "POST",
14815
- headers: {
14816
- "content-type": "application/json",
14817
- authorization: authHeader(opts.apiKey)
14818
- },
14819
- body: JSON.stringify({ model: opts.model, input: text })
14820
- });
14821
- if (!resp.ok) {
14822
- const body = await resp.text().catch(() => "");
14823
- throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
14824
- }
14825
- const data = await resp.json();
14826
- const emb = data.data?.[0]?.embedding;
14827
- return normalizeEmbedding(emb);
14828
- }
14829
- };
14830
- }
14831
- function createDashScopeProvider(opts) {
14832
- const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
14833
- return {
14834
- id: "dashscope",
14835
- model: opts.model,
14836
- async embed(text) {
14837
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
14838
- method: "POST",
14839
- headers: {
14840
- "content-type": "application/json",
14841
- authorization: authHeader(opts.apiKey)
14842
- },
14843
- body: JSON.stringify({
14844
- model: opts.model,
14845
- input: { texts: [text] }
14846
- })
14847
- });
14848
- if (!resp.ok) {
14849
- const body = await resp.text().catch(() => "");
14850
- throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
14851
- }
14852
- const data = await resp.json();
14853
- const emb = data?.output?.embeddings?.[0]?.embedding ?? data?.output?.embeddings?.[0]?.vector ?? data?.output?.embedding ?? data?.data?.[0]?.embedding;
14854
- return normalizeEmbedding(emb);
14855
- }
14856
- };
14857
- }
14858
-
14859
- // src/search/embed.ts
14860
- async function embedMemory(db, memoryId, provider, opts) {
14861
- const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
14862
- if (!row) return false;
14863
- if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
14864
- const model = opts?.model ?? provider.model;
14865
- const maxChars = opts?.maxChars ?? 2e3;
14866
- const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
14867
- const vector = await provider.embed(text);
14868
- upsertEmbedding(db, {
14869
- agent_id: row.agent_id,
14870
- memory_id: row.id,
14871
- model,
14872
- vector
14873
- });
14874
- return true;
14875
- }
14876
-
14877
14421
  // src/sleep/sync.ts
14878
14422
  init_memory();
14879
14423
 
@@ -15007,7 +14551,6 @@ function syncOne(db, input) {
15007
14551
  }
15008
14552
  case "update": {
15009
14553
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
15010
- createSnapshot(db, guardResult.existingId, "update", "sync");
15011
14554
  updateMemory(db, guardResult.existingId, { content: input.content });
15012
14555
  return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
15013
14556
  }
@@ -15015,7 +14558,6 @@ function syncOne(db, input) {
15015
14558
  if (!guardResult.existingId || !guardResult.mergedContent) {
15016
14559
  return { action: "skipped", reason: "Missing merge data" };
15017
14560
  }
15018
- createSnapshot(db, guardResult.existingId, "merge", "sync");
15019
14561
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
15020
14562
  return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
15021
14563
  }
@@ -15087,18 +14629,12 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
15087
14629
  init_memory();
15088
14630
  function runTidy(db, opts) {
15089
14631
  const threshold = opts?.vitalityThreshold ?? 0.05;
15090
- const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
15091
14632
  const agentId = opts?.agent_id;
15092
14633
  let archived = 0;
15093
14634
  let orphansCleaned = 0;
15094
- let snapshotsPruned = 0;
15095
14635
  const transaction = db.transaction(() => {
15096
14636
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
15097
14637
  for (const mem of decayed) {
15098
- try {
15099
- createSnapshot(db, mem.id, "delete", "tidy");
15100
- } catch {
15101
- }
15102
14638
  deleteMemory(db, mem.id);
15103
14639
  archived++;
15104
14640
  }
@@ -15110,35 +14646,15 @@ function runTidy(db, opts) {
15110
14646
  "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
15111
14647
  ).run();
15112
14648
  orphansCleaned = orphans.changes;
15113
- const memoriesWithSnapshots = agentId ? db.prepare(
15114
- `SELECT s.memory_id, COUNT(*) as cnt
15115
- FROM snapshots s
15116
- JOIN memories m ON m.id = s.memory_id
15117
- WHERE m.agent_id = ?
15118
- GROUP BY s.memory_id HAVING cnt > ?`
15119
- ).all(agentId, maxSnapshots) : db.prepare(
15120
- `SELECT memory_id, COUNT(*) as cnt FROM snapshots
15121
- GROUP BY memory_id HAVING cnt > ?`
15122
- ).all(maxSnapshots);
15123
- for (const { memory_id } of memoriesWithSnapshots) {
15124
- const pruned = db.prepare(
15125
- `DELETE FROM snapshots WHERE id NOT IN (
15126
- SELECT id FROM snapshots WHERE memory_id = ?
15127
- ORDER BY created_at DESC LIMIT ?
15128
- ) AND memory_id = ?`
15129
- ).run(memory_id, maxSnapshots, memory_id);
15130
- snapshotsPruned += pruned.changes;
15131
- }
15132
14649
  });
15133
14650
  transaction();
15134
- return { archived, orphansCleaned, snapshotsPruned };
14651
+ return { archived, orphansCleaned };
15135
14652
  }
15136
14653
 
15137
14654
  // src/sleep/govern.ts
15138
14655
  function runGovern(db, opts) {
15139
14656
  const agentId = opts?.agent_id;
15140
14657
  let orphanPaths = 0;
15141
- let orphanLinks = 0;
15142
14658
  let emptyMemories = 0;
15143
14659
  const transaction = db.transaction(() => {
15144
14660
  const pathResult = agentId ? db.prepare(
@@ -15147,23 +14663,11 @@ function runGovern(db, opts) {
15147
14663
  AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
15148
14664
  ).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
15149
14665
  orphanPaths = pathResult.changes;
15150
- const linkResult = agentId ? db.prepare(
15151
- `DELETE FROM links WHERE
15152
- agent_id = ? AND (
15153
- source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
15154
- target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
15155
- )`
15156
- ).run(agentId, agentId, agentId) : db.prepare(
15157
- `DELETE FROM links WHERE
15158
- source_id NOT IN (SELECT id FROM memories) OR
15159
- target_id NOT IN (SELECT id FROM memories)`
15160
- ).run();
15161
- orphanLinks = linkResult.changes;
15162
14666
  const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
15163
14667
  emptyMemories = emptyResult.changes;
15164
14668
  });
15165
14669
  transaction();
15166
- return { orphanPaths, orphanLinks, emptyMemories };
14670
+ return { orphanPaths, emptyMemories };
15167
14671
  }
15168
14672
 
15169
14673
  // src/sleep/boot.ts
@@ -15219,16 +14723,400 @@ function boot(db, opts) {
15219
14723
  };
15220
14724
  }
15221
14725
 
14726
+ // src/ingest/ingest.ts
14727
+ function slugify2(input) {
14728
+ return input.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, " ").trim().replace(/\s+/g, "-").slice(0, 64) || "item";
14729
+ }
14730
+ function classifyIngestType(text) {
14731
+ const lower = text.toLowerCase();
14732
+ if (/##\s*身份|\bidentity\b|\b我是\b|我是/.test(text)) {
14733
+ return "identity";
14734
+ }
14735
+ if (/##\s*情感|❤️|💕|爱你|感动|难过|开心|害怕|想念|表白/.test(text)) {
14736
+ return "emotion";
14737
+ }
14738
+ if (/##\s*决策|##\s*技术|选型|教训|\bknowledge\b|⚠️|复盘|经验/.test(text)) {
14739
+ return "knowledge";
14740
+ }
14741
+ if (/\d{4}-\d{2}-\d{2}|发生了|完成了|今天|昨日|刚刚|部署|上线/.test(text)) {
14742
+ return "event";
14743
+ }
14744
+ if (lower.length <= 12) return "event";
14745
+ return "knowledge";
14746
+ }
14747
+ function splitIngestBlocks(text) {
14748
+ const headingRegex = /^##\s+(.+)$/gm;
14749
+ const matches = [...text.matchAll(headingRegex)];
14750
+ const blocks = [];
14751
+ if (matches.length > 0) {
14752
+ for (let i = 0; i < matches.length; i++) {
14753
+ const match = matches[i];
14754
+ const start = match.index ?? 0;
14755
+ const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
14756
+ const raw = text.slice(start, end).trim();
14757
+ const lines = raw.split("\n");
14758
+ const title = lines[0].replace(/^##\s+/, "").trim();
14759
+ const content = lines.slice(1).join("\n").trim();
14760
+ if (content) blocks.push({ title, content });
14761
+ }
14762
+ return blocks;
14763
+ }
14764
+ const bullets = text.split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter(Boolean);
14765
+ if (bullets.length > 0) {
14766
+ return bullets.map((content, i) => ({ title: `bullet-${i + 1}`, content }));
14767
+ }
14768
+ const plain = text.trim();
14769
+ if (!plain) return [];
14770
+ return [{ title: "ingest", content: plain }];
14771
+ }
14772
+ function extractIngestItems(text, source) {
14773
+ const blocks = splitIngestBlocks(text);
14774
+ return blocks.map((block, index) => {
14775
+ const merged = `${block.title}
14776
+ ${block.content}`;
14777
+ const type = classifyIngestType(merged);
14778
+ const domain2 = type === "identity" ? "core" : type;
14779
+ const sourcePart = slugify2(source ?? "ingest");
14780
+ const uri = `${domain2}://ingest/${sourcePart}/${index + 1}-${slugify2(block.title)}`;
14781
+ return {
14782
+ index,
14783
+ title: block.title,
14784
+ content: block.content,
14785
+ type,
14786
+ uri
14787
+ };
14788
+ });
14789
+ }
14790
+ function ingestText(db, options) {
14791
+ const extracted = extractIngestItems(options.text, options.source);
14792
+ const dryRun = options.dryRun ?? false;
14793
+ const agentId = options.agentId ?? "default";
14794
+ if (dryRun) {
14795
+ return {
14796
+ extracted: extracted.length,
14797
+ written: 0,
14798
+ skipped: extracted.length,
14799
+ dry_run: true,
14800
+ details: extracted.map((item) => ({
14801
+ index: item.index,
14802
+ type: item.type,
14803
+ uri: item.uri,
14804
+ preview: item.content.slice(0, 80)
14805
+ }))
14806
+ };
14807
+ }
14808
+ let written = 0;
14809
+ let skipped = 0;
14810
+ const details = [];
14811
+ for (const item of extracted) {
14812
+ const result = syncOne(db, {
14813
+ content: item.content,
14814
+ type: item.type,
14815
+ uri: item.uri,
14816
+ source: `auto:${options.source ?? "ingest"}`,
14817
+ agent_id: agentId
14818
+ });
14819
+ if (result.action === "added" || result.action === "updated" || result.action === "merged") {
14820
+ written++;
14821
+ } else {
14822
+ skipped++;
14823
+ }
14824
+ details.push({
14825
+ index: item.index,
14826
+ type: item.type,
14827
+ uri: item.uri,
14828
+ action: result.action,
14829
+ reason: result.reason,
14830
+ memoryId: result.memoryId
14831
+ });
14832
+ }
14833
+ return {
14834
+ extracted: extracted.length,
14835
+ written,
14836
+ skipped,
14837
+ dry_run: false,
14838
+ details
14839
+ };
14840
+ }
14841
+
14842
+ // src/ingest/watcher.ts
14843
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync, watch } from "fs";
14844
+ import { join, relative, resolve } from "path";
14845
+ function runAutoIngestWatcher(options) {
14846
+ const workspaceDir = resolve(options.workspaceDir);
14847
+ const memoryDir = join(workspaceDir, "memory");
14848
+ const memoryMdPath = join(workspaceDir, "MEMORY.md");
14849
+ const debounceMs = options.debounceMs ?? 1200;
14850
+ const initialScan = options.initialScan ?? true;
14851
+ const logger = options.logger ?? console;
14852
+ const timers = /* @__PURE__ */ new Map();
14853
+ const watchers = [];
14854
+ const stats = {
14855
+ triggers: 0,
14856
+ filesProcessed: 0,
14857
+ extracted: 0,
14858
+ written: 0,
14859
+ skipped: 0,
14860
+ errors: 0
14861
+ };
14862
+ let stopped = false;
14863
+ let queue = Promise.resolve();
14864
+ const toSource = (absPath) => {
14865
+ const rel = relative(workspaceDir, absPath).replace(/\\/g, "/");
14866
+ return rel || absPath;
14867
+ };
14868
+ const isTrackedMarkdownFile = (absPath) => {
14869
+ if (!absPath.endsWith(".md")) return false;
14870
+ if (resolve(absPath) === memoryMdPath) return true;
14871
+ const rel = relative(memoryDir, absPath).replace(/\\/g, "/");
14872
+ if (rel.startsWith("..") || rel === "") return false;
14873
+ return !rel.includes("/");
14874
+ };
14875
+ const ingestFile = (absPath, reason) => {
14876
+ if (stopped) return;
14877
+ if (!existsSync(absPath)) {
14878
+ logger.log(`[auto-ingest] skip missing file: ${toSource(absPath)} (reason=${reason})`);
14879
+ return;
14880
+ }
14881
+ let isFile = false;
14882
+ try {
14883
+ isFile = statSync(absPath).isFile();
14884
+ } catch (err) {
14885
+ stats.errors += 1;
14886
+ logger.warn(`[auto-ingest] stat failed for ${toSource(absPath)}: ${String(err)}`);
14887
+ return;
14888
+ }
14889
+ if (!isFile) return;
14890
+ try {
14891
+ const text = readFileSync2(absPath, "utf-8");
14892
+ const source = toSource(absPath);
14893
+ const result = ingestText(options.db, {
14894
+ text,
14895
+ source,
14896
+ agentId: options.agentId
14897
+ });
14898
+ stats.filesProcessed += 1;
14899
+ stats.extracted += result.extracted;
14900
+ stats.written += result.written;
14901
+ stats.skipped += result.skipped;
14902
+ logger.log(
14903
+ `[auto-ingest] file=${source} reason=${reason} extracted=${result.extracted} written=${result.written} skipped=${result.skipped}`
14904
+ );
14905
+ } catch (err) {
14906
+ stats.errors += 1;
14907
+ logger.error(`[auto-ingest] ingest failed for ${toSource(absPath)}: ${String(err)}`);
14908
+ }
14909
+ };
14910
+ const scheduleIngest = (absPath, reason) => {
14911
+ if (stopped) return;
14912
+ if (!isTrackedMarkdownFile(absPath)) return;
14913
+ stats.triggers += 1;
14914
+ const previous = timers.get(absPath);
14915
+ if (previous) clearTimeout(previous);
14916
+ const timer = setTimeout(() => {
14917
+ timers.delete(absPath);
14918
+ queue = queue.then(() => {
14919
+ ingestFile(absPath, reason);
14920
+ }).catch((err) => {
14921
+ stats.errors += 1;
14922
+ logger.error(`[auto-ingest] queue error: ${String(err)}`);
14923
+ });
14924
+ }, debounceMs);
14925
+ timers.set(absPath, timer);
14926
+ };
14927
+ const safeWatch = (dir, onEvent) => {
14928
+ if (!existsSync(dir)) {
14929
+ logger.warn(`[auto-ingest] watch path does not exist, skipping: ${dir}`);
14930
+ return;
14931
+ }
14932
+ try {
14933
+ const watcher = watch(dir, { persistent: true }, (eventType, filename) => {
14934
+ if (!filename) return;
14935
+ onEvent(eventType, filename.toString());
14936
+ });
14937
+ watchers.push(watcher);
14938
+ logger.log(`[auto-ingest] watching ${dir}`);
14939
+ } catch (err) {
14940
+ stats.errors += 1;
14941
+ logger.error(`[auto-ingest] failed to watch ${dir}: ${String(err)}`);
14942
+ }
14943
+ };
14944
+ safeWatch(workspaceDir, (eventType, filename) => {
14945
+ if (filename === "MEMORY.md") {
14946
+ scheduleIngest(join(workspaceDir, filename), `workspace:${eventType}`);
14947
+ }
14948
+ });
14949
+ safeWatch(memoryDir, (eventType, filename) => {
14950
+ if (filename.endsWith(".md")) {
14951
+ scheduleIngest(join(memoryDir, filename), `memory:${eventType}`);
14952
+ }
14953
+ });
14954
+ if (initialScan) {
14955
+ scheduleIngest(memoryMdPath, "initial");
14956
+ if (existsSync(memoryDir)) {
14957
+ for (const file2 of readdirSync(memoryDir)) {
14958
+ if (file2.endsWith(".md")) {
14959
+ scheduleIngest(join(memoryDir, file2), "initial");
14960
+ }
14961
+ }
14962
+ }
14963
+ }
14964
+ return {
14965
+ close: () => {
14966
+ if (stopped) return;
14967
+ stopped = true;
14968
+ for (const timer of timers.values()) {
14969
+ clearTimeout(timer);
14970
+ }
14971
+ timers.clear();
14972
+ for (const watcher of watchers) {
14973
+ try {
14974
+ watcher.close();
14975
+ } catch {
14976
+ }
14977
+ }
14978
+ logger.log(
14979
+ `[auto-ingest] stopped triggers=${stats.triggers} files=${stats.filesProcessed} extracted=${stats.extracted} written=${stats.written} skipped=${stats.skipped} errors=${stats.errors}`
14980
+ );
14981
+ }
14982
+ };
14983
+ }
14984
+
15222
14985
  // src/mcp/server.ts
15223
14986
  var DB_PATH = process.env.AGENT_MEMORY_DB ?? "./agent-memory.db";
15224
14987
  var AGENT_ID = process.env.AGENT_MEMORY_AGENT_ID ?? "default";
14988
+ var PRIORITY_WEIGHT = {
14989
+ 0: 4,
14990
+ 1: 3,
14991
+ 2: 2,
14992
+ 3: 1
14993
+ };
14994
+ function formatMemory(memory, score) {
14995
+ return {
14996
+ id: memory.id,
14997
+ uri: null,
14998
+ content: memory.content,
14999
+ type: memory.type,
15000
+ priority: memory.priority,
15001
+ vitality: memory.vitality,
15002
+ score,
15003
+ updated_at: memory.updated_at
15004
+ };
15005
+ }
15006
+ function formatWarmBootNarrative(identities, emotions, knowledges, events, totalStats) {
15007
+ const now2 = Date.now();
15008
+ const sevenDaysAgo = now2 - 7 * 24 * 60 * 60 * 1e3;
15009
+ const recentEvents = events.filter((e) => new Date(e.updated_at).getTime() >= sevenDaysAgo);
15010
+ const olderEventCount = Math.max(0, events.length - recentEvents.length);
15011
+ const avgVitalitySource = [...identities, ...emotions, ...knowledges, ...events];
15012
+ const avgVitality = avgVitalitySource.length ? avgVitalitySource.reduce((s, m) => s + m.vitality, 0) / avgVitalitySource.length : 0;
15013
+ const lines = [];
15014
+ lines.push("## \u{1FAAA} \u6211\u662F\u8C01");
15015
+ if (identities.length === 0) {
15016
+ lines.push("\u6682\u65E0\u8EAB\u4EFD\u8BB0\u5FC6\u3002");
15017
+ } else {
15018
+ for (const m of identities.slice(0, 6)) {
15019
+ lines.push(`- ${m.content.slice(0, 140)}`);
15020
+ }
15021
+ }
15022
+ lines.push("", "## \u{1F495} \u6700\u8FD1\u7684\u60C5\u611F");
15023
+ if (emotions.length === 0) {
15024
+ lines.push("\u6682\u65E0\u60C5\u611F\u8BB0\u5FC6\u3002");
15025
+ } else {
15026
+ for (const m of emotions.slice(0, 6)) {
15027
+ lines.push(`- ${m.content.slice(0, 140)}\uFF08vitality: ${m.vitality.toFixed(2)}\uFF09`);
15028
+ }
15029
+ }
15030
+ lines.push("", "## \u{1F9E0} \u5173\u952E\u77E5\u8BC6");
15031
+ if (knowledges.length === 0) {
15032
+ lines.push("\u6682\u65E0\u77E5\u8BC6\u8BB0\u5FC6\u3002");
15033
+ } else {
15034
+ lines.push(`\u5171 ${knowledges.length} \u6761\u6D3B\u8DC3\u77E5\u8BC6\u8BB0\u5FC6`);
15035
+ for (const m of knowledges.slice(0, 8)) {
15036
+ lines.push(`- ${m.content.slice(0, 140)}\uFF08vitality: ${m.vitality.toFixed(2)}\uFF09`);
15037
+ }
15038
+ }
15039
+ lines.push("", "## \u{1F4C5} \u8FD1\u671F\u4E8B\u4EF6");
15040
+ if (recentEvents.length === 0) {
15041
+ lines.push("\u6700\u8FD1 7 \u5929\u65E0\u4E8B\u4EF6\u8BB0\u5FC6\u3002");
15042
+ } else {
15043
+ lines.push("\u6700\u8FD1 7 \u5929\u5185\u7684\u4E8B\u4EF6\uFF1A");
15044
+ for (const m of recentEvents.slice(0, 8)) {
15045
+ const dateLabel = m.updated_at.slice(5, 10);
15046
+ lines.push(`- [${dateLabel}] ${m.content.slice(0, 120)}`);
15047
+ }
15048
+ }
15049
+ if (olderEventCount > 0) {
15050
+ lines.push(`- ... \u53CA ${olderEventCount} \u6761\u66F4\u65E9\u4E8B\u4EF6`);
15051
+ }
15052
+ lines.push("", "## \u{1F4CA} \u8BB0\u5FC6\u6982\u51B5");
15053
+ lines.push(
15054
+ `\u603B\u8BA1 ${totalStats.total} \u6761 | identity: ${totalStats.by_type.identity ?? 0} | emotion: ${totalStats.by_type.emotion ?? 0} | knowledge: ${totalStats.by_type.knowledge ?? 0} | event: ${totalStats.by_type.event ?? 0}`
15055
+ );
15056
+ lines.push(`\u5E73\u5747 vitality: ${avgVitality.toFixed(2)}`);
15057
+ return lines.join("\n");
15058
+ }
15059
+ function getSummaryStats(db, agentId) {
15060
+ const row = db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories WHERE agent_id = ?").get(agentId);
15061
+ return { total: row.total, avgVitality: row.avg };
15062
+ }
15063
+ function getMemoryUri(db, memoryId, agentId) {
15064
+ const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
15065
+ return row?.uri ?? "(no-uri)";
15066
+ }
15067
+ function formatReflectReport(input) {
15068
+ const lines = [];
15069
+ lines.push("## \u{1F319} Sleep Cycle \u62A5\u544A", "");
15070
+ if (input.phase === "decay" || input.phase === "all") {
15071
+ const decay = input.decaySummary ?? { updated: 0, decayed: 0, belowThreshold: 0 };
15072
+ lines.push("### Decay\uFF08\u8870\u51CF\uFF09");
15073
+ lines.push(`\u5904\u7406 ${decay.updated} \u6761\u8BB0\u5FC6\uFF0C\u5176\u4E2D ${decay.decayed} \u6761 vitality \u4E0B\u964D\u3002`);
15074
+ const details = (input.decayDetails ?? []).slice(0, 8);
15075
+ if (details.length > 0) {
15076
+ for (const d of details) {
15077
+ lines.push(`- ${d.uri} | ${d.type} P${d.priority} | ${d.oldVitality.toFixed(2)} \u2192 ${d.newVitality.toFixed(2)} | ${d.content.slice(0, 64)}`);
15078
+ }
15079
+ if ((input.decayDetails?.length ?? 0) > details.length) {
15080
+ lines.push(`- ... \u53CA ${(input.decayDetails?.length ?? 0) - details.length} \u6761\u66F4\u591A\u8870\u51CF\u8BB0\u5F55`);
15081
+ }
15082
+ }
15083
+ lines.push("");
15084
+ }
15085
+ if (input.phase === "tidy" || input.phase === "all") {
15086
+ const tidy = input.tidySummary ?? { archived: 0, orphansCleaned: 0 };
15087
+ lines.push("### Tidy\uFF08\u6574\u7406\uFF09");
15088
+ lines.push(`\u5F52\u6863 ${tidy.archived} \u6761\u4F4E\u6D3B\u529B\u8BB0\u5FC6\uFF0C\u6E05\u7406\u5B64\u513F\u8DEF\u5F84 ${tidy.orphansCleaned} \u6761\u3002`);
15089
+ const archived = (input.archivedDetails ?? []).slice(0, 8);
15090
+ if (archived.length > 0) {
15091
+ for (const a of archived) {
15092
+ lines.push(`- \u5F52\u6863 ${a.uri} | P${a.priority} vitality=${a.vitality.toFixed(2)} | ${a.content.slice(0, 64)}`);
15093
+ }
15094
+ if ((input.archivedDetails?.length ?? 0) > archived.length) {
15095
+ lines.push(`- ... \u53CA ${(input.archivedDetails?.length ?? 0) - archived.length} \u6761\u66F4\u591A\u5F52\u6863\u8BB0\u5F55`);
15096
+ }
15097
+ }
15098
+ lines.push("");
15099
+ }
15100
+ if (input.phase === "govern" || input.phase === "all") {
15101
+ const govern = input.governSummary ?? { orphanPaths: 0, emptyMemories: 0 };
15102
+ lines.push("### Govern\uFF08\u6CBB\u7406\uFF09");
15103
+ lines.push(`\u5B64\u513F\u8DEF\u5F84\uFF1A${govern.orphanPaths} \u6761`);
15104
+ lines.push(`\u7A7A\u8BB0\u5FC6\uFF1A${govern.emptyMemories} \u6761`);
15105
+ lines.push("");
15106
+ }
15107
+ lines.push("### \u{1F4CA} \u603B\u7ED3");
15108
+ const delta = input.after.total - input.before.total;
15109
+ const deltaLabel = delta > 0 ? `+${delta}` : `${delta}`;
15110
+ lines.push(`\u8BB0\u5FC6\u603B\u6570\uFF1A${input.before.total} \u2192 ${input.after.total}\uFF08${deltaLabel}\uFF09`);
15111
+ lines.push(`\u5E73\u5747 vitality\uFF1A${input.before.avgVitality.toFixed(2)} \u2192 ${input.after.avgVitality.toFixed(2)}`);
15112
+ return lines.join("\n");
15113
+ }
15225
15114
  function createMcpServer(dbPath, agentId) {
15226
15115
  const db = openDatabase({ path: dbPath ?? DB_PATH });
15227
15116
  const aid = agentId ?? AGENT_ID;
15228
- const embeddingProvider = getEmbeddingProviderFromEnv();
15229
15117
  const server = new McpServer({
15230
15118
  name: "agent-memory",
15231
- version: "2.1.0"
15119
+ version: "3.0.0"
15232
15120
  });
15233
15121
  server.tool(
15234
15122
  "remember",
@@ -15242,12 +15130,6 @@ function createMcpServer(dbPath, agentId) {
15242
15130
  },
15243
15131
  async ({ content, type, uri, emotion_val, source }) => {
15244
15132
  const result = syncOne(db, { content, type, uri, emotion_val, source, agent_id: aid });
15245
- if (embeddingProvider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
15246
- try {
15247
- await embedMemory(db, result.memoryId, embeddingProvider, { agent_id: aid });
15248
- } catch {
15249
- }
15250
- }
15251
15133
  return {
15252
15134
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15253
15135
  };
@@ -15255,62 +15137,49 @@ function createMcpServer(dbPath, agentId) {
15255
15137
  );
15256
15138
  server.tool(
15257
15139
  "recall",
15258
- "Search memories using intent-aware BM25 search with priority weighting. Automatically classifies query intent (factual/temporal/causal/exploratory).",
15140
+ "Search memories using BM25 full-text retrieval with inline priority\xD7vitality weighting.",
15259
15141
  {
15260
15142
  query: external_exports.string().describe("Search query (natural language)"),
15261
15143
  limit: external_exports.number().default(10).describe("Max results to return")
15262
15144
  },
15263
15145
  async ({ query, limit }) => {
15264
- const { intent, confidence } = classifyIntent(query);
15265
- const strategy = getStrategy(intent);
15266
- const raw = await searchHybrid(db, query, { agent_id: aid, embeddingProvider, limit: limit * 2 });
15267
- const results = rerank(raw, { ...strategy, limit });
15268
- const output = {
15269
- intent,
15270
- confidence,
15271
- count: results.length,
15272
- memories: results.map((r) => ({
15273
- id: r.memory.id,
15274
- content: r.memory.content,
15275
- type: r.memory.type,
15276
- priority: r.memory.priority,
15277
- vitality: r.memory.vitality,
15278
- score: r.score,
15279
- updated_at: r.memory.updated_at
15280
- }))
15281
- };
15282
- for (const r of results) {
15146
+ const expandedLimit = Math.max(limit * 2, limit);
15147
+ const raw = searchBM25(db, query, { agent_id: aid, limit: expandedLimit });
15148
+ const scored = raw.map((r) => {
15149
+ const weight = PRIORITY_WEIGHT[r.memory.priority] ?? 1;
15150
+ const vitality = Math.max(0.1, r.memory.vitality);
15151
+ return {
15152
+ memory: r.memory,
15153
+ score: r.score * weight * vitality
15154
+ };
15155
+ }).sort((a, b) => b.score - a.score).slice(0, limit);
15156
+ for (const r of scored) {
15283
15157
  recordAccess(db, r.memory.id);
15284
15158
  }
15159
+ const output = {
15160
+ count: scored.length,
15161
+ memories: scored.map((r) => formatMemory(r.memory, r.score))
15162
+ };
15285
15163
  return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
15286
15164
  }
15287
15165
  );
15288
15166
  server.tool(
15289
15167
  "recall_path",
15290
- "Read memory at a specific URI path, or list memories under a URI prefix. Supports multi-hop traversal.",
15168
+ "Read memory at a specific URI path, or list memories under a URI prefix.",
15291
15169
  {
15292
15170
  uri: external_exports.string().describe("URI path (e.g. core://user/name) or prefix (e.g. core://user/)"),
15293
- traverse_hops: external_exports.number().default(0).describe("Multi-hop graph traversal depth (0 = direct only)")
15171
+ traverse_hops: external_exports.number().default(0).describe("Traversal depth (deprecated, reserved for compatibility)")
15294
15172
  },
15295
- async ({ uri, traverse_hops }) => {
15173
+ async ({ uri }) => {
15296
15174
  const path = getPathByUri(db, uri, aid);
15297
15175
  if (path) {
15298
15176
  const mem = getMemory(db, path.memory_id);
15299
15177
  if (mem && mem.agent_id === aid) {
15300
15178
  recordAccess(db, mem.id);
15301
- let related = [];
15302
- if (traverse_hops > 0) {
15303
- const hops = traverse(db, mem.id, traverse_hops, aid);
15304
- related = hops.map((h) => {
15305
- const m = getMemory(db, h.id);
15306
- if (!m || m.agent_id !== aid) return { id: h.id, content: "", relation: h.relation, hop: h.hop };
15307
- return { id: h.id, content: m.content, relation: h.relation, hop: h.hop };
15308
- });
15309
- }
15310
15179
  return {
15311
15180
  content: [{
15312
15181
  type: "text",
15313
- text: JSON.stringify({ found: true, memory: mem, related }, null, 2)
15182
+ text: JSON.stringify({ found: true, memory: mem }, null, 2)
15314
15183
  }]
15315
15184
  };
15316
15185
  }
@@ -15320,7 +15189,7 @@ function createMcpServer(dbPath, agentId) {
15320
15189
  const memories = paths.map((p) => {
15321
15190
  const m = getMemory(db, p.memory_id);
15322
15191
  if (!m || m.agent_id !== aid) return { uri: p.uri, content: void 0, type: void 0, priority: void 0 };
15323
- return { uri: p.uri, content: m.content, type: m.type, priority: m.priority };
15192
+ return { uri: p.uri, content: m.content, type: m.type, priority: m.priority, vitality: m.vitality };
15324
15193
  });
15325
15194
  return {
15326
15195
  content: [{
@@ -15334,21 +15203,33 @@ function createMcpServer(dbPath, agentId) {
15334
15203
  );
15335
15204
  server.tool(
15336
15205
  "boot",
15337
- "Load core identity memories (P0) and system://boot entries. Call this when starting a new session.",
15338
- {},
15339
- async () => {
15340
- const result = boot(db, { agent_id: aid });
15341
- const output = {
15342
- count: result.identityMemories.length,
15343
- bootPaths: result.bootPaths,
15344
- memories: result.identityMemories.map((m) => ({
15345
- id: m.id,
15346
- content: m.content,
15347
- type: m.type,
15348
- priority: m.priority
15349
- }))
15350
- };
15351
- return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
15206
+ "Load startup memories. Default output is narrative markdown; pass format=json for legacy output.",
15207
+ {
15208
+ format: external_exports.enum(["narrative", "json"]).default("narrative").optional()
15209
+ },
15210
+ async ({ format }) => {
15211
+ const outputFormat = format ?? "narrative";
15212
+ const base = boot(db, { agent_id: aid });
15213
+ if (outputFormat === "json") {
15214
+ const jsonOutput = {
15215
+ count: base.identityMemories.length,
15216
+ bootPaths: base.bootPaths,
15217
+ memories: base.identityMemories.map((m) => ({
15218
+ id: m.id,
15219
+ content: m.content,
15220
+ type: m.type,
15221
+ priority: m.priority
15222
+ }))
15223
+ };
15224
+ return { content: [{ type: "text", text: JSON.stringify(jsonOutput, null, 2) }] };
15225
+ }
15226
+ const identity = listMemories(db, { agent_id: aid, type: "identity", limit: 12 });
15227
+ const emotion = listMemories(db, { agent_id: aid, type: "emotion", min_vitality: 0.1, limit: 12 }).sort((a, b) => b.vitality - a.vitality);
15228
+ const knowledge = listMemories(db, { agent_id: aid, type: "knowledge", min_vitality: 0.1, limit: 16 }).sort((a, b) => b.vitality - a.vitality);
15229
+ const event = listMemories(db, { agent_id: aid, type: "event", min_vitality: 0, limit: 24 }).sort((a, b) => b.vitality - a.vitality);
15230
+ const stats = countMemories(db, aid);
15231
+ const narrative = formatWarmBootNarrative(identity.length > 0 ? identity : base.identityMemories, emotion, knowledge, event, stats);
15232
+ return { content: [{ type: "text", text: narrative }] };
15352
15233
  }
15353
15234
  );
15354
15235
  server.tool(
@@ -15362,7 +15243,6 @@ function createMcpServer(dbPath, agentId) {
15362
15243
  const mem = getMemory(db, id);
15363
15244
  if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
15364
15245
  if (hard) {
15365
- createSnapshot(db, id, "delete", "forget");
15366
15246
  const { deleteMemory: deleteMemory2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
15367
15247
  deleteMemory2(db, id);
15368
15248
  return { content: [{ type: "text", text: JSON.stringify({ action: "deleted", id }) }] };
@@ -15376,97 +15256,80 @@ function createMcpServer(dbPath, agentId) {
15376
15256
  };
15377
15257
  }
15378
15258
  );
15379
- server.tool(
15380
- "link",
15381
- "Create or query associations between memories (knowledge graph).",
15382
- {
15383
- action: external_exports.enum(["create", "query", "traverse"]).describe("Action to perform"),
15384
- source_id: external_exports.string().optional().describe("Source memory ID"),
15385
- target_id: external_exports.string().optional().describe("Target memory ID (for create)"),
15386
- relation: external_exports.enum(["related", "caused", "reminds", "evolved", "contradicts"]).optional().describe("Relation type"),
15387
- max_hops: external_exports.number().default(2).describe("Max traversal depth (for traverse action)")
15388
- },
15389
- async ({ action, source_id, target_id, relation, max_hops }) => {
15390
- if (action === "create" && source_id && target_id && relation) {
15391
- const link = createLink(db, source_id, target_id, relation, 1, aid);
15392
- return { content: [{ type: "text", text: JSON.stringify({ created: link }) }] };
15393
- }
15394
- if (action === "query" && source_id) {
15395
- const links = getLinks(db, source_id, aid);
15396
- return { content: [{ type: "text", text: JSON.stringify({ links }) }] };
15397
- }
15398
- if (action === "traverse" && source_id) {
15399
- const nodes = traverse(db, source_id, max_hops, aid);
15400
- const detailed = nodes.map((n) => {
15401
- const m = getMemory(db, n.id);
15402
- if (!m || m.agent_id !== aid) return { ...n, content: void 0 };
15403
- return { ...n, content: m.content };
15404
- });
15405
- return { content: [{ type: "text", text: JSON.stringify({ nodes: detailed }) }] };
15406
- }
15407
- return { content: [{ type: "text", text: '{"error": "Invalid action or missing params"}' }] };
15408
- }
15409
- );
15410
- server.tool(
15411
- "snapshot",
15412
- "View or rollback memory snapshots (version history).",
15413
- {
15414
- action: external_exports.enum(["list", "rollback"]).describe("list snapshots or rollback to one"),
15415
- memory_id: external_exports.string().optional().describe("Memory ID (for list)"),
15416
- snapshot_id: external_exports.string().optional().describe("Snapshot ID (for rollback)")
15417
- },
15418
- async ({ action, memory_id, snapshot_id }) => {
15419
- if (action === "list" && memory_id) {
15420
- const mem = getMemory(db, memory_id);
15421
- if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Memory not found"}' }] };
15422
- const snaps = getSnapshots(db, memory_id);
15423
- return { content: [{ type: "text", text: JSON.stringify({ snapshots: snaps }) }] };
15424
- }
15425
- if (action === "rollback" && snapshot_id) {
15426
- const snap = getSnapshot(db, snapshot_id);
15427
- if (!snap) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
15428
- const mem = getMemory(db, snap.memory_id);
15429
- if (!mem || mem.agent_id !== aid) return { content: [{ type: "text", text: '{"error": "Snapshot not found"}' }] };
15430
- const ok = rollback(db, snapshot_id);
15431
- return { content: [{ type: "text", text: JSON.stringify({ rolled_back: ok }) }] };
15432
- }
15433
- return { content: [{ type: "text", text: '{"error": "Invalid action or missing params"}' }] };
15434
- }
15435
- );
15436
15259
  server.tool(
15437
15260
  "reflect",
15438
- "Trigger sleep cycle phases: decay (Ebbinghaus), tidy (archive + cleanup), or govern (orphan removal).",
15261
+ "Trigger sleep cycle phases and return a human-readable markdown report.",
15439
15262
  {
15440
15263
  phase: external_exports.enum(["decay", "tidy", "govern", "all"]).describe("Which sleep phase to run")
15441
15264
  },
15442
15265
  async ({ phase }) => {
15443
- const results = {};
15266
+ const before = getSummaryStats(db, aid);
15267
+ let decaySummary;
15268
+ let tidySummary;
15269
+ let governSummary;
15270
+ const decayDetails = [];
15271
+ const archivedDetails = [];
15444
15272
  if (phase === "decay" || phase === "all") {
15445
- results.decay = runDecay(db, { agent_id: aid });
15273
+ const beforeRows = db.prepare("SELECT id, type, priority, vitality, content FROM memories WHERE agent_id = ?").all(aid);
15274
+ const beforeMap = new Map(beforeRows.map((r) => [r.id, r]));
15275
+ decaySummary = runDecay(db, { agent_id: aid });
15276
+ const afterRows = db.prepare("SELECT id, vitality FROM memories WHERE agent_id = ?").all(aid);
15277
+ for (const row of afterRows) {
15278
+ const prev = beforeMap.get(row.id);
15279
+ if (!prev) continue;
15280
+ if (row.vitality < prev.vitality - 1e-3) {
15281
+ decayDetails.push({
15282
+ uri: getMemoryUri(db, row.id, aid),
15283
+ type: prev.type,
15284
+ priority: prev.priority,
15285
+ oldVitality: prev.vitality,
15286
+ newVitality: row.vitality,
15287
+ content: prev.content
15288
+ });
15289
+ }
15290
+ }
15291
+ decayDetails.sort((a, b) => b.oldVitality - b.newVitality - (a.oldVitality - a.newVitality));
15446
15292
  }
15447
15293
  if (phase === "tidy" || phase === "all") {
15448
- results.tidy = runTidy(db, { agent_id: aid });
15294
+ const candidates = db.prepare("SELECT id, content, vitality, priority FROM memories WHERE agent_id = ? AND vitality < 0.05 AND priority >= 3").all(aid);
15295
+ tidySummary = runTidy(db, { agent_id: aid });
15296
+ for (const c of candidates) {
15297
+ const uriBeforeDelete = getMemoryUri(db, c.id, aid);
15298
+ const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(c.id);
15299
+ if (!exists) {
15300
+ archivedDetails.push({
15301
+ uri: uriBeforeDelete,
15302
+ content: c.content,
15303
+ vitality: c.vitality,
15304
+ priority: c.priority
15305
+ });
15306
+ }
15307
+ }
15449
15308
  }
15450
15309
  if (phase === "govern" || phase === "all") {
15451
- results.govern = runGovern(db, { agent_id: aid });
15452
- }
15453
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
15310
+ governSummary = runGovern(db, { agent_id: aid });
15311
+ }
15312
+ const after = getSummaryStats(db, aid);
15313
+ const report = formatReflectReport({
15314
+ phase,
15315
+ decaySummary,
15316
+ decayDetails,
15317
+ tidySummary,
15318
+ archivedDetails,
15319
+ governSummary,
15320
+ before,
15321
+ after
15322
+ });
15323
+ return { content: [{ type: "text", text: report }] };
15454
15324
  }
15455
15325
  );
15456
15326
  server.tool(
15457
15327
  "status",
15458
- "Get memory system statistics: counts by type/priority, health metrics.",
15328
+ "Get memory system statistics: counts by type/priority and health metrics.",
15459
15329
  {},
15460
15330
  async () => {
15461
15331
  const stats = countMemories(db, aid);
15462
15332
  const lowVitality = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(aid);
15463
- const totalSnapshots = db.prepare(
15464
- `SELECT COUNT(*) as c
15465
- FROM snapshots s
15466
- JOIN memories m ON m.id = s.memory_id
15467
- WHERE m.agent_id = ?`
15468
- ).get(aid);
15469
- const totalLinks = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(aid);
15470
15333
  const totalPaths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(aid);
15471
15334
  return {
15472
15335
  content: [{
@@ -15474,8 +15337,6 @@ function createMcpServer(dbPath, agentId) {
15474
15337
  text: JSON.stringify({
15475
15338
  ...stats,
15476
15339
  paths: totalPaths.c,
15477
- links: totalLinks.c,
15478
- snapshots: totalSnapshots.c,
15479
15340
  low_vitality: lowVitality.c,
15480
15341
  agent_id: aid
15481
15342
  }, null, 2)
@@ -15483,11 +15344,104 @@ function createMcpServer(dbPath, agentId) {
15483
15344
  };
15484
15345
  }
15485
15346
  );
15347
+ server.tool(
15348
+ "ingest",
15349
+ "Extract structured memories from markdown text and write via syncOne().",
15350
+ {
15351
+ text: external_exports.string().describe("Markdown/plain text to ingest"),
15352
+ source: external_exports.string().optional().describe("Source annotation, e.g. memory/2026-02-23.md"),
15353
+ dry_run: external_exports.boolean().default(false).optional().describe("Preview extraction without writing")
15354
+ },
15355
+ async ({ text, source, dry_run }) => {
15356
+ const result = ingestText(db, {
15357
+ text,
15358
+ source,
15359
+ dryRun: dry_run,
15360
+ agentId: aid
15361
+ });
15362
+ return {
15363
+ content: [{
15364
+ type: "text",
15365
+ text: JSON.stringify(result, null, 2)
15366
+ }]
15367
+ };
15368
+ }
15369
+ );
15370
+ server.tool(
15371
+ "surface",
15372
+ "Lightweight readonly memory surfacing: keyword OR search + priority\xD7vitality\xD7hitRatio ranking (no access recording).",
15373
+ {
15374
+ keywords: external_exports.array(external_exports.string()).min(1).describe("Keywords to surface related memories"),
15375
+ limit: external_exports.number().min(1).max(20).default(5).optional().describe("Max results (default 5, max 20)"),
15376
+ types: external_exports.array(external_exports.enum(["identity", "emotion", "knowledge", "event"])).optional().describe("Optional type filter"),
15377
+ min_vitality: external_exports.number().min(0).max(1).default(0.1).optional().describe("Minimum vitality filter")
15378
+ },
15379
+ async ({ keywords, limit, types, min_vitality }) => {
15380
+ const maxResults = limit ?? 5;
15381
+ const minVitality = min_vitality ?? 0.1;
15382
+ const normalizedKeywords = keywords.map((k) => k.trim()).filter(Boolean);
15383
+ const candidates = /* @__PURE__ */ new Map();
15384
+ for (const kw of normalizedKeywords) {
15385
+ const results = searchBM25(db, kw, { agent_id: aid, limit: 50, min_vitality: minVitality });
15386
+ for (const r of results) {
15387
+ const existing = candidates.get(r.memory.id);
15388
+ if (existing) {
15389
+ existing.hits += 1;
15390
+ } else {
15391
+ candidates.set(r.memory.id, { memory: r.memory, hits: 1 });
15392
+ }
15393
+ }
15394
+ }
15395
+ const scored = [...candidates.values()].filter((c) => c.memory.vitality >= minVitality).filter((c) => types?.length ? types.includes(c.memory.type) : true).map((c) => {
15396
+ const weight = PRIORITY_WEIGHT[c.memory.priority] ?? 1;
15397
+ const hitRatio = normalizedKeywords.length > 0 ? c.hits / normalizedKeywords.length : 0;
15398
+ const score = weight * c.memory.vitality * hitRatio;
15399
+ return {
15400
+ memory: c.memory,
15401
+ hits: c.hits,
15402
+ score,
15403
+ hitRatio
15404
+ };
15405
+ }).sort((a, b) => b.score - a.score).slice(0, maxResults);
15406
+ const output = {
15407
+ count: scored.length,
15408
+ results: scored.map((s) => ({
15409
+ id: s.memory.id,
15410
+ uri: getMemoryUri(db, s.memory.id, aid),
15411
+ type: s.memory.type,
15412
+ priority: s.memory.priority,
15413
+ vitality: s.memory.vitality,
15414
+ content: s.memory.content,
15415
+ score: s.score,
15416
+ keyword_hits: s.hits,
15417
+ updated_at: s.memory.updated_at
15418
+ }))
15419
+ };
15420
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
15421
+ }
15422
+ );
15486
15423
  return { server, db };
15487
15424
  }
15488
15425
  async function main() {
15489
- const { server } = createMcpServer();
15426
+ const { server, db } = createMcpServer();
15490
15427
  const transport = new StdioServerTransport();
15428
+ const autoIngestEnabled = process.env.AGENT_MEMORY_AUTO_INGEST !== "0";
15429
+ const workspaceDir = process.env.AGENT_MEMORY_WORKSPACE ?? `${process.env.HOME ?? "."}/.openclaw/workspace`;
15430
+ const agentId = process.env.AGENT_MEMORY_AGENT_ID ?? "default";
15431
+ const watcher = autoIngestEnabled ? runAutoIngestWatcher({
15432
+ db,
15433
+ workspaceDir,
15434
+ agentId
15435
+ }) : null;
15436
+ const shutdown = () => {
15437
+ try {
15438
+ watcher?.close();
15439
+ } catch {
15440
+ }
15441
+ };
15442
+ process.once("SIGINT", shutdown);
15443
+ process.once("SIGTERM", shutdown);
15444
+ process.once("exit", shutdown);
15491
15445
  await server.connect(transport);
15492
15446
  }
15493
15447
  var isMain = process.argv[1]?.includes("server");