Skip to content

History pagination never runs: legacy /api/history route shadows the paginated handler #125

Description

@jdmanring

The scroll-up history pager added in 45ee5a71 never activates. A pre-existing route on the sessions router shadows the paginated history endpoint, so the backend returns the full history on every request and has_more_before is never sent.

Root cause

routes/session_routes.py builds its router with prefix="/api" (session_routes.py:125) and defines a legacy GET /history/{sid} (session_routes.py:748) — effective path /api/history/{sid}. That handler ignores limit/offset and returns the entire history.

routes/history/history_routes.py:118 defines the paginated GET /api/history/{session_id} (get_session_history), which is where limit/offset/has_more_before come from (history_routes.py:161).

Both routes match the same path. app.py registers the sessions router first (app.py:658) and the history router second (app.py:687), and FastAPI dispatches to the first matching route — so the legacy handler wins and the paginated one is dead.

The two handlers have coexisted at /api/history since the e5c99a5e base, but the collision was benign until 45ee5a71: before then get_session_history also returned the full history, so which handler won made no difference. 45ee5a71 added the pagination branch (limit/offset/has_more_before, history_routes.py:161) to get_session_history — which is what made the shadowing consequential, because the handler that now paginates is the one that never runs. It shipped without removing (or reordering) the legacy handler.

Impact

GET /api/history/{id}?limit=24 returns every message, with no total and no has_more_before. The frontend then short-circuits at if (!box || !pageInfo || !pageInfo.has_more_before) return; (sessions.js:176), so _installHistoryPager never installs and the whole conversation is rendered into the DOM on session open — the unbounded growth the pager was written to prevent. This hits long histories hardest, and mobile in particular, which is what 45ee5a71 ("Polish mobile UI and editor workflows") set out to improve.

Reproduction

  1. Open a session with more messages than one page (> HISTORY_PAGE_LIMIT_DESKTOP).
  2. curl '.../api/history/<id>?limit=24' — the response carries the full history, with no total and no has_more_before.
  3. In the UI, scroll to the top; no older-message paging occurs, because the pager never installed.

Fix

Remove the legacy get_history handler (session_routes.py:748-755). get_session_history already serves the no-limit case through its fallback and returns the same {role, content, metadata} shape as ChatMessage.to_dict(), so it subsumes the route. The four no-limit callers — documentLibrary copy and preview, sessions.js "Copy Chat", and the archived-session peek — read only role/content. The fallback additionally strips inline base64 media and drops hidden compaction rows, both of which are improvements here (e.g. "Copy Chat" previously JSON.stringify'd raw base64 image bytes into the clipboard).

Environment

Present on upstream/dev at c67deaa6 (route order and both handlers as cited). #5090 (6f6cb6ea) is unrelated — it only moved the history routes into routes/history/; it neither introduced nor fixed this.

The fork's develop carried the same collision, and its own scroll pager (_fetchOlderFromServer) was equally inert. Fixed on develop in 268d713c, with an end-to-end regression guard in tests/test_chat_history_render_paging_playwright.py.

Branch: fix/chat-history-dom-eviction (from upstream-mirror), commit 1 of 2. Upstream-candidate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingupstream-candidateBug or feature suitable for contributing to upstream

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions