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 identifier
  • timestamp: ISO 8601 creation time
  • cwd: Current working directory where the session originated
  • parentSession: 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:

  1. Creates a byId Map for O(1) entry lookups
  2. Calls SDK helpers piBuildSessionContext and piBuildContextEntries to determine the correct conversation branch and ordering
  3. 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 via normalizeToolCalls, optionally strips large base64 images, and defers assistant thinking blocks
  • compaction: Transforms into a custom UI message displaying the summary and token statistics (tokensBefore)
  • branch_summary and custom_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 session header must appear as the first line and contains critical metadata including id, timestamp, and cwd.
  • pi-web uses lib/session-reader.ts to parse files via the Pi SDK's SessionManager, reading headers with a 64KB buffer and streaming entries through getEntries().
  • The parser transforms raw SDK entries into UI-ready AgentMessage objects using buildSessionContext and entryToUiMessage, handling special cases like compaction summaries and tool results.
  • Tool call formats are reconciled in lib/normalize.ts to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →