Skip to content

Session timeline run_id filter can miss older sessions #1086

Description

@wayyoungboy

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:

  1. Call list_memories(..., run_id=run_id, limit=SESSION_TIMELINE_FETCH_CAP, ...) for records stored with the canonical run ID.
  2. 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).
  3. Merge records by memory ID before the existing _memory_run_id() filter and sorting.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions