Skip to main content

Overview

Pi sessions are append-only conversation trees stored as JSONL files. Sessions support:
  • Persistent conversation history
  • Non-destructive branching (try different approaches)
  • Context compaction (summarize old context when approaching limits)
  • Session forking (copy from another project)

Session Structure

JSONL Format

Each line is a JSON object representing an entry:
{"type":"session","version":3,"id":"abc123","timestamp":"2024-01-01T00:00:00.000Z","cwd":"/home/user/project"}
{"type":"message","id":"def456","parentId":null,"timestamp":"2024-01-01T00:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"Hello"}],"timestamp":1704067200000}}
{"type":"message","id":"ghi789","parentId":"def456","timestamp":"2024-01-01T00:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"timestamp":1704067205000}}
Location: packages/coding-agent/src/core/session-manager.ts:27

Entry Types

Sessions contain multiple entry types:
type SessionEntry =
  | SessionMessageEntry      // User, assistant, or toolResult messages
  | ThinkingLevelChangeEntry // Thinking level changes
  | ModelChangeEntry         // Model changes
  | CompactionEntry          // Context compaction summaries
  | BranchSummaryEntry       // Branch summaries
  | CustomEntry              // Extension state (not sent to LLM)
  | CustomMessageEntry       // Extension messages (sent to LLM)
  | LabelEntry               // User-defined bookmarks
  | SessionInfoEntry;        // Session metadata (display name)
Location: packages/coding-agent/src/core/session-manager.ts:136

Tree Structure

Sessions use id and parentId to form a tree:
Session Header
  ├─ Entry A (parentId: null)
  │   ├─ Entry B (parentId: A)
  │   │   └─ Entry C (parentId: B)  ← leaf
  │   └─ Entry D (parentId: A)       ← branch point
  │       └─ Entry E (parentId: D)  ← alternate branch
  └─ Entry F (parentId: null)       ← separate root
The leaf pointer tracks the current position. Appending creates a child of the current leaf.

Branching

Branching moves the leaf pointer without modifying history:
import { SessionManager } from "@mariozechner/pi-coding-agent";

const session = SessionManager.create(process.cwd());

// Append some messages
session.appendMessage(userMessage1);   // Entry A
session.appendMessage(assistantMsg1);  // Entry B (child of A)
session.appendMessage(userMessage2);   // Entry C (child of B)

// Branch from Entry A
session.branch("entry-a-id");

// Next append creates a child of A (not C)
session.appendMessage(userMessage3);   // Entry D (child of A)
Result:
A
├─ B
│  └─ C
└─ D  ← new leaf
Location: packages/coding-agent/src/core/session-manager.ts:1111

Session Lifecycle

Creating Sessions

import { SessionManager } from "@mariozechner/pi-coding-agent";

// Create new session (auto-generates file path)
const session = SessionManager.create(process.cwd());

// Open existing session
const session = SessionManager.open("/path/to/session.jsonl");

// Continue most recent session
const session = SessionManager.continueRecent(process.cwd());

// Fork from another project
const session = SessionManager.forkFrom(
  "/other/project/session.jsonl",
  process.cwd()
);
Location: packages/coding-agent/src/core/session-manager.ts:1246

Session Directories

By default, sessions are stored under ~/.pi/agent/sessions/:
~/.pi/agent/sessions/
├── --home-user-project1--/
│   ├── 2024-01-01T12-00-00-000Z_abc123.jsonl
│   └── 2024-01-02T09-30-00-000Z_def456.jsonl
└── --home-user-project2--/
    └── 2024-01-03T14-15-00-000Z_ghi789.jsonl
Each directory is encoded from the working directory path.

Appending Entries

// Append messages
session.appendMessage({
  role: "user",
  content: [{ type: "text", text: "Hello" }],
  timestamp: Date.now(),
});

// Append model changes
session.appendModelChange("anthropic", "claude-sonnet-4-20250514");

// Append thinking level changes
session.appendThinkingLevelChange("high");

// Append custom entries (extension state)
session.appendCustomEntry("my-extension", { state: "data" });

// Append custom messages (sent to LLM)
session.appendCustomMessageEntry(
  "context",
  "Additional context",
  true,  // display in UI
  { source: "external" }
);
Location: packages/coding-agent/src/core/session-manager.ts:818

Context Building

The session manager builds the LLM context by walking from the leaf to the root:
const { messages, thinkingLevel, model } = session.buildSessionContext();

// Or build from a specific entry
const context = session.buildSessionContext(entryId);

Compaction Handling

When a compaction entry is encountered:
  1. Emit the summary as a compactionSummary message
  2. Emit kept messages (from firstKeptEntryId up to the compaction)
  3. Emit messages after the compaction
// Example path with compaction:
[
  Entry A (user),
  Entry B (assistant),
  Entry C (user),
  Entry D (compaction, firstKeptEntryId: C),  ← summary point
  Entry E (user),
  Entry F (assistant)
]

// Resulting messages:
[
  { role: "compactionSummary", summary: "..." },  // From Entry D
  { role: "user", ... },                          // Entry C
  { role: "user", ... },                          // Entry E
  { role: "assistant", ... }                      // Entry F
]
Location: packages/coding-agent/src/core/session-manager.ts:306

Compaction

Compaction summarizes old context to make room for new conversations.

When Compaction Triggers

Compaction triggers automatically when:
contextTokens > contextWindow - reserveTokens
Default settings:
  • reserveTokens: 16384 (reserve for new conversation)
  • keepRecentTokens: 20000 (keep recent messages)
Location: packages/coding-agent/src/core/compaction/compaction.ts:114

Compaction Algorithm

  1. Find cut point: Walk backwards from newest, accumulate message sizes until reaching keepRecentTokens
  2. Generate summary: Use LLM to create structured summary:
## Goal
[What is the user trying to accomplish?]

## Constraints & Preferences
- [User requirements]

## Progress
### Done
- [x] Completed tasks

### In Progress
- [ ] Current work

## Key Decisions
- **Decision**: Rationale

## Next Steps
1. What should happen next

## Critical Context
- Important data/examples

## Files
- Read: file1.ts, file2.ts
- Modified: file3.ts
  1. Create compaction entry: Append to session with summary and firstKeptEntryId
Location: packages/coding-agent/src/core/compaction/compaction.ts:705

Manual Compaction

import { AgentSession } from "@mariozechner/pi-coding-agent";

const session = new AgentSession({ /* ... */ });

// Compact with optional custom instructions
const result = await session.compact(
  "Focus on architectural decisions"
);

console.log("Compacted from", result.tokensBefore, "tokens");
console.log("Summary:", result.summary);

Extension-Provided Compaction

Extensions can override compaction:
pi.on("session_before_compact", async (event, ctx) => {
  const { preparation, signal } = event;
  
  // Generate custom summary
  const summary = await generateStructuredSummary(
    preparation.messagesToSummarize,
    signal
  );
  
  return {
    compaction: {
      summary,
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      details: { customData: "..." },
    },
  };
});
Location: packages/coding-agent/src/core/extensions/types.ts:414

Session Navigation

Tree Traversal

// Get the full tree structure
const tree = session.getTree();

// Walk from specific entry to root
const path = session.getBranch(entryId);

// Get all entries (flat list)
const entries = session.getEntries();

// Get specific entry
const entry = session.getEntry(entryId);

// Get direct children
const children = session.getChildren(entryId);
Location: packages/coding-agent/src/core/session-manager.ts:1062

Labels

Label entries for navigation:
// Set label
session.appendLabelChange(entryId, "Before refactor");

// Get label
const label = session.getLabel(entryId);

// Clear label
session.appendLabelChange(entryId, undefined);
Labels appear in the tree:
interface SessionTreeNode {
  entry: SessionEntry;
  children: SessionTreeNode[];
  label?: string;  // Resolved from latest LabelEntry
}
Location: packages/coding-agent/src/core/session-manager.ts:995

Branch Summarization

When branching, optionally summarize the abandoned path:
import { AgentSession } from "@mariozechner/pi-coding-agent";

const session = new AgentSession({ /* ... */ });

// Navigate to different branch with summary
await session.navigateTree(entryId, {
  summarize: true,
  customInstructions: "Focus on why we're changing direction",
  label: "Tried async approach",
});
This creates a BranchSummaryEntry that:
  • Captures context from the abandoned path
  • Gets injected as a branchSummary message in the new path
  • Allows the LLM to understand what was tried before
Location: packages/coding-agent/src/core/compaction/branch-summarization.ts

Session Metadata

Display Names

Set user-friendly names for sessions:
session.appendSessionInfo("Refactor authentication");

const name = session.getSessionName();
// "Refactor authentication"
Display names appear in session selectors and lists.

Session Info

Query session metadata:
const sessions = await SessionManager.list(process.cwd());

for (const info of sessions) {
  console.log(info.name);              // Display name
  console.log(info.path);              // File path
  console.log(info.cwd);               // Working directory
  console.log(info.created);           // Creation date
  console.log(info.modified);          // Last activity
  console.log(info.messageCount);      // Total messages
  console.log(info.firstMessage);      // First user message
  console.log(info.parentSessionPath); // Parent (if forked)
}
Location: packages/coding-agent/src/core/session-manager.ts:164

Session Forking

Fork sessions between projects:
// Fork session from another project into current project
const session = SessionManager.forkFrom(
  "/other/project/sessions/session.jsonl",
  process.cwd(),
  "/path/to/target/sessions"  // optional
);
Forking:
  • Copies all entries from the source session
  • Updates the cwd to the target directory
  • Stores parentSession reference to the source
  • Generates a new session ID
Location: packages/coding-agent/src/core/session-manager.ts:1292

Best Practices

Label entries before major changes:
session.appendLabelChange(entryId, "Before database migration");
Name sessions based on the task:
session.appendSessionInfo("Add user authentication");
To try different approaches, branch from the decision point:
// Instead of editing the last user message
session.branch(previousEntryId);
await session.prompt("Try async approach");
Capture why you’re changing direction:
await session.navigateTree(entryId, {
  summarize: true,
  customInstructions: "Sync approach had race conditions",
  label: "Tried sync approach",
});

Next Steps

Architecture

Understand the system architecture

Extensions

Build custom extensions

Build docs developers (and LLMs) love