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

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

// 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 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.

// 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 and produces the {messages, entryIds, thinkingLevel, model} structure consumed by the useAgentSession React hook.

// 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 rewrites these objects on-the-fly during context building, ensuring consistent rendering across all session files regardless of creation date.

// 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 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.

// 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
}

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.

// 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. 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
  • 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
  • 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 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.

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 →