# How Pi-Web Manages and Persists Data: JSON-Lines Session Architecture

> Discover how Pi-Web manages and persists data using JSON-Lines session architecture. Learn about conversational state storage, in-memory UI data, and localStorage caching.

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

---

**Pi-Web stores all conversational state in immutable JSON-Lines (`.jsonl`) files under `~/.pi/agent/sessions/`, while transient UI data like text drafts remain in memory and model preferences are cached in `localStorage`.**

The **pi-web** application (from the `agegr/pi-web` repository) implements a hybrid persistence strategy that separates durable conversation history from ephemeral interface state. By leveraging the π (pi) coding agent SDK for disk operations, the application ensures session integrity across server restarts while keeping browser-specific data lightweight and isolated.

## Session Storage with JSON-Lines

Pi-Web delegates all persistent storage to the **π SDK**, which writes session data as append-only JSON-Lines files. Each session receives a unique file path constructed from the current working directory, timestamp, and UUID.

### File Structure and Location

Session files reside in the user’s home directory following a strict hierarchy:

```

~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl

```

According to the source code in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the first line of every file contains a **session header** (type `session`) that stores metadata including the session ID, working directory, and parent session references. Subsequent lines record typed events such as `message`, `model_change`, and `compaction`.

### Immutability and Branching

Session files are immutable once written. When users fork conversations or navigate branches, the `AgentSessionWrapper` creates a fresh `.jsonl` file rather than modifying the original. The new file’s header references the parent session ID, establishing a linked history while preserving the original data, as documented in the [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md) lifecycle specifications.

## Session Management and Caching

The backend retrieves and transforms session data through a dedicated reader module that optimizes for performance via aggressive caching.

### Reading Sessions with session-reader.ts

The [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) module provides the primary interface between the SDK storage and the React UI:

1. **`readSessionHeader()`** (lines 66-94) performs lazy reads of the first line to retrieve metadata without loading the entire file.
2. **`SessionManager.listAll()`** enumerates all session files in the sessions directory (line 18).
3. **`SessionManager.open(filePath).getEntries()`** streams the full conversation history (lines 99-100).
4. **`buildSessionContext()`** converts raw SDK entries into UI-ready messages by invoking `piBuildSessionContext` and normalizing tool calls and image payloads (lines 4-40, 24-35).

### Global Caching Strategy

To survive Hot-Reloads and reduce filesystem I/O, [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) maintains two global caches:

- **`__piSessionPathCache`**: Maps session IDs to absolute file paths.
- **`__piPathToSessionIdCache`**: Maps file paths back to session IDs.

These caches are populated during the initial `listAll()` call and reused for subsequent lookups, ensuring repeated session access remains performant.

## Transient State vs. Persistent Storage

Not all data in Pi-Web survives page reloads. The application deliberately separates durable conversation history from temporary UI state.

### Draft Storage (Ephemeral)

User-typed text and attached images are stored in the module-scoped `drafts` Map defined in [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts). These drafts survive only within the current browser tab and are cleared when the tab unloads. The store exposes `setDraft()` and `getDraft()` functions for the React components to persist user input during navigation without committing to the session file.

```typescript
// Add or retrieve draft data (ephemeral)
import { setDraft, getDraft } from "@/lib/draft-store";

setDraft("session-42", { value: "Hello", images: [] });
const draft = getDraft("session-42");   // { value: "Hello", images: [] }

```

### Model Configuration

Model selections and tool presets follow a split persistence model:

- **Global models**: Stored in `~/.pi/agent/models.json` and accessed via [`lib/models-config-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts).
- **User preferences**: The last-selected tool preset is stored in `localStorage` (managed by [`tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/tool-preset-preference.ts)) and reapplied when new sessions start, but never written back to the session file.

## Security and Path Resolution

File system access is strictly gated to prevent directory traversal attacks. The [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) module implements `samePath()` checks to validate that requested files reside within allowed roots.

### Worktree Resolution

The [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) module resolves the current working directory to project roots, including git worktrees. This resolved path, combined with explicitly added roots, forms the allow-list used by [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) to validate API requests.

```typescript
// Load session header efficiently (no full file read)
import { readSessionHeader } from "@/lib/session-reader";
const header = await readSessionHeader("/home/user/.pi/agent/sessions/~/proj/20241012_abc123.jsonl");

// Retrieve full conversation context
import { getSessionEntries, buildSessionContext } from "@/lib/session-reader";
const entries = getSessionEntries(filePath);
const ctx = buildSessionContext(entries, leafId);
const messages = ctx.messages;   // Ready for React component rendering

```

## Summary

- **Persistent storage**: All conversation history, model changes, and session metadata are stored in immutable `.jsonl` files in `~/.pi/agent/sessions/`, managed by the π SDK and accessed via [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts).
- **Caching layer**: Global variables `__piSessionPathCache` and `__piPathToSessionIdCache` eliminate redundant filesystem operations during session lookups.
- **Ephemeral state**: Draft text and images live only in the browser’s memory via [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts), disappearing when the tab closes.
- **Configuration**: Model definitions persist in `~/.pi/agent/models.json`, while tool presets use `localStorage` for user-specific defaults.
- **Security**: Path access is restricted to resolved project roots and git worktrees through [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) and [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts).

## Frequently Asked Questions

### Where does pi-web store conversation history?

Pi-Web stores all conversation history in JSON-Lines (`.jsonl`) files located at `~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl`. Each line represents an event such as a message or model change, with the first line containing the session header metadata. These files are managed by the π SDK and accessed through the `SessionManager` class.

### How does pi-web handle unsaved user input?

Unsaved text and images are stored in an in-memory Map managed by [`lib/draft-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/draft-store.ts). This ephemeral storage survives component re-renders and navigation within the same browser tab, but is cleared completely when the tab unloads. Drafts are never written to disk, ensuring only confirmed messages persist to the session files.

### What happens when I fork a conversation in pi-web?

Forking creates an entirely new `.jsonl` session file rather than modifying the existing one. The `AgentSessionWrapper` writes the new file with an updated header referencing the original session as its parent, leaving the original file untouched. This immutable approach preserves complete conversation history while enabling branching workflows.

### How does pi-web secure file system access?

File access is restricted through an allow-list built from the session’s current working directory, resolved git worktrees, and explicitly added roots. The `samePath()` function in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) performs platform-agnostic path comparisons to prevent directory traversal, ensuring the API can only access files within authorized project boundaries.