# How Session List Caching Avoids File Re-Scanning in pi-web

> Learn how pi-web's session list caching avoids redundant file rescans. Discover its TTL and generation-based invalidation for efficient data management.

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

---

**The `pi-web` codebase eliminates redundant directory scans by storing session lists in a global cache with a 30-second TTL and a generation-based invalidation system that forces fresh scans only after mutations.**

Every time the UI requests the list of Pi sessions, the system risks triggering expensive file system operations and Git process spawns. To prevent this, `agegr/pi-web` implements a robust **session list caching** layer in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) that minimizes I/O while ensuring data consistency across concurrent requests.

## Generation Counter for Cache Invalidation

The caching system relies on a global generation counter to track data freshness. Each time a session is created, deleted, or modified, the `invalidateSessionListCache()` function increments `globalThis.__piSessionListGeneration`. This counter acts as a logical clock; any cached result bearing an older generation number is automatically treated as stale.

```typescript
// lib/session-reader.ts (line 99-101)
export function invalidateSessionListCache() {
  globalThis.__piSessionListGeneration++;
}

```

RPC handlers in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) invoke this function immediately after mutation operations (such as forking or deleting sessions), ensuring the next read operation triggers a fresh scan rather than returning outdated data.

## Time-Based TTL Check

Before attempting a new scan, `listAllSessions()` validates the existing cache entry against a time-to-live threshold. The cached result stored in `globalThis.__piSessionListCache` includes a timestamp recorded at creation. If the elapsed time is less than the default 30-second TTL, the function returns the cached array of `SessionInfo` objects immediately, bypassing the file system entirely.

```typescript
// lib/session-reader.ts (line 52-56)
const cache = globalThis.__piSessionListCache;
if (cache && (Date.now() - cache.timestamp) < 30000) {
  return cache.sessions;
}

```

## Coalescing In-Flight Requests

When the cache is missing or expired, the system must execute `loadAllSessions()` to scan the session directory. To prevent thundering-herd problems during high-frequency access, concurrent callers receive the same shared promise stored in `globalThis.__piSessionListPromise`. This promise is bound to the current generation number, ensuring that only requests from the same logical generation share the asynchronous work.

```typescript
// lib/session-reader.ts (line 58-62)
if (!globalThis.__piSessionListPromise || 
    globalThis.__piSessionListGeneration !== currentGen) {
  globalThis.__piSessionListPromise = loadAllSessions(currentGen);
}
return globalThis.__piSessionListPromise;

```

## Populating the Cache and Path Registry

During a fresh scan, `loadAllSessions()` queries the Pi SDK via `SessionManager.listAll()` and enriches each entry with metadata including project roots and work-tree branches. While iterating through results, the function also populates a **path-to-id cache** by calling `cacheSessionPath()`, which registers each session's file path for fast subsequent lookups.

```typescript
// lib/session-reader.ts (line 17-31)
async function loadAllSessions(expectedGen: number) {
  const sessions = await SessionManager.listAll();
  const enriched = sessions.map(s => ({
    ...s,
    projectRoot: resolveProjectRoot(s),
    branch: getWorktreeBranch(s)
  }));
  
  enriched.forEach(s => cacheSessionPath(s.id, s.path));
  return enriched;
}

```

## Cache Guarding After Mutation

Race conditions between mutations and in-flight scans are prevented through generation checking. When the scan promise resolves, the `then` handler verifies that the generation counter hasn't changed since the scan started. If `globalThis.__piSessionListGeneration` no longer matches the expected value, the result is discarded rather than written to the cache, preventing stale data from overwriting a newer valid cache.

```typescript
// lib/session-reader.ts (line 65-70)
.then(sessions => {
  if (globalThis.__piSessionListGeneration === expectedGen) {
    globalThis.__piSessionListCache = {
      sessions,
      timestamp: Date.now(),
      generation: expectedGen
    };
  }
  return sessions;
})

```

## Promise Cleanup

Once the scan completes—whether successfully or with an error—the temporary promise reference is cleared from `globalThis.__piSessionListPromise`. This cleanup allows subsequent requests to initiate fresh scans if the cache remains invalid, preventing memory leaks and ensuring error recovery.

```typescript
// lib/session-reader.ts (line 72-77)
.finally(() => {
  if (globalThis.__piSessionListGeneration === expectedGen) {
    globalThis.__piSessionListPromise = undefined;
  }
});

```

## Practical Usage Examples

### Fetching the Session List in UI Components

React hooks and API routes import `listAllSessions()` to retrieve cached data instantly when the TTL is valid.

```typescript
import { listAllSessions } from "@/lib/session-reader";

export async function getSessionList() {
  // Returns cached data if within the 30-second window
  const sessions = await listAllSessions();
  return sessions;
}

```

### Invalidating Cache After Mutations

After performing destructive operations, explicitly invalidate the cache to force a fresh scan on the next request.

```typescript
import { invalidateSessionListCache } from "@/lib/session-reader";

export async function deleteSession(id: string) {
  await someDeleteRpc(id);
  // Forces fresh scan on next listAllSessions() call
  invalidateSessionListCache();
}

```

### Resolving Session Paths

The path-to-id cache enables fast resolution of session file paths without repeated lookups.

```typescript
import { resolveSessionPath } from "@/lib/session-reader";

const path = await resolveSessionPath("c3f9e2a1-...");

```

## Summary

- **Dual-layer invalidation**: The system combines a 30-second TTL with a generation counter to balance performance and consistency.
- **Request coalescing**: Concurrent calls share a single promise during scans, eliminating redundant Git processes and file system operations.
- **Race condition safety**: Generation checking prevents stale scan results from overwriting fresh cache entries when mutations occur mid-flight.
- **Path caching**: Secondary caching of session paths via `cacheSessionPath()` accelerates individual session lookups.
- **Global state management**: All cache state resides in `globalThis` properties (`__piSessionListCache`, `__piSessionListGeneration`, `__piSessionListPromise`), making the mechanism persistent across module reloads during development.

## Frequently Asked Questions

### How long does the session list cache remain valid?

The default time-to-live is **30 seconds**. After this period expires, the next call to `listAllSessions()` triggers a fresh scan of the session directory. Additionally, any mutation operation that calls `invalidateSessionListCache()` immediately invalidates the cache regardless of age.

### What prevents stale data from appearing after a session is deleted?

The generation counter mechanism ensures consistency. When `invalidateSessionListCache()` increments `globalThis.__piSessionListGeneration`, all existing cache entries are logically expired. Even if an in-flight scan completes after the deletion, the generation mismatch prevents that stale result from being stored in the cache.

### How does the system handle multiple simultaneous requests for the session list?

The system implements **request coalescing** through `globalThis.__piSessionListPromise`. When the cache is expired and a scan is already in progress, subsequent callers receive the same pending promise rather than spawning additional scans. This shared promise is tied to a specific generation number, ensuring isolation between different cache epochs.

### Where is the cache data actually stored?

All caching metadata resides in global properties attached to `globalThis`: `__piSessionListCache` stores the result array and timestamp, `__piSessionListGeneration` tracks the logical version, and `__piSessionListPromise` holds the shared in-flight promise. This approach persists data across hot module reloads and prevents memory leaks in serverless environments.