Skip to content

feat(history): bound memory + O(turns) detail with turn pagination - #795

Open
sjawhar wants to merge 1 commit into
LuthienResearch:mainfrom
trajectory-labs-pbc:feat/history-detail-performance
Open

feat(history): bound memory + O(turns) detail with turn pagination#795
sjawhar wants to merge 1 commit into
LuthienResearch:mainfrom
trajectory-labs-pbc:feat/history-detail-performance

Conversation

@sjawhar

@sjawhar sjawhar commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes the conversation-history detail path fast and bounded-memory for very large sessions (the admin-dashboard slowness/OOM Sami reported). Complements #752 (session-list cursor pagination) and #753 (perf harness): #752 explicitly defers conversation-page turn pagination to a follow-up — this PR is that follow-up, plus the underlying O(turns) read fix.

Heads-up for maintainers: this began as fork-local work before I saw #752. It overlaps #752 on the session list (this PR makes the list read session_summaries instead of scanning payloads; #752 adds cursor pagination + HTML fragments to the list). Happy to drop/rebase the list portion onto #752's approach — flag how you'd like them reconciled. The detail portion is independent of #752.

Problem

Request payloads are cumulative — each transaction.request_recorded event stores the entire conversation so far. The detail endpoint read+parsed every turn's full cumulative payload, so a full-session read was O(turns²). A 1,771-turn session returned 69MB and did not finish in 60s. In Postgres this is worsened by TOAST: reading any field detoasts the whole cumulative blob per row.

Changes

Bounded memory

  • List endpoint reads precomputed session_summaries (no full-payload scan) with a one-time preview backfill.
  • Detail + markdown/JSONL exports stream one call at a time (bounded memory).

O(turns) detail

  • Single-anchor reconstruction: the largest cumulative request array is parsed once; per-turn deltas are sliced from it via a raw→parsed prefix map (the parser expands one raw message into several ConversationMessages). Fast path with a correctness fallback for request-modified / non-monotonic sessions. Output is byte-identical to the per-turn build.

Turn pagination (the piece #752 deferred)

  • GET /api/history/sessions/{id} takes offset/limit over turns (omit offset ⇒ newest page); only the window's payloads are read, so each request is bounded. total_turns / interventions / models are computed cheaply and stay constant across pages.
  • Frontend renders server-provided per-turn deltas directly; newest page loads first and auto-scrolls to the latest turn, scroll-up loads+prepends older pages (position preserved), display order unchanged. Exports remain full-session.

Result

1,771-turn detail: ~110–217s → ~3s per page, bounded memory, rendered conversation unchanged.

Tests

Full suite 3101 passed, ruff + pyright clean. Coverage includes windowed-vs-full byte-identity at non-zero offset, boundary-delta correctness, whole-session stats invariance across windows, an O(window) boundedness guard, and Postgres+SQLite parity. Verified live in a real browser against the deployed build.

Fixes admin dashboard slowness/OOM on large sessions reported by Sami.

- List reads session_summaries (no full-payload scan); one-time preview backfill.
- Detail/exports stream bounded-memory; O(turns) single-anchor request
  reconstruction (cumulative payloads parsed once, deltas sliced via raw->parsed
  prefix map); fast path with slow fallback for request-modified/non-monotonic.
- Conversation-detail turn pagination (offset/limit window) so each request is
  bounded — this is the conversation-page lazy-loading deferred by LuthienResearch#752.
- Frontend renders server-provided per-turn deltas directly; newest page loads
  first, scroll-up loads older; whole-session stats invariant across pages.

Relates to LuthienResearch#752 (list cursor pagination) / LuthienResearch#753 (perf harness).
@scottwofford

Copy link
Copy Markdown
Member

Claude-generated merge-queue triage of all open Luthien PRs, requested by Scott (Jul 7, 2026). Advisory only; Scott has not yet acted on these recommendations.

Recommendation: merge; top of the review queue.

This is the most complete fix available for the reported dashboard slowness: it bounds server compute on the detail path (single-anchor reconstruction instead of re-parsing every cumulative payload), adds turn pagination, and moves the session list onto precomputed summaries, with no schema migration and strong test discipline (byte-identity tests of windowed vs. full builds, Postgres/SQLite parity). It was also verified live at the scale that hurts.

Suggested path to merge:

  1. Approve the workflow runs so CI actually runs in this repo (the first-time-contributor gate has kept checks from running here).
  2. One focused review pass on history/service.py, especially the lazy summary backfill that runs on the first unfiltered list request per process: on a large existing dataset that first load will be slow, and multi-worker deployments will duplicate the (idempotent) work. A startup or offline backfill is a reasonable follow-up rather than a blocker.
  3. Note the API contract change: GET /api/history/sessions/{id} now returns the newest 50-turn page by default. The bundled frontend is updated and exports remain full-session, but any external consumer of the raw endpoint needs to pass offset/limit.

Overlap note: #803 touches the same files with a smaller payload-focused fix; the plan is to resolve in favor of this PR (see the comment there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants