From 637330471ff926cec74008bb1a94a76d9e55471b Mon Sep 17 00:00:00 2001 From: hylin Date: Sat, 12 Sep 2026 02:40:17 +0800 Subject: [PATCH 1/3] fix(scheduler): keep unrelated filtering for a single memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `filter_unrelated_and_redundant_memories` returned early on `len(memories) <= 1`, skipping both of the filtering steps it is responsible for. That guard is correct for `filter_redundant_memories` — one memory cannot be redundant with itself — but this method also removes memories that are unrelated to the query history, and that check is per-memory: it applies just as well to a list of one. `filter_unrelated_memories`, which does only that step, has no such guard. So the two paths disagreed on the same input: given one off-topic memory, the unrelated-only filter dropped it while the combined filter kept it and never consulted the LLM at all. The log lines record the copy: the redundancy filter says "no redundancy to filter" (accurate), while the combined one says "no filtering needed" (not accurate — unrelated filtering was still needed). The guard was carried over without widening its condition. The prompt confirms the intent. MEMORY_COMBINED_FILTERING_PROMPT asks for two steps, the first being "Unrelated Memory Removal ... Has no semantic connection to any query in the history", which needs no second memory to be meaningful. This runs on the working-memory replacement path (`optimized_scheduler.replace_working_memory`), so a single unrelated memory survived into working memory. Drop the guard. The `not memories` and `not query_history` early returns are untouched, and the LLM-failure path still conservatively keeps everything. --- .../memory_manage_modules/memory_filter.py | 4 -- tests/mem_scheduler/test_retriever.py | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/memos/mem_scheduler/memory_manage_modules/memory_filter.py b/src/memos/mem_scheduler/memory_manage_modules/memory_filter.py index 25b9a98f3..a11ee88ea 100644 --- a/src/memos/mem_scheduler/memory_manage_modules/memory_filter.py +++ b/src/memos/mem_scheduler/memory_manage_modules/memory_filter.py @@ -234,10 +234,6 @@ def filter_unrelated_and_redundant_memories( logger.info("No query history provided - keeping all memories") return memories, True - if len(memories) <= 1: - logger.info("Only one memory - no filtering needed") - return memories, True - logger.info( f"Starting combined unrelated and redundant filtering for {len(memories)} memories against {len(query_history)} queries" ) diff --git a/tests/mem_scheduler/test_retriever.py b/tests/mem_scheduler/test_retriever.py index 35c8b7f3a..0c7b6da2a 100644 --- a/tests/mem_scheduler/test_retriever.py +++ b/tests/mem_scheduler/test_retriever.py @@ -360,3 +360,53 @@ def test_filter_unrelated_memories_conservative_filtering(self): # Should return all memories self.assertEqual(result, memories) self.assertTrue(success_flag) + + def test_combined_filtering_still_filters_a_single_unrelated_memory(self): + """A lone memory must still go through unrelated filtering. + + The combined filter used to return early on `len(memories) <= 1`, a + guard that only makes sense for redundancy (one memory cannot be + redundant with itself). Unrelated filtering is per-memory, so skipping + it left an off-topic memory sitting in working memory. + """ + query_history = ["What is my deployment pipeline?"] + memories = [TextualMemoryItem(memory="The user's cat is named Whiskers")] + + self.llm.generate.return_value = json.dumps( + { + "kept_memories": [], + "unrelated_removed_count": 1, + "redundant_removed_count": 0, + "reasoning": "No semantic connection to the deployment query", + } + ) + + result, success_flag = self.retriever.filter_unrelated_and_redundant_memories( + query_history=query_history, memories=memories + ) + + self.assertEqual(result, []) + self.assertTrue(success_flag) + # The LLM must actually be consulted, not short-circuited. + self.assertTrue(self.llm.generate.called) + + def test_combined_filtering_keeps_a_single_relevant_memory(self): + """The counterpart: one on-topic memory must survive the filter.""" + query_history = ["What is my deployment pipeline?"] + memories = [TextualMemoryItem(memory="Deployment uses a blue-green pipeline")] + + self.llm.generate.return_value = json.dumps( + { + "kept_memories": [0], + "unrelated_removed_count": 0, + "redundant_removed_count": 0, + "reasoning": "Directly answers the deployment query", + } + ) + + result, success_flag = self.retriever.filter_unrelated_and_redundant_memories( + query_history=query_history, memories=memories + ) + + self.assertEqual(result, memories) + self.assertTrue(success_flag) From 1cd8ee76ddcc169ee2b78fe81b1840cdcb65311f Mon Sep 17 00:00:00 2001 From: hylin Date: Sat, 12 Sep 2026 12:51:43 +0800 Subject: [PATCH 2/3] test(scheduler): assert conservative filtering uses llm --- tests/mem_scheduler/test_retriever.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mem_scheduler/test_retriever.py b/tests/mem_scheduler/test_retriever.py index 0c7b6da2a..a40203316 100644 --- a/tests/mem_scheduler/test_retriever.py +++ b/tests/mem_scheduler/test_retriever.py @@ -360,6 +360,7 @@ def test_filter_unrelated_memories_conservative_filtering(self): # Should return all memories self.assertEqual(result, memories) self.assertTrue(success_flag) + self.assertTrue(self.llm.generate.called) def test_combined_filtering_still_filters_a_single_unrelated_memory(self): """A lone memory must still go through unrelated filtering. From 1bf82c0f624809c9b2305a0e9b1dcf4f9bc090b9 Mon Sep 17 00:00:00 2001 From: hylin Date: Fri, 18 Sep 2026 23:33:09 +0800 Subject: [PATCH 3/3] test(scheduler): assert the kept-memory path also consults the llm Per the Open Code Review note on #2383: with a single memory the removed `len(memories) <= 1` guard also returned `(memories, True)`, so asserting only the return value left this test passing on the pre-fix code. Asserting the LLM was called is the observable difference, matching the sibling test. --- tests/mem_scheduler/test_retriever.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/mem_scheduler/test_retriever.py b/tests/mem_scheduler/test_retriever.py index a40203316..a05c35a67 100644 --- a/tests/mem_scheduler/test_retriever.py +++ b/tests/mem_scheduler/test_retriever.py @@ -411,3 +411,6 @@ def test_combined_filtering_keeps_a_single_relevant_memory(self): self.assertEqual(result, memories) self.assertTrue(success_flag) + # Without this, the early return would satisfy the assertions above too: + # it also returned (memories, True) for a single memory. + self.assertTrue(self.llm.generate.called)