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
}
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.
// 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 inlib/session-reader.ts - The first line of each
.jsonlfile contains a session header parsed byreadSessionHeader()to extract metadata includingcwdand parent references buildSessionContext()transforms raw SDK entries into UI-ready message arrays with normalized tool-call fields vialib/normalize.ts- Forking creates new
.jsonlfiles throughAgentSessionWrapper.send()while navigation updates branch pointers within existing files usingnavigateTree() - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →