Understanding the .jsonl Session File Format for Pi and How pi-web Parses It
Pi stores every chat session as a line-delimited JSON file where each line represents a distinct event—such as session headers, model changes, or messages—and pi-web parses these files using a dedicated reader module that transforms SDK entries into UI-ready message objects.
The pi-web repository (agegr/pi-web) provides the web interface for Pi, an AI agent framework that persists conversation history in .jsonl files. Understanding how the .jsonl session file format for Pi is structured and parsed is essential for developers building custom tools or debugging session state. The parsing logic is implemented in lib/session-reader.ts and relies on the Pi SDK's low-level reader to transform line-delimited JSON into React-friendly data structures.
Structure of the Pi .jsonl Session File
Pi session files follow a strict line-delimited schema documented in AGENTS.md. Each line is a valid JSON object representing a discrete event in the session lifecycle.
Session Header Entry
The first line of every .jsonl file must be a session header entry. In lib/session-reader.ts, the readSessionHeader function specifically looks for this object by reading up to 64KB of the file and verifying that the parsed JSON has a type field equal to "session".
The header contains metadata fields:
type: Always"session"version: Schema version (e.g.,3)id: Unique session identifiertimestamp: ISO 8601 creation timecwd: Current working directory where the session originatedparentSession: Optional ID for branched conversations
Entry Types and Schema
Following the header, subsequent lines represent various event types. The getSessionEntries function in lib/session-reader.ts retrieves these as SessionEntry objects via the SDK's SessionManager.open(filePath).getEntries() method.
| Entry Type | Key Fields | Purpose |
|---|---|---|
model_change |
provider, modelId |
Records switches between AI models (e.g., GPT-4 to Claude) |
message |
role, content |
Chat content with roles: user, assistant, toolResult, or bashExecution |
toolResult |
toolCallId, content |
Embedded within messages; stores execution results from tool calls |
compaction |
summary, firstKeptEntryId, tokensBefore |
Summarization events that compress earlier conversation history |
session_info |
name |
Optional UI metadata like user-defined session names |
branch_summary |
- | Metadata for conversation branching points |
How pi-web Parses .jsonl Session Files
The parsing pipeline in lib/session-reader.ts transforms raw SDK entries into the AgentMessage format required by the React frontend. This process involves four distinct stages.
Locating Session Files
The listAllSessions() function initiates the process by calling SessionManager.listAll() from the Pi SDK. It builds a cached map of session IDs to absolute file paths, enabling quick resolution via resolveSessionPath(sessionId).
// From lib/session-reader.ts
export async function listAllSessions(): Promise<Map<string, string>> {
const sessions = await SessionManager.listAll();
const map = new Map<string, string>();
for (const session of sessions) {
map.set(session.id, session.filePath);
}
return map;
}
Reading the Header
Before loading the full file, readSessionHeader(filePath) performs validation. It reads the first 64KB of the file, extracts the first line, and JSON-parses it. If the type field does not equal "session", the function returns null, indicating an invalid session file.
Loading Raw Entries with SessionManager
Once validated, getSessionEntries(filePath) uses the SDK's SessionManager.open(filePath).getEntries() to stream every line as a typed SessionEntry array. This low-level operation handles the line-delimited JSON parsing efficiently without loading the entire file into a single JSON array.
Converting Entries to UI Messages
The buildSessionContext(entries, leafId, options) function orchestrates the transformation:
- Creates a
byIdMap for O(1) entry lookups - Calls SDK helpers
piBuildSessionContextandpiBuildContextEntriesto determine the correct conversation branch and ordering - Iterates through selected entries, converting each via
entryToUiMessage
The entryToUiMessage(entry, options) function in lib/session-reader.ts handles type-specific conversions:
message: Normalizes content vianormalizeToolCalls, optionally strips large base64 images, and defers assistant thinking blockscompaction: Transforms into a custom UI message displaying the summary and token statistics (tokensBefore)branch_summaryandcustom_message: Convert to user-visible messages with appropriate metadata flags
Normalizing Tool Call Payloads
Tool calls require format reconciliation between the SDK and UI. The normalizeToolCalls function in lib/normalize.ts bridges this gap by transforming SDK-shaped objects {type: "toolCall", id, name, arguments} into the UI-expected format {toolCallId, toolName, input}.
Practical Code Examples
Loading a Complete Session Context
import {
resolveSessionPath,
readSessionHeader,
getSessionEntries,
buildSessionContext
} from '@/lib/session-reader';
async function loadSessionContext(sessionId: string) {
const path = await resolveSessionPath(sessionId);
if (!path) throw new Error('Session not found');
const header = readSessionHeader(path);
if (!header) throw new Error('Invalid session file');
const entries = getSessionEntries(path);
const ctx = buildSessionContext(entries); // default options
return ctx; // { messages: AgentMessage[], entryIds: string[], ... }
}
Example .jsonl File Structure
{"type":"session","version":3,"id":"c5f0...","timestamp":"2023-04-01T12:00:00Z","cwd":"/home/user/project"}
{"type":"model_change","id":"01ab...","provider":"openai","modelId":"gpt-4"}
{"type":"message","id":"02cd...","parentId":null,"message":{"role":"user","content":"Explain the .jsonl format"}}
{"type":"message","id":"03ef...","parentId":"02cd...","message":{"role":"assistant","content":[{"type":"text","text":"..."}]}}
Summary
- Pi persists chat sessions as line-delimited JSON (.jsonl) files where each line is an independent JSON object representing events like headers, messages, or model changes.
- The
sessionheader must appear as the first line and contains critical metadata includingid,timestamp, andcwd. - pi-web uses
lib/session-reader.tsto parse files via the Pi SDK'sSessionManager, reading headers with a 64KB buffer and streaming entries throughgetEntries(). - The parser transforms raw SDK entries into UI-ready
AgentMessageobjects usingbuildSessionContextandentryToUiMessage, handling special cases like compaction summaries and tool results. - Tool call formats are reconciled in
lib/normalize.tsto ensure compatibility between the SDK's data structures and the React frontend's expectations.
Frequently Asked Questions
What is the .jsonl file extension used for in Pi sessions?
Pi uses the .jsonl extension to indicate line-delimited JSON format. Unlike standard JSON files that contain a single root object or array, each line in a Pi session file is a complete, independent JSON object representing a discrete event such as a session header, model change, user message, or compaction summary. This structure allows the Pi SDK and pi-web to append new events efficiently without rewriting the entire file.
How does pi-web validate that a file is a valid Pi session?
The readSessionHeader function in lib/session-reader.ts performs validation by reading the first 64KB of the candidate file and extracting the first line. It JSON-parses this line and checks whether the resulting object has a type property equal to "session". If the check fails or the file cannot be parsed, the function returns null, preventing invalid files from being processed as chat sessions.
What is the difference between a compaction entry and a regular message?
A compaction entry represents a summarization event where Pi has compressed earlier conversation history to save context window space. It contains a summary field with the condensed content, a firstKeptEntryId pointing to the oldest retained message, and tokensBefore indicating the token count prior to compression. Regular message entries contain the actual chat content with role and content fields. In the UI, compaction entries render as special system messages showing the summary and statistics.
Where does pi-web handle the conversion of tool call formats?
Tool call normalization occurs in two stages. First, entryToUiMessage in lib/session-reader.ts extracts tool call data from message entries. Then, it delegates to normalizeToolCalls in lib/normalize.ts, which reconciles the Pi SDK's format—using fields like id, name, and arguments—with the pi-web UI's expected format using toolCallId, toolName, and input. This ensures that tool invocations render consistently across different model providers in the React interface.
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 →