From ed5ee7b9ff3cc20477ba722af234aaeb9c100253 Mon Sep 17 00:00:00 2001 From: Mgeeeek Date: Fri, 8 May 2026 20:58:08 +0530 Subject: [PATCH] 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: " 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). CI nudge in pyproject.toml because path filters in ci.yml exclude plugin-only PRs but build_mem0/build_embedchain are required. Manual verification: same key on different $USER values resolves to identical user_id; MEM0_USER_ID override bypasses cache and key derivation; cache invalidates on key change; bash and python implementations produce identical output for all four priority levels. --- mem0-plugin/scripts/_identity.py | 59 +++++++++++++++++++++++++ mem0-plugin/scripts/_identity.sh | 57 ++++++++++++++++++++++++ mem0-plugin/scripts/on_pre_compact.py | 7 ++- mem0-plugin/scripts/on_session_start.sh | 15 +++++++ mem0-plugin/scripts/on_user_prompt.sh | 5 ++- pyproject.toml | 1 + 6 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 mem0-plugin/scripts/_identity.py create mode 100644 mem0-plugin/scripts/_identity.sh diff --git a/mem0-plugin/scripts/_identity.py b/mem0-plugin/scripts/_identity.py new file mode 100644 index 000000000..0725be58e --- /dev/null +++ b/mem0-plugin/scripts/_identity.py @@ -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" diff --git a/mem0-plugin/scripts/_identity.sh b/mem0-plugin/scripts/_identity.sh new file mode 100644 index 000000000..3e0b62a17 --- /dev/null +++ b/mem0-plugin/scripts/_identity.sh @@ -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 diff --git a/mem0-plugin/scripts/on_pre_compact.py b/mem0-plugin/scripts/on_pre_compact.py index fb59e8d5c..427f1fa45 100755 --- a/mem0-plugin/scripts/on_pre_compact.py +++ b/mem0-plugin/scripts/on_pre_compact.py @@ -18,8 +18,11 @@ import json import logging import os import sys -import urllib.request import urllib.error +import urllib.request + +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) @@ -207,7 +210,7 @@ def main(): log.debug("No transcript_path provided") return - user_id = os.environ.get("MEM0_USER_ID", os.environ.get("USER", "default")) + user_id = resolve_user_id() lines = tail_lines(transcript_path, MAX_TAIL_LINES) if not lines: diff --git a/mem0-plugin/scripts/on_session_start.sh b/mem0-plugin/scripts/on_session_start.sh index 353a17130..138d1d2de 100755 --- a/mem0-plugin/scripts/on_session_start.sh +++ b/mem0-plugin/scripts/on_session_start.sh @@ -11,9 +11,24 @@ # even if jq is missing or stdin is malformed. set -uo pipefail +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 diff --git a/mem0-plugin/scripts/on_user_prompt.sh b/mem0-plugin/scripts/on_user_prompt.sh index 70f8eaa44..1a2807683 100755 --- a/mem0-plugin/scripts/on_user_prompt.sh +++ b/mem0-plugin/scripts/on_user_prompt.sh @@ -26,7 +26,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 <