How Pi-Web Manages and Persists Data: JSON-Lines Session Architecture
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, 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 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 module provides the primary interface between the SDK storage and the React UI:
readSessionHeader()(lines 66-94) performs lazy reads of the first line to retrieve metadata without loading the entire file.SessionManager.listAll()enumerates all session files in the sessions directory (line 18).SessionManager.open(filePath).getEntries()streams the full conversation history (lines 99-100).buildSessionContext()converts raw SDK entries into UI-ready messages by invokingpiBuildSessionContextand 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 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. 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.
// 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.jsonand accessed vialib/models-config-store.ts. - User preferences: The last-selected tool preset is stored in
localStorage(managed bytool-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 module implements samePath() checks to validate that requested files reside within allowed roots.
Worktree Resolution
The 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 to validate API requests.
// 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
.jsonlfiles in~/.pi/agent/sessions/, managed by the π SDK and accessed vialib/session-reader.ts. - Caching layer: Global variables
__piSessionPathCacheand__piPathToSessionIdCacheeliminate redundant filesystem operations during session lookups. - Ephemeral state: Draft text and images live only in the browser’s memory via
lib/draft-store.ts, disappearing when the tab closes. - Configuration: Model definitions persist in
~/.pi/agent/models.json, while tool presets uselocalStoragefor user-specific defaults. - Security: Path access is restricted to resolved project roots and git worktrees through
lib/path-security.tsandlib/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. 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 performs platform-agnostic path comparisons to prevent directory traversal, ensuring the API can only access files within authorized project boundaries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →