From 346ff7b2535aef4b35def4f90681368e011db7c3 Mon Sep 17 00:00:00 2001 From: Lym308758 <64622709+Lym308758@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:20:15 +0800 Subject: [PATCH] fix(memory): rank capacity eviction per record, not per fact_id (#7454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(memory): rank capacity eviction per record, not per fact_id select_facts_for_capacity keyed every score by fact_id, so an imported batch that repeats an id collapsed into a single dict entry: the last record's score won, and both the ranking pass and the evicted audit read that one value for every position. A 0.9-confidence record could therefore be evicted while its own 0.1-confidence duplicate survived. The import path keeps caller-supplied ids without checking them for uniqueness, and any over-cap import lands here. Score by position instead: a parallel list next to the indexed facts, so ranking and the audit read each record's own score. The public ``scores`` dict stays keyed by fact_id with last-write-wins semantics, keeping the DeerMem benchmark contract and the callers that never read it unchanged. Red on main: test_confidence_policy_scores_duplicate_fact_ids_per_record fails with the highest-confidence record evicted; green on this branch. * fix(utils): keep share-less UNC roots out of the extended-length prefix extended_length_path prefixed every absolute UNC path with \\?\UNC\, but Win32 only accepts \\?\UNC\server\share\... — a share-less root (\\server, \\server\, or a bare \) is not a valid device path, so every Win32 call handed the prefixed spelling fails. Only prefix when both the server and the share are present; otherwise return the ordinary spelling unchanged. Red on main: the \\server, \\server\, //, and //server cases of test_windows_extended_spelling assert the forged prefix; green on this branch. --- .../backends/deermem/deermem/core/eviction.py | 12 ++++++--- .../harness/deerflow/utils/host_paths.py | 8 +++++- backend/tests/test_extended_length_paths.py | 4 +++ backend/tests/test_memory_eviction.py | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/eviction.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/eviction.py index 1f2e69c417..b773f63c5a 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/eviction.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/eviction.py @@ -179,9 +179,13 @@ def select_facts_for_capacity( usage = usage or {} indexed_facts = list(enumerate(facts)) scores: dict[str, FactEvictionScore] = {} + # Per-position scores: the input list may repeat a fact_id, and the public + # ``scores`` dict keeps only the last write per id, so ranking and the + # evicted audit must read each record's own score, not the shared id's. + scored: list[FactEvictionScore] = [] for index, fact in indexed_facts: fact_id = str(fact.get("id") or f"__missing_{index}") - scores[fact_id] = _score_fact( + score = _score_fact( fact, policy=policy, usage=usage.get(fact_id), @@ -192,6 +196,8 @@ def select_facts_for_capacity( confirmation_half_life_days=confirmation_half_life_days, access_half_life_days=access_half_life_days, ) + scores[fact_id] = score + scored.append(score) if len(indexed_facts) <= max_facts: return FactEvictionDecision( @@ -203,7 +209,7 @@ def select_facts_for_capacity( ranked = sorted( indexed_facts, - key=lambda item: (-scores[str(item[1].get("id") or f"__missing_{item[0]}")].value, item[0]), + key=lambda item: (-scored[item[0]].value, item[0]), ) reserved_count = 0 selected_indexes: set[int] = set() @@ -227,7 +233,7 @@ def select_facts_for_capacity( if index in selected_indexes: continue fact_id = str(fact.get("id") or f"__missing_{index}") - score = scores[fact_id] + score = scored[index] evicted.append( EvictedFact( fact_id=fact_id, diff --git a/backend/packages/harness/deerflow/utils/host_paths.py b/backend/packages/harness/deerflow/utils/host_paths.py index 55f1b20511..851a4a76ff 100644 --- a/backend/packages/harness/deerflow/utils/host_paths.py +++ b/backend/packages/harness/deerflow/utils/host_paths.py @@ -24,7 +24,13 @@ def extended_length_path(path: Path) -> Path: return path absolute = os.path.abspath(path) if absolute.startswith("\\\\"): - return Path("\\\\?\\UNC\\" + absolute[2:]) + # Win32 only accepts \\?\UNC\server\share\...; a share-less UNC root + # (\\server, \\server\, or a bare \) is not a valid device path, so + # keep its ordinary spelling instead of forging an invalid one. + server, separator, share = absolute[2:].partition("\\") + if server and separator and share: + return Path("\\\\?\\UNC\\" + absolute[2:]) + return Path(absolute) return Path("\\\\?\\" + absolute) diff --git a/backend/tests/test_extended_length_paths.py b/backend/tests/test_extended_length_paths.py index 440be37afc..83682c67cd 100644 --- a/backend/tests/test_extended_length_paths.py +++ b/backend/tests/test_extended_length_paths.py @@ -17,6 +17,10 @@ from deerflow.utils import host_paths (r"C:\shelf\..\documents", r"\\?\C:\documents"), ("C:/shelf/documents", r"\\?\C:\shelf\documents"), (r"\\server\share\shelf", r"\\?\UNC\server\share\shelf"), + (r"\\server", r"\\server"), + ("\\\\server\\", "\\\\server\\"), + (r"//", r"\\"), + (r"//server", r"\\server"), (r"\\?\C:\shelf", r"\\?\C:\shelf"), (r"\\?\UNC\server\share\shelf", r"\\?\UNC\server\share\shelf"), (r"\\.\C:\shelf", r"\\.\C:\shelf"), diff --git a/backend/tests/test_memory_eviction.py b/backend/tests/test_memory_eviction.py index 8e41658355..648bc079c3 100644 --- a/backend/tests/test_memory_eviction.py +++ b/backend/tests/test_memory_eviction.py @@ -79,6 +79,31 @@ def test_confidence_policy_coerces_non_float_confidence() -> None: assert [fact["id"] for fact in decision.kept] == ["b", "c"] +def test_confidence_policy_scores_duplicate_fact_ids_per_record() -> None: + """Duplicate ids must not collide: rank and audit each record by its own score.""" + facts = [ + _fact("dup", confidence=0.9), + _fact("dup", confidence=0.1), + _fact("other", confidence=0.5), + ] + + decision = select_facts_for_capacity( + facts, + max_facts=1, + policy=EVICTION_POLICY_CONFIDENCE, + now=NOW, + ) + + # The highest-confidence record wins even though its id repeats. + assert [fact["id"] for fact in decision.kept] == ["dup"] + assert decision.kept[0]["confidence"] == 0.9 + assert [item.fact_id for item in decision.evicted] == ["dup", "other"] + assert [item.score for item in decision.evicted] == [0.1, 0.5] + # Public contract unchanged: scores stays keyed by fact_id (last write wins). + assert decision.scores["dup"].value == pytest.approx(0.1) + assert set(decision.scores) == {"dup", "other"} + + @pytest.mark.parametrize( "policy", [EVICTION_POLICY_CONFIDENCE, EVICTION_POLICY_HYBRID_V1],