# Pi Web Session File Entry Types: Complete Guide to session, model_change, message, and More

> Explore Pi Web session file entry types including session, model_change, and message. Understand how Pi Web transforms UI data into chat elements and state.

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

---

**Pi Web stores every chat session as a line-delimited JSON file where each line represents a distinct entry type—such as `session`, `message`, or `model_change`—that the UI transforms into chat bubbles, metadata, or internal state.**

The `agegr/pi-web` repository uses a `.jsonl` format to persist conversation state, enabling features like model switching, session compaction, and branching. Understanding these **session file entry types** is essential for developers building extensions, debugging session data, or integrating with the Pi SDK.

## Overview of the Pi Web Session File Format

Each session file is a line-delimited JSON (`.jsonl`) document where every line is a self-contained object with a `type` field. The first line must be a `session` entry (the header), followed by any combination of entries that chronicle the conversation's evolution. According to the source code in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the system validates this structure through `readSessionHeader()` before processing subsequent lines.

The entry type system serves two purposes: it provides an append-only audit log of the conversation, and it allows the UI to reconstruct the chat state through `entryToUiMessage()` and related transformation functions.

## Core Session File Entry Types

### session (Header Entry)

The **`session`** entry appears exclusively as the first line of the file and contains global metadata. It stores the session ID, version, creation timestamp, current working directory, and an optional `parentSession` link for branched conversations.

In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 66-91), the `readSessionHeader()` function validates that the file begins with this entry type before returning the header object. This entry does not render as a chat bubble; instead, it initializes the `AgentSession` state.

### message (Chat Messages)

The **`message`** entry represents actual chat content and appears throughout the file where `type: "message"` is specified. Each message includes a `role` field—`"user"`, `"assistant"`, `"toolResult"`, or `"bashExecution"`—and a `content` array containing text blocks, images, or thinking placeholders.

The conversion to UI components occurs in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 100-118) via `entryToUiMessage()`. For assistant messages, the system optionally defers thinking blocks based on configuration settings, while tool results may have base-64 images omitted to improve performance.

### model_change (Model Switching)

When a user switches AI models or thinking levels, the system appends a **`model_change`** entry. This records the new provider, model ID, and optional thinking level parameters.

Defined in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) (lines 215-226), this entry type does not generate a separate UI bubble. Instead, [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) (lines 1440-1450) detects persisted `model_change` entries to trigger session reloads and update the running `AgentSession` configuration.

### compaction (File Optimization)

The **`compaction`** entry appears when the Pi SDK automatically rewrites old session entries to conserve space. It contains a human-readable `summary`, the `tokensBefore` count, and the ID of the first preserved entry.

In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 120-129), `entryToUiMessage()` converts compaction entries into custom messages with `customType: "compaction"`, allowing the UI to display a notice that history has been summarized.

### branch_summary (Branch Exploration)

When users briefly explore an alternate conversation branch via the *Continue* button and then return, the system writes a **`branch_summary`** entry. The `summary` field contains a concise description of that side-branch's content.

The UI renders these as user-role messages through the case handler in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 131-138), providing context about explored but abandoned conversation paths.

### custom_message (UI Notifications)

Arbitrary UI-only messages—such as tool-call status indicators or system notices—use the **`custom_message`** entry type. These include a `customType` identifier, content payload, display flags, and optional details.

As implemented in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) (lines 140-148), these entries become custom message bubbles that extensions can inject without affecting the underlying conversation logic.

### session_info (Mutable Metadata)

The **`session_info`** entry stores mutable session metadata, particularly user-defined session names. Unlike static header information, these values can update throughout the session lifetime.

