# How pi-web Session List Caching Prevents Redundant File System Scans

> Learn how pi-web session list caching prevents redundant file system scans by using an in-memory cache and promise coalescing to improve performance. Discover how mutations trigger cache invalidation.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-23

---

**pi-web eliminates redundant file system scans by storing session lists in a TTL-based in-memory cache, deduplicating concurrent requests through promise coalescing, and invalidating entries only when mutations occur.**

The pi-web repository (`agegr/pi-web`) serves as the web interface for Pi sessions, requiring frequent access to the list of active sessions located in `~/.pi/agent/sessions/`. Without optimization, every UI interaction would trigger expensive directory traversals and Git process spawns. The codebase solves this through a robust **pi-web session list caching** mechanism implemented in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) that balances performance with data consistency.

## The Three-Layer Caching Strategy

The caching system employs three coordinated mechanisms to minimize I/O overhead while ensuring stale data is never served.

### TTL-Based Freshness Checks

At the core of the implementation lies `globalThis.__piSessionListCache`, a global variable storing the cached array. When `listAllSessions()` is invoked, it first validates cache freshness against `SESSION_LIST_CACHE_TTL_MS` (set to `30000` ms at line 95 of [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)). If the timestamp at lines [52‑55] indicates the cache is still fresh, the function returns the cached array immediately, bypassing all file system operations.

### Promise Coalescing for Concurrent Requests

When the cache expires or initializes, the system prevents redundant work during concurrent requests by creating a single *coalesced promise* attached to `globalThis.__piSessionListPromise`. As implemented at lines [57‑62] and [78‑80], this pattern ensures `loadAllSessions()` executes exactly once even if multiple components simultaneously request the session list. Each generation of requests shares the same promise instance until resolution.

### Generation-Based Invalidation

The cache tracks state through `globalThis.__piSessionListGeneration`. When mutations occur—such as session creation, deletion, or renaming—the function `invalidateSessionListCache()` bumps this generation counter (lines [97‑100]) and clears the cached data. This forces the next invocation to trigger a fresh scan, guaranteeing consistency without requiring expensive validation checks on every read.

## How Mutating Operations Trigger Invalidation

The caching system remains transparent to users because mutation sites explicitly invalidate stale data. In [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) (lines [147‑455]), every session-mutating RPC—including fork, delete, and rename operations—calls `invalidateSessionListCache()` immediately after completing the mutation. Similarly, the REST endpoint in `app/api/sessions/[id]/route.ts` invalidates the cache when handling PATCH or DELETE requests.

This pattern mirrors the per-CWD caching found in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) (where `resolveProject` caches project metadata), demonstrating a consistent architectural approach to expensive I/O operations throughout the codebase.

## Practical Implementation Examples

Fetch the session list efficiently using the cached helper:

```typescript
// Example: fetch the current session list (uses cache if still fresh)
import { listAllSessions } from "@/lib/session-reader";

async function showSessions() {
  const sessions = await listAllSessions();   // ← cached after first call
  console.table(sessions.map(s => ({ id: s.id, name: s.name })));
}

```

Explicitly invalidate the cache after creating sessions to ensure immediate consistency:

```typescript
// Example: explicitly invalidate the cache after creating a new session
import { invalidateSessionListCache } from "@/lib/session-reader";

async function createSession() {
  // …call the API that creates a session…
  await fetch("/api/agent/new", { method: "POST", body: JSON.stringify({ cwd: "/repo" }) });

  // Force the next listAllSessions() call to rescan the filesystem
  invalidateSessionListCache();
}

```

## Summary

- **TTL-based freshness**: The cache remains valid for 30 seconds (configurable via `SESSION_LIST_CACHE_TTL_MS`), eliminating repetitive scans during rapid UI interactions.
- **Promise coalescing**: Concurrent requests for `listAllSessions()` share a single execution of `loadAllSessions()` via `globalThis.__piSessionListPromise`, preventing redundant Git and file system operations under load.
- **Explicit invalidation**: Mutating operations in [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) and API routes call `invalidateSessionListCache()`, ensuring the generation counter increments and subsequent reads fetch fresh data.
- **Global state management**: The implementation uses `globalThis` to maintain `__piSessionListCache`, `__piSessionListPromise`, and `__piSessionListGeneration` across the application lifecycle.

## Frequently Asked Questions

### How long does pi-web cache the session list?

According to [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) line 95, the default **time-to-live (TTL) is 30 seconds** (`SESSION_LIST_CACHE_TTL_MS = 30000`). After this duration, the next call to `listAllSessions()` will trigger a fresh file system scan.

### What happens when multiple components request sessions simultaneously?

The system uses **promise coalescing** via `globalThis.__piSessionListPromise` (lines [57‑62]). Even if ten components request the list at once, `loadAllSessions()` executes only one time, and all callers receive the same resolved array.

### When is the session cache explicitly invalidated?

Any operation that modifies session state—such as creating, deleting, forking, or renaming a session—calls `invalidateSessionListCache()`. This function appears in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) (lines [147‑455]) and `app/api/sessions/[id]/route.ts`, ensuring the generation counter increments immediately after mutations.

### Does this caching affect real-time session status updates?

The cache only affects the **list of sessions**, not individual session state. While the list itself may lag up to 30 seconds behind reality (or until an invalidation occurs), active session monitoring occurs through separate mechanisms. For immediate consistency after mutations, the codebase explicitly invalidates the cache.