# How pi-web Integrates with the pi Agent's .jsonl Session File Format

> Discover how pi-web seamlessly integrates with the pi agent's .jsonl session file format. Learn how it converts JSONL data into UI-ready context for React components.

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

---

**pi-web integrates with the pi agent's .jsonl session file format by acting as a thin wrapper around the pi-coding-agent SDK, reading headers and entries from line-delimited JSON files and converting them into UI-ready context for React components.**

pi-web serves as the official web front-end for the **pi-coding-agent** SDK, providing a React-based interface for AI coding sessions. All conversation history, model switches, tool invocations, and branching metadata persist in `.jsonl` (line-delimited JSON) files managed by the SDK. The web application reads, writes, and manipulates these files directly through a lightweight abstraction layer that bridges low-level session APIs to Next.js API routes and UI components.

## Session Discovery and Path Caching

Before rendering any chat interface, pi-web must locate available sessions on disk. In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), the system calls the SDK's `SessionManager.listAll()` to retrieve raw session metadata. The implementation maintains two global maps—`__piSessionPathCache` and `__piPathToSessionIdCache`—to translate between filesystem paths and session IDs used in UI routes.

This bidirectional mapping enables fast lookups for API endpoints like `/api/sessions/[id]` and `/api/sessions/[id]/fork` without repeated directory scanning. The session list is stored in `globalThis.__piSessionListCache` to avoid rescanning the `~/.pi/agent/sessions` directory on every request.

```typescript
// lib/session-reader.ts
export async function listAllSessions(): Promise<SessionInfo[]> {
  const sessions = await SessionManager.listAll();
  sessions.forEach(cacheSessionPath);
  return sessions.map(convertToUIFormat);
}

```

## Reading the .jsonl Session Structure

The integration relies on sequential reading of `.jsonl` files to reconstruct session state. Each file follows a specific structure where the first line contains a session header, followed by chronological entries representing messages, compactions, and branch events.

### Extracting Session Headers

The `readSessionHeader()` function in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) reads up to 64KB from the start of a `.jsonl` file, parsing the first newline-delimited JSON object where `type:"session"`. This header supplies critical metadata including the working directory (`cwd`), creation timestamp, and parent-session references required to display session lineage in the UI.

```typescript
// lib/session-reader.ts#L88-L115
export async function readSessionHeader(filePath: string): Promise<SessionHeader | null> {
  const fd = await fs.open(filePath, 'r');
  const buffer = Buffer.alloc(65536); // 64KB limit
  const { bytesRead } = await fd.read(buffer, 0, 65536, 0);
  await fd.close();
  
  const firstLine = buffer.toString('utf8', 0, bytesRead).split('\n')[0];
  const header = JSON.parse(firstLine);
  return header.type === 'session' ? header : null;
}

```

### Loading and Transforming Entries

To render chat history, `getSessionEntries()` opens the file via `SessionManager.open(filePath).getEntries()`, returning raw `SessionEntry` objects from the SDK. The `buildSessionContext()` function then processes these entries through `piBuildSessionContext` and `piBuildContextEntries` to compute the active thinking level, current model, and linear message array for the selected branch.

This transformation occurs in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) and produces the `{messages, entryIds, thinkingLevel, model}` structure consumed by the `useAgentSession` React hook.

```typescript
// lib/session-reader.ts#L26-L62
export async function buildSessionContext(filePath: string) {
  const entries = await getSessionEntries(filePath);
  const context = piBuildSessionContext(entries);
  const messages = piBuildContextEntries(context).map(normalizeToolCalls);
  
  return {
    messages,
    entryIds: context.entryIds,
    thinkingLevel: context.thinkingLevel,
    model: context.model
  };
}

```

## Normalizing Legacy Data Formats

Legacy SDK versions stored tool invocations as `{type:"toolCall", id, name, arguments}`, while pi-web's UI expects `{toolCallId, toolName, input}`. The `normalizeToolCalls()` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) rewrites these objects on-the-fly during context building, ensuring consistent rendering across all session files regardless of creation date.

```typescript
// lib/normalize.ts#L7-L31
function normalizeToolCalls(entry: any): AgentMessage {
  if (entry.type === 'toolCall') {
    return {
      ...entry,
      toolCallId: entry.id,
      toolName: entry.name,
      input: entry.arguments
    };
  }
  return entry;
}

```

## Session Manipulation and Branching

