Skip to content

fix(scheduler): keep unrelated filtering for a single memory in combined filter - #2361

Open
linhongyu510 wants to merge 2 commits into
MemTensor:dev-v2.0.34from
linhongyu510:fix/combined-filter-single-memory
Open

fix(scheduler): keep unrelated filtering for a single memory in combined filter#2361
linhongyu510 wants to merge 2 commits into
MemTensor:dev-v2.0.34from
linhongyu510:fix/combined-filter-single-memory

Conversation

@linhongyu510

Copy link
Copy Markdown

What's wrong

MemoryFilter.filter_unrelated_and_redundant_memories starts with three early
returns. The third one is wrong:

if len(memories) <= 1:
    logger.info("Only one memory - no filtering needed")
    return memories, True

This method is responsible for two filtering steps, and they have different
arity requirements:

step needs 2+ memories?
redundancy removal — "keep the most informative of several similar memories" yes, one memory cannot be redundant with itself
unrelated removal — "drop memories with no semantic connection to any query" no, this is per-memory

The guard skips both. With a single memory, unrelated filtering never runs and
the LLM is never called.

The two paths disagree on the same input

filter_unrelated_memories (line 18), which performs only the unrelated step,
has no len(memories) <= 1 guard — it filters a single memory correctly.
filter_redundant_memories (line 105) has the guard, and there it is right.

So for one off-topic memory the two code paths return opposite results.
Reproduced against the real MemoryFilter with a mocked LLM:

memory  = "The user's cat is named Whiskers."
queries = ["what is my deployment pipeline?", "how do I roll back a release?"]

filter_unrelated_and_redundant_memories -> ["The user's cat is named Whiskers."]   LLM called: False
filter_unrelated_memories               -> []                                     LLM called: True

Why this looks like a copy that was not adjusted

The log messages preserve the history:

  • memory_filter.py:139 (redundancy filter) — "Only one memory - no redundancy to filter" — accurate.
  • memory_filter.py:238 (combined filter) — "Only one memory - no filtering needed" — not accurate; unrelated filtering was still needed.

The guard was carried over from the redundancy-only method without widening its
condition, and the log text was softened rather than re-derived.

MEMORY_COMBINED_FILTERING_PROMPT
(src/memos/templates/mem_scheduler_prompts.py:279) states the intent
explicitly — it asks for two steps, the first being:

Unrelated Memory Removal: Remove memories that are completely unrelated to
the user's query history … Has no semantic connection to any query in the history

Nothing there requires a second memory to be present.

Impact

The combined filter runs on the working-memory replacement path,
OptimizedScheduler.replace_working_memory
(src/memos/mem_scheduler/optimized_scheduler.py:318). When reranking leaves a
single candidate, an unrelated memory survives into working memory and is then
carried into subsequent prompts.

The fix

Delete the guard. Behaviour that is deliberately preserved:

  • not memories([], True) unchanged.
  • not query_history → keep everything unchanged (no relevance signal available).
  • LLM failure → still conservatively returns all memories with success_flag=False.

Tests

Two cases added to tests/mem_scheduler/test_retriever.py, following the
existing convention in that file (self.retriever, MagicMock(spec=BaseLLM),
json.dumps(...) for the mocked response):

  • test_combined_filtering_still_filters_a_single_unrelated_memory — one
    off-topic memory must be dropped, and asserts llm.generate.called so the
    guard cannot come back as a silent short-circuit.
  • test_combined_filtering_keeps_a_single_relevant_memory — the counterpart, so
    the fix cannot pass by simply discarding lone memories.

Note the mocked response uses kept_memories, which is the key this method
actually reads (memory_filter.py:264) — not relevant_memories, which is the
unrelated-only filter's schema.

Verification

Environment note: the repo uses poetry; poetry was unavailable locally, so the
venv was built with uv (uv pip install -e .). pika had to be installed
separately — without it tests/mem_scheduler/ fails to import via
src/memos/dependency.py:47. uv.lock was left untouched.

