Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a type field. Session entries form a tree structure via id/parentId fields, enabling in-place branching without creating new files.
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
Where <path> is the working directory with / replaced by -.
Sessions can be removed by deleting their .jsonl files under ~/.pi/agent/sessions/.
Pi also supports deleting sessions interactively from /resume (select a session and press Ctrl+D, then confirm). When available, pi uses the trash CLI to avoid permanent deletion.
Sessions have a version field in the header:
- Version 1: Linear entry sequence (legacy, auto-migrated on load)
- Version 2: Tree structure with
id/parentIdlinking - Version 3: Renamed
hookMessagerole tocustom(extensions unification)
Existing sessions are automatically migrated to the current version (v3) when loaded.
src/core/session-manager.ts- Session entry typespackages/agent-core/src/types.ts-AgentMessagepackages/ai/src/types.ts-UserMessage,AssistantMessage,ToolResultMessage,Usage,ToolCall,ImageContent,TextContent
All entries (except SessionHeader) extend SessionEntryBase:
interface SessionEntryBase {
type: string;
id: string; // 8-char hex ID
parentId: string | null; // Parent entry ID (null for first entry)
timestamp: string; // ISO timestamp
}First line of the file. Metadata only, not part of the tree (no id/parentId).
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}For sessions with a parent (created via /fork or newSession({ parentSession })):
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}A message in the conversation. The message field contains an AgentMessage.
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}Emitted when the user switches models mid-session.
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}Emitted when the user changes the thinking/reasoning level.
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}Created when context is compacted. Stores a summary of earlier messages.
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}Optional fields:
details: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for extension implementations)fromHook:trueif generated by an extension,false/undefinedif pi-generated
Created when switching branches via /tree with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}Optional fields:
details: File tracking data ({ readFiles: string[], modifiedFiles: string[] }) for default implementation, arbitrary for custom implementationfromHook:trueif generated by an extension
Extension state persistence. Does NOT participate in LLM context.
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}Use customType to identify your extension's entries on reload.
Extension-injected messages that DO participate in LLM context.
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}Fields:
content: String or(TextContent | ImageContent)[](same as UserMessage)display:true= show in TUI with distinct styling,false= hiddendetails: Optional extension-specific metadata (not sent to LLM)
User-defined bookmark/marker on an entry.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Set label to undefined to clear a label.
Session metadata (e.g., user-defined display name). Set via /name command or pi.setSessionName() in extensions.
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}The session name is displayed in the session selector (/resume) instead of the first message when set.
Entries form a tree:
- First entry has
parentId: null - Each subsequent entry points to its parent via
parentId - Branching creates new children from an earlier entry
- The "leaf" is the current position in the tree
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branch
buildSessionContext() walks from the current leaf to the root, producing the message list for the LLM:
- Collects all entries on the path
- Extracts current model and thinking level settings
- If a
CompactionEntryis on the path:- Emits the summary first
- Then messages from
firstKeptEntryIdto compaction - Then messages after compaction
- Converts
BranchSummaryEntryandCustomMessageEntryto appropriate message formats
import { readFileSync } from "fs";
const lines = readFileSync("session.jsonl", "utf8").trim().split("\n");
for (const line of lines) {
const entry = JSON.parse(line);
switch (entry.type) {
case "session":
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
break;
case "message":
console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
break;
case "compaction":
console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
break;
case "branch_summary":
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
break;
case "custom":
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
break;
case "custom_message":
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
break;
case "label":
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
break;
case "model_change":
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
break;
case "thinking_level_change":
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
break;
}
}Key methods for working with sessions programmatically:
SessionManager.create(cwd, sessionDir?)- New sessionSessionManager.open(path, sessionDir?)- Open existingSessionManager.continueRecent(cwd, sessionDir?)- Continue most recent or create newSessionManager.inMemory(cwd?)- No file persistence
appendMessage(message)- Add messageappendThinkingLevelChange(level)- Record thinking changeappendModelChange(provider, modelId)- Record model changeappendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)- Add compactionappendCustomEntry(customType, data?)- Extension state (not in context)appendSessionInfo(name)- Set session display nameappendCustomMessageEntry(customType, content, display, details?)- Extension message (in context)appendLabelChange(targetId, label)- Set/clear label
getLeafId()- Current positiongetLeafEntry()- Get current leaf entrygetEntry(id)- Get entry by IDgetBranch(fromId?)- Walk from entry to rootgetTree()- Get full tree structuregetChildren(parentId)- Get direct childrengetLabel(id)- Get label for entrybranch(entryId)- Move leaf to earlier entryresetLeaf()- Reset leaf to null (before any entries)branchWithSummary(entryId, summary, details?, fromHook?)- Branch with context summary
buildSessionContext()- Get messages for LLMgetEntries()- All entries (excluding header)getHeader()- Session metadatagetSessionName()- Get display name from latest session_info entry