Skip to content

perf(sessions): read transcripts incrementally for the picker - #740

Merged
Juliusolsson05 merged 3 commits into
mainfrom
perf/session-index-incremental-prompts
Sep 3, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
perf/session-index-incremental-prompts

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

src/main/sessionIndex.ts extracted a transcript's user prompts by reading the whole file, splitting it and parsing every line, cached by mtime only. The picker lists the ten most recently modified transcripts — by definition the live sessions, whose mtime moves every few seconds — so every open was a cache miss for exactly the largest files: 122 MB across the top ten on this machine (one Codex rollout alone 89 MB), read, split into millions of sliced strings and parsed on the main thread to show four prompts each. Search did the same for all 2,150 transcripts on disk per query (including a 148 MB rollout), and the module-global cache never evicted. Details in #735.

What changed

  • A cache entry is a parsed byte range [parsedFrom, parsedTo) plus the prompts folded from it, validated by mtime and size:
    • growth reads only [parsedTo, size) and folds complete lines (a partial trailing line waits for its newline);
    • listing (need = promptsPerSession) folds the tail in a growing window (256 KiB ×4) until it has enough prompts or reaches the head; Codex's cwd, which lives only in the head session_meta, comes from one bounded head read;
    • search (need = 'all') extends to the head once and is incremental afterwards;
    • a rewrite (size below the parsed range, or mtime moved with size unchanged) re-parses from scratch.
  • Adjacent-duplicate prompts are still collapsed, including across a chunk seam (older occurrence wins, as before).
  • The cache is an LRU of 512 entries; search considers the 500 most recently modified candidates (results are recency-ranked and capped at 20).
  • extractPromptsFromFile is exported with test hooks for the cache.

Unchanged: per-provider prompt filters, newest-first order and promptsPerSession slicing, cwd resolution order, the discovery walks.

Linked issues

Fixes #735. Refs #103, #93, #96. Enables #739 (smart resume).

Verification

  • NODE_ENV=test npx vitest run --project unit src/main/sessionIndex.prompts.test.ts — 7 tests against temp JSONL files: tail-first listing returns the newest K without parsing from the head; growth folds only appended bytes and leaves a partial trailing line for later; search extends to the head once then stays incremental; an adjacent duplicate across the tail-window seam is collapsed keeping the older copy; Codex cwd comes from the head; a shorter rewrite re-parses; missing file returns empty and the LRU evicts.
  • npx tsc -p tsconfig.node.json --noEmit — clean.

Known limitations

  • A tail-only parse keeps the newer of two adjacent identical prompts when the older one lies outside the read window; a later full read (search) corrects it. Same text, timestamp differs.
  • Transcript decoding is still on the main thread; a cold search over 500 candidates is a full read per file the first time.

Plan: docs/superpowers/plans/2026-09-03-session-index-incremental-prompts.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa

Juliusolsson05 and others added 3 commits September 3, 2026 13:06
The picker lists the ten most recently modified transcripts — the live
sessions, whose mtime moves every few seconds — and extracted their
prompts by reading, splitting and parsing the whole file, cached only by
mtime. Every open therefore re-read all of them from byte 0 (122 MB on the
machine this was measured on) on the main thread to show four prompts
each; search did the same for every transcript on disk per query.

A cache entry is now a parsed byte range: growth folds only the appended
bytes, a listing folds the tail in a growing window until it has the
prompts it needs, search extends to the head once and is incremental after,
and the cache is an LRU of 512. Search considers the 500 most recently
modified candidates. Prompt filtering, ordering and cwd resolution are
unchanged.

Fixes #735
Refs #103, #93, #96

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…nst rewrites

Review follow-ups: overlapping extractions of one transcript (the picker
lists on open and again from its debounced effect) both folded the same
chunk into the shared cache entry and left duplicated prompts behind — calls
are now chained per key; a growth read compares the last 64 bytes of the
parsed range first so a rewrite that ends larger starts over; backward
widening grows geometrically so a multi-MB record costs one pass; the
search bound is per provider and applied after the cwd filter, with the
cache sized above the bounds; need is clamped to one; the Codex head is
read for a cwd at most once. Tests cover concurrency, the long-line path,
mtime-only and grown rewrites, empty and partial files, a multi-byte seam,
and a real LRU.

Refs #735

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Self-review pass (Fable adversarial review + an orchestrated Agent Code reviewer) — resolved in HEAD:

  • Blocker: concurrent calls corrupted the shared entry → extractions are serialised per transcript key; test races two full reads and two growth reads on a warm entry.
  • Quadratic widening on a huge single line → step grows geometrically; test folds an 8 MB record and bounds bytes read to < 3× file size.
  • Rewrite that grows the file merged onto stale prompts → growth reads compare the last 64 bytes of the parsed range first; tests cover grown, shrunk, and mtime-only rewrites (utimesSync).
  • Search bound before the cwd filter → per-provider bound (400) applied after the cwd filter; cache raised to 1,024 and the coupling documented.
  • Nits: need clamped to ≥1, Codex head cwd read once (headCwdChecked), cwd-parity difference stated in the plan, LRU test now discriminates from FIFO, growth test asserts bytes read, empty/partial/multi-byte-seam cases added.

Not adopted: an age-based search bound — sessions older than the 400 most recent per provider are not searchable; stated in the code comment as the price of never freezing on a keystroke.

@Juliusolsson05
Juliusolsson05 merged commit 4a54325 into main Sep 3, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the perf/session-index-incremental-prompts branch September 3, 2026 20:46
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.

perf(sessions): session picker re-reads every live transcript in full and search parses all 2,150 files per query

1 participant