fix(memory): rank capacity eviction per record, not per fact_id (#7454)

* 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.
This commit is contained in:
Lym308758 2026-10-25 13:20:15 +08:00 committed by GitHub
parent 2ce9bdc003
commit 346ff7b253
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 45 additions and 4 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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"),

View File

@ -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],