Beyond passive reading, pi-web actively modifies `.jsonl` files through RPC commands that wrap SDK functionality.

### Forking Sessions

When users initiate a fork, the front-end sends `{type:"fork", entryId}` to the RPC manager. The `AgentSessionWrapper.send()` method in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) validates the request and invokes the SDK's `SessionManager` to create a new branched `.jsonl` file—either empty or copied up to the fork point. After creation, the system invalidates the session list cache and shuts down the current wrapper, forcing the UI to reconnect to the new session ID.

```typescript
// lib/rpc-manager.ts#L43-L78
async send(command: { type: 'fork'; entryId: string }) {
  const newSession = await this.sessionManager.fork({
    parentPath: this.filePath,
    entryId: command.entryId
  });
  
  cacheSessionPath(newSession.id, newSession.path);
  invalidateSessionListCache();
  this.shutdown(); // Force UI reconnect
}

```

### Navigating Branch Trees

The **Continue** button triggers `navigate_tree` commands handled by `AgentSessionWrapper`. Calling `this.inner.navigateTree(targetId, {})` updates the SDK's internal branch pointer without creating new files. The UI rebuilds context by calling `buildSessionContext()` on the same file, retrieving a different slice of entries based on the new leaf position.

## Real-Time Synchronization via SSE

During active agent execution, `AgentSessionWrapper.start()` subscribes to the SDK's event stream and forwards events—such as `agent_start`, `agent_end`, and `compaction_start`—to the front-end through Server-Sent Events at `/api/agent/[id]/events`. This maintains real-time synchronization between the disk state and UI without polling the `.jsonl` file directly.

```typescript
// lib/rpc-manager.ts#L11-L23
start() {
  this.inner.events.on('agent_start', (e) => this.broadcast(e));
  this.inner.events.on('agent_end', (e) => this.broadcast(e));
  this.inner.events.on('compaction_start', (e) => {
    invalidateSessionListCache();
    this.broadcast(e);
  });
}

```

## Cache Invalidation Strategy

To prevent excessive disk I/O, pi-web implements aggressive caching with explicit invalidation. Any operation mutating the `.jsonl` file—including prompt completion, forking, compaction, or model changes—calls `invalidateSessionListCache()` in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts). This increments `__piSessionListGeneration`, forcing the next request to rescan the sessions directory and ensuring the sidebar reflects current session states.

## Summary

- pi-web discovers sessions via `SessionManager.listAll()` and maintains bidirectional path-to-ID caches in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)
- The first line of each `.jsonl` file contains a session header parsed by `readSessionHeader()` to extract metadata including `cwd` and parent references
- `buildSessionContext()` transforms raw SDK entries into UI-ready message arrays with normalized tool-call fields via [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts)
- Forking creates new `.jsonl` files through `AgentSessionWrapper.send()` while navigation updates branch pointers within existing files using `navigateTree()`
- Server-Sent Events stream real-time updates from the SDK to the React front-end via `/api/agent/[id]/events`
- Cache invalidation ensures UI synchronization after any disk mutation by bumping `__piSessionListGeneration`

## Frequently Asked Questions

### What is the structure of a pi agent .jsonl session file?

Each session file uses line-delimited JSON format where the first line contains a header object with `type:"session"`, `cwd`, and timestamp. Subsequent lines represent chronological entries including messages, tool calls, compactions, and branch summaries. This append-only structure supports efficient reads and branching operations without rewriting existing data.

### How does pi-web handle legacy tool-call formats?

The `normalizeToolCalls()` function in [`lib/normalize.ts`](https://github.com/agegr/pi-web/blob/main/lib/normalize.ts) automatically transforms legacy `{type:"toolCall", id, name, arguments}` objects into the modern `{toolCallId, toolName, input}` schema during context building. This ensures UI components render historical sessions correctly regardless of when they were created.

### Can multiple users access the same .jsonl session simultaneously?

While the underlying SDK supports concurrent reads, pi-web's `AgentSessionWrapper` manages exclusive access during write operations through the RPC manager. The global session cache (`globalThis.__piSessionListCache`) ensures consistent state across API routes, though simultaneous modifications from multiple web clients may require additional coordination at the application layer.

### How does forking affect the original session file?

Forking creates an entirely new `.jsonl` file rather than modifying the original. The SDK's `SessionManager` either initializes an empty child session or copies entries up to the fork point, preserving the parent file's integrity. The UI then reconnects to the new session ID while maintaining a reference to the parent through the session header metadata.