Compare commits

...

6 Commits

Author SHA1 Message Date
Mgeeeek d50121cdf6 Merge remote-tracking branch 'origin/main' into fix/plugin-hook-cleanup 2026-05-09 19:09:20 +05:30
Mgeeeek 8b36f7c50e fix(plugin): expire stale state captures + document plugin-update restart
Memory expiration + recency
  Hook-side captures (session_state in on_pre_compact.py and
  compact_summary in capture_compact_summary.py) now set
  expiration_date = today + 90 days. These types describe a single
  moment of project state and become stale clutter after a quarter.
  Durable types written by the agent (decision, anti_pattern,
  convention, user_preference, task_learning, environmental) stay
  unexpired by design -- a decision from a year ago is still a
  decision.

  Skill updated with two new sections:
    - "Expiration: high-churn vs durable" -- table mapping each
      metadata.type to whether it should carry an expiration_date.
    - "Recency filter on recall" -- agent should layer a
      created_at >= <90 days ago> filter on recall when the user
      asks about *current* state, and skip it when looking for
      durable facts.

Plugin-update / MCP reconnect
  When the plugin updates, the MCP server connection in the running
  client goes stale -- there's no in-process reconnection path from
  inside the plugin. Documenting the workaround is the realistic
  fix: README now has an "Updating the plugin" section explaining
  /restart (Claude Code), quit-and-relaunch (Cursor), restart
  (Codex). The auth header re-reads MEM0_API_KEY from the new
  shell, so users don't have to re-enter the key.
2026-05-08 21:39:28 +05:30
Mgeeeek 2946fc9da5 feat(plugin): coding-focused custom-category setup script
mem0 auto-tags every memory with one or more `categories` from a
project-level list. The default list is consumer-oriented (food,
hobbies, music, ...), which produces meaningless tags for code
workflows.

Per-request `custom_categories` is not supported on the managed API,
so the fix has to be a one-time project-level configuration. New
script `setup_coding_categories.py` does this:

  - Dry-run by default: prints current vs proposed taxonomy, exits.
  - `--apply` flag actually calls `project.update(custom_categories=...)`.
  - Falls back gracefully when mem0ai SDK isn't installed or
    MEM0_API_KEY is missing/invalid (friendly error, no stack trace).

Recommended taxonomy:
  architecture_decisions, anti_patterns, task_learnings,
  tooling_setup, bug_fixes, coding_conventions, user_preferences

Skill clarification: `metadata.type` (agent-applied explicit tag,
used in filters) and `categories` (platform-applied auto-tag, used
in dashboards) are complementary; agent should keep using
`metadata.type` and not try to set `categories` per-request.

README: section under Step 2 explaining how to run the script.
2026-05-08 21:30:42 +05:30
Mgeeeek 0fd7cb9c5a fix(plugin): preserve structure on PreCompact + capture compact summary
Two changes to the compaction flow:

PreCompact: agent stores its summary with infer=False
  on_pre_compact.sh now instructs the agent to call add_memory with
  infer=False. The agent has full conversation context and writes a
  structured summary; without infer=False the platform runs a second
  LLM extraction pass over that summary, losing structure and
  producing fragmented facts. With it, the structured text is
  preserved verbatim. Same guidance added to mem0-mcp/SKILL.md so
  the agent applies it to other already-extracted writes (decisions,
  anti-patterns, conventions).

