From f4d10ef42d721bfc2de4427bf0cfc550fb766020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=93=88=E5=9F=BA=E7=B1=B3?= <140241684+BlueX888@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:54:02 +0800 Subject: [PATCH] fix(memory): read the complete scope in the substring search fallback (#6732) * fix(memory): read the complete scope in the substring search fallback _search_substring iterated list_facts without paginating, so the adapter-less fallback only ever saw the default page of 100 facts. With 150 matching facts in a scope (max_facts allows up to 500), memory_search returned 100 results for top_k=150 and silently dropped every match past the first page; DeerMem.search keeps these results whenever they are non-empty, so its uncapped fallback never ran. Read the complete scope instead, as the scoped index rebuild already does. * test(memory): drop in-page fact-100 from the past-page assertion fact-100 was the last id inside list_facts' old first page (``matched[0:100]`` in insertion order), so its presence does not distinguish the fixed search from the pre-fix page; fact-101 and fact-150 (plus the ``len(results) == 150`` check) carry the regression signal. Keep a boundary note so the remaining ids are not misread. --- .../backends/deermem/deermem/core/storage.py | 7 +++-- .../tests/test_memory_retrieval_adapter.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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()