diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py index 9f38aef87c..bdf2c9cc27 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py @@ -1974,8 +1974,11 @@ class FileMemoryStorage(MemoryStorage): return [] results: list[dict[str, Any]] = [] for scope in scopes: - facts = self.list_facts(filters=filters, **self._scope_kwargs(scope)) - for fact in facts: + # The ranking covers the whole scope, so read the complete scope + # rather than a list_facts page. + for fact in self.load(**self._scope_kwargs(scope)).get("facts", []): + if filters and not all(key in fact and fact.get(key) == value for key, value in filters.items()): + continue content = fact.get("content") if isinstance(content, str) and query_lower in content.lower(): # 0.0 is a persisted confidence _normalize_fact accepts; only unset/null defaults. diff --git a/backend/tests/test_memory_retrieval_adapter.py b/backend/tests/test_memory_retrieval_adapter.py index b47cb0dea9..ef8cea92bf 100644 --- a/backend/tests/test_memory_retrieval_adapter.py +++ b/backend/tests/test_memory_retrieval_adapter.py @@ -318,3 +318,29 @@ def test_substring_fallback_keeps_zero_confidence_weight(tmp_path: Path) -> None assert scores == {"zero": 0.0, "half": 0.5} finally: storage.close() + + +def test_substring_fallback_searches_every_fact_of_a_large_scope(tmp_path: Path) -> None: + """The fallback must read the whole scope, not the first page of 100 facts. + + With 150 facts (max_facts allows up to 500) and no retrieval adapter, a + top_k of 200 returned only the first 100 matches: the fallback iterated + list_facts' default page instead of the complete scope, so facts past + the page silently vanished from search results. + """ + storage = FileMemoryStorage(DeerMemConfig(storage_path=str(tmp_path), max_facts=200)) + scope = {"userId": "alice", "agentName": "agent-a"} + try: + facts = [_fact(f"fact-{index:03d}", f"zeta common memory number {index:03d}") for index in range(1, 151)] + storage.apply_changes({"upserts": facts}, user_id="alice", agent_name="agent-a") + + results = storage.search_facts("zeta", scopes=[scope], top_k=200) + + assert len(results) == 150 + returned_ids = {item["fact"]["id"] for item in results} + # fact-001..fact-100 sat inside the old first page too (``matched[0:100]`` + # in insertion order), so fact-101 and fact-150 are the ids only a + # complete-scope read can return. + assert {"fact-101", "fact-150"} <= returned_ids + finally: + storage.close()