Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
15d932b
fix(memory/mem0): use per-turn user_id in sync_turn — Codex PR#33 P1
dizhaky Jun 27, 2026
ffb95e7
fix(memory/memgw): scope prefetch result by user_id — Codex PR#33 P1
dizhaky Jun 27, 2026
07a2258
fix(memory): pass user_id through prefetch_all — Codex PR#33 P1
dizhaky Jun 27, 2026
4de996e
fix(conversation_loop): pass agent._user_id to prefetch_all — Codex P…
dizhaky Jun 27, 2026
825cc5a
fix(gateway): refresh agent identity on cached-agent reuse — Codex PR…
dizhaky Jun 27, 2026
6f46176
fix(memory): add user_id param to prefetch() in base class
dizhaky Jun 27, 2026
ebb9a28
fix(memory): add user_id param to prefetch() in mem0
dizhaky Jun 27, 2026
b45971f
fix(memory): add user_id param to prefetch() in honcho
dizhaky Jun 27, 2026
dc669f6
fix(memory): add user_id param to prefetch() in hindsight
dizhaky Jun 27, 2026
4611cbc
fix(memory): add user_id param to prefetch() in holographic
dizhaky Jun 27, 2026
f5115b8
fix(memory): add user_id param to prefetch() in byterover
dizhaky Jun 27, 2026
ba76529
fix(memory): add user_id param to prefetch() in openviking
dizhaky Jun 27, 2026
8180a1f
fix(memory): add user_id param to prefetch() in supermemory
dizhaky Jun 27, 2026
599862b
fix(memory): add user_id param to prefetch() in retaindb
dizhaky Jun 27, 2026
fee0aab
fix(tests): add user_id to mock prefetch() to match MemoryProvider si…
dizhaky Jun 27, 2026
f2c663d
fix(tests): add user_id to mock prefetch() to match MemoryProvider si…
dizhaky Jun 27, 2026
16e73b0
ci: re-trigger checks after ready-for-review
dizhaky Jun 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,10 @@ def run_conversation(
if agent._memory_manager:
try:
_query = original_user_message if isinstance(original_user_message, str) else ""
_ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
_ext_prefetch_cache = agent._memory_manager.prefetch_all(
_query,
user_id=getattr(agent, "_user_id", "") or "",
) or ""
except Exception:
pass

Expand Down
4 changes: 2 additions & 2 deletions agent/memory_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def build_system_prompt(self) -> str:

# -- Prefetch / recall ---------------------------------------------------

def prefetch_all(self, query: str, *, session_id: str = "") -> str:
def prefetch_all(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Collect prefetch context from all providers.

Returns merged context text labeled by provider. Empty providers
Expand All @@ -345,7 +345,7 @@ def prefetch_all(self, query: str, *, session_id: str = "") -> str:
parts = []
for provider in self._providers:
try:
result = provider.prefetch(query, session_id=session_id)
result = provider.prefetch(query, session_id=session_id, user_id=user_id)
if result and result.strip():
parts.append(result)
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion agent/memory_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def system_prompt_block(self) -> str:
"""
return ""

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Recall relevant context for the upcoming turn.

Called before each API call. Return formatted text to inject as
Expand Down
8 changes: 8 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16363,6 +16363,14 @@ def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None:
except KeyError:
pass
self._init_cached_agent_for_turn(agent, _interrupt_depth)
# Refresh caller identity — source may differ from the prior turn
# in shared-thread sessions (thread_sessions_per_user=False).
agent._user_id = source.user_id or ''
agent._user_name = source.user_name or ''
agent._chat_id = source.chat_id or ''
agent._chat_name = source.chat_name or ''
agent._chat_type = source.chat_type or ''
agent._thread_id = source.thread_id or ''
logger.debug("Reusing cached agent for session %s", session_key)

if agent is None:
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/byterover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def system_prompt_block(self) -> str:
"important facts, brv_status to check state."
)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Run brv query synchronously before the agent's first LLM call.

Blocks until the query completes (up to _QUERY_TIMEOUT seconds), ensuring
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/hindsight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,7 @@ def system_prompt_block(self) -> str:
f"hindsight_retain to store facts."
)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
if self._prefetch_thread and self._prefetch_thread.is_alive():
logger.debug("Prefetch: waiting for background thread to complete")
self._prefetch_thread.join(timeout=3.0)
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/holographic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def system_prompt_block(self) -> str:
f"Use fact_feedback to rate facts after using them (trains trust scores)."
)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
if not self._retriever or not query:
return ""
try:
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/honcho/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ def system_prompt_block(self) -> str:

return header

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Return base context (representation + card) plus dialectic supplement.

Assembles two layers:
Expand Down
6 changes: 4 additions & 2 deletions plugins/memory/mem0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def system_prompt_block(self) -> str:
"mem0_profile for a full overview."
)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
if self._prefetch_thread and self._prefetch_thread.is_alive():
self._prefetch_thread.join(timeout=3.0)
with self._prefetch_lock:
Expand Down Expand Up @@ -274,14 +274,16 @@ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: st
if self._is_breaker_open():
return

effective_user_id = user_id or self._user_id

def _sync():
try:
client = self._get_client()
messages = [
{"role": "user", "content": user_content},
{"role": "assistant", "content": assistant_content},
]
client.add(messages, **self._write_filters())
client.add(messages, user_id=effective_user_id, agent_id=self._agent_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mem0 sync prefetch scope split

High Severity

When a memgw agent is reused, queue_prefetch may perform memory recall using a stale self._user_id (from agent initialization) instead of the current user_id provided. This can lead to a user receiving prefetched memory content belonging to a different user, as the prefetch guard checks the intended user for the result, not the user actually used for the recall.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 16e73b0. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in subsequent merged PRs:

This PR (#39) was superseded by #41 and closed without merging.

Reviewed and confirmed by Claude Code.


Generated by Claude Code

self._record_success()
except Exception as e:
self._record_failure()
Expand Down
10 changes: 9 additions & 1 deletion plugins/memory/memgw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def __init__(self):
self._prefetch_method = 'recall'
self._user_id = ''
self._prefetch_result = ''
self._prefetch_result_user: str = ''
self._prefetch_lock = threading.Lock()
self._prefetch_thread: threading.Thread | None = None
# Monotonic generation: only the latest queued prefetch may store its
Expand Down Expand Up @@ -284,12 +285,17 @@ def _format_recall(payload: dict) -> str:
lines.append(f'- {snippet}')
return '\n'.join(lines)

def prefetch(self, query: str, *, session_id: str = '') -> str:
def prefetch(self, query: str, *, session_id: str = '', user_id: str = '') -> str:
if self._prefetch_thread and self._prefetch_thread.is_alive():
self._prefetch_thread.join(timeout=3.0)
with self._prefetch_lock:
# Discard a result queued for a different user to prevent cross-user leak.
if user_id and self._prefetch_result_user and self._prefetch_result_user != user_id:
self._prefetch_result = ''
self._prefetch_result_user = ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memgw prefetch discard skips empty

Medium Severity

The new cross-user prefetch guard only clears cached results when both the current user_id and _prefetch_result_user are truthy. If the active turn has an empty user_id, a buffer tagged for another user can still be returned from prefetch().

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 16e73b0. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in PR #44 (fix(mem0): scope prefetch search and result cache by user_id): the guard now also discards cached results when user_id is empty or falsy (or not cached[1]), preventing a stale result from leaking into an anonymous/empty-user turn.

Reviewed and confirmed by Claude Code.


Generated by Claude Code

result = self._prefetch_result
self._prefetch_result = ''
self._prefetch_result_user = ''
if not result:
return ''
return f'## Memory Gateway\n{result}'
Expand All @@ -299,6 +305,7 @@ def on_session_switch(self, new_session_id: str, **kwargs) -> None:
# its cached result, so the new session can't be fed stale context.
with self._prefetch_lock:
self._prefetch_result = ''
self._prefetch_result_user = ''
self._prefetch_gen += 1

def queue_prefetch(self, query: str, *, session_id: str = '', user_id: str = '') -> None:
Expand Down Expand Up @@ -329,6 +336,7 @@ def _run():
with self._prefetch_lock:
if my_gen == self._prefetch_gen:
self._prefetch_result = text
self._prefetch_result_user = user_id
self._record_success()
except Exception as e:
self._record_failure()
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/openviking/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ def system_prompt_block(self) -> str:
"viking_remember, viking_add_resource."
)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Return prefetched results from the background thread."""
if self._prefetch_thread and self._prefetch_thread.is_alive():
self._prefetch_thread.join(timeout=3.0)
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/retaindb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ def _reasoning_level(query: str) -> str:
return "medium"
return "high"

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
"""Consume prefetched results and return them as a context block."""
with self._lock:
context = self._context_result
Expand Down
2 changes: 1 addition & 1 deletion plugins/memory/supermemory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ def system_prompt_block(self) -> str:
lines.append(f"\n{self._custom_container_instructions}")
return "\n".join(lines)

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
if not self._active or not self._auto_recall or not self._client or not query.strip():
return ""
try:
Expand Down
3 changes: 2 additions & 1 deletion tests/agent/test_memory_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def initialize(self, session_id, **kwargs):
def system_prompt_block(self) -> str:
return self._prompt_block

def prefetch(self, query, *, session_id=""):
def prefetch(self, query, *, session_id="", user_id=""):
self.prefetch_queries.append(query)
return self._prefetch_result

Expand Down Expand Up @@ -1248,3 +1248,4 @@ def test_no_compressor_no_injection(self):
"""Gate is moot without a context_compressor."""
tools, names, engine_names = self._run_context_engine_injection(None, None)
assert tools == []

3 changes: 2 additions & 1 deletion tests/agent/test_memory_user_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def initialize(self, session_id: str, **kwargs) -> None:
def system_prompt_block(self) -> str:
return ""

def prefetch(self, query: str, *, session_id: str = "") -> str:
def prefetch(self, query: str, *, session_id: str = "", user_id: str = "") -> str:
return ""

def sync_turn(self, user_content, assistant_content, *, session_id="", user_id=""):
Expand Down Expand Up @@ -357,3 +357,4 @@ def test_user_id_none_by_default(self):
agent._user_id = None
assert agent._user_id is None


Loading