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:
@@ -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"],
|
||||
|
||||
@@ -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 "?")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user