Evidence
Recent commit 1d13b003349d06c5441411fb6df3758381a13161 / PR #1051 added the session timeline APIs. The behavior is still present on github/main at 9648cd53fc8e5bd97f491571c45c564777bf6443.
In src/server/services/memory_service.py, _load_session_memories() accepts run_id, but the call to self.list_memories(...) does not pass that run_id down to storage. It always loads a bounded snapshot (SESSION_TIMELINE_FETCH_CAP = 5000) and only then filters by run_id in memory.
Relevant code on 9648cd53fc8e5bd97f491571c45c564777bf6443:
src/server/services/memory_service.py:55 defines SESSION_TIMELINE_FETCH_CAP = 5000
src/server/services/memory_service.py:869-885 calls list_memories(user_id=..., agent_id=..., limit=5000, offset=0, sort_by="created_at", order=...) without run_id
src/server/services/memory_service.py:890 filters run_id after the bounded snapshot has already been fetched
Minimal reproduction using a mocked MemoryService.list_memories on 9648cd53fc8e5bd97f491571c45c564777bf6443:
list_memories_run_id None
fetched_limit 5000
timeline_total 0
events []
The mock returns the target session if run_id="target-run" is pushed to storage, but returns 5000 newer unrelated records when run_id is omitted. Because _load_session_memories() omits run_id, /api/v1/memories/timeline?run_id=target-run can report zero events even though matching records exist outside the latest 5000-record global snapshot.
This affects all APIs using _load_session_memories() with run_id:
GET /api/v1/memories/sessions?run_id=...
GET /api/v1/memories/session-stats?run_id=...
GET /api/v1/memories/timeline?run_id=...
Why this is user-visible
The route query parameter says it filters by run/session ID, but the current implementation only filters inside the newest global snapshot. Busy installations can therefore return empty or partial timelines for older sessions.
Minimal fix proposal
When run_id is provided, fetch targeted records before applying the snapshot projection:
- Call
list_memories(..., run_id=run_id, limit=SESSION_TIMELINE_FETCH_CAP, ...) for records stored with the canonical run ID.
- Also preserve alias support by fetching bounded metadata matches for
session_id == run_id and thread_id == run_id (the current projection treats those as run aliases).
- Merge records by memory ID before the existing
_memory_run_id() filter and sorting.
- Add a regression test where more than
SESSION_TIMELINE_FETCH_CAP newer unrelated memories exist, and list_timeline_events(run_id=...) still returns the targeted older session.
This keeps the current alias behavior while making exact run_id queries use storage-side filtering.
Evidence
Recent commit
1d13b003349d06c5441411fb6df3758381a13161/ PR #1051 added the session timeline APIs. The behavior is still present ongithub/mainat9648cd53fc8e5bd97f491571c45c564777bf6443.In
src/server/services/memory_service.py,_load_session_memories()acceptsrun_id, but the call toself.list_memories(...)does not pass thatrun_iddown to storage. It always loads a bounded snapshot (SESSION_TIMELINE_FETCH_CAP = 5000) and only then filters byrun_idin memory.Relevant code on
9648cd53fc8e5bd97f491571c45c564777bf6443:src/server/services/memory_service.py:55definesSESSION_TIMELINE_FETCH_CAP = 5000src/server/services/memory_service.py:869-885callslist_memories(user_id=..., agent_id=..., limit=5000, offset=0, sort_by="created_at", order=...)withoutrun_idsrc/server/services/memory_service.py:890filtersrun_idafter the bounded snapshot has already been fetchedMinimal reproduction using a mocked
MemoryService.list_memorieson9648cd53fc8e5bd97f491571c45c564777bf6443:The mock returns the target session if
run_id="target-run"is pushed to storage, but returns 5000 newer unrelated records whenrun_idis omitted. Because_load_session_memories()omitsrun_id,/api/v1/memories/timeline?run_id=target-runcan report zero events even though matching records exist outside the latest 5000-record global snapshot.This affects all APIs using
_load_session_memories()withrun_id:GET /api/v1/memories/sessions?run_id=...GET /api/v1/memories/session-stats?run_id=...GET /api/v1/memories/timeline?run_id=...Why this is user-visible
The route query parameter says it filters by run/session ID, but the current implementation only filters inside the newest global snapshot. Busy installations can therefore return empty or partial timelines for older sessions.
Minimal fix proposal
When
run_idis provided, fetch targeted records before applying the snapshot projection:list_memories(..., run_id=run_id, limit=SESSION_TIMELINE_FETCH_CAP, ...)for records stored with the canonical run ID.session_id == run_idandthread_id == run_id(the current projection treats those as run aliases)._memory_run_id()filter and sorting.SESSION_TIMELINE_FETCH_CAPnewer unrelated memories exist, andlist_timeline_events(run_id=...)still returns the targeted older session.This keeps the current alias behavior while making exact
run_idqueries use storage-side filtering.