fix(recall): cap default limit to 30 + relevance-bound OR-broadening
Some checks failed
ci/woodpecker/push/deploy Pipeline failed
ci/woodpecker/push/build Pipeline failed

memory_recall was returning almost the entire store instead of a small
set of relevant matches. Two compounding causes, both fixed here:

1. Default limit was 10000 (commit d03a77a "effectively unlimited").
   recall_memories/MemoryRecall and the memory_recall MCP tool now
   default to 30 (ceiling stays 10000 for callers that opt in).

2. The OR-broadening fallback (fires when the precise AND-match is
   sparse) ordered by the importance hybrid and padded up to `limit`,
   so with limit=10000 it flooded results with high-importance but
   irrelevant memories. It now orders OR-matches by ts_rank(relevance)
   DESC and applies a minimum-rank floor (OR_BROADEN_MIN_RANK=0.01) to
   drop rows that merely contain a query word incidentally.

Tests: add test_recall_default_limit_is_capped (asserts 30 passed to
fetch) and test_recall_or_broadening_is_relevance_bounded (asserts the
OR query is relevance-ordered + rank-floored). Full suite 176 green,
ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-04 18:48:11 +00:00
parent 1fa6c2031e
commit 1c0193f011
3 changed files with 64 additions and 4 deletions

View file

@ -162,6 +162,56 @@ async def test_recall_returns_all_memories(client):
assert results[0]["owner"] == "testuser"
@pytest.mark.asyncio
async def test_recall_default_limit_is_capped(client):
"""Default recall limit must be a small top-N (30), not the whole store.
Regression for the 'recall returns the entire ~1460-memory store' bug:
the default was 10000, so a default call returned everything.
"""
ac, conn, app_mod = client
conn.fetch.return_value = [_make_memory_row(id=1)]
async with ac:
resp = await ac.post(
"/api/memories/recall",
json={"context": "singleword"}, # 1 word -> no OR-broadening
headers={"Authorization": "Bearer test-key"},
)
assert resp.status_code == 200
# fetch(sql, user_id, query_text, limit[, category]) -> limit is the 4th arg
first_args = conn.fetch.call_args_list[0].args
assert first_args[3] == 30, f"default recall limit should be 30, got {first_args[3]}"
@pytest.mark.asyncio
async def test_recall_or_broadening_is_relevance_bounded(client):
"""When the precise AND-match is sparse, the OR-broadening fallback must
order by relevance (ts_rank) and apply a minimum-rank floor not pad up to
`limit` ordered by the importance hybrid (which floods with high-importance
but irrelevant memories).
"""
ac, conn, app_mod = client
conn.fetch.side_effect = [
[_make_memory_row(id=1)], # AND-match: 1 row (< limit -> triggers OR)
[_make_memory_row(id=2)], # OR-broadening result
]
async with ac:
resp = await ac.post(
"/api/memories/recall",
json={"context": "two words"}, # >1 word -> OR-broadening fires
headers={"Authorization": "Bearer test-key"},
)
assert resp.status_code == 200
assert len(conn.fetch.call_args_list) == 2, "OR-broadening query should fire"
or_sql = conn.fetch.call_args_list[1].args[0]
assert "ts_rank(search_vector, query) DESC" in or_sql, "OR matches must be ordered by relevance"
assert "ts_rank(search_vector, query) >" in or_sql, "OR matches must have a minimum-rank floor"
@pytest.mark.asyncio
async def test_recall_redacts_sensitive_memories(client):
ac, conn, app_mod = client