tests/mem_scheduler/test_retriever.py                      18 passed
tests/mem_scheduler/                                       67 passed, 1 skipped, 1 failed
ruff 0.11.8 check  (version pinned in .pre-commit-config.yaml)   All checks passed
ruff 0.11.8 format --check                                 2 files already formatted

The one failure, test_scheduler.py::TestGeneralScheduler::test_dynamic_cache_layers_access,
is pre-existing: it fails identically on an unmodified dev-v2.0.34.

Wider suite, excluding two files that need torch
(tests/llms/test_hf.py, tests/memories/activation/test_kv.py) — identical
before and after this change, so no regression:

baseline:   11 failed, 763 passed, 8 skipped, 25 errors
with fix:   11 failed, 763 passed, 8 skipped, 25 errors

Those remaining failures/errors are missing optional dependencies in my local
environment, not related to this change.

Reverse-verified: with the source change stashed and the new tests kept,
test_combined_filtering_still_filters_a_single_unrelated_memory fails and the
other 17 pass — so the test pins this specific behaviour rather than passing
incidentally.

AI disclosure

This change was prepared with AI assistance. The defect was found by auditing
the three filter methods against each other, then confirmed by executing the
real MemoryFilter class; every claim above (log line numbers, prompt text,
call site, test counts, baseline comparison) was verified by running the code
rather than inferred.

`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.
@Memtensor-AI Memtensor-AI added area:scheduler 调度模块 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Sep 11, 2026
@Memtensor-AI

Memtensor-AI commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2361
Task: 297a7fe1db86e02e
Base: dev-v2.0.34
Head: fix/combined-filter-single-memory

🔍 OpenCodeReview found 1 issue(s) in this PR.


1. tests/mem_scheduler/test_retriever.py (L361-L362)

The test is missing an assertion that self.llm.generate was actually called. Without it, this test cannot catch a regression of the early-return bug it documents: if len(memories) <= 1 were re-introduced, the function would return memories immediately (which equals the expected memories), so result == memories would still pass while the LLM was never consulted.

The companion test test_combined_filtering_still_filters_a_single_unrelated_memory correctly asserts self.assertTrue(self.llm.generate.called) — the same assertion should be added here for symmetry and correctness.

💡 Suggested Change

Before:

        self.assertEqual(result, memories)
        self.assertTrue(success_flag)

After:

        self.assertEqual(result, memories)
        self.assertTrue(success_flag)
        # The LLM must actually be consulted, not short-circuited.
        self.assertTrue(self.llm.generate.called)

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: All 18 tests fail in setUp during SchedulerFactory.from_config initialization, before any test logic runs. The traceback is truncated but points to scheduler construction, not to the code changes under test. [advisory, non-gating] AI-generated tests on branch test/auto-gen-ee015907ff793e43-20260912024636: 68/68 passed — these do NOT affect the PR verdict; review the branch manually.
Branch: fix/combined-filter-single-memory

@linhongyu510

Copy link
Copy Markdown
Author

Addressed the automated review suggestion in b60c48d4 by asserting the conservative-filtering test actually calls the LLM. Local verification: target test 1 passed; full tests/mem_scheduler/test_retriever.py 18 passed; Ruff check/format and the complete pre-commit suite passed. The existing autotest failure is still reported by the bot as an environment issue during SchedulerFactory.from_config setup.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: All 18 tests fail at setUp during SchedulerFactory.from_config initialization, before any test logic runs. The traceback is truncated but the failure occurs in the scheduler's init chain, unrelated to the diff's changes in memory_filter.py or the new test cases. [advisory, non-gating] AI-generated tests on branch test/auto-gen-297a7fe1db86e02e-20260912125650: 80/80 passed — these do NOT affect the PR verdict; review the branch manually.
Branch: fix/combined-filter-single-memory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:scheduler 调度模块 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants