Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ def init_agent(
# Persistent memory (MEMORY.md + USER.md) -- loaded from disk
agent._memory_store = None
agent._memory_enabled = False
agent._memory_sync_recall = False
agent._user_profile_enabled = False
agent._memory_nudge_interval = 10
agent._turns_since_memory = 0
Expand All @@ -939,6 +940,7 @@ def init_agent(
try:
mem_config = _agent_cfg.get("memory", {})
agent._memory_enabled = mem_config.get("memory_enabled", False)
agent._memory_sync_recall = mem_config.get("sync_recall", False)
agent._user_profile_enabled = mem_config.get("user_profile_enabled", False)
agent._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
if agent._memory_enabled or agent._user_profile_enabled:
Expand Down
15 changes: 12 additions & 3 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,16 +568,25 @@ def run_conversation(
except Exception:
pass

# External memory provider: prefetch once before the tool loop.
# External memory provider: recall once before the tool loop.
# Reuse the cached result on every iteration to avoid re-calling
# prefetch_all() on each tool call (10 tool calls = 10x latency + cost).
# provider recall on each tool call (10 tool calls = 10x latency + cost).
# Use original_user_message (clean input) — user_message may contain
# injected skill content that bloats / breaks provider queries.
_ext_prefetch_cache = ""
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 ""
if getattr(agent, "_memory_sync_recall", False):
_ext_prefetch_cache = agent._memory_manager.recall_sync_all(
_query,
session_id=agent.session_id or "",
) or ""
else:
_ext_prefetch_cache = agent._memory_manager.prefetch_all(
_query,
session_id=agent.session_id or "",
) or ""
except Exception:
pass

Expand Down
19 changes: 19 additions & 0 deletions agent/memory_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,25 @@ def queue_prefetch_all(self, query: str, *, session_id: str = "") -> None:
provider.name, e,
)

def recall_sync_all(self, query: str, *, session_id: str = "") -> str:
"""Synchronously recall context from all providers using the current query.

Use in place of prefetch_all() when memory.sync_recall is enabled.
Slower but always returns context relevant to the current message.
"""
parts = []
for provider in self._providers:
try:
result = provider.recall_sync(query, session_id=session_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When porting onto current main, normalize this query exactly as prefetch_all() does at agent/memory_manager.py:501-503; otherwise /skill and bundle-expanded turns send provider recall the full injected skill body rather than the user instruction.

if result and result.strip():
parts.append(result)
except Exception as e:
logger.debug(
"Memory provider '%s' recall_sync failed (non-fatal): %s",
provider.name, e,
)
return "\n\n".join(parts)

# -- Sync ----------------------------------------------------------------

def sync_all(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
Expand Down
16 changes: 16 additions & 0 deletions agent/memory_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
that do background prefetching should override this.
"""

def recall_sync(self, query: str, *, session_id: str = "") -> str:
"""Recall relevant context synchronously using the current turn's query.

Unlike prefetch(), which returns a background result queued for the
*previous* turn, recall_sync() performs a fresh lookup for the
*current* query. This guarantees relevance at the cost of added latency.

Default: fires queue_prefetch() with the current query then immediately
calls prefetch() which joins the background thread. This is correct for
all providers that join their thread inside prefetch() (honcho, hindsight,
mem0, openviking). Providers whose prefetch() reads shared state without
joining (retaindb) should override this method.
"""
self.queue_prefetch(query, session_id=session_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This generic composition is not synchronous for current Honcho: its prefetch_context() starts a daemon thread and its prefetch() only consumes a ready cache (plugins/memory/honcho/session.py:666-695). Please provide an explicit synchronous Honcho path (or narrow this contract) before claiming current-query relevance.

return self.prefetch(query, session_id=session_id)

def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
"""Persist a completed turn to the backend.

Expand Down
11 changes: 11 additions & 0 deletions plugins/memory/retaindb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,17 @@ def system_prompt_block(self) -> str:

# ── Background prefetch (fires at turn-end, consumed next turn-start) ──

def recall_sync(self, query: str, *, session_id: str = "") -> str:
"""RetainDB override: prefetch() reads shared state without joining threads,
so the base queue_prefetch+prefetch pattern would race. Run the three
fetchers synchronously instead."""
if not self._client:
return ""
self._prefetch_context(query)
self._prefetch_dialectic(query)
self._prefetch_agent_model()
return self.prefetch(query, session_id=session_id)

def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
"""Fire context + dialectic + agent model prefetches in background."""
if not self._client:
Expand Down
9 changes: 5 additions & 4 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1988,10 +1988,11 @@ def _sync_external_memory_for_turn(
original_user_message, final_response,
session_id=self.session_id or "",
)
self._memory_manager.queue_prefetch_all(
original_user_message,
session_id=self.session_id or "",
)
if not getattr(self, "_memory_sync_recall", False):
self._memory_manager.queue_prefetch_all(
original_user_message,
session_id=self.session_id or "",
)
except Exception:
pass

Expand Down
Loading