Files
odysseus/tests/test_consolidate_memory_explicit_drops.py
T
Mazen Tamer Salah d71284194b fix(memory): only delete memories the model explicitly drops in tidy (#3455)
* fix(memory): only delete memories the model explicitly drops in tidy

The AI memory-tidy path computed deletions as the complement of the model's
`keep` list (`if mid not in keep_ids: continue`). When the model returned a
valid response that simply omitted some existing ids — a common LLM lapse — every
omitted memory was silently deleted, even though it was neither a duplicate nor
listed in `drop`.

Honor the explicit `drop` set instead: delete only ids the model dropped (minus
any it saw only truncated), and preserve everything else, still applying cleaned
text/category from `keep`.

Adds tests/test_consolidate_memory_explicit_drops.py: a memory the model omits
from both keep and drop survives; an explicitly dropped one is removed.

* refactor(memory): remove now-dead keep_ids from tidy

After deletion switched to drop_ids and text/category rewrites to cleaned_by_id,
keep_ids was written but never read. Remove the init, the .add(mid) in the keep
loop, and the truncated .update() (its truncated-protection is already covered by
`drop_ids -= truncated_ids`). Pure deletion, no behavior change; tests stay green.

Addresses review feedback on #3455.

---------

Co-authored-by: Kenny Van de Maele <kenny@kvandemaele.be>
2026-06-08 18:54:45 +02:00

58 lines
1.9 KiB
Python

"""Memory consolidation must delete only memories the model explicitly drops.
The AI tidy path computed deletions as the complement of the model's `keep`
list, so any memory the model simply omitted (a common LLM lapse) was silently
deleted. The fix honors the explicit `drop` set, so an omitted memory survives.
"""
import asyncio
import json
import src.builtin_actions as ba
class _FakeMM:
saved = None
def __init__(self, *args, **kwargs):
pass
def load_all(self):
return [
{"id": "a", "owner": "alice", "text": "Likes dark roast coffee", "category": "preference"},
{"id": "b", "owner": "alice", "text": "Likes dark roast coffee too", "category": "preference"},
{"id": "c", "owner": "alice", "text": "Lives in Cairo", "category": "fact"},
]
def save(self, entries):
_FakeMM.saved = list(entries)
def test_omitted_memory_survives_only_explicit_drop(monkeypatch):
import src.memory
import src.endpoint_resolver
import src.llm_core
_FakeMM.saved = None
monkeypatch.setattr(src.memory, "MemoryManager", _FakeMM)
monkeypatch.setattr(
src.endpoint_resolver, "resolve_endpoint",
lambda kind, owner=None: ("http://x/v1", "model", {}),
)
async def fake_llm(**kwargs):
# Model keeps 'a', drops 'b', and OMITS 'c' entirely.
return json.dumps({
"keep": [{"id": "a", "text": "Likes dark roast coffee", "category": "preference"}],
"drop": [{"id": "b", "reason": "duplicate of a"}],
})
monkeypatch.setattr(src.llm_core, "llm_call_async", fake_llm)
msg, ok = asyncio.run(ba.action_consolidate_memory("alice"))
assert ok, msg
ids = {m["id"] for m in _FakeMM.saved}
assert "c" in ids, "omitted memory must NOT be deleted"
assert "a" in ids
assert "b" not in ids, "explicitly dropped memory should be removed"