SessionStart-compact: capture the platform-generated summary
  PreCompact fires BEFORE Claude Code generates its compact summary,
  so the summary is unreachable from that hook. SessionStart-compact
  fires AFTER, so a new background helper (capture_compact_summary.py)
  reads the transcript at that point, finds the isCompactSummary=true
  entry, and stores its content as a separate memory tagged
  metadata.type=compact_summary. The post-compaction bootstrap text
  now tells the agent to layer its recall queries across
  session_state (its own pre-compact summary), compact_summary (the
  platform's condensed view), and topical types like decision /
  anti_pattern.

Net effect: three complementary memory types from one session
boundary, none of which used to be filterable or even captured
correctly.
2026-05-08 21:13:00 +05:30
Mgeeeek ed15fc1386 fix(plugin): deterministic mem0 user_id resolution
Hooks previously fell back to $USER when MEM0_USER_ID wasn't set,
producing a different user_id on every machine for the same person.
Result: a single account with memories scattered across many user
buckets, none of which can see each other.

Resolution priority (same in bash and python):
  1. MEM0_USER_ID env var (explicit override)
  2. ~/.mem0/identity.json cache (pinned to MEM0_API_KEY fingerprint)
  3. Derived: "mem0-" + sha256(MEM0_API_KEY)[:12]
  4. Fallback: $USER, else "default"

Same MEM0_API_KEY across machines now yields the same user_id without
the user having to set MEM0_USER_ID by hand on every laptop.

Resolver shipped as two tiny files instead of a shared module:
  _identity.sh -- sourced by bash hooks, exports MEM0_RESOLVED_USER_ID
  _identity.py -- imported by on_pre_compact.py, exposes resolve_user_id()

Hook integration:
  on_user_prompt.sh -- sources resolver, USER_ID interpolated into rubric
  on_session_start.sh -- emits an "Active user_id: <X>" header before the
    bootstrap text, so the agent's MCP search_memories/add_memory calls
    use the same bucket the hooks write to (closes the agent-side half
    of the symptom)
  on_pre_compact.py -- replaces inline env lookup with resolve_user_id()

Existing memories under previous $USER values are not auto-migrated.
The cache file regenerates on key rotation (fingerprint mismatch).
2026-05-08 21:02:24 +05:30
Mgeeeek 8cb4f09ded fix(plugin): mem0-plugin hook cleanup
Six independent fixes that make hook behaviour deterministic, debuggable,
and free of silent footguns.

Persist session_id in stored memories
  on_pre_compact.py now extracts session_id from the hook input and writes
  it under metadata.session_id, enabling search-by-session.

Skip SessionStart bootstrap when MEM0_API_KEY is missing
  on_session_start.sh exits 0 early instead of emitting bootstrap text that
  instructs the agent to call mem0 MCP tools that would all fail without
  a key.

Tighten block_memory_write.sh regex
  The old `*/memory/*.md` pattern blocked legitimate paths like
  docs/memory/architecture.md. New pattern blocks `*/MEMORY.md` and
  `*/.claude/memory/*` only -- the actual surfaces we want to protect.

Opt-in debug logging via MEM0_DEBUG=1
  Each hook (bash + python) redirects stderr / adds a FileHandler to
  ~/.mem0/hooks.log when MEM0_DEBUG is set. No helper module, no
  rotation -- KISS. Default behaviour is unchanged.

Drop the duplicate PreCompact registration
  hooks.json and cursor-hooks.json had on_pre_compact.sh AND
  on_pre_compact.py registered on PreCompact, producing duplicate
  session-state writes. Drop the python entry. The python script stays
  alive -- on_stop.sh still spawns it as a session-end safety net.

CI nudge
  Adds a single comment line to pyproject.toml because the repo's
  ci.yml path filters exclude mem0-plugin/-only PRs but the
  build_mem0/build_embedchain status checks are required by branch
  protection. Track the proper fix (path-conditional CI) as a
  follow-up.
2026-05-07 21:53:49 +05:30
17 changed files with 604 additions and 26 deletions
+26
View File
@@ -157,6 +157,32 @@ After installing, confirm the MCP server is connected:
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Complements the lifecycle hooks on Codex.
## Updating the plugin
When the plugin updates (new version pulled from the marketplace, or a fresh local install), the MCP server connection in your existing Claude Code / Cursor / Codex session is left holding a stale handle and stops responding. **Restart your client to reconnect:**
- **Claude Code:** run `/restart` in the prompt, or close and reopen the CLI.
- **Cursor:** quit and relaunch.
- **Codex:** restart the editor session.
Your `MEM0_API_KEY` doesn't need to be re-entered — the auth header is re-read from your environment on the new session. The plugin's MCP config uses `${MEM0_API_KEY}` interpolation at session start, not at install time, so as long as the env var is set persistently (in your shell profile or `~/.claude/settings.json` `env` block), reconnection is automatic on restart.
If reconnection still fails after a restart, check that `MEM0_API_KEY` is reachable in the new shell (`echo $MEM0_API_KEY`) and confirm you're using a key that starts with `m0-` (from https://app.mem0.ai/dashboard/api-keys, not a legacy token).
## Optional: tune categories for coding workflows
mem0 auto-tags every memory with one or more `categories` from a project-level list. The default list is consumer-oriented (`food`, `hobbies`, `music` …) — useful for chat assistants, less so for code. A one-shot script in this plugin replaces it with a coding-focused taxonomy:
```bash
# Dry-run first -- prints current vs proposed, no changes:
python mem0-plugin/scripts/setup_coding_categories.py
# Actually write:
python mem0-plugin/scripts/setup_coding_categories.py --apply
```
Requires the `mem0ai` Python SDK (`pip install mem0ai`) and `MEM0_API_KEY` set. New memories will then auto-tag against `architecture_decisions`, `anti_patterns`, `task_learnings`, `tooling_setup`, `bug_fixes`, `coding_conventions`, `user_preferences`. Re-run with a different list any time; `project.update(custom_categories=[...])` always replaces.
## MCP Tools
Once installed, the following tools are available:
-4
View File
@@ -15,10 +15,6 @@
"preCompact": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.sh"
},
{
"command": "python3 ${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.py",
"timeout": 30
}
],
"stop": [
-6
View File
@@ -30,12 +30,6 @@
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.sh",
"statusMessage": "Preparing pre-compaction summary..."
},
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.py",
"statusMessage": "Saving session state to mem0...",
"timeout": 30
}
]
}
+59
View File
@@ -0,0 +1,59 @@
"""Resolve mem0 user_id with deterministic priority.
Resolution priority:
1. MEM0_USER_ID env var (explicit override)
2. ~/.mem0/identity.json cache (pinned to current MEM0_API_KEY fingerprint)
3. Derived: "mem0-" + sha256(MEM0_API_KEY)[:12]
4. Fallback: $USER, else "default"
Same MEM0_API_KEY across machines yields the same user_id, which fixes
the "47 user buckets per account" symptom from running on multiple
laptops with different $USER values.
"""
from __future__ import annotations
import hashlib
import json
import os
from datetime import datetime, timezone
_CACHE_PATH = os.path.expanduser("~/.mem0/identity.json")
def resolve_user_id() -> str:
explicit = os.environ.get("MEM0_USER_ID", "").strip()
if explicit:
return explicit
api_key = os.environ.get("MEM0_API_KEY", "").strip()
if api_key:
digest = hashlib.sha256(api_key.encode("utf-8")).hexdigest()
fingerprint = digest[:8]
try:
with open(_CACHE_PATH, "r") as f:
cached = json.load(f)
if cached.get("api_key_fingerprint") == fingerprint and cached.get("user_id"):
return cached["user_id"]
except (OSError, json.JSONDecodeError):
pass
derived = "mem0-" + digest[:12]
try:
os.makedirs(os.path.dirname(_CACHE_PATH), exist_ok=True)
with open(_CACHE_PATH, "w") as f:
json.dump(
{
"user_id": derived,
"source": "api_key",
"api_key_fingerprint": fingerprint,
"resolved_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
},
f,
)
except OSError:
pass
return derived
return os.environ.get("USER") or "default"
+57
View File
@@ -0,0 +1,57 @@
# Source this file. Sets MEM0_RESOLVED_USER_ID.
#
# Resolution priority:
# 1. MEM0_USER_ID env var (explicit override)
# 2. ~/.mem0/identity.json cache (pinned to current MEM0_API_KEY fingerprint)
# 3. Derived: "mem0-" + sha256(MEM0_API_KEY)[:12]
# 4. Fallback: $USER, else "default"
#
# Same MEM0_API_KEY across machines yields the same user_id, which fixes
# the "47 user buckets per account" symptom from running on multiple
# laptops with different $USER values.
_mem0_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum | cut -d' ' -f1
else
shasum -a 256 | cut -d' ' -f1
fi
}
_mem0_resolve_identity() {
if [ -n "${MEM0_USER_ID:-}" ]; then
printf '%s' "$MEM0_USER_ID"
return
fi
local api_key="${MEM0_API_KEY:-}"
local cache="$HOME/.mem0/identity.json"
if [ -n "$api_key" ]; then
local digest
digest=$(printf '%s' "$api_key" | _mem0_sha256)
local fp="${digest:0:8}"
if [ -f "$cache" ]; then
local cached_fp cached_id
cached_fp=$(jq -r '.api_key_fingerprint // ""' "$cache" 2>/dev/null)
cached_id=$(jq -r '.user_id // ""' "$cache" 2>/dev/null)
if [ "$cached_fp" = "$fp" ] && [ -n "$cached_id" ]; then
printf '%s' "$cached_id"
return
fi
fi
local derived="mem0-${digest:0:12}"
mkdir -p "$HOME/.mem0" 2>/dev/null && \
printf '{"user_id":"%s","source":"api_key","api_key_fingerprint":"%s","resolved_at":"%s"}\n' \
"$derived" "$fp" "$(date -u +%FT%TZ)" > "$cache" 2>/dev/null
printf '%s' "$derived"
return
fi
printf '%s' "${USER:-default}"
}
MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)"
export MEM0_RESOLVED_USER_ID
+5 -1
View File
@@ -13,6 +13,10 @@
set -euo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
@@ -22,7 +26,7 @@ if [ -z "$FILE_PATH" ]; then
fi
case "$FILE_PATH" in
*/MEMORY.md|*/memory/*.md|*/.claude/*/memory/*)
*/MEMORY.md|*/.claude/memory/*)
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
exit 2
;;
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Capture the post-compaction summary into mem0.
PreCompact hooks fire BEFORE the summary is generated, so they can't
store the actual compact-summary text. This script runs at
SessionStart with source=compact, reads the transcript, finds the
most recent entry flagged isCompactSummary=true, and stores it as a
memory tagged metadata.type=compact_summary.
Input: JSON on stdin with transcript_path, session_id, source
Output: stderr logs only (exit 0 always -- must not block)
Spawned in the background by on_session_start.sh; the user-facing
bootstrap text continues without waiting on the network.
"""
from __future__ import annotations
import json
import logging
import os
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
log = logging.getLogger("mem0-compact-summary")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 2000
MAX_SUMMARY_CHARS = 50000
# Compact summaries describe a single session's state -- stale after a quarter.
COMPACT_SUMMARY_EXPIRY_DAYS = 90
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def find_compact_summary(lines: list[str]) -> str:
"""Walk transcript backwards, return text content of the most recent
entry flagged isCompactSummary=true. Empty string if none found."""
for line in reversed(lines):
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if not entry.get("isCompactSummary"):
continue
message = entry.get("message", {})
content = message.get("content", [])
if isinstance(content, str):
return content[:MAX_SUMMARY_CHARS]
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
return "\n".join(parts).strip()[:MAX_SUMMARY_CHARS]
return ""
def store_summary(api_key: str, summary: str, user_id: str, session_id: str) -> bool:
expires = (date.today() + timedelta(days=COMPACT_SUMMARY_EXPIRY_DAYS)).isoformat()
body = {
"messages": [{"role": "user", "content": summary}],
"user_id": user_id,
"metadata": {
"type": "compact_summary",
"source": "session-start-compact",
"session_id": session_id,
},
"infer": False,
"expiration_date": expires,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v1/memories/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Compact summary stored")
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def main():
api_key = os.environ.get("MEM0_API_KEY", "")
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
return
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
log.debug("No valid JSON on stdin")
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
log.debug("No transcript_path provided")
return
session_id = hook_input.get("session_id", "")
user_id = resolve_user_id()
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
summary = find_compact_summary(lines)
if not summary:
log.debug("No isCompactSummary entry found")
return
log.info("Capturing compact summary (%d chars)", len(summary))
store_summary(api_key, summary, user_id, session_id)
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
+26 -4
View File
@@ -18,8 +18,12 @@ import json
import logging
import os
import sys
import urllib.request
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
log = logging.getLogger("mem0-capture")
log.setLevel(logging.DEBUG)
@@ -27,11 +31,25 @@ _handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-capture] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-capture] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 500
MAX_USER_MESSAGES = 30
MAX_BASH_COMMANDS = 20
MAX_ASSISTANT_TEXT = 10000
# session_state captures churn fast (active codebase, files in flight). Past
# ~3 months they're stale noise. Durable facts (decisions, conventions) are
# stored separately by the agent without an expiration_date.
SESSION_STATE_EXPIRY_DAYS = 90
def tail_lines(filepath: str, n: int) -> list[str]:
@@ -149,8 +167,9 @@ def build_content(state: dict, source: str) -> str:
return "\n".join(parts)
def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
def store_memory(api_key: str, content: str, user_id: str, source: str, session_id: str = "") -> bool:
"""Store session state as a memory via the Mem0 REST API."""
expires = (date.today() + timedelta(days=SESSION_STATE_EXPIRY_DAYS)).isoformat()
body = {
"messages": [
{"role": "user", "content": content}
@@ -159,7 +178,9 @@ def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
"metadata": {
"type": "session_state",
"source": source,
"session_id": session_id,
},
"expiration_date": expires,
}
data = json.dumps(body).encode("utf-8")
@@ -207,7 +228,8 @@ def main():
log.debug("No transcript_path provided")
return
user_id = os.environ.get("MEM0_USER_ID", os.environ.get("USER", "default"))
session_id = hook_input.get("session_id", "")
user_id = resolve_user_id()
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
@@ -228,7 +250,7 @@ def main():
len(state["bash_commands"]),
)
store_memory(api_key, content, user_id, source)
store_memory(api_key, content, user_id, source, session_id)
if __name__ == "__main__":
+20 -6
View File
@@ -5,12 +5,16 @@
# the full context before it gets compressed.
#
# Output: Text instructions injected into Claude's context.
# Claude still has the full conversation and can write an accurate summary.
# A companion Python script (on_pre_compact.py) also runs to capture
# transcript state directly via the Mem0 REST API as a safety net.
# Claude still has the full conversation and can write an accurate summary,
# which it stores via add_memory(infer=False) so the platform preserves
# the structure verbatim instead of running a second extraction pass.
set -euo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
cat <<'EOF'
## CRITICAL: Pre-Compaction Session Summary
@@ -18,7 +22,9 @@ Context compaction is about to happen. You are about to lose most of your conver
### Step 1: Store session summary
Call `add_memory` with a thorough summary covering ALL of the following:
Call `add_memory` with `infer=False` and a thorough summary covering ALL of the following.
`infer=False` is critical here: you've already done the extraction work yourself using full context. Without it, the platform runs a second LLM pass that loses your structure and pulls fragmented facts. With it, your summary is preserved verbatim.
```
## Session Summary (Pre-Compaction)
@@ -44,11 +50,19 @@ Call `add_memory` with a thorough summary covering ALL of the following:
the post-compaction agent continue without asking redundant questions]
```
Include metadata: `{"type": "session_state", "source": "pre-compaction"}`
Tool call shape:
```
add_memory(
messages=[{"role":"user","content":"<the summary above>"}],
user_id="<the active user_id from the SessionStart bootstrap>",
metadata={"type":"session_state","source":"pre-compaction"},
infer=False,
)
```
### Step 2: Store any unstored learnings
If there are learnings from this session that you haven't stored yet, store them as separate memories:
If there are learnings from this session that you haven't stored yet, store them as separate memories with `infer=False` (same reasoning -- you've already extracted the fact, don't re-extract):
- Failed approaches -> metadata `{"type": "anti_pattern"}`
- Successful strategies -> metadata `{"type": "task_learning"}`
- Architecture decisions -> metadata `{"type": "decision"}`
+37 -4
View File
@@ -11,9 +11,34 @@
# even if jq is missing or stdin is malformed.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
# Skip the bootstrap entirely if no API key is configured -- the agent
# would otherwise be told to call mem0 MCP tools that will all fail.
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
# Identity line is emitted before every bootstrap variant so the agent
# uses the same user_id the hooks resolved. Without this, the agent's
# search_memories/add_memory MCP calls may bind to a different bucket
# than what the hooks write to.
echo "## Mem0 Identity"
echo ""
echo "Active user_id: \`$MEM0_RESOLVED_USER_ID\`"
echo ""
echo "Always include \`{\"user_id\": \"$MEM0_RESOLVED_USER_ID\"}\` (wrapped in an \`AND\` clause) in every \`search_memories\` filter and as \`user_id\` on every \`add_memory\` call. This keeps memories under one bucket regardless of which machine you're on."
echo ""
if [ "$SOURCE" = "startup" ]; then
cat <<'EOF'
## Mem0 Session Bootstrap
@@ -40,14 +65,22 @@ Continue where you left off.
EOF
elif [ "$SOURCE" = "compact" ]; then
# Capture the just-generated compact summary in the background.
# PreCompact fires too early to see this entry; SessionStart-compact
# is the first place isCompactSummary=true is in the transcript.
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
cat <<'EOF'
## Mem0 Post-Compaction Recovery
Context was just compacted. You may have lost important session context.
Context was just compacted. The Claude Code-generated compact summary
is being captured to mem0 in the background as `metadata.type=compact_summary`.
1. Call `search_memories` with queries related to what you were working on to reload relevant knowledge.
2. Check for any session state memories that were saved before compaction.
3. Continue working based on the recovered context.
1. Call `search_memories` to reload context, layering up to three angles:
- `metadata.type=session_state` -- the rich pre-compaction summary you wrote
- `metadata.type=compact_summary` -- the platform-generated condensed summary just now
- `metadata.type=decision` / `anti_pattern` -- specific facts you stored during the session
2. Continue working from the recovered context.
EOF
fi
+4
View File
@@ -12,6 +12,10 @@
set -euo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
+4
View File
@@ -17,6 +17,10 @@
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
+4
View File
@@ -9,6 +9,10 @@
set -euo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
+8 -1
View File
@@ -13,6 +13,10 @@
# must never block the user's prompt.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
@@ -26,7 +30,10 @@ if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
USER_ID="${MEM0_USER_ID:-${USER:-default}}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
USER_ID="$MEM0_RESOLVED_USER_ID"
cat <<EOF
## Memory check
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Replace mem0's default category taxonomy with one tuned for coding workflows.
mem0 auto-tags every memory with one or more `categories`. By default the list
is consumer-oriented (food, hobbies, music, ...), which is meaningless for code.
This script replaces the project's category list with a coding-focused one.
The change is project-level (per the platform docs, per-request overrides are
not supported on the managed API). Run once per project; future memories will
be tagged using the new list automatically.
Usage:
python setup_coding_categories.py # dry-run: show current vs proposed, no changes
python setup_coding_categories.py --apply # actually call project.update()
Requires the mem0ai Python SDK and MEM0_API_KEY to be set.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
CODING_CATEGORIES = [
{
"architecture_decisions": (
"Design choices, system structure, technology selection, trade-offs evaluated, "
"and architectural patterns adopted in the project."
)
},
{
"anti_patterns": (
"Approaches that failed, debugging dead-ends, common mistakes to avoid, "
"and lessons learned from things that didn't work."
)
},
{
"task_learnings": (
"Strategies and approaches that succeeded for specific tasks, including tooling "
"tricks, workflow shortcuts, and effective problem-solving patterns."
)
},
{
"tooling_setup": (
"Development environment, build tools, dependencies, package managers, deploy "
"pipelines, and configuration steps for the project."
)
},
{
"bug_fixes": (
"Specific bug fixes with root cause analysis, the fix applied, and how the bug "
"was diagnosed -- useful for recognising similar issues later."
)
},
{
"coding_conventions": (
"Code style, naming patterns, file organisation, error-handling conventions, "
"and team agreements about how code is written in this project."
)
},
{
"user_preferences": (
"User's stated preferences for tools, libraries, languages, formatting, "
"and ways of working."
)
},
]
def _print_categories(label: str, cats):
print(f"=== {label} ===")
if cats:
print(json.dumps(cats, indent=2))
else:
print("(none / using mem0 defaults)")
print()
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument(
"--apply",
action="store_true",
help="Actually call project.update(). Without this flag, runs in dry-run mode.",
)
args = ap.parse_args()
if not os.environ.get("MEM0_API_KEY"):
print("ERROR: MEM0_API_KEY is not set. Export it and try again.", file=sys.stderr)
return 1
try:
from mem0 import MemoryClient
except ImportError:
print(
"ERROR: the mem0ai Python SDK is not installed.\n"
"Install with: pip install mem0ai\n"
"Then re-run this script.",
file=sys.stderr,
)
return 1
try:
client = MemoryClient()
except Exception as e:
print(
f"ERROR initialising MemoryClient: {e}\n"
"Most commonly this is an invalid MEM0_API_KEY -- check the key at "
"https://app.mem0.ai/dashboard/api-keys",
file=sys.stderr,
)
return 1
try:
current = client.project.get(fields=["custom_categories"])
current_cats = current.get("custom_categories") if isinstance(current, dict) else None
except Exception as e:
print(f"ERROR fetching current categories: {e}", file=sys.stderr)
return 1
_print_categories("Current project categories", current_cats)
_print_categories("Proposed coding categories", CODING_CATEGORIES)
if not args.apply:
print("Dry-run only -- no changes made. Re-run with --apply to write.")
return 0
print("Applying coding categories...")
try:
response = client.project.update(custom_categories=CODING_CATEGORIES)
except Exception as e:
print(f"ERROR applying update: {e}", file=sys.stderr)
return 1
print("Done.", response if response else "")
return 0
if __name__ == "__main__":
sys.exit(main())
+39
View File
@@ -97,8 +97,47 @@ Extract key learnings and store them using the `add_memory` tool:
- **Environment/setup discoveries** -> Include metadata `{"type": "environmental"}`
- **Conventions established** -> Include metadata `{"type": "convention"}`
> `metadata.type` (which you set explicitly) and `categories` (which the platform auto-tags after the project's custom-category list — see `scripts/setup_coding_categories.py`) are complementary. Always set `metadata.type` for explicit filtering; the platform fills in `categories` on its own. Don't try to set `categories` on `add_memory` calls — per-request overrides aren't supported on the managed API.
### Expiration: high-churn vs durable
Some memory types are state snapshots that go stale fast; others are durable facts that should outlive the session that created them. Mark the difference with `expiration_date` on writes.
| Type | Expiration | Why |
|---|---|---|
| `session_state`, `compact_summary` | `expiration_date` ≈ today + 90 days | Describe a single moment of project state. Useless after a quarter; clutter the recall surface. |
| `decision`, `anti_pattern`, `convention`, `user_preference`, `task_learning`, `environmental` | omit `expiration_date` | Durable facts. A decision made last year is still a decision; same for a convention or a user preference. |
`add_memory` accepts `expiration_date` as a string (`"YYYY-MM-DD"`). The two server-side hooks (`on_pre_compact.py`, `capture_compact_summary.py`) already set this for the types they write. When you write directly via the MCP tool, follow the same rule.
### Recency filter on recall
When the user is asking about *current* state ("where were we", "what's the active task", "the latest decision on X"), filter recall to recent memories so stale snapshots don't surface:
```python
# Last 90 days only
{"AND": [{"user_id": "<id>"}, {"metadata": {"type": "session_state"}}, {"created_at": {"gte": "<90 days ago, YYYY-MM-DD>"}}]}
```
Skip the recency filter when the user is asking about durable facts ("what conventions does this project use", "have we hit this bug before") — those are timeless and recency would hide them.
Memories can be as detailed as needed -- include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
### Use `infer=False` for already-structured content
When you've done the extraction work yourself — pre-compaction summaries, decisions, anti-patterns, conventions you've explicitly identified — pass `infer=False` so the platform stores your text verbatim instead of running a second extraction pass over it.
```python
add_memory(
messages=[{"role": "user", "content": "<your structured fact>"}],
user_id="<active user_id>",
metadata={"type": "decision"},
infer=False,
)
```
Stick to one mode per distinct piece of content — don't mix `infer=True` (default) and `infer=False` for the same fact, you'll get duplicates. Default (`infer=True`) is right for raw conversational signal you want extracted; `infer=False` is right for pre-extracted structure.
## Before losing context
If context is about to be compacted or the session is ending, store a comprehensive session summary:
+1
View File
@@ -154,3 +154,4 @@ known-first-party = ["mem0", "mem0_cli"]
profile = "black"
known_first_party = ["mem0", "mem0_cli"]
# isort scope kept aligned with [tool.ruff.lint.isort] above.
# black-equivalent profile here matches the formatter behaviour ruff applies.