How Session Branching and Compaction Work in pi-ai
Session branching and compaction in pi-ai use an append-only JSON-L tree structure where branch() moves the leaf pointer to create alternate conversation histories, while appendCompaction() replaces old entries with summaries to manage token limits.
The pi-ai coding agent (part of the badlogic/pi-mono repository) implements a sophisticated session management system that treats conversation history as a version-controlled tree rather than a linear log. This design enables non-destructive branching and intelligent context compression through compaction.
Understanding the Append-Only Session Tree
pi-ai stores every interaction in an append-only JSON-L file that represents a tree of session entries. Each entry contains an id and a parentId, forming a directed acyclic graph capable of maintaining multiple conversation branches simultaneously.
The SessionManager class in [packages/coding-agent/src/core/session-manager.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) maintains a leafId pointer that marks the current active node. All append operations create children of this leaf, extending the conversation history from the current position.
Entry types include:
message– User, assistant, or custom messagesbranch_summary– Human-readable summary of abandoned branchescompaction– Summary replacing a long history segmentmodel_change,thinking_level_change– Meta-information influencing LLM calls
Session Branching in pi-ai
Branching allows users to explore alternative conversation paths without losing the original history. The system achieves this by repositioning the leaf pointer to any previous entry, causing subsequent appends to create a new branch from that point.
Creating a Branch with branch()
The branch() method (lines 1110–1116 in session-manager.ts) validates the target entry ID and updates leafId to point to that node:
// Move the leaf pointer to an earlier entry
session.branch(entryId);
Subsequent calls to appendMessage() or appendCompaction() create children of the repositioned leaf, effectively forking the conversation tree while preserving the original timeline intact.
Branching with Summaries using branchWithSummary()
For better context management when abandoning a branch, branchWithSummary() (lines 1127–1147) creates a branch and inserts a human-readable summary of the discarded path:
const summaryId = session.branchWithSummary(
entryId, // where to branch from (or null for root)
"User switched topic to …", // free-form summary
{ extra: "metadata" }, // optional extension data
false // fromHook flag (false = core-generated)
);
The summary is rendered for the LLM by createBranchSummaryMessage in [packages/coding-agent/src/core/messages.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) (lines 100–107), ensuring the model understands the context switch without processing the entire abandoned branch.
Session Compaction in pi-ai
As conversations grow, they eventually exceed token limits. Compaction solves this by replacing a contiguous block of old entries with a single summary entry, preserving context while freeing up token budget for new messages.
How Compaction Works
The appendCompaction() method (lines 636–665 in session-manager.ts) creates a CompactionEntry that records the summary text and the ID of the first entry to keep:
const compactionId = session.appendCompaction(
"Conversation was trimmed to keep recent context", // summary
firstKeptEntryId, // id of the first entry we keep
tokensBeforeCompaction, // token count before compacting
{ model: "claude-3-sonnet-20240229" }, // optional hook data
true // fromHook = true (generated by extension)
);
The summary text is converted to LLM-compatible format by createCompactionSummaryMessage in messages.ts (lines 109–119), allowing the model to understand the historical context without processing the full message history.
Reconstructing Context with Compaction
During context reconstruction, buildSessionContext() (lines 302–414) walks from the current leaf to the root. When encountering a compaction entry, it:
- Emits the compaction summary first
- Emits only the kept entries (those whose
id≥firstKeptEntryId) - Emits everything after the compaction point
This ensures the LLM receives a concise representation of the full conversation while preserving the logical ordering of recent turns.
Extracting and Managing Branches
To export only the active conversation path without abandoned branches, pi-ai provides createBranchedSession() (lines 1516–1588):
const branchedFile = session.createBranchedSession(activeLeafId);
// `branchedFile` contains a new JSONL file with just the root-to-leaf path.
This method builds a new session file containing only the selected branch, copies relevant label entries, and updates the header to point to the original file as its parent, enabling clean exports and archival of specific conversation threads.
Complete Workflow Example
Here is a complete example demonstrating branching and compaction in a typical interaction:
import { SessionManager } from "@mariozechner/pi-ai";
// 1️⃣ Create or resume a session
const session = SessionManager.continueRecent(process.cwd());
// 2️⃣ Normal conversation
session.appendMessage({ role: "user", content: "Explain recursion." });
session.appendMessage({ role: "assistant", content: "Recursion is …" });
// 3️⃣ User decides to try a different approach → branch
session.branch(session.getLeafId()!); // move leaf back
session.appendMessage({ role: "user", content: "Show me an example in Python." });
// 4️⃣ After many messages we hit a token limit → compact
const firstKept = session.appendMessage({ role: "assistant", content: "Here is the final result." });
session.appendCompaction(
"Early discussion trimmed – see summary above.",
firstKept,
1200
);
// 5️⃣ Build the LLM context for the next API call
const { messages } = session.buildSessionContext(); // ← includes compaction summary + kept messages
Summary
- pi-ai uses an append-only JSON-L tree where entries reference parents via
parentId, enabling non-destructive branching and full history preservation. - Branching repositions the
leafIdpointer to any historical entry viabranch()orbranchWithSummary(), creating new conversation paths while keeping original branches intact. - Compaction replaces old entries with summaries using
appendCompaction(), managed bybuildSessionContext()to reconstruct concise LLM context that respects token limits. - Key implementation files include
packages/coding-agent/src/core/session-manager.tsfor tree management andpackages/coding-agent/src/core/messages.tsfor summary message formatting.
Frequently Asked Questions
How does pi-ai handle multiple conversation branches without losing data?
pi-ai maintains every conversation branch in a single append-only JSON-L file using a directed acyclic graph structure. Each entry stores a parentId referencing its predecessor, allowing multiple leaf nodes to coexist. The SessionManager tracks only the active leafId, but all historical branches remain in the file and can be reactivated or exported using createBranchedSession().
What is the difference between branching and compaction in pi-ai?
Branching creates alternate conversation paths by moving the leaf pointer to a previous entry, enabling exploration of different approaches without destroying the original timeline. Compaction is a space-saving mechanism that replaces a block of old entries with a summary text to stay within token limits. While branching preserves full granularity for exploration, compaction intentionally loses individual messages in favor of a condensed representation.
When should I use branchWithSummary() instead of branch()?
Use branchWithSummary() when abandoning a conversation path that contains important context the LLM should remember, such as when switching topics or discarding a failed approach. The method inserts a branch_summary entry containing your human-readable description, which buildSessionContext() renders as a system message. Use branch() alone when the abandoned branch requires no explanation or when you intend to return to it later without summarizing the detour.
How does buildSessionContext() reconstruct conversation history after compaction?
buildSessionContext() traverses from the current leaf toward the root, building a message list in reverse chronological order. When it encounters a compaction entry, it inserts the compaction summary first, then filters the subsequent entries to include only those with IDs greater than or equal to the compaction's firstKeptEntryId. This ensures the LLM receives the summary of discarded history followed by the complete recent context, maintaining logical flow while respecting token budgets.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →