From d6d2588ef516c176174c3ddcdaf9d3bfa9e2a3cf Mon Sep 17 00:00:00 2001 From: Kartik Date: Tue, 14 Jul 2026 20:03:36 +0530 Subject: [PATCH] fix(mem0-plugin): store assistant-authored summaries with role="assistant" (#6316) --- .claude-plugin/marketplace.json | 2 +- .cursor-plugin/marketplace.json | 2 +- docs/changelog/sdk.mdx | 18 +++ docs/integrations/antigravity.mdx | 4 +- docs/integrations/claude-code.mdx | 11 +- docs/integrations/codex.mdx | 11 +- docs/integrations/cursor.mdx | 5 +- .../mem0-plugin/.claude-plugin/plugin.json | 2 +- .../mem0-plugin/.codex-plugin/plugin.json | 2 +- .../mem0-plugin/.cursor-plugin/plugin.json | 2 +- integrations/mem0-plugin/plugin.json | 2 +- .../scripts/capture_compact_summary.py | 5 +- .../scripts/capture_session_summary.py | 6 +- .../mem0-plugin/tests/test_message_roles.py | 144 ++++++++++++++++++ 14 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 integrations/mem0-plugin/tests/test_message_roles.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 51668aff8..6954d4af7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "mem0", "source": "./integrations/mem0-plugin", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.", - "version": "0.2.12" + "version": "0.2.13" } ] } diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 40527fb93..2dae82c22 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "mem0", "source": "./integrations/mem0-plugin", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.", - "version": "0.2.12" + "version": "0.2.13" } ] } diff --git a/docs/changelog/sdk.mdx b/docs/changelog/sdk.mdx index d2b480e9d..7543c8b98 100644 --- a/docs/changelog/sdk.mdx +++ b/docs/changelog/sdk.mdx @@ -1825,6 +1825,15 @@ A full-featured command-line interface for Mem0, available in both Python and No + + +**Fixes:** +- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`. Affects Claude Code, Cursor, Codex, and Antigravity, which share these hooks. + +Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them. + + + **New Features:** @@ -2148,6 +2157,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex + + +**Fixes:** +- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`. + +Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them. + + + **New Features:** diff --git a/docs/integrations/antigravity.mdx b/docs/integrations/antigravity.mdx index c76ecd7f6..f759e7ec9 100644 --- a/docs/integrations/antigravity.mdx +++ b/docs/integrations/antigravity.mdx @@ -65,7 +65,9 @@ The plugin uses the same shell scripts as Claude Code, Cursor, and Codex: hooks | **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message | | **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools | | **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories | -| **Stop** | `Stop` | Stores a session summary when the session ends | +| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) | + +What you type is stored as yours. What the agent produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences. ## Troubleshooting diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx index a571f813d..7e6df670e 100644 --- a/docs/integrations/claude-code.mdx +++ b/docs/integrations/claude-code.mdx @@ -143,9 +143,11 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc | **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts | | **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context | | **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories | -| **Stop** | `Stop` | Stores a session summary when the session ends | +| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) | | **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted | +What you type is stored as yours. What Claude produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences. + ## Example Workflow ```text @@ -153,16 +155,17 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc You: Let's refactor the auth module to use JWT tokens instead of sessions. # Claude searches memories, finds nothing relevant, proceeds with the work. -# After completing the task, Mem0 stores: +# Mem0 stores what you said as yours: +# - Your preference: "Prefers TypeScript, uses ESLint" +# ...and what Claude did as the assistant's, in the session summary: # - Decision: "Migrated auth from sessions to JWT tokens" # - Files modified: auth/middleware.ts, auth/token.ts -# - User preference: "Prefers TypeScript, uses ESLint" # Session 2 (days later): Related work You: Add refresh token rotation to the auth system. # Claude searches memories, retrieves the JWT migration context. -# Knows the file structure, decisions made, and user preferences. +# Knows the file structure, decisions made, and your stated preferences. # Continues seamlessly without re-explaining the codebase. ``` diff --git a/docs/integrations/codex.mdx b/docs/integrations/codex.mdx index 5737a579e..d06d228dd 100644 --- a/docs/integrations/codex.mdx +++ b/docs/integrations/codex.mdx @@ -125,20 +125,23 @@ When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to | **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message | | **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context | | **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories | -| **Stop** | `Stop` | Stores a session summary when the session ends | +| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) | | **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted | +What you type is stored as yours. What Codex produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences. + ## Example Workflow ```text # Task 1: Setting up a new service You: Create a REST API for the notifications service using Express and TypeScript. -# Codex searches memories, finds user preferences from prior tasks. -# After completing the task, Mem0 stores: +# Codex searches memories, finds your preferences from prior tasks. +# Mem0 stores what you said as yours: +# - Your preference: "Prefers explicit error types over generic catch-all" +# ...and what Codex did as the assistant's, in the session summary: # - Decision: "Notifications service uses Express + TypeScript + Zod validation" # - Convention: "All API routes follow /api/v1/{resource} pattern" -# - Preference: "User prefers explicit error types over generic catch-all" # Task 2 (days later): Extending the service You: Add WebSocket support for real-time notification delivery. diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx index cfaa5e208..e02c593b4 100644 --- a/docs/integrations/cursor.mdx +++ b/docs/integrations/cursor.mdx @@ -96,10 +96,11 @@ Once installed, the following tools are available in every Cursor session: You: The API endpoint /users is taking 3 seconds. Help me optimize it. # Cursor agent searches memories, proceeds with investigation. -# After completing the task, Mem0 stores: +# Mem0 stores what you said as yours: +# - Your preference: "Prefers query-level fixes over caching" +# ...and what the agent did as the assistant's: # - Learning: "N+1 query in UserService.getAll(): fixed with eager loading" # - Decision: "Added database index on users.email column" -# - Preference: "User prefers query-level fixes over caching" # Session 2 (next week): Similar issue You: The /orders endpoint is also slow, same pattern as before. diff --git a/integrations/mem0-plugin/.claude-plugin/plugin.json b/integrations/mem0-plugin/.claude-plugin/plugin.json index a2be0f0ee..de77fc2ea 100644 --- a/integrations/mem0-plugin/.claude-plugin/plugin.json +++ b/integrations/mem0-plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mem0", - "version": "0.2.12", + "version": "0.2.13", "description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.", "author": { "name": "Mem0", diff --git a/integrations/mem0-plugin/.codex-plugin/plugin.json b/integrations/mem0-plugin/.codex-plugin/plugin.json index 382808b46..a705b3f72 100644 --- a/integrations/mem0-plugin/.codex-plugin/plugin.json +++ b/integrations/mem0-plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mem0", - "version": "0.2.12", + "version": "0.2.13", "description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.", "author": { "name": "Mem0", diff --git a/integrations/mem0-plugin/.cursor-plugin/plugin.json b/integrations/mem0-plugin/.cursor-plugin/plugin.json index 5ca6205ce..cdceabb02 100644 --- a/integrations/mem0-plugin/.cursor-plugin/plugin.json +++ b/integrations/mem0-plugin/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mem0", - "version": "0.2.12", + "version": "0.2.13", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.", "author": { "name": "Mem0", diff --git a/integrations/mem0-plugin/plugin.json b/integrations/mem0-plugin/plugin.json index 4636c32ef..0bed60d4a 100644 --- a/integrations/mem0-plugin/plugin.json +++ b/integrations/mem0-plugin/plugin.json @@ -1,7 +1,7 @@ { "id": "mem0", "name": "mem0", - "version": "0.1.4", + "version": "0.1.5", "description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.", "author": { "name": "Mem0", "email": "support@mem0.ai" }, "publisher": "mem0ai", diff --git a/integrations/mem0-plugin/scripts/capture_compact_summary.py b/integrations/mem0-plugin/scripts/capture_compact_summary.py index d6e70b356..244913e49 100644 --- a/integrations/mem0-plugin/scripts/capture_compact_summary.py +++ b/integrations/mem0-plugin/scripts/capture_compact_summary.py @@ -104,8 +104,11 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro } if branch: metadata["branch"] = branch + # The compact summary is model-authored prose, in the first person and with no + # framing to mark it as such. Under role="user" mem0 reads "I recommend X" as + # the human saying it and stores "User recommends X". body = { - "messages": [{"role": "user", "content": summary}], + "messages": [{"role": "assistant", "content": summary}], "user_id": user_id, "app_id": project_id, "metadata": metadata, diff --git a/integrations/mem0-plugin/scripts/capture_session_summary.py b/integrations/mem0-plugin/scripts/capture_session_summary.py index 441ed9a6e..2f0279179 100644 --- a/integrations/mem0-plugin/scripts/capture_session_summary.py +++ b/integrations/mem0-plugin/scripts/capture_session_summary.py @@ -175,8 +175,12 @@ def store_summary( if files: metadata["files_touched"] = files[:20] + # summary_prompt wraps the assistant's own last message. Mem0 extracts "facts + # about the user" from each message and role is the only signal telling it who + # spoke, so role="user" here turns Claude's opinions into the human's stated + # preferences ("User prefers dropping Redis..."). body = { - "messages": [{"role": "user", "content": summary_prompt}], + "messages": [{"role": "assistant", "content": summary_prompt}], "user_id": user_id, "app_id": project_id, "run_id": session_id, diff --git a/integrations/mem0-plugin/tests/test_message_roles.py b/integrations/mem0-plugin/tests/test_message_roles.py new file mode 100644 index 000000000..0a36ca66e --- /dev/null +++ b/integrations/mem0-plugin/tests/test_message_roles.py @@ -0,0 +1,144 @@ +"""Regression tests: assistant-authored text must never be posted as role="user". + +The Stop hook (capture_session_summary) and the post-compact hook +(capture_compact_summary) both ship *model-authored* prose to +POST /v3/memories/add/. Mem0's fact extractor renders each message as +"{role}: {content}" and is instructed to extract "facts and preferences about +the user" — so role is the only signal separating what the human said from what +Claude said. + +Posting Claude's own words under role="user" made the extractor read Claude's +first-person prose ("I recommend pgvector", "I found the bug in auth.py") as the +*human's* statements and store them under their user_id. The Stop hook fires on +every assistant turn, so this corrupted memory on nearly every message. +""" + +from __future__ import annotations + +import json + + +class _FakeResp: + status = 200 + + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + +def _capture(monkeypatch, module): + """Patch urlopen so store_summary posts nowhere; capture the request body.""" + captured: dict = {} + + def fake_urlopen(req, timeout=0): + captured["body"] = json.loads(req.data.decode("utf-8")) + return _FakeResp() + + monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen) + return captured + + +# Claude's own voice — first-person prose that must never be attributed to the human. +ASSISTANT_PROSE = ( + "I traced the root cause to auth.py and I recommend we switch to pgvector " + "for the vector store. I'll refactor the session handler next." +) + + +def test_session_summary_posts_assistant_prose_as_assistant(monkeypatch): + """Stop hook: the last assistant message must be tagged role="assistant".""" + import capture_session_summary as css + + captured = _capture(monkeypatch, css) + + css.store_summary( + api_key="test-key", + summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, []), + user_id="u1", + session_id="s1", + project_id="p1", + branch="main", + files=[], + ) + + messages = captured["body"]["messages"] + for msg in messages: + if ASSISTANT_PROSE in msg["content"]: + assert msg["role"] == "assistant", ( + "Claude's own words were posted as role='user' — mem0 will extract " + "them as facts about the human. Got role=%r" % msg["role"] + ) + break + else: + raise AssertionError("assistant prose never made it into the payload") + + +def test_compact_summary_posts_assistant_prose_as_assistant(monkeypatch): + """Post-compact hook: the compact summary is model-authored, not user-authored.""" + import capture_compact_summary as ccs + + captured = _capture(monkeypatch, ccs) + + ccs.store_summary( + api_key="test-key", + summary=ASSISTANT_PROSE, + user_id="u1", + session_id="s1", + project_id="p1", + branch="main", + ) + + messages = captured["body"]["messages"] + for msg in messages: + if ASSISTANT_PROSE in msg["content"]: + assert msg["role"] == "assistant", ( + "Compact summary (written by Claude) was posted as role='user'. Got role=%r" % msg["role"] + ) + break + else: + raise AssertionError("assistant prose never made it into the payload") + + +def test_no_user_role_message_carries_assistant_prose(monkeypatch): + """Belt and braces: no user-role message may contain the assistant's words.""" + import capture_session_summary as css + + captured = _capture(monkeypatch, css) + + css.store_summary( + api_key="test-key", + summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, ["auth.py"]), + user_id="u1", + session_id="s1", + project_id="p1", + branch="main", + files=["auth.py"], + ) + + for msg in captured["body"]["messages"]: + if msg["role"] == "user": + assert ASSISTANT_PROSE not in msg["content"], ( + "A user-role message carries Claude's prose — this is the misattribution bug." + ) + + +def test_auto_capture_preserves_real_roles(): + """auto_capture is the reference: it must pass roles through untouched.""" + import auto_capture + + lines = [ + json.dumps({"type": "user", "message": {"role": "user", "content": "why is the build failing on main?"}}), + json.dumps( + { + "type": "assistant", + "message": {"role": "assistant", "content": [{"type": "text", "text": ASSISTANT_PROSE}]}, + } + ), + ] + + messages = auto_capture.extract_recent_exchanges(lines) + + assert [m["role"] for m in messages] == ["user", "assistant"] + assert ASSISTANT_PROSE in messages[1]["content"]