# Understanding the Session File Format Structure in pi-web: JSONL Entries and Entry-ID Mapping

> Explore the pi-web session file format structure using JSONL entries and entry-ID mapping. Understand how pi-web stores conversations and maps UI elements for efficient data access.

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

---

**pi-web stores conversations as JSON Lines files (`.jsonl`) under `~/.pi/agent/sessions/`, where each line represents a typed entry (session header, model changes, or messages), and the frontend maps UI elements to these entries via parallel `entryIds[]` and `messages[]` arrays in `SessionContext`.**

The **session file format structure** in agegr/pi-web defines how conversational state persists across sessions. According to the source code in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) and [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), pi-web uses a line-delimited JSON format that supports branching, forking, and tree navigation through unique entry identifiers.

## File Location and Naming Convention

Session files reside in the Pi data directory using a hierarchical structure that encodes the project context:

```text
~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl

```

- **`<encoded-cwd>`**: Base64-encoded path of the working directory
- **`<timestamp>_<uuid>`**: Chronological prefix with unique identifier

This organization allows pi-web to group conversations by project while maintaining unique session identification.

## JSONL Entry Types

Every line in a session file is a self-contained JSON object where the **`type`** field determines its semantic role. The file always begins with a mandatory header, followed by chronological event entries.

### Session Header

The first line must contain a `session` entry that defines conversation metadata:

```json
{"type":"session","version":3,"id":"c2e9b4d5-1a2b-4c3d-9e8f-0123456789ab","timestamp":"2026-08-17T12:00:00Z","cwd":"/home/user/project","parentSession":null}

```

Key fields include:
- **`version`**: Format version (currently 3)
- **`id`**: Session UUID
- **`cwd`**: Absolute path of the working directory
- **`parentSession`**: Optional path to a parent session file when forked

### Model Change Events

When the underlying LLM switches during a conversation, pi-web appends a `model_change` entry:

```json
{"type":"model_change","id":"a1b2c3d4","parentId":null,"provider":"zenmux","modelId":"claude-sonnet-4-6","timestamp":"2026-08-17T12:00:01Z"}

```

This entry tracks the **`provider`** and **`modelId`**, with **`parentId`** linking to the previous entry in the sequence.

### Message Entries

The conversation content consists of `message` entries with three distinct roles:

**User Messages** capture text input:

```json
{"type":"message","id":"msg001","parentId":"a1b2c3d4","message":{"role":"user","content":"Explain JSON-Lines format."}}

```

**Assistant Messages** contain model responses, which may include text arrays or tool calls:

```json
{"type":"message","id":"msg002","parentId":"msg001","message":{"role":"assistant","content":["JSON-Lines is a text format where each line is a valid JSON object..."]}}

```

**Tool Result Messages** store execution outputs:

```json
{"type":"message","id":"msg003","parentId":"msg002","message":{"role":"toolResult","toolCallId":"tc-123","content":["Tool execution completed successfully"]}}

```

Each message entry carries a unique **`id`** (typically 8-character hex) and references its parent via **`parentId`**, forming a linked chain that preserves conversation flow.

### Compaction and Metadata Entries

**Compaction** entries optimize storage by replacing large message blocks with summaries:

```json
{"type":"compaction","id":"cmp001","parentId":"msg100","summary":"Previous discussion about API design","firstKeptEntryId":"msg050","tokensBefore":15000}

```

Optional **`session_info`** entries allow user-defined metadata such as custom session names or tags.

## Entry-ID to Message Mapping

The frontend relies on a one-to-one correspondence between JSON Lines entries and displayed UI messages. In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the **`SessionContext`** interface exposes two parallel arrays:

- **`messages[]`**: Rendered chat messages (user, assistant, and tool results)
- **`entryIds[]`**: The originating entry `id` from the `.jsonl` file for each message

This mapping enables two critical operations:

1. **Session Forking**: The UI passes the `entryId` corresponding to a selected message to `POST /api/agent/[id]` with the `fork` command, creating a new session file rooted at that specific entry.

2. **Tree Navigation**: The `navigate_tree` command uses the `entryId` (referenced as `leafId` in API calls) to jump to specific conversation leaves within the same session file.

