# How Pi Web Stores Conversations: Understanding the Session File Format (.jsonl)

> Discover how Pi Web stores conversations using the .jsonl session file format. Learn about its append-only structure and immutable history of events.

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

---

**Pi Web persists every chat session as an append-only JSON Lines (`.jsonl`) file where each line represents a discrete event—such as messages, model changes, or compaction markers—linked by parent-child relationships to form an immutable history.**

The `agegr/pi-web` repository implements a robust logging system that stores conversation state in plain-text JSONL files located under the user's Pi data directory (`~/.pi/agent/sessions/`). This format enables features like session forking, tree navigation, and historical compaction while maintaining a complete audit trail of every interaction. Understanding the schema defined in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) is essential for developers building tools or integrations that read or manipulate Pi session data.

## What Is the JSONL Session File Format?

Pi Web treats each session as a **linear, immutable log** where new entries are always appended to the end of the file and earlier lines never change. This append-only design guarantees a reproducible audit trail and simplifies conflict resolution during concurrent access.

Each line in the file is a valid JSON object representing a specific session event. The `entryIds[]` array built by [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) mirrors the line order, enabling the UI to map displayed messages back to their underlying entry IDs for operations like **fork** or **navigate_tree**.

## Schema and Entry Types Defined in lib/types.ts

The TypeScript definitions in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) establish the contract for every possible entry type. Each object includes a `type` discriminator field and maintains hierarchical links via `parentId`.

### Session Header

The first line of every `.jsonl` file contains the session metadata:

- **`type`**: `"session"`
- **`version`**: Schema version (e.g., `3`)
- **`id`**: UUID for the session
- **`timestamp`**: ISO 8601 creation time
- **`cwd`**: Current working directory where the session originated
- **`parentSession`** (optional): Absolute path to the parent file when this session was created via a fork

### Model Changes

When the user switches LLM providers or models, Pi records a `model_change` entry:

- **`type`**: `"model_change"`
- **`provider`**: The AI provider (e.g., `"zenmux"`)
- **`modelId`**: Specific model identifier (e.g., `"claude-sonnet-4-6"`)
- **`parentId`**: References the previous entry in the chain

### Messages

Conversation content uses the `message` type with a nested `message` object containing a `role` field:

- **User messages**: `role: "user"` with plain text `content`
- **Assistant messages**: `role: "assistant"` with `content` as an array of blocks (text, image, thinking, or toolCall)
- **Tool results**: `role: "toolResult"` including `toolCallId` and result content

### Compaction Markers

To prevent unbounded file growth, Pi can replace a range of historical entries with a single `compaction` record:

- **`type`**: `"compaction"`
- **`summary`**: Textual summary of the compacted range
- **`firstKeptEntryId`**: The entry ID where the retained history begins
- **`tokensBefore`**: Token count prior to compaction

### Session Info

Optional human-readable metadata for UI display:

- **`type`**: `"session_info"`
- **`name`**: Display name for the session

## How Parent-Child Relationships Work

Every entry except the root carries a **`parentId`** linking it to the immediately preceding entry, forming a singly-linked list that represents the conversation timeline. When you fork a session, Pi creates a new `.jsonl` file with a fresh header where the `parentSession` field points to the absolute path of the original file, while the new file's internal `parentId` chain starts fresh.

This design allows the UI to reconstruct conversation trees and navigate between branches without modifying historical data.

## Working with Session Files

### Reading Session Headers and Contexts

Use the utilities in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) to access session data without manual file parsing:

```typescript
import { readSessionHeader, resolveSessionPath } from '@/lib/session-reader';
import { SessionHeader, SessionEntry } from '@/lib/types';

// Resolve a session ID to its file path (cached for performance)
const filePath = await resolveSessionPath('a1b2c3-uuid');

// Read the header (first line) without loading the whole file
const header: SessionHeader | null = readSessionHeader(filePath);
console.log('Session cwd:', header?.cwd);

// Load the full session (using Pi's SessionManager under the hood)
const { entries, context } = await piBuildSessionContext(filePath);

```

### Creating Forked Sessions

Forking creates a new independent file while preserving the lineage reference:

```typescript
import { startRpcSession } from '@/lib/rpc-manager';

// Fork the current session – Pi Web will write a new .jsonl file
const forkResult = await startRpcSession({
  sessionId: currentId,
  command: 'fork',
  // optional: initial tool list, model, etc.
});
console.log('New forked session ID:', forkResult.newSessionId);

```

### Normalizing Tool Call Data

Pi stores tool calls internally as `{type: "toolCall", id, name, arguments}`, but the UI expects `{toolCallId, toolName, input}`. The `normalizeToolCalls()` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) bridges this gap:

```typescript
import { normalizeToolCalls } from '@/lib/normalize';

// Convert UI-side tool call data to Pi's on-disk schema
const rawCall = {
  toolCallId: 't123',
  toolName: 'search',
  input: { query: 'JSONL format' },
};
const normalized = normalizeToolCalls([rawCall])[0];

// Push as a new entry (handled by AgentSession.prompt internally)
await session.send({ type: 'toolCall', ...normalized });

```

## Summary

- **Append-only architecture**: New entries are appended to `.jsonl` files in `~/.pi/agent/sessions/`; existing lines remain immutable to ensure data integrity.
- **Type-discriminated schema**: [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) defines entry types including `session`, `model_change`, `message`, `compaction`, and `session_info`.
- **Linked history**: The `parentId` field creates a singly-linked chain of events, while `parentSession` enables session forking without data duplication.
- **Compaction support**: Long conversations are managed via `compaction` entries that summarize historical ranges to control file size.
- **Normalization layer**: [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) handles conversion between the UI model and the JSONL storage format, particularly for tool call representations.

## Frequently Asked Questions

### What is the difference between parentId and parentSession in the JSONL format?

The `parentId` field links an individual entry to the immediately preceding entry within the same file, creating a linear history chain. The `parentSession` field appears only in the session header of forked sessions and contains the absolute file path to the original session that was forked, establishing a cross-file lineage relationship.

### How does Pi Web handle tool calls in the session file format?

Tool calls are stored in assistant messages as content blocks with `type: "toolCall"`, containing `id`, `name`, and `arguments` fields. When tool execution completes, a separate message entry with `role: "toolResult"` records the output, referencing the original call via `toolCallId`. The `normalizeToolCalls()` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) converts between this storage format and the UI's expected structure.

### Can I manually edit a .jsonl session file?

While the files are plain text JSON, manual editing is discouraged because Pi Web relies on the append-only guarantee and specific ID relationships. Editing existing lines breaks the immutability contract and may cause the [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) parser or `SessionContext` builder to fail when reconstructing the conversation tree.

### Where does Pi Web store session metadata like display names?

Display names are stored as `session_info` entry types within the `.jsonl` file itself, written as discrete lines with `type: "session_info"`. All other configuration settings (model preferences, provider settings) live in the Pi configuration directory outside the session files, keeping the JSONL logs focused solely on conversation history and state transitions.