fix(mem0-plugin): store assistant-authored summaries with role="assistant" (#6316)

This commit is contained in:
Kartik
2026-07-14 20:03:36 +05:30
committed by GitHub
parent 6c1741e3a4
commit d6d2588ef5
14 changed files with 197 additions and 19 deletions
+1 -1
View File
@@ -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"
}
]
}
+1 -1
View File
@@ -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"
}
]
}
+18
View File
@@ -1825,6 +1825,15 @@ A full-featured command-line interface for Mem0, available in both Python and No
<Tabs>
<Tab title="Mem0 Plugin">
<Update label="2026-07-14" description="mem0-plugin v0.2.13">
**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.
</Update>
<Update label="2026-06-30" description="mem0-plugin v0.2.12">
**New Features:**
@@ -2148,6 +2157,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Antigravity">
<Update label="2026-07-14" description="Antigravity plugin v0.1.5">
**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.
</Update>
<Update label="2026-06-30" description="Antigravity plugin v0.1.4">
**New Features:**
+3 -1
View File
@@ -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
+7 -4
View File
@@ -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.
```
+7 -4
View File
@@ -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.
+3 -2
View File
@@ -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.
@@ -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",
@@ -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",
@@ -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",
+1 -1
View File
@@ -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",
@@ -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,
@@ -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,
@@ -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"]