Sessions are stored as append-only JSONL (JSON Lines) files. Each line is a self-contained JSON object representing either the session header or a session entry.
File Location
Sessions are stored at:
~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<uuid>.jsonl
The <encoded-cwd> is the working directory path with path separators replaced by --. For example, a project at /home/user/my-project would store sessions in ~/.pi/agent/sessions/--home--user--my-project--/.
Session Versions
The current version is exported as CURRENT_SESSION_VERSION (value: 3). Old sessions are automatically migrated when loaded.
Content Block Types
Content blocks are the building blocks of messages. They appear in the content arrays of user and assistant messages.
TextContent
interface TextContent {
type: "text";
text: string;
textSignature?: string;
}
ImageContent
interface ImageContent {
type: "image";
data: string; // base64-encoded image data
mimeType: string; // e.g., "image/png", "image/jpeg"
}
ThinkingContent
interface ThinkingContent {
type: "thinking";
thinking: string;
thinkingSignature?: string;
}
interface ToolCall {
type: "toolCall";
id: string;
name: string;
arguments: Record<string, any>;
thoughtSignature?: string;
}
Base Message Types
These are the standard LLM message types defined in @mariozechner/pi-ai.
UserMessage
interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number;
}
AssistantMessage
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[];
api: string; // e.g., "anthropic-messages", "openai-completions"
provider: string; // e.g., "anthropic", "openai"
model: string; // e.g., "claude-sonnet-4-20250514"
usage: Usage;
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
errorMessage?: string;
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;
};
}
interface ToolResultMessage<TDetails = any> {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
details?: TDetails;
isError: boolean;
timestamp: number;
}
Extended Message Types
The coding agent extends the base AgentMessage union with custom message types via declaration merging.
BashExecutionMessage
Represents a bash command executed via the ! or !! prefix in interactive mode.
interface BashExecutionMessage {
role: "bashExecution";
command: string;
output: string;
exitCode: number | undefined;
cancelled: boolean;
truncated: boolean;
fullOutputPath?: string;
timestamp: number;
/** If true, this message is excluded from LLM context (!!) */
excludeFromContext?: boolean;
}
CustomMessage
Extension-injected messages via sendCustomMessage().
interface CustomMessage<T = unknown> {
role: "custom";
customType: string;
content: string | (TextContent | ImageContent)[];
display: boolean; // true = rendered in TUI, false = hidden
details?: T;
timestamp: number;
}
BranchSummaryMessage
Generated when navigating the session tree with summarization enabled.
interface BranchSummaryMessage {
role: "branchSummary";
summary: string;
fromId: string;
timestamp: number;
}
CompactionSummaryMessage
Generated when context compaction occurs.
interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string;
tokensBefore: number;
timestamp: number;
}
AgentMessage Union
The full message union type:
type Message = UserMessage | AssistantMessage | ToolResultMessage;
type AgentMessage =
| Message
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;
Session Entry Types
Every session entry (except the header) extends SessionEntryBase:
interface SessionEntryBase {
type: string;
id: string; // UUID for this entry
parentId: string | null; // UUID of parent entry (null for root)
timestamp: string; // ISO 8601 timestamp
}
The first line of every session file. Not a session entry (no id/parentId).
interface SessionHeader {
type: "session";
version?: number; // Current: 3
id: string; // Session UUID
timestamp: string;
cwd: string; // Working directory
parentSession?: string; // Path to parent session (for forks)
}
Example:
{
"type": "session",
"version": 3,
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2025-01-15T10:30:00.000Z",
"cwd": "/home/user/project"
}
SessionMessageEntry
Wraps any AgentMessage (user, assistant, tool result, bash execution, custom, etc.).
interface SessionMessageEntry extends SessionEntryBase {
type: "message";
message: AgentMessage;
}
Example (user message):
{
"type": "message",
"id": "uuid-1",
"parentId": null,
"timestamp": "2025-01-15T10:30:01.000Z",
"message": {
"role": "user",
"content": "What files are in the current directory?",
"timestamp": 1705312201000
}
}
Example (assistant message):
{
"type": "message",
"id": "uuid-2",
"parentId": "uuid-1",
"timestamp": "2025-01-15T10:30:02.000Z",
"message": {
"role": "assistant",
"content": [
{ "type": "text", "text": "Let me check..." },
{
"type": "toolCall",
"id": "tc-1",
"name": "bash",
"arguments": { "command": "ls -la" }
}
],
"api": "anthropic-messages",
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"usage": {
"input": 100,
"output": 50,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 150,
"cost": {
"input": 0.001,
"output": 0.0005,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0.0015
}
},
"stopReason": "toolUse",
"timestamp": 1705312202000
}
}
Example (tool result):
{
"type": "message",
"id": "uuid-3",
"parentId": "uuid-2",
"timestamp": "2025-01-15T10:30:03.000Z",
"message": {
"role": "toolResult",
"toolCallId": "tc-1",
"toolName": "bash",
"content": [
{
"type": "text",
"text": "total 42\ndrwxr-xr-x 5 user user 4096 Jan 15 10:00 .\n..."
}
],
"isError": false,
"timestamp": 1705312203000
}
}
ModelChangeEntry
Records when the user switches models.
interface ModelChangeEntry extends SessionEntryBase {
type: "model_change";
provider: string;
modelId: string;
}
Example:
{
"type": "model_change",
"id": "uuid-4",
"parentId": "uuid-3",
"timestamp": "2025-01-15T10:31:00.000Z",
"provider": "openai",
"modelId": "gpt-4o"
}
ThinkingLevelChangeEntry
Records when the thinking level is changed.
interface ThinkingLevelChangeEntry extends SessionEntryBase {
type: "thinking_level_change";
thinkingLevel: string;
}
Example:
{
"type": "thinking_level_change",
"id": "uuid-5",
"parentId": "uuid-4",
"timestamp": "2025-01-15T10:31:01.000Z",
"thinkingLevel": "high"
}
CompactionEntry
Created when context compaction occurs. Stores the summary and metadata.
interface CompactionEntry<T = unknown> extends SessionEntryBase {
type: "compaction";
summary: string;
firstKeptEntryId: string; // UUID of first entry kept after compaction
tokensBefore: number; // Token count before compaction
/** Extension-specific data (e.g., file tracking) */
details?: T;
/** True if generated by an extension, undefined/false if pi-generated */
fromHook?: boolean;
}
The default details type for pi-generated compaction entries:
interface CompactionDetails {
readFiles: string[];
modifiedFiles: string[];
}
Example:
{
"type": "compaction",
"id": "uuid-10",
"parentId": "uuid-9",
"timestamp": "2025-01-15T11:00:00.000Z",
"summary": "## Goal\nImplement user authentication...\n## Progress\n- Created login form...",
"firstKeptEntryId": "uuid-8",
"tokensBefore": 50000,
"details": {
"readFiles": ["src/auth.ts"],
"modifiedFiles": ["src/login.tsx", "src/api/auth.ts"]
}
}
BranchSummaryEntry
Created when navigating the session tree with summarization enabled. Captures context from the abandoned branch.
interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
type: "branch_summary";
fromId: string; // Entry ID where the branch was abandoned
summary: string;
/** Extension-specific data (not sent to LLM) */
details?: T;
/** True if generated by an extension, false if pi-generated */
fromHook?: boolean;
}
The default details type:
interface BranchSummaryDetails {
readFiles: string[];
modifiedFiles: string[];
}
Example:
{
"type": "branch_summary",
"id": "uuid-15",
"parentId": "uuid-5",
"timestamp": "2025-01-15T11:30:00.000Z",
"fromId": "uuid-14",
"summary": "## Goal\nRefactor database layer...\n## Progress\n- Extracted repository pattern...",
"details": { "readFiles": ["src/db.ts"], "modifiedFiles": ["src/repo.ts"] }
}
CustomEntry
Extension-specific data storage. Does NOT participate in LLM context. Used to persist extension state across session reloads.
interface CustomEntry<T = unknown> extends SessionEntryBase {
type: "custom";
customType: string; // Extension identifier
data?: T;
}
Example:
{
"type": "custom",
"id": "uuid-20",
"parentId": "uuid-19",
"timestamp": "2025-01-15T12:00:00.000Z",
"customType": "git-checkpoint",
"data": { "commitHash": "abc123", "branch": "feature/auth" }
}
CustomMessageEntry
Extension-injected messages that DO participate in LLM context. Converted to user messages in buildSessionContext().
interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
type: "custom_message";
customType: string;
content: string | (TextContent | ImageContent)[];
details?: T; // Extension-specific metadata (not sent to LLM)
display: boolean; // true = styled display in TUI, false = hidden
}
Example:
{
"type": "custom_message",
"id": "uuid-21",
"parentId": "uuid-20",
"timestamp": "2025-01-15T12:01:00.000Z",
"customType": "context-inject",
"content": "The user prefers functional programming patterns.",
"display": false
}
LabelEntry
User-defined bookmark or marker on an entry.
interface LabelEntry extends SessionEntryBase {
type: "label";
targetId: string; // Entry being labeled
label: string | undefined; // Label text, or undefined to clear
}
Example:
{
"type": "label",
"id": "uuid-22",
"parentId": "uuid-21",
"timestamp": "2025-01-15T12:02:00.000Z",
"targetId": "uuid-10",
"label": "checkpoint-before-refactor"
}
SessionInfoEntry
Session metadata such as a user-defined display name.
interface SessionInfoEntry extends SessionEntryBase {
type: "session_info";
name?: string;
}
Example:
{
"type": "session_info",
"id": "uuid-23",
"parentId": "uuid-22",
"timestamp": "2025-01-15T12:03:00.000Z",
"name": "Auth feature implementation"
}
SessionEntry Union
type SessionEntry =
| SessionMessageEntry
| ThinkingLevelChangeEntry
| ModelChangeEntry
| CompactionEntry
| BranchSummaryEntry
| CustomEntry
| CustomMessageEntry
| LabelEntry
| SessionInfoEntry;
/** Raw file entry (includes header) */
type FileEntry = SessionHeader | SessionEntry;
Tree Structure
Sessions form a tree via id and parentId fields. Each entry is a child of its parent, enabling branching:
root (parentId: null)
[user: "Help me build an API"]
/ \
uuid-2 uuid-7
[assistant: "I'll help..."] [assistant: "Let me..."]
| |
uuid-3 uuid-8
[user: "Use Express"] [user: "Use Fastify"]
| |
uuid-4 uuid-9 <-- current leaf
[assistant: "Setting up..."] [assistant: "Setting up..."]
|
uuid-5
[compaction: summary of above]
|
uuid-6
[user: "Add authentication"]
Key concepts:
- Root entry: The first entry has
parentId: null
- Leaf pointer: Tracks the current position in the tree. New entries are always appended as children of the current leaf.
- Branching: Moving the leaf to an earlier entry creates a branch. The next append creates a new child of that entry, diverging from the original path.
- Append-only: Entries are never modified or deleted. History is immutable.
Context Building
buildSessionContext() reconstructs the LLM context by walking from the current leaf to the root:
- Walk from the leaf entry up to the root, collecting all entries along the path
- If a
CompactionEntry is found along the path, inject the compaction summary as a CompactionSummaryMessage and skip all entries before firstKeptEntryId
- If a
BranchSummaryEntry is found, inject the branch summary as a BranchSummaryMessage
- Convert
SessionMessageEntry entries to their contained AgentMessage
- Convert
CustomMessageEntry entries to user messages
- Extract the latest
ModelChangeEntry and ThinkingLevelChangeEntry from the path
- Return the resolved
SessionContext
interface SessionContext {
messages: AgentMessage[];
thinkingLevel: string;
model: { provider: string; modelId: string } | null;
}
Parsing
import { parseSessionEntries } from "@mariozechner/pi-coding-agent";
import { readFileSync } from "fs";
const content = readFileSync("/path/to/session.jsonl", "utf-8");
const entries: FileEntry[] = parseSessionEntries(content);
// First entry is always the header
const header = entries[0] as SessionHeader;
console.log("Session ID:", header.id);
console.log("Working directory:", header.cwd);
// Remaining entries are session entries
const sessionEntries = entries.slice(1) as SessionEntry[];
for (const entry of sessionEntries) {
switch (entry.type) {
case "message":
console.log(
`${entry.message.role}: ${JSON.stringify(entry.message.content).slice(0, 100)}`,
);
break;
case "compaction":
console.log(`Compaction: ${entry.tokensBefore} tokens summarized`);
break;
case "model_change":
console.log(`Model changed to ${entry.provider}/${entry.modelId}`);
break;
}
}
SessionManager API
Static Creation Methods
// Create a new session with file persistence
const sm = SessionManager.create(cwd: string, sessionDir?: string);
// Open an existing session file
const sm = SessionManager.open(path: string, sessionDir?: string);
// Continue the most recent session, or create new if none
const sm = SessionManager.continueRecent(cwd: string, sessionDir?: string);
// Create an in-memory session (no file persistence)
const sm = SessionManager.inMemory(cwd?: string);
// Fork from another project's session into the current project
const sm = SessionManager.forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string);
Static Listing
// List sessions for a specific directory
const sessions: SessionInfo[] = await SessionManager.list(cwd, sessionDir?, onProgress?);
// List all sessions across all project directories
const allSessions: SessionInfo[] = await SessionManager.listAll(onProgress?);
SessionInfo contains:
interface SessionInfo {
path: string;
id: string;
cwd: string; // Working directory
name?: string; // User-defined display name
parentSessionPath?: string; // Parent session (if forked)
created: Date;
modified: Date;
messageCount: number;
firstMessage: string;
allMessagesText: string;
}
Instance Methods - Management
sm.newSession(options?: { parentSession?: string }); // Start a new session
sm.setSessionFile(sessionFile: string); // Switch to a different file
sm.isPersisted(): boolean; // Whether this session is persisted to disk
sm.getCwd(): string; // Get working directory
sm.getSessionDir(): string; // Get session directory
sm.getSessionId(): string; // Get session UUID
sm.getSessionFile(): string | undefined; // Get session file path
sm.getSessionName(): string | undefined; // Get display name
Instance Methods - Appending
sm.appendMessage(message): string; // Append a message entry
sm.appendThinkingLevelChange(thinkingLevel: string): string; // Record thinking level change
sm.appendModelChange(provider: string, modelId: string): string; // Record model change
sm.appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?): string;
sm.appendCustomEntry(customType: string, data?: unknown): string;
sm.appendCustomMessageEntry(customType, content, display, details?): string;
sm.appendSessionInfo(name: string): string;
sm.appendLabelChange(targetId: string, label: string | undefined): string;
All append methods return the new entry's UUID and advance the leaf pointer.
Instance Methods - Tree Navigation
sm.getLeafId(): string | null; // Current leaf entry UUID
sm.getLeafEntry(): SessionEntry | undefined; // Current leaf entry
sm.getEntry(id: string): SessionEntry | undefined; // Get entry by UUID
sm.getChildren(parentId: string): SessionEntry[]; // Get direct children
sm.getLabel(id: string): string | undefined; // Get label for entry
sm.getBranch(fromId?: string): SessionEntry[]; // Walk from entry to root
sm.getTree(): SessionTreeNode[]; // Full tree structure
sm.branch(branchFromId: string): void; // Move leaf to earlier entry
sm.resetLeaf(): void; // Reset leaf to before root
sm.branchWithSummary(id, summary, details?, fromHook?): string;
sm.createBranchedSession(leafId: string): string | undefined;
interface SessionTreeNode {
entry: SessionEntry;
children: SessionTreeNode[];
label?: string; // Resolved label, if any
}
Instance Methods - Context
sm.buildSessionContext(): SessionContext; // Build LLM context from current leaf
sm.getHeader(): SessionHeader | null; // Get session header
sm.getEntries(): SessionEntry[]; // Get all entries (shallow copy)