How Session Forking Creates Independent .jsonl Files in pi-web

Session forking in pi-web creates isolated conversation branches by copying the original session's history up to a specific message into a new .jsonl file, then linking the two via metadata headers while ensuring the original file remains immutable.

Every chat session in pi-web is persisted as an append-only JSON Lines (.jsonl) file under ~/.pi/agent/sessions/. When users click the Fork button to explore alternative conversation paths, the system does not modify existing history; instead, it spawns entirely new session files that can evolve independently while maintaining a logical parent-child relationship through file metadata.

The Session File Architecture

pi-web stores session data as line-delimited JSON files where each line represents a chat entry or command. These files reside in the user's home directory under ~/.pi/agent/sessions/ and follow a naming convention that includes timestamps and unique identifiers.

According to the architectural documentation in AGENTS.md, each session file contains a header with metadata fields. When a fork occurs, the new file includes a parentSession field pointing to the original file path. This design allows the UI to render fork hierarchies in the sidebar while keeping the underlying data completely separate.

The Fork Command Flow

Frontend Dispatch in useAgentSession.ts

The forking process begins in the React frontend when a user initiates a fork on a specific message. The useAgentSession.ts hook dispatches a command object with type: "fork" to the server-side RPC layer.

// hooks/useAgentSession.ts (around line 1227)
const handleFork = async (entryId: string) => {
  await sendAgentCommand(sessionId, {
    type: "fork",
    entryId,  // ID of the message to fork from
  });
};

This command travels through the agent session hook and reaches the RPC manager via the established WebSocket or IPC connection.

RPC Manager Processing

The server-side handler in lib/rpc-manager.ts (lines 407–434) processes the fork request through a dedicated case block. Before executing, it validates two critical conditions: no shell command can be currently running (this.shellCommandRunning), and the provided entryId must exist within the current session.

// lib/rpc-manager.ts (excerpt from lines 407-434)
case "fork": {
  if (this.shellCommandRunning) {
    throw new Error("Cannot fork while a shell command is running");
  }
  const entry = this.session.getEntryById(msg.entryId);
  if (!entry) throw new Error("Invalid entry ID for forking");

  // Create a new session file that copies history up to the fork point
  const forkedPath = sourceManager.createBranchedSession(entry.parentId);
  if (!forkedPath) throw new Error("Failed to create forked session");
  this.newSessionFile = forkedPath;   // Path of the new .jsonl file

  // Clean up the wrapper for the original session
  this.destroy();
  return;
}

Creating the Independent Session File

Branched Session Creation via createBranchedSession

The core file duplication logic resides in the source manager's createBranchedSession method (implemented in lib/session-file-references.ts and related core helpers). This utility receives the parentId of the entry being forked and performs a selective copy operation.

Rather than duplicating the entire file, the function reads entries from the original session up to but excluding the fork point, then writes them to a newly generated file path.

// lib/session-file-references.ts (simplified implementation)
export function createBranchedSession(parentId: string): string | null {
  const srcPath = this.sessionFilePath;
  const dstPath = generateNewSessionPath();   // e.g., "…/sessions/…/2024…_uuid.jsonl"

  // Copy file up to parentId (excluding the fork point)
  const entries = readEntriesUpTo(srcPath, parentId);
  writeEntries(dstPath, entries);

  // Add header linking back to the original session
  prependHeader(dstPath, { parentSession: srcPath });
  return dstPath;
}

Metadata Linking and Headers

After copying the historical entries, the new file receives a header containing the parentSession field. This metadata establishes the visual parent-child relationship displayed in the UI's "Expand forks" and "Collapse forks" tree view, while the actual file contents remain completely independent from the original.

Wrapper Destruction for Isolation

Immediately after creating the new file, the RPC manager calls this.destroy() on the current AgentSessionWrapper. As documented in AGENTS.md under "Fork must destroy the wrapper immediately," this cleanup step is crucial for data integrity. Destroying the wrapper releases any file locks and ensures subsequent requests for the original session ID will be reinstantiated from the untouched original .jsonl file, preventing cross-contamination between the parent and child sessions.

Resulting File System Structure

After a successful fork operation, the sessions directory contains two distinct files that can diverge independently:

~/.pi/agent/sessions/
├─ 2024-08-01_abc123.jsonl          # Original session (unchanged)

└─ 2024-08-01_def456.jsonl          # Forked session (parentSession → abc123.jsonl)

Each file maintains its own append-only history. The original session continues to record new messages in abc123.jsonl, while the forked conversation writes to def456.jsonl, with both files containing disjoint sets of entries after the fork point.

Summary

  • Session forking creates new .jsonl files rather than modifying existing session history, ensuring data immutability.
  • The fork command flows from hooks/useAgentSession.ts to lib/rpc-manager.ts, which validates the request and invokes createBranchedSession.
  • The branched session copies only the history up to the fork point, excluding the message being forked from.
  • Metadata headers with parentSession fields maintain visual hierarchy without creating file dependencies.
  • Wrapper destruction (this.destroy()) immediately after forking guarantees file isolation and prevents locking conflicts.

Frequently Asked Questions

What happens to the original session file when forking?

The original .jsonl file remains completely untouched. The forking mechanism performs a read-only operation on the source file to copy historical entries, then immediately destroys the session wrapper to release any locks. All subsequent writes occur in the newly created fork file, leaving the original session's data intact and appendable.

Can I fork a session while a shell command is running?

No. The lib/rpc-manager.ts explicitly checks this.shellCommandRunning before processing any fork request. If a shell command is active, the RPC manager throws an error stating "Cannot fork while a shell command is running." This prevents race conditions and ensures the session state is stable during the file copy operation.

How does pi-web maintain the parent-child relationship between forked sessions?

pi-web stores the relationship in the JSONL header metadata of the new file. When createBranchedSession generates the forked file, it prepends a header containing a parentSession field that references the original file's path. The UI uses this metadata to render fork trees in the sidebar while the underlying files remain logically independent.

Where are forked session files stored locally?

All session files, including forks, are stored in the ~/.pi/agent/sessions/ directory within the user's home folder. Forked files follow the same naming conventions as original sessions (typically including timestamps and UUIDs) and reside alongside their parent files, with no separate subdirectory hierarchy for branches.

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 →