Bug Description
The three session-cache backends implement the same CacheDBInterface, and get_latest_qa_entries is annotated -> list[SessionQAEntry]. On an empty/unknown session the SQL and FS adapters return [] for every last_n. The Redis adapter does not: when last_n == 1 and the session has no entries it returns None.
The repo already treats []-on-empty as the intended contract and even names this divergence. In cognee/tests/unit/infrastructure/databases/cache/test_sql_adapter_crud.py:
async def test_empty_session_returns_empty_list_for_all_last_n(adapter):
"""[] on empty for every last_n value — including last_n=1 (FS-style, not Redis quirk)."""
assert await adapter.get_latest_qa_entries("u1", "missing", last_n=1) == []
The SQL adapter was written to avoid the quirk, but the Redis adapter that has it was never fixed.
Root cause — cognee/infrastructure/databases/cache/redis/RedisAdapter.py, the last_n == 1 fast path of get_latest_qa_entries:
if last_n == 1:
data = await self.async_redis.lindex(session_key, -1)
return [SessionQAEntry.model_validate_json(data)] if data else None # <-- None on empty
data = await self.async_redis.lrange(session_key, -last_n, -1)
return [SessionQAEntry.model_validate_json(d) for d in data] if data else []
last_n == 1 is a hot path: session_turn.py calls get_session(..., last_n=1) on every turn to load the previous entry, which routes to get_latest_qa_entries(..., last_n=1). SessionManager.get_session only survives an empty session on Redis today because of a defensive if entries is None: guard (cognee/infrastructure/session/session_manager.py:489) that exists purely to paper over this. Any other caller that iterates the result (including the backward-compat get_latest_qa, as the return type invites) hits TypeError: 'NoneType' object is not iterable on Redis while working fine on SQLite/FS.
Steps to Reproduce
- Configure the Redis cache backend (
CACHE_BACKEND=redis).
- For a brand-new session with no stored QA, call
await cache.get_latest_qa_entries(user_id, "new_session", last_n=1) (equivalently SessionManager.get_session(user_id=..., session_id="new_session", last_n=1)).
- The Redis backend returns
None; the SQLite/FS backends return [].
Expected Behavior
get_latest_qa_entries returns [] on an empty/unknown session for every last_n, on every backend — matching its declared return type and the SQL/FS adapters (and the contract the SQL adapter test already documents).
Actual Behavior
On the Redis backend with last_n == 1, an empty session returns None, diverging from the other backends and the return-type annotation, and forcing downstream is None guards.
Environment
- OS: macOS 15 (also reproducible on any OS)
- Python version: 3.13
- Cognee version: current
dev
- Cache backend: redis (
CACHE_BACKEND=redis)
Logs/Error Messages
E assert None == []
cognee/tests/unit/infrastructure/databases/cache/test_redis_adapter_crud.py: AssertionError
Additional Context
Scope note: the same method also returns all entries (rather than []) when last_n <= 0 on the Redis/FS backends (lrange(key, -0, -1) / entries[-0:]), whereas SQL returns []. That path is currently guarded upstream by SessionManager._validate_session_params (which rejects last_n < 1), so I'm treating it as out of scope and only fixing the documented last_n == 1 empty-session contract violation.
Pre-submission Checklist
Bug Description
The three session-cache backends implement the same
CacheDBInterface, andget_latest_qa_entriesis annotated-> list[SessionQAEntry]. On an empty/unknown session the SQL and FS adapters return[]for everylast_n. The Redis adapter does not: whenlast_n == 1and the session has no entries it returnsNone.The repo already treats
[]-on-empty as the intended contract and even names this divergence. Incognee/tests/unit/infrastructure/databases/cache/test_sql_adapter_crud.py:The SQL adapter was written to avoid the quirk, but the Redis adapter that has it was never fixed.
Root cause —
cognee/infrastructure/databases/cache/redis/RedisAdapter.py, thelast_n == 1fast path ofget_latest_qa_entries:last_n == 1is a hot path:session_turn.pycallsget_session(..., last_n=1)on every turn to load the previous entry, which routes toget_latest_qa_entries(..., last_n=1).SessionManager.get_sessiononly survives an empty session on Redis today because of a defensiveif entries is None:guard (cognee/infrastructure/session/session_manager.py:489) that exists purely to paper over this. Any other caller that iterates the result (including the backward-compatget_latest_qa, as the return type invites) hitsTypeError: 'NoneType' object is not iterableon Redis while working fine on SQLite/FS.Steps to Reproduce
CACHE_BACKEND=redis).await cache.get_latest_qa_entries(user_id, "new_session", last_n=1)(equivalentlySessionManager.get_session(user_id=..., session_id="new_session", last_n=1)).None; the SQLite/FS backends return[].Expected Behavior
get_latest_qa_entriesreturns[]on an empty/unknown session for everylast_n, on every backend — matching its declared return type and the SQL/FS adapters (and the contract the SQL adapter test already documents).Actual Behavior
On the Redis backend with
last_n == 1, an empty session returnsNone, diverging from the other backends and the return-type annotation, and forcing downstreamis Noneguards.Environment
devCACHE_BACKEND=redis)Logs/Error Messages
Additional Context
Scope note: the same method also returns all entries (rather than
[]) whenlast_n <= 0on the Redis/FS backends (lrange(key, -0, -1)/entries[-0:]), whereas SQL returns[]. That path is currently guarded upstream bySessionManager._validate_session_params(which rejectslast_n < 1), so I'm treating it as out of scope and only fixing the documentedlast_n == 1empty-session contract violation.Pre-submission Checklist