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

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, 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 (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 (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 (lines 215-226), this entry type does not generate a separate UI bubble. Instead, 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 (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 (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 (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 (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 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
// 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 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:

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:

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) 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 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 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) 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, 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 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.

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 →