fix(mem0-plugin): use app_id entity scoping, fix tour, add branch tagging

Tour was filtering on metadata.type which missed platform-categorized
memories. Rewritten to use get_memories (list all) + group by platform
categories field. All write hooks now use app_id as top-level entity
param instead of metadata.project_id. Branch stamped on every write.
This commit is contained in:
kartik-mem0
2026-05-21 16:38:48 +05:30
parent 606ede7c0a
commit cf6db6ef88
14 changed files with 169 additions and 86 deletions
+14 -10
View File
@@ -22,7 +22,7 @@ import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
from _project import resolve_project_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-auto-import")
log.setLevel(logging.DEBUG)
@@ -77,8 +77,15 @@ def save_hashes(hashes: dict[str, str]) -> None:
log.warning("Could not save hash store: %s", e)
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str) -> bool:
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str, branch: str = "") -> bool:
"""POST a project profile memory to the Mem0 REST API."""
metadata = {
"type": "project_profile",
"file": filename,
"source": "auto-import",
}
if branch:
metadata["branch"] = branch
body = {
"messages": [
{
@@ -87,12 +94,8 @@ def post_memory(api_key: str, content: str, user_id: str, filename: str, project
}
],
"user_id": user_id,
"metadata": {
"type": "project_profile",
"file": filename,
"project_id": project_id,
"source": "auto-import",
},
"app_id": project_id,
"metadata": metadata,
"infer": False,
}
@@ -128,8 +131,9 @@ def main() -> None:
cwd = os.environ.get("MEM0_CWD", "").strip() or os.getcwd()
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
log.debug("Auto-import started: cwd=%s project=%s user=%s", cwd, project_id, user_id)
log.debug("Auto-import started: cwd=%s project=%s user=%s branch=%s", cwd, project_id, user_id, branch)
hashes = load_hashes()
updated = False
@@ -169,7 +173,7 @@ def main() -> None:
log.debug("Cannot read %s: %s", filename, e)
continue
if post_memory(api_key, content, user_id, filename, project_id):
if post_memory(api_key, content, user_id, filename, project_id, branch):
hashes[hash_key] = current_hash
updated = True
# on API failure we don't update the hash — retry next session
@@ -97,16 +97,18 @@ def find_compact_summary(lines: list[str]) -> str:
def store_summary(api_key: str, summary: str, user_id: str, session_id: str, project_id: str = "", branch: str = "") -> bool:
expires = (date.today() + timedelta(days=COMPACT_SUMMARY_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "compact_summary",
"source": "session-start-compact",
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
body = {
"messages": [{"role": "user", "content": summary}],
"user_id": user_id,
"metadata": {
"type": "compact_summary",
"source": "session-start-compact",
"session_id": session_id,
"project_id": project_id,
"branch": branch,
},
"app_id": project_id,
"metadata": metadata,
"infer": False,
"expiration_date": expires,
}
+9 -7
View File
@@ -171,18 +171,20 @@ def build_content(state: dict, source: str) -> str:
def store_memory(api_key: str, content: str, user_id: str, source: str, session_id: str = "", project_id: str = "", branch: str = "") -> bool:
"""Store session state as a memory via the Mem0 REST API."""
expires = (date.today() + timedelta(days=SESSION_STATE_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "session_state",
"source": source,
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
body = {
"messages": [
{"role": "user", "content": content}
],
"user_id": user_id,
"metadata": {
"type": "session_state",
"source": source,
"session_id": session_id,
"project_id": project_id,
"branch": branch,
},
"app_id": project_id,
"metadata": metadata,
"expiration_date": expires,
}
+2 -1
View File
@@ -55,7 +55,8 @@ 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","project_id":"<the active project_id>","branch":"<the active branch>"},
app_id="<the active project_id / app_id from SessionStart>",
metadata={"type":"session_state","source":"pre-compaction","branch":"<active branch>"},
infer=False,
)
```
+23 -6
View File
@@ -38,11 +38,11 @@ if command -v python3 >/dev/null 2>&1; then
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')
project_id = os.environ.get('MEM0_PROJECT_ID', '')
app_id = os.environ.get('MEM0_PROJECT_ID', '')
body = json.dumps({
'query': 'project context',
'user_id': user_id,
'filters': {'AND': [{'user_id': user_id}, {'metadata': {'project_id': project_id}}]},
'filters': {'AND': [{'user_id': user_id}, {'app_id': app_id}]},
'limit': 100,
}).encode()
req = urllib.request.Request(
@@ -72,14 +72,30 @@ echo "## Mem0 Active"
echo ""
echo "\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT\`"
echo ""
echo "Always include \`user_id\` + \`metadata.project_id\` in every \`search_memories\` filter and \`add_memory\` call:"
echo "Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:"
echo "- user_id: \`$MEM0_RESOLVED_USER_ID\`"
echo "- project_id: \`$MEM0_PROJECT_ID\`"
echo "- branch: \`$MEM0_BRANCH\` (include in session-state / compact-summary metadata)"
echo "- app_id: \`$MEM0_PROJECT_ID\` (project scope — passed as top-level \`app_id\`, NOT in metadata)"
echo ""
if [ "$SOURCE" = "startup" ]; then
cat <<'EOF'
# First-run detection: auto-trigger onboarding for new projects
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${MEM0_PROJECT_ID}"
if [ ! -f "$_ONBOARD_MARKER" ]; then
cat <<'EOF'
## Mem0 First Run — Automatic Onboarding
This is the first time mem0 is running for this project. You MUST run the onboarding wizard NOW before doing anything else.
Execute the `/mem0:onboard` skill immediately. This will:
1. Verify the API key
2. Detect and import project files (CLAUDE.md, AGENTS.md, .cursorrules, etc.)
3. Install coding categories optimized for development
4. Show a summary of what was set up
Do NOT skip this. Do NOT ask the user if they want to onboard. Just run `/mem0:onboard` now.
EOF
else
cat <<'EOF'
## Mem0 Session Bootstrap
You have access to persistent memory via the mem0 MCP tools. Before doing anything else:
@@ -90,6 +106,7 @@ You have access to persistent memory via the mem0 MCP tools. Before doing anythi
IMPORTANT: Do NOT skip this step. Always bootstrap context first.
EOF
fi
# Auto-import declarative project files in background
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
+1 -1
View File
@@ -54,7 +54,7 @@ Memories can be as detailed as needed — include full context, reasoning, code
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
Always include `"project_id"` in the metadata of any memory you store.
Always include `app_id` (the active project_id from SessionStart) as a top-level parameter in every `add_memory` call.
EOF
# Capture transcript state in the background via Mem0 REST API
+1 -1
View File
@@ -51,7 +51,7 @@ ${REPORT_BLOCK}Before finishing, check if there are important learnings from thi
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.
Always include \`"project_id"\` in the metadata of any memory you store.
Always include \`app_id\` (the active project_id from SessionStart) as a top-level parameter in every \`add_memory\` call.
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
EOF
+1 -1
View File
@@ -35,7 +35,7 @@ ${REPORT_BLOCK}Before finishing, check if there are important learnings from thi
4. Did you learn anything about the user's preferences? -> Store with metadata \`{"type": "user_preference"}\`
5. Were there environment/setup discoveries? -> Store with metadata \`{"type": "environmental"}\`
Always include \`"project_id"\` in the metadata of any memory you store.
Always include \`app_id\` (the active project_id from session start) as a top-level parameter in every \`add_memory\` call.
If nothing notable happened, it's fine to skip. Only store genuinely useful learnings.
EOF
+1 -1
View File
@@ -31,7 +31,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 \`"project_id": "$MEM0_PROJECT_ID"\` in metadata for all memories.
Include \`app_id\` = \`"$MEM0_PROJECT_ID"\` as a top-level parameter in every \`add_memory\` call (not in metadata).
EOF
exit 0
+6 -6
View File
@@ -60,12 +60,12 @@ improve your answer. The agent -- not this hook -- owns this decision.
- Phrase queries as **nouns** ("auth module decisions"), not full sentences.
- Filter shape: the root must be a logical operator (\`AND\` / \`OR\` / \`NOT\`)
with an array, and metadata uses a **nested** object (not dotted keys).
Combine \`user_id\` with one \`metadata.type\` clause per call:
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "decision"}}]}\` -- design / architecture
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "anti_pattern"}}]}\` -- debugging, error handling
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "user_preference"}}]}\` -- tooling, stack, style
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "convention"}}]}\` -- established patterns
- Or scope with just \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}]}\` when no metadata filter fits.
Combine \`user_id\` + \`app_id\` with one \`metadata.type\` clause per call:
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "decision"}}]}\` -- design / architecture
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "anti_pattern"}}]}\` -- debugging, error handling
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "user_preference"}}]}\` -- tooling, stack, style
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "convention"}}]}\` -- established patterns
- Or scope with just \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}]}\` when no metadata filter fits.
- Empty results are normal -- proceed without context.
EOF