fix: rank memories globally before budget walk

Per-type ranking was still starving later types: when a p05 query
matched a 'knowledge' memory best but 'project' came first in the
type order, the project-type candidates filled the budget before
the knowledge-type pool was even ranked.

Collect all candidates into a single pool, dedupe by id, then
rank the whole pool once against the query before walking the
flat budget. Python's stable sort preserves insertion order (which
still reflects the caller's memory_types order) as a natural
tiebreaker when scores are equal.

Regression surfaced by the retrieval eval harness:
p05-vendor-signal still missing 'Zygo' after 5aeeb1c — the vendor
memory was type=knowledge but never reached the ranker because
type=project consumed the budget first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 12:55:10 -04:00
parent 5aeeb1cad1
commit 37331d53ef

View File

@@ -389,29 +389,37 @@ def get_memories_for_context(
if not query_tokens: if not query_tokens:
query_tokens = None query_tokens = None
# Flat budget across types so paragraph-length project memories # Collect ALL candidates across the requested types into one
# aren't starved by an even slice. Types are still walked in order # pool, then rank globally before the budget walk. Ranking per
# (identity/preference first when they're the input), so earlier # type and walking types in order would starve later types when
# types still get first pick when the budget is tight. # the first type's candidates filled the budget — even if a
# later-type candidate matched the query perfectly. Type order
# is preserved as a stable tiebreaker inside
# ``_rank_memories_for_query`` via Python's stable sort.
pool: list[Memory] = []
seen_ids: set[str] = set()
for mtype in memory_types: for mtype in memory_types:
# Raise the fetch limit above the budget slice so query-relevance for mem in get_memories(
# ordering has a real pool to rerank. Without a query, the extras
# just fall off the end harmlessly.
candidates = get_memories(
memory_type=mtype, memory_type=mtype,
project=project, project=project,
min_confidence=0.5, min_confidence=0.5,
limit=30, limit=30,
) ):
if query_tokens is not None: if mem.id in seen_ids:
candidates = _rank_memories_for_query(candidates, query_tokens)
for mem in candidates:
entry = f"[{mem.memory_type}] {mem.content}"
entry_len = len(entry) + 1
if entry_len > available - used:
continue continue
selected_entries.append(entry) seen_ids.add(mem.id)
used += entry_len pool.append(mem)
if query_tokens is not None:
pool = _rank_memories_for_query(pool, query_tokens)
for mem in pool:
entry = f"[{mem.memory_type}] {mem.content}"
entry_len = len(entry) + 1
if entry_len > available - used:
continue
selected_entries.append(entry)
used += entry_len
if not selected_entries: if not selected_entries:
return "", 0 return "", 0