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, and choosing the wrong one costs you the work you actually wanted. /tree moves within the session. /fork starts a new session from an earlier message. /clone copies the current branch into a new session. The distinction is not cosmetic — it decides whether the abandoned attempt is still there when you come back.
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 current position is the leaf.
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branch
That last line is the part that changes how you read a session. An alternate branch begins with a branch_summary entry, not with a bare user message. Leaving a branch can hand you a summary of what you left.
Wrong: “the active branch is the session — anything not on it is gone.” Correct: it is one path through a tree.
extensions.mdmakes the consequence explicit: rebuild state fromgetBranch(), “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["new branch, same file"]
P -->|"/fork"| F["new file, from an earlier user message"]
P -->|"/clone"| C["new file, from the active branch"]
T --> T2["the branch you left stays, and can be summarized forward"]
F --> F2["nothing carries over; the new file has no record of it"]
C --> C2["the active branch only; abandoned branches do not come"]
Only /tree can leave a pointer back to what you left, and only when you accept the summary: the branch_summary entry records it in fromId. That is why /tree is the only one of the three that can hand you a summary of the abandoned path. If that attempt taught you something you will need, it belongs in the same file. If it is a task you would hand to someone else, it does not.
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. Change it, submit it, and you have a new branch that forks from that prompt.
- 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:
- Find common ancestor — the deepest node shared by old and new positions.
- Collect entries — walk from the old leaf back to that ancestor.
- Prepare with budget — include messages up to the token budget, newest first.
- Generate summary — call the LLM with the structured format.
- Append entry — save a
BranchSummaryEntryat 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 refuse 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 prompt and defaults to no summary. If you prefer losing the context to paying for it, that is your setting.
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 an edit on one branch, another branch will not see it — because from that branch’s point of view it never happened. 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.”
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.
The most common mistake is reaching for /fork when /tree was right. A fork gives you two sessions, two names, two entries in the picker, and no summary from the abandoned path — because the new session has no record of what you left.
Everything here is one mechanism at three scales: entries, branches, sessions. The next chapter goes one level down and defines the entry types themselves.