Session File Format
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.
File Location
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
Where <path> is the working directory with / replaced by -.
Deleting Sessions
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.
Session Version
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.
Source Files
Source on GitHub (pi-mono):
packages/coding-agent/src/core/session-manager.ts- Session entry types and SessionManagerpackages/coding-agent/src/core/messages.ts- Extended message types (BashExecutionMessage, CustomMessage, etc.)packages/ai/src/types.ts- Base message types (UserMessage, AssistantMessage, ToolResultMessage)packages/agent/src/types.ts- AgentMessage union type
For TypeScript definitions in your project, inspect node_modules/@earendil-works/pi-coding-agent/dist/ and node_modules/@earendil-works/pi-ai/dist/.
Message Types
Session entries contain AgentMessage objects. Understanding these types is essential for parsing sessions and writing extensions.
Content Blocks
Messages contain arrays of typed content blocks:
interface TextContent {
type: 'text'
text: string
}
interface ImageContent {
type: 'image'
data: string // base64 encoded
mimeType: string // e.g., "image/jpeg", "image/png"
}
interface ThinkingContent {
type: 'thinking'
thinking: string
}
interface ToolCall {
type: 'toolCall'
id: string
name: string
arguments: Record<string, any>
}
Base Message Types (from pi-ai)
interface UserMessage {
role: 'user'
content: string | (TextContent | ImageContent)[]
timestamp: number // Unix ms
}
interface AssistantMessage {
role: 'assistant'
content: (TextContent | ThinkingContent | ToolCall)[]
api: string
provider: string
model: string
usage: Usage
stopReason: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted'
errorMessage?: string
timestamp: number
}
interface ToolResultMessage {
role: 'toolResult'
toolCallId: string
toolName: string
content: (TextContent | ImageContent)[]
details?: any // Tool-specific metadata
isError: boolean
timestamp: number
}
interface Usage {
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens: number
cost: {
input: number
output: number
cacheRead: number
cacheWrite: number
total: number
}
}
Extended Message Types (from pi-coding-agent)
interface BashExecutionMessage {
role: 'bashExecution'
command: string
output: string
exitCode: number | undefined
cancelled: boolean
truncated: boolean
fullOutputPath?: string
excludeFromContext?: boolean // true for !! prefix commands
timestamp: number
}
interface CustomMessage {
role: 'custom'
customType: string // Extension identifier
content: string | (TextContent | ImageContent)[]
display: boolean // Show in TUI
details?: any // Extension-specific metadata
timestamp: number
}
interface BranchSummaryMessage {
role: 'branchSummary'
summary: string
fromId: string // Entry we branched from
timestamp: number
}
interface CompactionSummaryMessage {
role: 'compactionSummary'
summary: string
tokensBefore: number
timestamp: number
}
AgentMessage Union
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage
Entry Base
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
}
Entry Types
SessionHeader
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, /clone, 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"
}
SessionMessageEntry
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}}
ModelChangeEntry
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"
}
ThinkingLevelChangeEntry
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"
}
CompactionEntry
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: Implementation-specific data (e.g.,{ readFiles: string[], modifiedFiles: string[] }for default, or custom data for extensions)fromHook:trueif generated by an extension,false/undefinedif pi-generated (legacy field name)
BranchSummaryEntry
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, or custom data for extensionsfromHook:trueif generated by an extension,false/undefinedif pi-generated (legacy field name)
CustomEntry
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. Interactive mode can render custom entries via pi.registerEntryRenderer(customType, renderer), but they still do not participate in LLM context.
CustomMessageEntry
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)
LabelEntry
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.
SessionInfoEntry
Session metadata (e.g., user-defined display name). Set via /name, --name / -n, 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.
Tree Structure
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
Context Building
buildContextEntries() walks from the current leaf to the root, producing the active entry list while honoring compaction:
- Collects all entries on the path
- If a
CompactionEntryis on the path:- Includes the compaction entry first
- Then entries from
firstKeptEntryIdto compaction - Then entries after compaction
- Preserves non-message entries in the selected range so interactive mode can render them
buildSessionContext() builds on that entry list to produce the message list for the LLM:
- Extracts current model and thinking level settings from the full path
- Converts selected entries to messages:
message-> storedAgentMessagecompaction->compactionSummarybranch_summary->branchSummarycustom_message->CustomMessagecustom-> no context message
Parsing Example
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
}
}
SessionManager API
Key methods for working with sessions programmatically.
Static Creation Methods
SessionManager.create(cwd, sessionDir?)- New sessionSessionManager.open(path, sessionDir?)- Open existing session fileSessionManager.continueRecent(cwd, sessionDir?)- Continue most recent or create newSessionManager.inMemory(cwd?)- No file persistenceSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- Fork session from another project
Static Listing Methods
SessionManager.list(cwd, sessionDir?, onProgress?)- List sessions for a directorySessionManager.listAll(onProgress?)- List all sessions across all projects
Instance Methods - Session Management
newSession(options?)- Start a new session (options:{ parentSession?: string })setSessionFile(path)- Switch to a different session filecreateBranchedSession(leafId)- Extract branch to new session file
Instance Methods - Appending (all return entry ID)
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
Instance Methods - Tree Navigation
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
Instance Methods - Context & Info
buildContextEntries()- Get active branch entries with compaction appliedbuildSessionContext()- Get messages, thinkingLevel, and model for LLMgetEntries()- All entries (excluding header)getHeader()- Session header metadatagetSessionName()- Get display name from latest session_info entrygetCwd()- Working directorygetSessionDir()- Session storage directorygetSessionId()- Session UUIDgetSessionFile()- Session file path (undefined for in-memory)isPersisted()- Whether session is saved to disk