As documented in [`AGENTS.md`](https://github.com/agegr/pi-web/blob/main/AGENTS.md), this parallel array structure ensures every rendered message traces back to its exact source line in the JSONL file.

## Reading and Writing Session Files

### Creation and Updates

The [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) module handles file initialization and incremental writes. When a new session starts, it writes the `session` header line. As the conversation progresses, it atomically appends `model_change` and `message` entries to the `.jsonl` file.

### Parsing and Normalization

When loading a session, [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) streams the file line-by-line and constructs the `SessionContext`. The reader normalizes tool call formats via [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) before populating the `messages[]` and `entryIds[]` arrays, ensuring consistent UI rendering regardless of provider-specific message formats.

## Practical Implementation Examples

### Minimal Session File Structure

The following illustrates a complete session with header, model change, and message sequence:

```json
{"type":"session","version":3,"id":"c2e9b4d5-1a2b-4c3d-9e8f-0123456789ab","timestamp":"2026-08-17T12:00:00Z","cwd":"/home/user/project","parentSession":null}
{"type":"model_change","id":"a1b2c3d4","parentId":null,"provider":"openai","modelId":"gpt-4o","timestamp":"2026-08-17T12:00:01Z"}
{"type":"message","id":"msg001","parentId":"a1b2c3d4","message":{"role":"user","content":"Explain JSON-Lines."}}
{"type":"message","id":"msg002","parentId":"msg001","message":{"role":"assistant","content":["JSON-Lines (JSONL) is a text format for structured data..."]}}
{"type":"message","id":"msg003","parentId":"msg002","message":{"role":"toolResult","toolCallId":"tc-123","content":["Tool execution completed"]}}

```

### Consuming Entry IDs in TypeScript

To fork a session or navigate the conversation tree, reference the `entryIds[]` array from `SessionContext`:

```typescript
import { SessionContext } from '@/lib/session-reader';

function forkFromMessage(ctx: SessionContext, messageIndex: number) {
  const entryId = ctx.entryIds[messageIndex];
  
  fetch(`/api/agent/${ctx.sessionId}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cmd: 'fork', entryId })
  });
}

function navigateToLeaf(ctx: SessionContext, entryId: string) {
  fetch(`/api/agent/${ctx.sessionId}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cmd: 'navigate_tree', leafId: entryId })
  });
}

```

Both functions depend on the **entry-id to message mapping** generated by [`session-reader.ts`](https://github.com/agegr/pi-web/blob/main/session-reader.ts) to identify the correct branching point in the `.jsonl` file.

## Summary

- pi-web persists sessions as **JSON Lines files** (`.jsonl`) in `~/.pi/agent/sessions/<encoded-cwd>/` using timestamp-UUID filenames
- Each line contains a typed entry with a **`type`** field: `session` (header), `model_change`, `message` (user/assistant/toolResult), `compaction`, or `session_info`
- The **`SessionContext`** in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) maintains parallel **`messages[]`** and **`entryIds[]`** arrays, mapping UI elements to their source JSONL entry IDs
- **Entry IDs** enable session forking and tree navigation by referencing specific lines in the conversation history
- Files are written incrementally by [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) and parsed by [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) with normalization handled in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)

## Frequently Asked Questions

### What is the file extension for pi-web session files?

pi-web uses the **`.jsonl`** extension (JSON Lines format) for session storage. Each file contains one JSON object per line, making it human-readable and append-friendly for incremental updates as conversations progress.

### How does pi-web handle branching or forking conversations?

When a user forks a conversation, the UI identifies the **entry ID** corresponding to the selected message from the `entryIds[]` array. The frontend sends this ID to the API endpoint (`POST /api/agent/[id]`) with the `fork` command, which creates a new `.jsonl` file starting from that specific entry point while preserving the conversation history up to that branch.

### What is the purpose of the `parentId` field in session entries?

The **`parentId`** field creates a linked list structure within the session file, where each entry references the ID of its immediate predecessor. This chaining allows pi-web to reconstruct the conversation timeline and maintain proper ordering even when entries are compacted or when navigating to specific points in the conversation tree using the `navigate_tree` functionality.

### Where does pi-web store the mapping between UI messages and file entries?

The mapping occurs in **[`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)**, which constructs a `SessionContext` object containing two parallel arrays: `messages[]` (for the UI) and `entryIds[]` (the source IDs from the `.jsonl` file). This one-to-one correspondence allows the frontend to perform operations like forking and navigation by referencing the exact line in the session file that generated a specific message.