# Understanding the .jsonl Session File Format for Pi and How pi-web Parses It

> Learn the .jsonl session file format for Pi and how pi-web parses it. Discover how this line-delimited JSON structure records chat events and transforms SDK entries into UI-ready messages.

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

---

**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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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)`.

```typescript
// 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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

```typescript
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

```json
{"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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) extracts tool call data from message entries. Then, it delegates to `normalizeToolCalls` in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/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.