fix(mem0-plugin): store assistant-authored summaries with role="assistant" (#6316)
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"source": "./integrations/mem0-plugin",
|
"source": "./integrations/mem0-plugin",
|
||||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"source": "./integrations/mem0-plugin",
|
"source": "./integrations/mem0-plugin",
|
||||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||||
"version": "0.2.12"
|
"version": "0.2.13"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1825,6 +1825,15 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Mem0 Plugin">
|
<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">
|
<Update label="2026-06-30" description="mem0-plugin v0.2.12">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
@@ -2148,6 +2157,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
|||||||
|
|
||||||
<Tab title="Antigravity">
|
<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">
|
<Update label="2026-06-30" description="Antigravity plugin v0.1.4">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
|
|||||||
@@ -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 |
|
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
|
| **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 |
|
| **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
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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
|
## Example Workflow
|
||||||
|
|
||||||
```text
|
```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.
|
You: Let's refactor the auth module to use JWT tokens instead of sessions.
|
||||||
|
|
||||||
# Claude searches memories, finds nothing relevant, proceeds with the work.
|
# 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"
|
# - Decision: "Migrated auth from sessions to JWT tokens"
|
||||||
# - Files modified: auth/middleware.ts, auth/token.ts
|
# - Files modified: auth/middleware.ts, auth/token.ts
|
||||||
# - User preference: "Prefers TypeScript, uses ESLint"
|
|
||||||
|
|
||||||
# Session 2 (days later): Related work
|
# Session 2 (days later): Related work
|
||||||
You: Add refresh token rotation to the auth system.
|
You: Add refresh token rotation to the auth system.
|
||||||
|
|
||||||
# Claude searches memories, retrieves the JWT migration context.
|
# 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.
|
# Continues seamlessly without re-explaining the codebase.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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
|
## Example Workflow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
# Task 1: Setting up a new service
|
# Task 1: Setting up a new service
|
||||||
You: Create a REST API for the notifications service using Express and TypeScript.
|
You: Create a REST API for the notifications service using Express and TypeScript.
|
||||||
|
|
||||||
# Codex searches memories, finds user preferences from prior tasks.
|
# Codex searches memories, finds your preferences from prior tasks.
|
||||||
# After completing the task, Mem0 stores:
|
# 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"
|
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
|
||||||
# - Convention: "All API routes follow /api/v1/{resource} pattern"
|
# - 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
|
# Task 2 (days later): Extending the service
|
||||||
You: Add WebSocket support for real-time notification delivery.
|
You: Add WebSocket support for real-time notification delivery.
|
||||||
|
|||||||
@@ -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.
|
You: The API endpoint /users is taking 3 seconds. Help me optimize it.
|
||||||
|
|
||||||
# Cursor agent searches memories, proceeds with investigation.
|
# 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"
|
# - Learning: "N+1 query in UserService.getAll(): fixed with eager loading"
|
||||||
# - Decision: "Added database index on users.email column"
|
# - Decision: "Added database index on users.email column"
|
||||||
# - Preference: "User prefers query-level fixes over caching"
|
|
||||||
|
|
||||||
# Session 2 (next week): Similar issue
|
# Session 2 (next week): Similar issue
|
||||||
You: The /orders endpoint is also slow, same pattern as before.
|
You: The /orders endpoint is also slow, same pattern as before.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"version": "0.2.12",
|
"version": "0.2.13",
|
||||||
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
|
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mem0",
|
"name": "Mem0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"version": "0.2.12",
|
"version": "0.2.13",
|
||||||
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
|
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mem0",
|
"name": "Mem0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0",
|
"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.",
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mem0",
|
"name": "Mem0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "mem0",
|
"id": "mem0",
|
||||||
"name": "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.",
|
"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" },
|
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||||
"publisher": "mem0ai",
|
"publisher": "mem0ai",
|
||||||
|
|||||||
@@ -104,8 +104,11 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
|
|||||||
}
|
}
|
||||||
if branch:
|
if branch:
|
||||||
metadata["branch"] = 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 = {
|
body = {
|
||||||
"messages": [{"role": "user", "content": summary}],
|
"messages": [{"role": "assistant", "content": summary}],
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"app_id": project_id,
|
"app_id": project_id,
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
|
|||||||
@@ -175,8 +175,12 @@ def store_summary(
|
|||||||
if files:
|
if files:
|
||||||
metadata["files_touched"] = files[:20]
|
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 = {
|
body = {
|
||||||
"messages": [{"role": "user", "content": summary_prompt}],
|
"messages": [{"role": "assistant", "content": summary_prompt}],
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"app_id": project_id,
|
"app_id": project_id,
|
||||||
"run_id": session_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"]
|
||||||
Reference in New Issue
Block a user