From 018c32a7790ed531f11d43eb45f86a12add6f74c Mon Sep 17 00:00:00 2001 From: hylin Date: Sat, 12 Sep 2026 02:40:17 +0800 Subject: [PATCH 1/2] 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 b60c48d40c5967ce367bfcfb29cde759b0bab9ba Mon Sep 17 00:00:00 2001 From: hylin Date: Sat, 12 Sep 2026 12:51:43 +0800 Subject: [PATCH 2/2] 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.