Defined in [`lib/types.ts`](https://github.com/agegr/pi-web/blob/main/lib/types.ts) (lines 236-247), this entry type feeds the session-list sidebar rather than the chat view, allowing users to rename sessions without modifying the immutable header.

### tool_call (Internal Representation)

While not a top-level entry type, **`tool_call`** appears nested within assistant `message` entries when the AI invokes tools. The SDK stores these as `{type:"toolCall", id, name, arguments}` objects.

The [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) file transforms these into `{toolCallId, toolName, input}` structures via `normalizeToolCalls()`, ensuring consistent field naming between the SDK representation and the UI display.

## How the UI Processes Session Entries

The transformation from raw JSON lines to rendered chat bubbles follows a four-stage pipeline:

1. **Header validation** — `readSessionHeader()` confirms the first line is a valid `session` entry
2. **Entry loading** — `getSessionEntries()` retrieves the complete array from `SessionManager`
3. **Context building** — `buildSessionContext()` creates ID mappings and filters entries
4. **UI transformation** — `entryToUiMessage()` converts supported entries to `AgentMessage` objects

```typescript
// Simplified flow from lib/session-reader.ts
const header = await readSessionHeader(filePath);
const entries = await getSessionEntries(sessionManager);
const context = buildSessionContext(entries, {
  deferThinking: true,
  deferToolResultImages: true
});

```

For tool calls specifically, the system invokes `normalizeToolCalls()` from [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) to strip SDK-specific artifacts before rendering, ensuring that tool results display cleanly within assistant message bubbles.

## Working with Session Files Programmatically

To read session data manually, parse the first line as a header, then iterate remaining lines as `SessionEntry` union types:

```typescript
import { readSessionHeader, getSessionEntries } from './lib/session-reader';
import { SessionEntry } from './lib/types';

// Read header
const header = await readSessionHeader('./sessions/chat.jsonl');

// Process entries
const entries: SessionEntry[] = await getSessionEntries(sessionManager);

entries.forEach(entry => {
  switch (entry.type) {
    case 'message':
      console.log(`Message from ${entry.message.role}`);
      break;
    case 'model_change':
      console.log(`Switched to ${entry.model}`);
      break;
    case 'compaction':
      console.log(`Compacted ${entry.tokensBefore} tokens`);
      break;
  }
});

```

When handling `message` entries containing tool calls, normalize the structure before processing:

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

const normalized = normalizeToolCalls(message.content);

```

## Summary

- **Pi Web** uses a `.jsonl` format where each line represents a specific **session file entry type** including `session`, `message`, `model_change`, `compaction`, `branch_summary`, `custom_message`, and `session_info`.
- The `session` entry (lines 66-91 in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)) must appear first as the file header.
- **`message`** entries convert to chat bubbles via `entryToUiMessage()`, while **`model_change`** updates the active configuration without UI rendering.
- **`compaction`** and **`branch_summary`** entries provide metadata about session history and explored branches.
- Tool calls require normalization through [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) before UI display.
- Entries like `session_info` and `custom_message` support sidebar labeling and extension notifications respectively.

## Frequently Asked Questions

### What happens if the session file doesn't start with a session entry?

The `readSessionHeader()` function in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) will throw a validation error. This function explicitly checks that the first line contains `type: "session"` (lines 66-91) and extracts global metadata before processing any subsequent entries. Without this header, the session cannot be loaded into the UI.

### How does Pi Web handle model switches during an active conversation?

When the user selects a different model, the Pi SDK appends a `model_change` entry to the session file. The `useAgentSession` hook (lines 1440-1450 in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts)) detects this persisted change and triggers a session reload to update the `AgentSession` configuration. The UI does not render a separate bubble for this event; it applies the change silently to the running session state.

### Can I manually inspect tool calls in a session file?

Yes, though they appear as nested objects within `message` entries rather than standalone top-level entries. Look for `type: "toolCall"` objects inside assistant messages. To process these programmatically, pass the message content through `normalizeToolCalls()` from [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts), which converts SDK-specific fields like `id` and `name` into UI-standard `toolCallId` and `toolName` properties.

### Why are some entries like compaction and branch_summary visible in the chat while session_info is not?

The `entryToUiMessage()` function in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) selectively converts entry types to UI messages. `compaction` (lines 120-129) and `branch_summary` (lines 131-138) have explicit case handlers that generate chat bubbles, while `session_info` is designed for sidebar metadata only. This architectural separation ensures that mutable metadata (like session names) updates the navigation panel without cluttering the conversation history.