@smyslenny/agent-memory 4.0.0-alpha.1 → 4.2.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.
@@ -89,6 +89,11 @@ function migrateDatabase(db, from, to) {
89
89
  v = 5;
90
90
  continue;
91
91
  }
92
+ if (v === 5) {
93
+ migrateV5ToV6(db);
94
+ v = 6;
95
+ continue;
96
+ }
92
97
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
93
98
  }
94
99
  }
@@ -174,6 +179,8 @@ function inferSchemaVersion(db) {
174
179
  const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
175
180
  const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
176
181
  const hasFeedbackEvents = tableExists(db, "feedback_events");
182
+ const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
183
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
177
184
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
178
185
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
179
186
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
@@ -200,6 +207,9 @@ function ensureIndexes(db) {
200
207
  if (tableExists(db, "maintenance_jobs")) {
201
208
  db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
202
209
  }
210
+ if (tableHasColumn(db, "memories", "emotion_tag")) {
211
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
212
+ }
203
213
  if (tableExists(db, "feedback_events")) {
204
214
  db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
205
215
  if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
@@ -333,11 +343,30 @@ function migrateV4ToV5(db) {
333
343
  throw e;
334
344
  }
335
345
  }
346
+ function migrateV5ToV6(db) {
347
+ if (tableHasColumn(db, "memories", "emotion_tag")) {
348
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
349
+ return;
350
+ }
351
+ try {
352
+ db.exec("BEGIN");
353
+ db.exec("ALTER TABLE memories ADD COLUMN emotion_tag TEXT;");
354
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
355
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
356
+ db.exec("COMMIT");
357
+ } catch (e) {
358
+ try {
359
+ db.exec("ROLLBACK");
360
+ } catch {
361
+ }
362
+ throw e;
363
+ }
364
+ }
336
365
  var SCHEMA_VERSION, SCHEMA_SQL;
337
366
  var init_db = __esm({
338
367
  "src/core/db.ts"() {
339
368
  "use strict";
340
- SCHEMA_VERSION = 5;
369
+ SCHEMA_VERSION = 6;
341
370
  SCHEMA_SQL = `
342
371
  -- Memory entries
343
372
  CREATE TABLE IF NOT EXISTS memories (
@@ -355,6 +384,7 @@ CREATE TABLE IF NOT EXISTS memories (
355
384
  source TEXT,
356
385
  agent_id TEXT NOT NULL DEFAULT 'default',
357
386
  hash TEXT,
387
+ emotion_tag TEXT,
358
388
  UNIQUE(hash, agent_id)
359
389
  );
360
390
 
@@ -934,8 +964,8 @@ function createMemory(db, input) {
934
964
  const timestamp = now();
935
965
  db.prepare(
936
966
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
937
- access_count, created_at, updated_at, source, agent_id, hash)
938
- VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
967
+ access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
968
+ VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
939
969
  ).run(
940
970
  id,
941
971
  input.content,
@@ -947,7 +977,8 @@ function createMemory(db, input) {
947
977
  timestamp,
948
978
  input.source ?? null,
949
979
  agentId,
950
- hash2
980
+ hash2,
981
+ input.emotion_tag ?? null
951
982
  );
952
983
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
953
984
  markEmbeddingDirtyIfNeeded(db, id, hash2, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -991,6 +1022,10 @@ function updateMemory(db, id, input) {
991
1022
  fields.push("source = ?");
992
1023
  values.push(input.source);
993
1024
  }
1025
+ if (input.emotion_tag !== void 0) {
1026
+ fields.push("emotion_tag = ?");
1027
+ values.push(input.emotion_tag);
1028
+ }
994
1029
  fields.push("updated_at = ?");
995
1030
  values.push(now());
996
1031
  values.push(id);
@@ -1032,6 +1067,10 @@ function listMemories(db, opts) {
1032
1067
  conditions.push("vitality >= ?");
1033
1068
  params.push(opts.min_vitality);
1034
1069
  }
1070
+ if (opts?.emotion_tag) {
1071
+ conditions.push("emotion_tag = ?");
1072
+ params.push(opts.emotion_tag);
1073
+ }
1035
1074
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1036
1075
  const limit = opts?.limit ?? 100;
1037
1076
  const offset = opts?.offset ?? 0;
@@ -14901,8 +14940,68 @@ function getPathsByPrefix(db, prefix, agent_id = "default") {
14901
14940
 
14902
14941
  // src/sleep/boot.ts
14903
14942
  init_memory();
14943
+ function formatRelativeDate(isoDate) {
14944
+ const diffMs = Date.now() - new Date(isoDate).getTime();
14945
+ const diffDays = Math.floor(diffMs / 864e5);
14946
+ if (diffDays <= 0) return "\u4ECA\u5929";
14947
+ if (diffDays === 1) return "\u6628\u5929";
14948
+ if (diffDays <= 7) return `${diffDays}\u5929\u524D`;
14949
+ return isoDate.slice(0, 10);
14950
+ }
14951
+ function loadWarmBootLayers(db, agentId) {
14952
+ const identity = listMemories(db, { agent_id: agentId, type: "identity", limit: 50 });
14953
+ const emotion = listMemories(db, { agent_id: agentId, type: "emotion", limit: 5 });
14954
+ const event = listMemories(db, { agent_id: agentId, type: "event", limit: 7 });
14955
+ const knowledge = listMemories(db, {
14956
+ agent_id: agentId,
14957
+ type: "knowledge",
14958
+ min_vitality: 0.5,
14959
+ limit: 10
14960
+ });
14961
+ return { identity, emotion, event, knowledge };
14962
+ }
14963
+ function formatNarrativeBoot(layers, agentName) {
14964
+ const lines = [];
14965
+ lines.push(`# ${agentName}\u7684\u56DE\u5FC6`);
14966
+ lines.push("");
14967
+ if (layers.identity.length > 0) {
14968
+ lines.push("## \u6211\u662F\u8C01");
14969
+ for (const mem of layers.identity) {
14970
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
14971
+ }
14972
+ lines.push("");
14973
+ }
14974
+ if (layers.emotion.length > 0) {
14975
+ lines.push("## \u6700\u8FD1\u7684\u5FC3\u60C5");
14976
+ for (const mem of layers.emotion) {
14977
+ const tag = mem.emotion_tag;
14978
+ const time3 = formatRelativeDate(mem.updated_at);
14979
+ const tagStr = tag ? `${tag}, ${time3}` : time3;
14980
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${tagStr})`);
14981
+ }
14982
+ lines.push("");
14983
+ }
14984
+ if (layers.event.length > 0) {
14985
+ lines.push("## \u6700\u8FD1\u53D1\u751F\u7684\u4E8B");
14986
+ for (const mem of layers.event) {
14987
+ const time3 = formatRelativeDate(mem.updated_at);
14988
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${time3})`);
14989
+ }
14990
+ lines.push("");
14991
+ }
14992
+ if (layers.knowledge.length > 0) {
14993
+ lines.push("## \u8FD8\u8BB0\u5F97\u7684\u77E5\u8BC6");
14994
+ for (const mem of layers.knowledge) {
14995
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
14996
+ }
14997
+ lines.push("");
14998
+ }
14999
+ return lines.join("\n");
15000
+ }
14904
15001
  function boot(db, opts) {
14905
15002
  const agentId = opts?.agent_id ?? "default";
15003
+ const format = opts?.format ?? "json";
15004
+ const agentName = opts?.agent_name ?? "Agent";
14906
15005
  const corePaths = opts?.corePaths ?? [
14907
15006
  "core://agent",
14908
15007
  "core://user",
@@ -14946,10 +15045,16 @@ function boot(db, opts) {
14946
15045
  }
14947
15046
  }
14948
15047
  }
14949
- return {
15048
+ const result = {
14950
15049
  identityMemories: [...memories.values()],
14951
15050
  bootPaths
14952
15051
  };
15052
+ if (format === "narrative") {
15053
+ const layers = loadWarmBootLayers(db, agentId);
15054
+ result.layers = layers;
15055
+ result.narrative = formatNarrativeBoot(layers, agentName);
15056
+ }
15057
+ return result;
14953
15058
  }
14954
15059
 
14955
15060
  // src/sleep/sync.ts
@@ -15436,9 +15541,11 @@ function fourCriterionGate(input) {
15436
15541
  const content = input.content.trim();
15437
15542
  const failed = [];
15438
15543
  const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
15439
- const minLength = priority <= 1 ? 4 : 8;
15440
- const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
15441
- if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
15544
+ const cjkCount = (content.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) ?? []).length;
15545
+ const effectiveLength = content.length + cjkCount * 2;
15546
+ const minLength = priority <= 1 ? 4 : 20;
15547
+ const specificity = effectiveLength >= minLength ? Math.min(1, effectiveLength / 50) : 0;
15548
+ if (specificity === 0) failed.push(`specificity (too short: effective ${effectiveLength} < ${minLength} chars)`);
15442
15549
  const tokens = tokenize(content);
15443
15550
  const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
15444
15551
  if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
@@ -15550,7 +15657,8 @@ async function syncOne(db, input) {
15550
15657
  agent_id: input.agent_id,
15551
15658
  uri: input.uri,
15552
15659
  provider: input.provider,
15553
- conservative: input.conservative
15660
+ conservative: input.conservative,
15661
+ emotion_tag: input.emotion_tag
15554
15662
  };
15555
15663
  const guardResult = await guard(db, memInput);
15556
15664
  switch (guardResult.action) {
@@ -15614,6 +15722,7 @@ function extractIngestItems(text, source) {
15614
15722
  const bulletItems = block.body.split(/\n+/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
15615
15723
  const lines = bulletItems.length > 0 ? bulletItems : [block.body.trim()];
15616
15724
  for (const line of lines) {
15725
+ if (isIngestNoise(line)) continue;
15617
15726
  index += 1;
15618
15727
  const uri = type === "identity" ? `core://ingest/${slugify2(block.title)}/${index}` : `${type}://${slugify2(source ?? "ingest")}/${index}`;
15619
15728
  items.push({
@@ -15626,6 +15735,31 @@ function extractIngestItems(text, source) {
15626
15735
  }
15627
15736
  return items;
15628
15737
  }
15738
+ var NOISE_PATTERNS = [
15739
+ /HEARTBEAT_OK/i,
15740
+ /安静模式/,
15741
+ /安静待命/,
15742
+ /不打扰/,
15743
+ /无新.{0,4}(delta|变化|事项|进展)/,
15744
+ /无变化/,
15745
+ /无紧急/,
15746
+ /深夜时段/,
15747
+ /继续安静/,
15748
+ /openclaw (status|gateway status|security audit|update status)/i,
15749
+ /session_status/,
15750
+ /危险区协议/,
15751
+ /没有紧急/,
15752
+ /没有新增状态变化/,
15753
+ /仍为.*critical/,
15754
+ /基线(未变|不变|仍)/,
15755
+ /PR\s*#\d+\s*(无变化|状态无变化)/,
15756
+ /距上次心跳/,
15757
+ /轻量复查/,
15758
+ /cron 会话.*\d+k/
15759
+ ];
15760
+ function isIngestNoise(content) {
15761
+ return NOISE_PATTERNS.some((pattern) => pattern.test(content));
15762
+ }
15629
15763
  async function ingestText(db, options) {
15630
15764
  const extracted = extractIngestItems(options.text, options.source);
15631
15765
  const dryRun = options.dryRun ?? false;
@@ -15830,21 +15964,26 @@ async function rememberMemory(db, input) {
15830
15964
  source: input.source,
15831
15965
  agent_id: input.agent_id,
15832
15966
  provider: input.provider,
15833
- conservative: input.conservative
15967
+ conservative: input.conservative,
15968
+ emotion_tag: input.emotion_tag
15834
15969
  });
15835
15970
  }
15836
15971
 
15837
15972
  // src/app/recall.ts
15838
15973
  async function recallMemory(db, input) {
15839
- return recallMemories(db, input.query, {
15974
+ const result = await recallMemories(db, input.query, {
15840
15975
  agent_id: input.agent_id,
15841
- limit: input.limit,
15976
+ limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
15842
15977
  min_vitality: input.min_vitality,
15843
15978
  lexicalLimit: input.lexicalLimit,
15844
15979
  vectorLimit: input.vectorLimit,
15845
15980
  provider: input.provider,
15846
15981
  recordAccess: input.recordAccess
15847
15982
  });
15983
+ if (input.emotion_tag) {
15984
+ result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
15985
+ }
15986
+ return result;
15848
15987
  }
15849
15988
 
15850
15989
  // src/app/surface.ts
@@ -16093,7 +16232,7 @@ async function surfaceMemories(db, input) {
16093
16232
  signals.set(memory.id, { memory });
16094
16233
  }
16095
16234
  }
16096
- const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).map((memory) => {
16235
+ const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).filter((memory) => input.emotion_tag ? memory.emotion_tag === input.emotion_tag : true).map((memory) => {
16097
16236
  const signal = signals.get(memory.id) ?? { memory };
16098
16237
  const memoryTokens = new Set(tokenize(memory.content));
16099
16238
  const lexicalOverlap = overlapScore2(memoryTokens, queryTokens);
@@ -16204,9 +16343,9 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
16204
16343
  const agentId = opts?.agent_id;
16205
16344
  return db.prepare(
16206
16345
  agentId ? `SELECT id, content, vitality, priority FROM memories
16207
- WHERE vitality < ? AND priority >= 3 AND agent_id = ?
16346
+ WHERE vitality < ? AND priority >= 2 AND agent_id = ?
16208
16347
  ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
16209
- WHERE vitality < ? AND priority >= 3
16348
+ WHERE vitality < ? AND priority >= 2
16210
16349
  ORDER BY vitality ASC`
16211
16350
  ).all(...agentId ? [threshold, agentId] : [threshold]);
16212
16351
  }
@@ -16304,7 +16443,8 @@ function rankEvictionCandidates(db, opts) {
16304
16443
  }
16305
16444
  function runGovern(db, opts) {
16306
16445
  const agentId = opts?.agent_id;
16307
- const maxMemories = opts?.maxMemories ?? 200;
16446
+ const envMax = Number.parseInt(process.env.AGENT_MEMORY_MAX_MEMORIES ?? "", 10);
16447
+ const maxMemories = opts?.maxMemories ?? (Number.isFinite(envMax) && envMax > 0 ? envMax : 200);
16308
16448
  let orphanPaths = 0;
16309
16449
  let emptyMemories = 0;
16310
16450
  let evicted = 0;
@@ -16763,10 +16903,11 @@ function createMcpServer(dbPath, agentId) {
16763
16903
  uri: external_exports.string().optional().describe("URI path (e.g. core://user/name, emotion://2026-02-20/love)"),
16764
16904
  emotion_val: external_exports.number().min(-1).max(1).default(0).describe("Emotional valence (-1 negative to +1 positive)"),
16765
16905
  source: external_exports.string().optional().describe("Source annotation (e.g. session ID, date)"),
16766
- agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
16906
+ agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16907
+ emotion_tag: external_exports.string().optional().describe("Emotion label for emotion-type memories (e.g. \u5B89\u5FC3, \u5F00\u5FC3, \u62C5\u5FC3)")
16767
16908
  },
16768
- async ({ content, type, uri, emotion_val, source, agent_id }) => {
16769
- const result = await rememberMemory(db, { content, type, uri, emotion_val, source, agent_id: agent_id ?? aid });
16909
+ async ({ content, type, uri, emotion_val, source, agent_id, emotion_tag }) => {
16910
+ const result = await rememberMemory(db, { content, type, uri, emotion_val, source, agent_id: agent_id ?? aid, emotion_tag });
16770
16911
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
16771
16912
  }
16772
16913
  );
@@ -16776,10 +16917,11 @@ function createMcpServer(dbPath, agentId) {
16776
16917
  {
16777
16918
  query: external_exports.string().describe("Search query (natural language)"),
16778
16919
  limit: external_exports.number().default(10).describe("Max results to return"),
16779
- agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)")
16920
+ agent_id: external_exports.string().optional().describe("Override agent scope (defaults to current agent)"),
16921
+ emotion_tag: external_exports.string().optional().describe("Filter results by emotion tag (e.g. \u5B89\u5FC3, \u5F00\u5FC3)")
16780
16922
  },
16781
- async ({ query, limit, agent_id }) => {
16782
- const result = await recallMemory(db, { query, limit, agent_id: agent_id ?? aid });
16923
+ async ({ query, limit, agent_id, emotion_tag }) => {
16924
+ const result = await recallMemory(db, { query, limit, agent_id: agent_id ?? aid, emotion_tag });
16783
16925
  return { content: [{ type: "text", text: JSON.stringify(formatRecallPayload(result), null, 2) }] };
16784
16926
  }
16785
16927
  );
@@ -16827,19 +16969,20 @@ function createMcpServer(dbPath, agentId) {
16827
16969
  "boot",
16828
16970
  "Load startup memories. Default output is narrative markdown; pass format=json for legacy output.",
16829
16971
  {
16830
- format: external_exports.enum(["narrative", "json"]).default("narrative").optional()
16972
+ format: external_exports.enum(["narrative", "json"]).default("narrative").optional(),
16973
+ agent_name: external_exports.string().optional().describe("Agent name for narrative header (default: Agent)")
16831
16974
  },
16832
- async ({ format }) => {
16975
+ async ({ format, agent_name }) => {
16833
16976
  const outputFormat = format ?? "narrative";
16834
- const base = boot(db, { agent_id: aid });
16977
+ const result = boot(db, { agent_id: aid, format: outputFormat, agent_name: agent_name ?? void 0 });
16835
16978
  if (outputFormat === "json") {
16836
16979
  return {
16837
16980
  content: [{
16838
16981
  type: "text",
16839
16982
  text: JSON.stringify({
16840
- count: base.identityMemories.length,
16841
- bootPaths: base.bootPaths,
16842
- memories: base.identityMemories.map((memory) => ({
16983
+ count: result.identityMemories.length,
16984
+ bootPaths: result.bootPaths,
16985
+ memories: result.identityMemories.map((memory) => ({
16843
16986
  id: memory.id,
16844
16987
  content: memory.content,
16845
16988
  type: memory.type,
@@ -16849,12 +16992,15 @@ function createMcpServer(dbPath, agentId) {
16849
16992
  }]
16850
16993
  };
16851
16994
  }
16995
+ if (result.narrative) {
16996
+ return { content: [{ type: "text", text: result.narrative }] };
16997
+ }
16852
16998
  const identity = listMemories(db, { agent_id: aid, type: "identity", limit: 12 });
16853
16999
  const emotion = listMemories(db, { agent_id: aid, type: "emotion", min_vitality: 0.1, limit: 12 }).sort((a, b) => b.vitality - a.vitality);
16854
17000
  const knowledge = listMemories(db, { agent_id: aid, type: "knowledge", min_vitality: 0.1, limit: 16 }).sort((a, b) => b.vitality - a.vitality);
16855
17001
  const event = listMemories(db, { agent_id: aid, type: "event", min_vitality: 0, limit: 24 }).sort((a, b) => b.vitality - a.vitality);
16856
17002
  const stats = countMemories(db, aid);
16857
- return { content: [{ type: "text", text: formatWarmBootNarrative(identity.length > 0 ? identity : base.identityMemories, emotion, knowledge, event, stats) }] };
17003
+ return { content: [{ type: "text", text: formatWarmBootNarrative(identity.length > 0 ? identity : result.identityMemories, emotion, knowledge, event, stats) }] };
16858
17004
  }
16859
17005
  );
16860
17006
  server.tool(