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 messages
  • branch_summary – Human-readable summary of abandoned branches
  • compaction – Summary replacing a long history segment
  • model_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:

  1. Emits the compaction summary first
  2. Emits only the kept entries (those whose idfirstKeptEntryId)
  3. 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 leafId pointer to any historical entry via branch() or branchWithSummary(), creating new conversation paths while keeping original branches intact.
  • Compaction replaces old entries with summaries using appendCompaction(), managed by buildSessionContext() to reconstruct concise LLM context that respects token limits.
  • Key implementation files include packages/coding-agent/src/core/session-manager.ts for tree management and packages/coding-agent/src/core/messages.ts for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →