Repository navigation
perf(sessions): cache unchanged session metadata across scans and restarts - #625
Conversation
…nates 70s cold load SDK SessionManager.listAll replays every byte of every session (~9.5 GiB) to build allMessagesText, which pi-web never consumes. Replace the inner scan with a pure-function memoization: F(file) -> ScannedSessionInfo (1:1 mirror of SDK semantics for the fields pi-web actually reads: id/cwd/name/created/modified/ messageCount/firstMessage/parentSessionPath) Invariant (append-only): pi only appends lines or rewrites wholesale; either changes (size, mtimeMs). So fingerprint fp=(size,mtimeMs) unchanged => bytes unchanged => cached F(file) === fresh scan. Changed/deleted/new files are always rescanned/dropped/added, so the incremental set equals a full rescan and sort(modified desc) matches SDK. Effect: cold /api/sessions 70s at 1992 sessions -> warm-hit stat-only pass (~200ms) with at most changed files rescanned; persisted index (pi-web-session-index.json, atomic 0600) survives restarts and is revalidated by stat on every load.
Bring in the stable session-entry React keys and node-identity regression assertion already merged with PR agegr#626. Resolve the session-reader type import conflict by removing both now-unused SDK types. Validated 867 tests, typecheck, lint, and full desktop/mobile E2E.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: adb9214f84
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( | ||
| cached && | ||
| cached.fp.size === fp.size && | ||
| cached.fp.mtimeMs === fp.mtimeMs | ||
| ) { |
There was a problem hiding this comment.
Detect same-size rewrites on coarse-timestamp filesystems
When session storage is on a filesystem with coarse mtime resolution, a same-length whole-file rewrite can leave both size and mtimeMs unchanged and incorrectly take this cache-hit path. In particular, the DELETE handler in app/api/sessions/[id]/route.ts cascade-reparents children by rewriting their JSONL headers, and session paths are normally fixed-length, so a quick reparent can retain the cached old parentSessionPath across forced scans and restarts until another write occurs. Include another file-identity/content signal or explicitly invalidate rewritten paths rather than treating this pair as authoritative.
AGENTS.md reference: AGENTS.md:L139-L140
Useful? React with 👍 / 👎.
…tarts (agegr#625) * perf(sessions): incremental list scan — fingerprint memoization eliminates 70s cold load SDK SessionManager.listAll replays every byte of every session (~9.5 GiB) to build allMessagesText, which pi-web never consumes. Replace the inner scan with a pure-function memoization: F(file) -> ScannedSessionInfo (1:1 mirror of SDK semantics for the fields pi-web actually reads: id/cwd/name/created/modified/ messageCount/firstMessage/parentSessionPath) Invariant (append-only): pi only appends lines or rewrites wholesale; either changes (size, mtimeMs). So fingerprint fp=(size,mtimeMs) unchanged => bytes unchanged => cached F(file) === fresh scan. Changed/deleted/new files are always rescanned/dropped/added, so the incremental set equals a full rescan and sort(modified desc) matches SDK. Effect: cold /api/sessions 70s at 1992 sessions -> warm-hit stat-only pass (~200ms) with at most changed files rescanned; persisted index (pi-web-session-index.json, atomic 0600) survives restarts and is revalidated by stat on every load. * fix(sessions): validate scan cache and preserve catalogue ordering --------- Co-authored-by: Nuctori <Nuctori@users.noreply.github.com> Co-authored-by: Alex Yang <agegcn@gmail.com>
…tarts (agegr#625) * perf(sessions): incremental list scan — fingerprint memoization eliminates 70s cold load SDK SessionManager.listAll replays every byte of every session (~9.5 GiB) to build allMessagesText, which pi-web never consumes. Replace the inner scan with a pure-function memoization: F(file) -> ScannedSessionInfo (1:1 mirror of SDK semantics for the fields pi-web actually reads: id/cwd/name/created/modified/ messageCount/firstMessage/parentSessionPath) Invariant (append-only): pi only appends lines or rewrites wholesale; either changes (size, mtimeMs). So fingerprint fp=(size,mtimeMs) unchanged => bytes unchanged => cached F(file) === fresh scan. Changed/deleted/new files are always rescanned/dropped/added, so the incremental set equals a full rescan and sort(modified desc) matches SDK. Effect: cold /api/sessions 70s at 1992 sessions -> warm-hit stat-only pass (~200ms) with at most changed files rescanned; persisted index (pi-web-session-index.json, atomic 0600) survives restarts and is revalidated by stat on every load. * fix(sessions): validate scan cache and preserve catalogue ordering --------- Co-authored-by: Nuctori <Nuctori@users.noreply.github.com> Co-authored-by: Alex Yang <agegcn@gmail.com>
…tarts (agegr#625) * perf(sessions): incremental list scan — fingerprint memoization eliminates 70s cold load SDK SessionManager.listAll replays every byte of every session (~9.5 GiB) to build allMessagesText, which pi-web never consumes. Replace the inner scan with a pure-function memoization: F(file) -> ScannedSessionInfo (1:1 mirror of SDK semantics for the fields pi-web actually reads: id/cwd/name/created/modified/ messageCount/firstMessage/parentSessionPath) Invariant (append-only): pi only appends lines or rewrites wholesale; either changes (size, mtimeMs). So fingerprint fp=(size,mtimeMs) unchanged => bytes unchanged => cached F(file) === fresh scan. Changed/deleted/new files are always rescanned/dropped/added, so the incremental set equals a full rescan and sort(modified desc) matches SDK. Effect: cold /api/sessions 70s at 1992 sessions -> warm-hit stat-only pass (~200ms) with at most changed files rescanned; persisted index (pi-web-session-index.json, atomic 0600) survives restarts and is revalidated by stat on every load. * fix(sessions): validate scan cache and preserve catalogue ordering --------- Co-authored-by: Nuctori <Nuctori@users.noreply.github.com> Co-authored-by: Alex Yang <agegcn@gmail.com> (cherry picked from commit 8cbafdd)
Batch upstream cherry-picks (agegr/pi-web) into the fork: Fixes - Guard against duplicate built-in command submissions (agegr#762) - Keep shell output across SSE reconnects; resume streaming output - See sessions written by another pi process (agegr#796) - Stop duplicating the first streamed chunk (agegr#835) - Deliver events to every listener when one unsubscribes mid-emit Features - Warn when attached images go to a model without image input (agegr#636) - Preview image attachments in the chat composer (agegr#735) - Render apply_patch tool calls as split diffs (agegr#744) - Scroll-to-latest floating button (agegr#845) Performance & deps - gzip large JSON responses (agegr#731) - Cache unchanged session metadata across scans and restarts (agegr#625) - Windowed session list (adapted to the fork's project-grouped sidebar) (agegr#626) - Next.js 16.3.6 + disable image optimizer; trim production install (agegr#948) Also includes the NSIS shortcut recreation hook (desktop icon fix).
Summary
Repeated session-list refreshes currently parse every JSONL file through
SessionManager.listAll(), including anallMessagesTextstring that pi-web does not consume. Cache the displayed metadata by file size and mtime, persist it across restarts, and rescan only new or changed files.The first scan without an index still reads all session files. This change primarily improves subsequent scans and restarts with a valid index; it does not eliminate first-load disk scanning.
Behavior
/api/sessions.The size/mtime fingerprint assumes ordinary appends and rewrites update file metadata. Same-size edits that restore the original mtime are not detected.
Validation
npm test: 848 passed, 0 failed.node_modules/.bin/tsc --noEmit --incremental false: passed.npm run lint: passed.Run the reproducible benchmark with
node --expose-gc lib/session-list-scanner.bench.mjs. Local medians across three trials on 2,000 synthetic sessions, 137.2 MiB total, 64 messages per file:These measure scanner latency, excluding process startup, SDK imports, API project resolution, and browser rendering. The OS filesystem cache is not cleared; these synthetic results are not an end-to-end benchmark of the original 9.5 GiB catalogue.