feat: same-turn memory retrieval in provider mode

prefetch() now reuses the in-flight background search when it matches
the current query, or falls back to a synchronous search (capped at 3s,
split across both requests in case-insensitive mode) so memories are
injected on the same turn instead of one turn behind.

- Track _prefetch_query to scope cached results to their query
- Reset _prefetch_result when starting a new background search to
  prevent stale cross-turn injection after a timed-out join
- Guard background thread writes against superseded queries
- Add optional per-request timeout override to LocalMem0Client
- Update AGENTS.md and README to describe the same-turn flow
This commit is contained in:
2026-08-15 19:52:25 +02:00
parent fad5eb7f1e
commit c79cf43ff8
4 changed files with 64 additions and 25 deletions
+11 -6
View File
@@ -39,17 +39,19 @@ class LocalMem0Client:
endpoint: str,
json: Optional[Dict] = None,
params: Optional[Dict] = None,
timeout: Optional[float] = None,
) -> Dict:
"""Make HTTP request with error handling."""
url = f"{self.base_url}{endpoint}"
effective_timeout = timeout if timeout is not None else self.timeout
try:
resp = self.session.request(
method, url, json=json, params=params, timeout=self.timeout
method, url, json=json, params=params, timeout=effective_timeout
)
resp.raise_for_status()
return resp.json()
except requests.exceptions.Timeout:
logger.error("Mem0 request timed out after %ss", self.timeout)
logger.error("Mem0 request timed out after %ss", effective_timeout)
raise
except requests.exceptions.ConnectionError as e:
logger.error("Failed to connect to Mem0 server at %s: %s", self.base_url, e)
@@ -66,6 +68,7 @@ class LocalMem0Client:
user_id: Optional[str] = None,
limit: int = 5,
case_insensitive: bool = False,
timeout: Optional[float] = None,
) -> List[Dict]:
"""Search memories by semantic similarity.
@@ -78,18 +81,19 @@ class LocalMem0Client:
user_id: User identifier
limit: Max results
case_insensitive: If True, search with both original and lowercase query
timeout: Optional per-request timeout override (seconds)
"""
if not case_insensitive:
payload = {"query": query, "limit": limit}
if user_id:
payload["user_id"] = user_id
result = self._request("POST", "/search", json=payload)
result = self._request("POST", "/search", json=payload, timeout=timeout)
return result.get("results", [])
# Case-insensitive mode: search with both original and lowercase
# Fetch 2x limit to ensure we get top N after merging
results_original = self._search_with_query(query, user_id, limit * 2)
results_lower = self._search_with_query(query.lower(), user_id, limit * 2)
results_original = self._search_with_query(query, user_id, limit * 2, timeout)
results_lower = self._search_with_query(query.lower(), user_id, limit * 2, timeout)
# Merge and deduplicate, keeping highest score
merged = {}
@@ -109,12 +113,13 @@ class LocalMem0Client:
query: str,
user_id: Optional[str] = None,
limit: int = 5,
timeout: Optional[float] = None,
) -> List[Dict]:
"""Internal search helper for case-insensitive mode."""
payload = {"query": query, "limit": limit}
if user_id:
payload["user_id"] = user_id
result = self._request("POST", "/search", json=payload)
result = self._request("POST", "/search", json=payload, timeout=timeout)
return result.get("results", [])
def get_all(self, user_id: Optional[str] = None) -> List[Dict]: