← Pi Agents

Branching and Forking

Three ways to go back — /tree, /fork and /clone — and how to choose when a wrong turn stays in the session or becomes separate work.

The agent picked a schema you did not want, edited eleven files, and is confidently three steps into an approach you have already decided against. You press Escape. Now what?

There are three answers for the conversation: /tree moves within the current session, /fork starts a new session from an earlier user message, and /clone copies the active branch into a new session. The distinction decides where the conversation history lives and which part of it the model sees next. None of these commands undoes the eleven edits already made on disk. If you also want the repository back in its earlier state, inspect and restore those working-tree changes separately, using version control or a backup.

The tree is the storage model

“Pi stores entries as a tree, so returning to an earlier point does not erase the branch you leave.” Each entry carries id and parentId; branching “creates new children from an earlier entry”. The active position identifies the history Pi will continue from. It is usually a leaf after a new entry is appended, but /tree can move it back to an existing entry that already has children.

[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
                                                           │
                                                           └─ [branch_summary] ─── [user msg] ← alternate branch

That last line shows one possible navigation result: Pi has appended a branch_summary before continuing on the other branch. A summary is optional, not a required first entry on every alternate branch. You can branch directly from an earlier entry without adding one; when you do accept a summary, it can carry useful context from the path you left.

Wrong: “the active branch is the session — anything not on it is gone.” Correct: it is one path through a tree. extensions.md makes the consequence explicit: rebuild state from getBranch(), “not from every file entry because abandoned branches represent alternative histories.”

Choosing a command

Action Result Use it when
/tree Moves within the current session file Related alternatives should stay together
/fork Creates a new session from an earlier user message The alternative should become separate work
/clone Copies the active branch into a new session You want a separate copy of the current state

sessions.md gives the use cases directly. What the table does not say is what each one does to the history:

    flowchart TD
  P["the entry you are at"]
    P -->|"/tree"| T["move within the same session file"]
  P -->|"/fork"| F["new file, from an earlier user message"]
  P -->|"/clone"| C["new file, from the active branch"]
    T --> T2["submit a new message to branch; old paths remain; summary optional"]
    F --> F2["earlier context carries over; the abandoned continuation stays in the old session"]
    C --> C2["copies the active path, not its abandoned siblings"]
  

Of these three commands, /tree is the one that can append a branch_summary linking the history you left (fromId) to the point where you resumed (parentId). /fork also retains a provenance link through the new session’s parentSession header, but it does not carry a summary of the discarded continuation into the new context. If you need the alternative paths and their possible summary together, stay in /tree; if the work deserves its own session, use /fork.

Inside /tree

The interaction has one rule worth knowing before you start. “In /tree, select a user message to put its text back in the editor. Edit and submit it to create another branch. Selecting an assistant response or another entry continues after that entry with an empty editor.”

So there are two selection outcomes:

  • Select a user message → its text returns to your editor, and the session moves to that message’s parent. Change the text and submit it to create a sibling of the original user message, not a child of it.
  • Select an assistant response or any other entry → you continue after that entry, with an empty editor.

The first is a new question; the second is a continuation from an existing point. settings.md gives you a shortcut that blurs this: doubleEscapeAction with values "tree", "fork" or "none" and a default of "tree" — “Action for double Escape with an empty editor.” If you press Escape twice on an empty editor, you land in /tree by default. Change it to "fork" if your reflex is to start fresh sessions.

The tree view opens filtered. treeFilterMode takes "default", "no-tools", "user-only", "labeled-only" or "all", default "default".

What leaving a branch costs you

“When you leave a branch, Pi can summarize it and attach that summary to the branch you enter. This preserves relevant work from the abandoned path without including every message from it.”

The mechanism, from compaction.md:

  1. Find common ancestor — the deepest node shared by old and new positions.
  2. Collect entries — walk from the old leaf back to that ancestor.
  3. Prepare with budget — include messages up to the token budget, newest first.
  4. Generate summary — call the LLM with the structured format.
  5. Append entry — save a BranchSummaryEntry at the navigation point.
Tree before navigation:

         ┌─ B ─ C ─ D (old leaf, being abandoned)
    A ───┤
         └─ E ─ F (target)

Common ancestor: A
Entries to summarize: B, C, D

After navigation with summary:

         ┌─ B ─ C ─ D
    A ───┤
         └─ E ─ F ─ [summary of B,C,D] (new leaf)

Note that the abandoned entries B, C and D are still in the tree after the move. The summary is an addition, not a replacement — the same property chapter 23 found for compaction.

The entry records both ends:

interface BranchSummaryEntry<T = unknown> {
  type: "branch_summary";
  id: string;
  parentId: string | null;
  timestamp: string;
  summary: string;
  fromId: string;      // Entry we navigated from
  usage?: Usage;       // LLM usage that generated the summary
  fromHook?: boolean;  // true if provided by extension (legacy field name)
  details?: T;         // implementation-specific data
}

parentId is “the entry from which the new branch continues” and fromId is “the previous leaf whose abandoned path was summarized.” Two different pointers, which is what lets you reconstruct where you jumped from.

You can decline the summary. settings.md exposes branchSummary.skipPrompt, default false, and branchSummary.reserveTokens with default 16384 — “Tokens reserved when summarizing branch history.” Setting skipPrompt to true skips the confirmation prompt and defaults to no summary. This saves the summarization step; it does not delete the abandoned messages from the session file. They remain available if you navigate back, but their content is not automatically carried into the branch you enter.

Doing it from a program

The same three operations exist over RPC, and the responses tell you whether an extension intervened.

{"type": "get_fork_messages"}
{
  "type": "response",
  "command": "get_fork_messages",
  "success": true,
  "data": {
    "messages": [
      {"entryId": "abc123", "text": "First prompt..."},
      {"entryId": "def456", "text": "Second prompt..."}
    ]
  }
}

Fork from one of them:

{"type": "fork", "entryId": "abc123"}

The response returns "text": "The original prompt text..." alongside "cancelled": false. The text matters for a UI: it is what you put back in your editor so the user can edit the original prompt rather than retype it. If an extension cancelled, you get {"cancelled": true} and no session was created.

clone takes no arguments and duplicates the active branch at the current position. Both fork and clone “can be canceled by a session_before_fork extension event handler.”

Reading the tree from outside

Two commands, and the distinction between them is the practical part of this chapter.

get_entries returns “all session entries in append order (excluding the session header).” Because the session is “an append-only tree of entries with stable ids, so an entry id works as a durable cursor,” you can pass the last id you saw as since and get “only entries strictly after it, even across client restarts.”

{"type": "get_entries", "since": "abc123"}

Critically: “Unlike get_messages, this includes pre-compaction history and abandoned branches.” If you are building a session viewer and you read get_messages, you are reading only the active branch. Use get_entries when you need everything.

The response also carries leafId — “the id of the current leaf entry (null for an empty session), so a client can tell in one round trip whether the active branch moved.” If since matches no entry id, the response is success: false.

get_tree gives the shape instead of the order:

{"type": "get_tree"}

Each node is {entry, children, label?, labelTimestamp?}. The result is an array, “because navigation APIs can create multiple roots; orphaned entries with broken parent chains also appear as roots.”

Branch-relative state

One consequence for extension authors. ContextEditEntry edits are “branch-relative: navigating to a point before the edit reveals the target’s original contribution again.”

That is the right semantics, and it is worth designing for. If your extension records a context edit after two branches diverge, the sibling branch does not see it; descendants of the edited entry do. extensions.md makes the matching rule for state explicit: “Reconstruct branch-sensitive state from ctx.sessionManager.getBranch() during session_start. Do not rebuild it from every file entry because abandoned branches represent alternative histories.” If your extension caches that state, also rebuild it when session_tree changes the active branch, rather than keeping the previous branch’s cached value.

Labels travel differently. A label entry is “a user-defined bookmark/marker on an entry,” and get_tree reports label and labelTimestamp per node. Setting label to undefined clears it. Labels are how you find your way back through a tree you have built up over days.

A decision procedure

Three questions, in order: will you come back to the alternative, does it deserve its own session listing, or are you simply worried about losing the current state? The first is /tree, the second /fork, the third /clone.

A common mistake is reaching for /fork when /tree would keep related alternatives easier to compare. A fork creates a separate session containing the history before the selected user message, but not the continuation you are abandoning. The original session still holds that continuation, and the new session records its origin in parentSession; what does not carry across automatically is a summary of the abandoned work.

Everything here is one mechanism at three scales: entries, branches, sessions. A branch you did not choose changes what reaches the model, which makes it a way of answering a question you have not finished asking. The next chapter takes the other direction: how to change a run that is still in progress.

What was run

Chapters 4, 5, 23 and 24 are tested on one shared history, so that they read as one subject, state over time, and not as four. It is three turns in a persisted session: question A and its answer; question B, which makes a bash tool call and then answers; question C and its answer. examples/harness/history.ts builds it, and each chapter reads a different property of the same run.

Chapter 24’s question is what changes when history branches? examples/ch24-branching/ branches that one history in process and, for /fork and /clone, on the shipped binary:

  • /tree on a user message returned its text for editing and moved the leaf to the message’s parent, and on an assistant response it continued after it with an empty editor;
  • submitting the edit created a sibling: two message entries under the same parent, in one session file;
  • the abandoned branch was not sent, and was not gone;
  • with a summary supplied by an extension, leaving a branch wrote a branch_summary entry whose fromId was the leaf you left and whose parentId was where you landed, with the abandoned entries still in the tree. The summary then reached the model on the new branch while the abandoned messages still did not;
  • getTree() showed both branches under their shared parent, and a label set with appendLabelChange() appeared on the node and was cleared by setting it to undefined;
  • over RPC, get_fork_messages listed the user prompts, fork started a new session file and returned the original prompt text to edit, and the new file’s header carried parentSession, linking it to the previous session;
  • clone copied the active branch into a new session file.