Skip to content

perf(sessions): cache unchanged session metadata across scans and restarts - #625

Merged
agegr merged 3 commits into
agegr:mainfrom
Nuctori:perf/l1-scanner
Sep 5, 2026
Merged

agegr merged 3 commits into
agegr:mainfrom
Nuctori:perf/l1-scanner

Conversation

@Nuctori

@Nuctori Nuctori commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Repeated session-list refreshes currently parse every JSONL file through SessionManager.listAll(), including an allMessagesText string 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

  • Retain the SDK's displayed metadata and catalogue order for equal activity timestamps, regardless of cache hits or concurrent read completion.
  • Validate persisted fingerprints, metadata, paths, and dates. Invalid entries are discarded and rebuilt from JSONL instead of breaking /api/sessions.
  • Revalidate files on every scan, remove deleted entries, and persist the index atomically with mode 0600. Unchanged files still require directory enumeration and stat calls, but no content reads.
  • Preserve the existing list cache and generation-based invalidation. Migrate its tests to the new scanner entry point and add real-file SDK parity and cache lifecycle tests.
  • No new dependencies or frontend changes. This remains independent of perf(sidebar): windowed session list — only mount the visible row window #626.

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.
  • Regression coverage includes corrupt indices, equal-time sorting, rename and explicit name clear, append, rewrite, create/delete, restart revalidation, partial lines, and project symlinks. Cache-hit tests assert that session contents are not reopened.

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:

Scenario Median
SDK full scan 320.9 ms
Scanner without index 294.5 ms
Scanner with memory index 9.5 ms
Persisted index in a new process 30.8 ms
One appended file 16.0 ms

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.

…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.
@agegr agegr changed the title perf(sessions): incremental list scan — fingerprint memoization eliminates 70s cold load perf(sessions): cache unchanged session metadata across scans and restarts Sep 5, 2026
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.
@agegr
agegr marked this pull request as ready for review September 5, 2026 08:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-05T08:11:12.464514Z adb9214 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@agegr
agegr merged commit 8cbafdd into agegr:main Sep 5, 2026
2 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +334 to +338
if (
cached &&
cached.fp.size === fp.size &&
cached.fp.mtimeMs === fp.mtimeMs
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

sleepinginsummer pushed a commit to sleepinginsummer/pi-web that referenced this pull request Sep 9, 2026
…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>
lclichen pushed a commit to lclichen/pi-web that referenced this pull request Sep 21, 2026
…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>
ghost0211 pushed a commit to ghost0211/pi-web that referenced this pull request Sep 29, 2026
…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)
ghost0211 added a commit to ghost0211/pi-web that referenced this pull request Sep 29, 2026
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).
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