fix(mem0-plugin): fix 3 critical + 5 important issues from review

Critical fixes:
- C1: Pin/dream update_memory now reads content first via get_memory
  before updating, preventing metadata-only calls that wipe content
- C2: SessionStart memory count uses GET /v3/memories/?page_size=1 for
  real count from pagination envelope instead of search limit=100
- C3: on_task_completed.sh uses ${MEM0_PROJECT_ID:-unknown} to avoid
  crash when identity resolution fails under set -u

Important fixes:
- I1: session_stats.py tracks recent_ids (memory IDs + timestamps) for
  /mem0:forget undo support. Capped at 50 entries.
- I2: /mem0:health --deep mode adds memory quality analysis: duplicate
  detection, stale entry scan, contradiction flags, orphan tagging
- I3: /mem0:stats ASCII histogram for category distribution with bar
  chart, sorted by count descending
- I4: mem0-mcp skill documents access counter pattern: get_memory then
  update_memory to track which memories are actually used
- I5: on_pre_commit.py reads COMMIT_EDITMSG first (current commit),
  falls back to HEAD only if EDITMSG unavailable

Tests: 91 -> 94 (3 new for recent_ids tracking)
This commit is contained in:
kartik-mem0
2026-05-21 18:46:08 +05:30
parent 70f2f60727
commit 1daa7ece66
11 changed files with 220 additions and 38 deletions
+19
View File
@@ -36,6 +36,25 @@ except ImportError:
def get_commit_message() -> str:
"""Read the current commit message from COMMIT_EDITMSG (pre-commit context).
Falls back to HEAD's message if COMMIT_EDITMSG doesn't exist yet
(e.g., when invoked outside the git hook context).
"""
try:
git_dir = subprocess.run(
["git", "rev-parse", "--git-dir"],
capture_output=True, text=True, timeout=5,
)
if git_dir.returncode == 0:
editmsg = os.path.join(git_dir.stdout.strip(), "COMMIT_EDITMSG")
if os.path.isfile(editmsg):
with open(editmsg) as f:
first_line = f.readline().strip()
if first_line and not first_line.startswith("#"):
return first_line
except Exception:
pass
try:
result = subprocess.run(
["git", "log", "-1", "--format=%s", "HEAD"],
+12 -16
View File
@@ -39,29 +39,25 @@ import json, os, urllib.request, urllib.error
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
app_id = os.environ.get('MEM0_PROJECT_ID', '')
body = json.dumps({
'query': 'project context',
'user_id': user_id,
'filters': {'AND': [{'user_id': user_id}, {'app_id': app_id}]},
'limit': 100,
}).encode()
# Use GET /v3/memories/ with page_size=1 to get total count from pagination
# without fetching all memories. The response envelope includes a count field.
params = f'user_id={user_id}&app_id={app_id}&page_size=1'
req = urllib.request.Request(
'https://api.mem0.ai/v3/memories/search/',
data=body,
headers={'Authorization': f'Token {api_key}', 'Content-Type': 'application/json'},
method='POST',
f'https://api.mem0.ai/v3/memories/?{params}',
headers={'Authorization': f'Token {api_key}'},
method='GET',
)
try:
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
if isinstance(data, list):
results = data
if isinstance(data, dict) and 'count' in data:
print(data['count'])
elif isinstance(data, list):
print(len(data))
elif isinstance(data, dict) and 'results' in data:
results = data['results']
print(len(data['results']))
else:
print('?'); raise SystemExit
n = len(results)
print(f'{n}+' if n >= 100 else str(n))
print('?')
except Exception:
print('?')
" 2>/dev/null || echo "?")
+6 -3
View File
@@ -7,18 +7,21 @@
# Input: JSON on stdin with task_id, task_subject, task_description
# Output: Text that becomes feedback to the model (exit 0)
set -euo pipefail
# Intentionally omit -e so the reminder always emits even if identity resolution fails.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" || true
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
_PROJECT="${MEM0_PROJECT_ID:-unknown}"
cat <<EOF
Task completed: "$TASK_SUBJECT"
@@ -31,7 +34,7 @@ Extract key learnings from this completed task and store them using the mem0 \`a
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
Only store genuinely useful learnings — skip if the task was trivial.
Include \`app_id\` = \`"$MEM0_PROJECT_ID"\` as a top-level parameter in every \`add_memory\` call (not in metadata).
Include \`app_id\` = \`"$_PROJECT"\` as a top-level parameter in every \`add_memory\` call (not in metadata).
EOF
exit 0
+12 -2
View File
@@ -42,17 +42,21 @@ def _save(stats: dict) -> None:
json.dump(stats, f)
MAX_RECENT_IDS = 50
def init() -> None:
_save({
"adds": 0,
"searches": 0,
"categories": [],
"category_counts": {},
"recent_ids": [],
"started": datetime.now().isoformat(),
})
def record_add(category: str = "") -> None:
def record_add(category: str = "", memory_id: str = "") -> None:
stats = _load()
stats["adds"] = stats.get("adds", 0) + 1
if category:
@@ -60,6 +64,11 @@ def record_add(category: str = "") -> None:
stats.setdefault("categories", []).append(category)
counts = stats.setdefault("category_counts", {})
counts[category] = counts.get(category, 0) + 1
if memory_id:
recent = stats.setdefault("recent_ids", [])
recent.append({"id": memory_id, "category": category, "ts": datetime.now().isoformat()})
if len(recent) > MAX_RECENT_IDS:
stats["recent_ids"] = recent[-MAX_RECENT_IDS:]
_save(stats)
@@ -108,7 +117,8 @@ def main() -> int:
init()
elif cmd == "add":
category = sys.argv[2] if len(sys.argv) > 2 else ""
record_add(category)
memory_id = sys.argv[3] if len(sys.argv) > 3 else ""
record_add(category, memory_id)
elif cmd == "search":
record_search()
elif cmd == "peek":