@hasna/mementos 0.10.15 → 0.10.17

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.
Files changed (2) hide show
  1. package/dist/mcp/index.js +89 -5
  2. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -9826,6 +9826,73 @@ ${lines.join(`
9826
9826
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
9827
9827
  }
9828
9828
  });
9829
+ server.tool("memory_health", "Comprehensive health check for memories. Detects: stale (old + 0 access), high-importance-forgotten (importance>=7 + not accessed in 60d), and possibly-superseded (newer memory with similar key). Returns actionable summary.", {
9830
+ stale_days: exports_external.coerce.number().optional().describe("Days with no access to consider a memory stale (default: 30)"),
9831
+ forgotten_days: exports_external.coerce.number().optional().describe("Days since access for high-importance memories (default: 60)"),
9832
+ project_id: exports_external.string().optional(),
9833
+ agent_id: exports_external.string().optional(),
9834
+ limit: exports_external.coerce.number().optional().describe("Max per category (default: 10)")
9835
+ }, async (args) => {
9836
+ try {
9837
+ const db = getDatabase();
9838
+ const staleDays = args.stale_days ?? 30;
9839
+ const forgottenDays = args.forgotten_days ?? 60;
9840
+ const limit = args.limit ?? 10;
9841
+ const extraWhere = [
9842
+ ...args.project_id ? ["project_id = ?"] : [],
9843
+ ...args.agent_id ? ["agent_id = ?"] : []
9844
+ ].join(" AND ");
9845
+ const extraParams = [
9846
+ ...args.project_id ? [args.project_id] : [],
9847
+ ...args.agent_id ? [args.agent_id] : []
9848
+ ];
9849
+ const base = `status = 'active' AND pinned = 0${extraWhere ? " AND " + extraWhere : ""}`;
9850
+ const stale = db.prepare(`SELECT id, key, value, importance, scope, created_at FROM memories
9851
+ WHERE ${base} AND access_count = 0 AND created_at < datetime('now', '-${staleDays} days')
9852
+ ORDER BY created_at ASC LIMIT ?`).all(...extraParams, limit);
9853
+ const forgotten = db.prepare(`SELECT id, key, value, importance, scope, accessed_at FROM memories
9854
+ WHERE ${base} AND importance >= 7
9855
+ AND (accessed_at IS NULL OR accessed_at < datetime('now', '-${forgottenDays} days'))
9856
+ ORDER BY importance DESC, COALESCE(accessed_at, created_at) ASC LIMIT ?`).all(...extraParams, limit);
9857
+ const dupes = db.prepare(`SELECT key, COUNT(*) as cnt, MAX(updated_at) as latest, MIN(created_at) as oldest
9858
+ FROM memories WHERE ${base}
9859
+ GROUP BY key HAVING cnt > 1
9860
+ ORDER BY cnt DESC LIMIT ?`).all(...extraParams, limit);
9861
+ const parts = [`Memory Health Report
9862
+ `];
9863
+ if (stale.length > 0) {
9864
+ parts.push(`\u26A0\uFE0F STALE (${stale.length}) \u2014 created ${staleDays}d+ ago, never accessed:`);
9865
+ for (const m of stale) {
9866
+ parts.push(` \u2022 [${m.importance}] ${m.key} (${m.scope}) \u2014 created ${m.created_at.slice(0, 10)}`);
9867
+ }
9868
+ parts.push("");
9869
+ }
9870
+ if (forgotten.length > 0) {
9871
+ parts.push(`\uD83D\uDD14 HIGH-IMPORTANCE FORGOTTEN (${forgotten.length}) \u2014 importance\u22657, not accessed in ${forgottenDays}d+:`);
9872
+ for (const m of forgotten) {
9873
+ parts.push(` \u2022 [${m.importance}] ${m.key} (${m.scope}) \u2014 last: ${m.accessed_at?.slice(0, 10) || "never"}`);
9874
+ }
9875
+ parts.push("");
9876
+ }
9877
+ if (dupes.length > 0) {
9878
+ parts.push(`\uD83D\uDD04 POSSIBLY SUPERSEDED (${dupes.length}) \u2014 same key with multiple versions:`);
9879
+ for (const d of dupes) {
9880
+ parts.push(` \u2022 ${d.key} \xD7 ${d.cnt} copies \u2014 newest: ${d.latest.slice(0, 10)}`);
9881
+ }
9882
+ parts.push("");
9883
+ }
9884
+ if (stale.length === 0 && forgotten.length === 0 && dupes.length === 0) {
9885
+ parts.push("\u2713 No health issues found. All memories look fresh.");
9886
+ } else {
9887
+ parts.push(`Summary: ${stale.length} stale, ${forgotten.length} forgotten, ${dupes.length} possibly-superseded.`);
9888
+ parts.push("Suggested actions: archive stale memories, review forgotten ones, merge duplicates.");
9889
+ }
9890
+ return { content: [{ type: "text", text: parts.join(`
9891
+ `) }] };
9892
+ } catch (e) {
9893
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
9894
+ }
9895
+ });
9829
9896
  server.tool("memory_search", "Search memories by keyword across key, value, summary, and tags", {
9830
9897
  query: exports_external.string(),
9831
9898
  scope: exports_external.enum(["global", "shared", "private"]).optional(),
@@ -10753,11 +10820,13 @@ Summary: ${newMems.length} new, ${updatedMems.length} updated, ${expiredMems.len
10753
10820
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
10754
10821
  }
10755
10822
  });
10756
- server.tool("memory_context", "Get memories relevant to current context, filtered by scope/importance/recency.", {
10823
+ server.tool("memory_context", "Get memories relevant to current context. Uses time-weighted scoring: score = importance \xD7 decay(age). Pinned memories are exempt. Returns effective_score on each memory.", {
10757
10824
  agent_id: exports_external.string().optional(),
10758
10825
  project_id: exports_external.string().optional(),
10759
10826
  scope: exports_external.enum(["global", "shared", "private"]).optional(),
10760
- limit: exports_external.coerce.number().optional()
10827
+ limit: exports_external.coerce.number().optional(),
10828
+ decay_halflife_days: exports_external.coerce.number().optional().describe("Importance half-life in days (default: 90). Lower = more weight on recent memories."),
10829
+ no_decay: exports_external.coerce.boolean().optional().describe("Set true to disable decay and sort purely by importance.")
10761
10830
  }, async (args) => {
10762
10831
  try {
10763
10832
  const filter = {
@@ -10765,16 +10834,31 @@ server.tool("memory_context", "Get memories relevant to current context, filtere
10765
10834
  agent_id: args.agent_id,
10766
10835
  project_id: args.project_id,
10767
10836
  status: "active",
10768
- limit: args.limit || 30
10837
+ limit: (args.limit || 30) * 2
10769
10838
  };
10770
10839
  const memories = listMemories(filter);
10771
10840
  if (memories.length === 0) {
10772
10841
  return { content: [{ type: "text", text: "No memories in current context." }] };
10773
10842
  }
10774
- for (const m of memories) {
10843
+ const halflifeDays = args.decay_halflife_days ?? 90;
10844
+ const now2 = Date.now();
10845
+ const scored = memories.map((m) => {
10846
+ let effectiveScore = m.importance;
10847
+ if (!args.no_decay && !m.pinned) {
10848
+ const ageMs = now2 - new Date(m.updated_at).getTime();
10849
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
10850
+ const decayFactor = Math.pow(0.5, ageDays / halflifeDays);
10851
+ effectiveScore = m.importance * decayFactor;
10852
+ }
10853
+ return { ...m, effective_score: Math.round(effectiveScore * 100) / 100 };
10854
+ });
10855
+ const limit = args.limit || 30;
10856
+ scored.sort((a, b) => b.effective_score - a.effective_score);
10857
+ const top = scored.slice(0, limit);
10858
+ for (const m of top) {
10775
10859
  touchMemory(m.id);
10776
10860
  }
10777
- const lines = memories.map((m) => `[${m.scope}/${m.category}] ${m.key}: ${m.value} (importance: ${m.importance})`);
10861
+ const lines = top.map((m) => `[${m.scope}/${m.category}] ${m.key}: ${m.value} (score: ${m.effective_score}, raw: ${m.importance}${m.pinned ? ", pinned" : ""})`);
10778
10862
  return { content: [{ type: "text", text: lines.join(`
10779
10863
  `) }] };
10780
10864
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.10.15",
3
+ "version": "0.10.17",
4
4
  "description": "Universal memory system for AI agents - CLI + MCP server + library API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",