fix(memory): apply Honcho read policies to malformed search metadata (#7890)

Co-authored-by: Cordis798 <284918518+Cordis798@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Cordis798 2026-11-05 07:04:15 +08:00 committed by GitHub
parent 728446fe0d
commit 6538496dbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 1 deletions

View File

@ -19,6 +19,9 @@ to the constructed backend and require backend recreation to change.
This directory owns memory capture, storage, retrieval, prompt injection, and model-driven memory tools.
Honcho search rejects non-object, non-null `metadata` at the HTTP boundary:
apply the read policy to the whole result, with value-free diagnostics.
#### Main components
- `manager.py` defines the backend-neutral `MemoryManager` contract.

View File

@ -71,6 +71,13 @@ cannot see each other's memory by construction.
workspace-scoped search, while the passive per-turn write middleware is
retained because Honcho's deriver learns from `add()` writes.
Search result `metadata` must be an object, null, or absent. The HTTP client
rejects other containers before returning results, so `failure_policy.read`
also covers metadata-shape failures: fail-open returns no search results and
fail-closed raises `MemoryReadError`. A malformed later result does not expose
a partial result set. Diagnostics name the result index and field, without
echoing metadata or memory content.
## Incremental conversation capture
Checkpoint history is cumulative. For messages with stable source IDs, the

View File

@ -106,4 +106,9 @@ class HonchoClient:
if filters:
payload["filters"] = filters
data = self._post(f"/v3/workspaces/{workspace}/search", payload)
return list(data) if isinstance(data, list) else []
results = list(data) if isinstance(data, list) else []
for index, item in enumerate(results):
metadata = item.get("metadata") if isinstance(item, dict) else None
if metadata is not None and not isinstance(metadata, dict):
raise HonchoRequestError(f"Honcho search results[{index}].metadata must be an object or null")
return results

View File

@ -0,0 +1,62 @@
"""Malformed remote search metadata must honor the configured read policy."""
import json
import logging
import httpx
import pytest
from deerflow.agents.memory.backends.honcho.client import HonchoClient
from deerflow.agents.memory.backends.honcho.honcho_manager import HonchoMemoryManager
from deerflow.agents.memory.manager import MemoryReadError
@pytest.mark.asyncio
@pytest.mark.parametrize("async_read", [False, True])
@pytest.mark.parametrize("policy", ["fail_open", "fail_closed"])
@pytest.mark.parametrize("metadata", ["private-invalid-metadata", ["private-invalid-metadata"], 7, True])
async def test_search_metadata_failure_honors_read_policy(metadata, policy, async_read, caplog):
manager = HonchoMemoryManager.from_config({"base_url": "http://honcho.test", "failure_policy": {"read": policy}})
manager._client.close()
requests = []
def respond(request):
requests.append(request)
assert json.loads(request.content) == {"query": "synthetic query", "limit": 5}
return httpx.Response(200, json=[{"content": "private-valid-content", "metadata": {"category": "preference"}}, {"content": "private-invalid-content", "metadata": metadata}])
manager._client = HonchoClient(manager._config, transport=httpx.MockTransport(respond))
async def search():
if async_read:
return await manager.asearch("synthetic query", user_id="synthetic-user")
return manager.search("synthetic query", user_id="synthetic-user")
try:
with caplog.at_level(logging.WARNING):
if policy == "fail_closed":
with pytest.raises(MemoryReadError) as error:
await search()
diagnostics = str(error.value)
else:
assert await search() == []
diagnostics = caplog.text
assert "metadata" in diagnostics
for secret in ("private-invalid-metadata", "private-valid-content", "private-invalid-content"):
assert secret not in diagnostics
assert len(requests) == 1
finally:
manager.close()
@pytest.mark.parametrize("metadata", [None, {}, {"category": "preference"}])
def test_search_preserves_valid_metadata(metadata):
manager = HonchoMemoryManager.from_config({"base_url": "http://honcho.test"})
manager._client.close()
manager._client = HonchoClient(manager._config, transport=httpx.MockTransport(lambda _: httpx.Response(200, json=[{"content": "Synthetic content", "metadata": metadata}])))
try:
result = manager.search("synthetic query", user_id="synthetic-user")
assert result[0]["content"] == "Synthetic content"
assert result[0]["category"] == ("preference" if metadata else "memory")
finally:
manager.close()