Context Management: Compaction and Branch Summaries
Last updated: Aug 17, 2026
$ pi –compact
TL;DR. Pi runs two summarization mechanisms when conversations grow too long: auto-compaction fires when contextTokens > contextWindow - reserveTokens, summarizes older messages into a CompactionEntry, and keeps the most recent keepRecentTokens (defaults 16384 and 20000 respectively); branch summarization fires on /tree navigation, walks from the old leaf back to the common ancestor, and appends a BranchSummaryEntry to the new leaf. Both use the same structured summary schema (Goal / Constraints / Preferences / Progress / Key Decisions / Next Steps / Critical Context + <read-files> / <modified-files>) and accumulate file operations cumulatively across compactions. Extensions can intercept either via the session_before_compact and session_before_tree events. Compaction and branch-summary requests use fresh routing session IDs and disable prompt-cache writes where the provider supports it.
This guide is the deep-dive companion to the official Compaction documentation. It pulls the rules from packages/coding-agent/docs/compaction.md and the implementation in packages/coding-agent/src/core/compaction/ so the mechanics are in one place. Where upstream changes, this guide will lag behind — always cross-check against the canonical doc.
When compaction fires
Auto-compaction triggers when the session’s token count exceeds the context window minus the reserve:
contextTokens > contextWindow - reserveTokens
reserveTokens defaults to 16384 and is the headroom left for the LLM’s reply; you can tune it in ~/.pi/agent/settings.json or <project-dir>/.pi/settings.json. You can also trigger compaction manually at any time with /compact [instructions] — the optional instructions focus the summary (“focus on the test failures”, “preserve the API contract”, “compress the agent’s exploration but keep every user decision”).
A compaction can also fire mid-session as overflow recovery when a single request would exceed the context window. In that case the aborted turn is retried after compaction (willRetry: true on the session_before_compact event).
How compaction walks history
When the threshold trips, pi runs the following pass against the session entries:
- Find the cut point. Walk backwards from the newest entry, accumulating token estimates until
keepRecentTokens(default 20000) is reached. - Extract the messages to summarize. Collect every entry from the session start (or from the previous compaction’s
firstKeptEntryId) up to the cut point. - Generate a structured summary. Call the LLM with the structured format below; pass the previous summary as iterative context when one exists.
- Append a
CompactionEntrycarrying the summary,firstKeptEntryId, andtokensBefore. - Rebuild context for the next request. The session rebuilds as
system prompt + summary + entries from firstKeptEntryId onward.
A repeated compaction’s “summarized span” starts at the previous compaction’s kept boundary, not at the previous compaction entry itself, falling back to the entry after the previous compaction if the kept entry is no longer in the path. This preserves messages that survived earlier passes by including them in the next summarization too. Pi also recalculates tokensBefore from the rebuilt session context before writing the new CompactionEntry, so the token count reflects the actual pre-compaction context being replaced.
Cut-point rules
Valid cut points are user messages, assistant messages, BashExecution messages, and custom messages (custom_message, branch_summary). Tool results must never be cut — they have to stay with the tool call they came from, otherwise the LLM would see a tool call without its outcome.
Split turns
Normally compaction cuts at turn boundaries (a user message and everything after it up to the next user message). When a single turn alone exceeds keepRecentTokens, the cut lands mid-turn at an assistant message and the entry is marked isSplitTurn: true. For split turns pi generates two summaries — a history summary for the context before the turn, and a turn-prefix summary for the early part of the oversized turn — and merges them into the single CompactionEntry summary.
The structured summary format
Both compaction and branch summarization use the same format. The summary block is wrapped in fenced markdown; read-files and modified-files are delimited in angle-bracket tags so the rest of the pi stack can extract them deterministically:
## Goal
[What the user is trying to accomplish]
## Constraints & Preferences
- [Requirements mentioned by user]
## Progress
### Done
- [x] [Completed tasks]
### In Progress
- [ ] [Current work]
### Blocked
- [Issues, if any]
## Key Decisions
- **[Decision]**: [Rationale]
## Next Steps
1. [What should happen next]
## Critical Context
- [Data needed to continue]
<read-files>
path/to/file1.ts
path/to/file2.ts
</read-files>
<modified-files>
path/to/changed.ts
</modified-files>
Before the LLM sees the messages, serializeConversation() flattens them to text:
[User]: What they said
[Assistant thinking]: Internal reasoning
[Assistant]: Response text
[Assistant tool calls]: read(path="foo.ts"); edit(path="bar.ts", ...)
[Tool result]: Output from tool
This prevents the model from treating the summary prompt as a conversation to continue. Tool results are truncated to 2000 characters during serialization; longer content is replaced with a marker showing how many characters were dropped. Truncation matters in practice: tool results (especially from read and bash) are usually the largest contributor to context size, and capping them keeps the summarization request itself within budget.
The CompactionEntry shape
Defined in session-manager.ts:
interface CompactionEntry<T = unknown> {
type: "compaction";
id: string;
parentId: string;
timestamp: number;
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
usage?: Usage; // LLM usage that generated the summary
fromHook?: boolean; // true if provided by extension (legacy field name)
details?: T; // implementation-specific data
}
// Default compaction uses this for details:
interface CompactionDetails {
readFiles: string[];
modifiedFiles: string[];
}
details is open — extensions store any JSON-serializable payload they need there. The default implementation tracks file operations cumulatively across compactions: when generating the next summary, pi extracts file ops from the messages being summarized and merges them with the previous compaction’s details.readFiles / details.modifiedFiles. Both built-in and extension-provided summaries store the LLM usage so the session’s running totals include the summarization cost.
Branch summarization on /tree
/tree opens the session tree navigator. When you navigate to a different branch, pi offers to summarize the abandoned side; if you accept, it:
- Finds the deepest common ancestor between the old leaf and the target.
- Walks back from the old leaf to that ancestor.
- Prepares the entries with a token budget (newest first).
- Calls the LLM with the same structured format.
- Appends a
BranchSummaryEntryto the new leaf — a singleBranchSummaryEntryper navigation, not per branch.
The branch summary lives at the navigation point so the new leaf has the abandoned work’s context without needing to walk back through the tree:
Tree before navigation:
┌─ B ─ C ─ D (old leaf, being abandoned)
A ───┤
└─ E ─ F (target)
After navigation with summary:
┌─ B ─ C ─ D
A ───┤
└─ E ─ F ─ [summary of B,C,D] (new leaf)
BranchSummaryEntry mirrors CompactionEntry but tracks fromId (the entry we navigated from) instead of firstKeptEntryId, and carries the same details shape (readFiles / modifiedFiles) so the cumulative file tracking works the same way across nested branch summaries.
Custom summarization via extensions
Extensions can intercept either pass. The two events are independent, and each fires before the corresponding action.
session_before_compact
Fires before auto-compaction or /compact. The event exposes the cut-point preparation plus the reason for the compaction:
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// preparation.messagesToSummarize - messages that will be summarized
// preparation.turnPrefixMessages - split-turn prefix (if isSplitTurn)
// preparation.previousSummary - previous compaction's summary text
// preparation.fileOps - extracted file operations
// preparation.tokensBefore - context tokens before compaction
// preparation.firstKeptEntryId - where kept messages start
// preparation.settings - the compaction settings block
// branchEntries - all entries on the current branch (for custom state)
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction
// signal - AbortSignal; pass it to LLM calls
return { cancel: true };
// or provide your own summary:
// return {
// compaction: {
// summary: "Your summary...",
// firstKeptEntryId: preparation.firstKeptEntryId,
// tokensBefore: preparation.tokensBefore,
// usage, // optional; included in session totals
// details: { /* custom data */ },
// },
// };
});
To use a different model for the summary, convert the prepared messages to text first:
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
const conversationText = serializeConversation(
convertToLlm(preparation.messagesToSummarize),
);
const { summary, usage } = await myModel.summarize(conversationText);
return {
compaction: { summary, firstKeptEntryId: preparation.firstKeptEntryId, tokensBefore: preparation.tokensBefore, usage },
};
See examples/extensions/custom-compaction.ts for a complete working example using a different model.
session_before_tree
Fires before /tree navigation, regardless of whether the user opts in to a summary:
pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
// preparation.targetId - where we're navigating to
// preparation.oldLeafId - current position (being abandoned)
// preparation.commonAncestorId - shared ancestor
// preparation.entriesToSummarize - entries that would be summarized
// preparation.userWantsSummary - whether the user opted in
// Cancel navigation entirely:
return { cancel: true };
// Or provide a custom summary (only used if userWantsSummary):
if (preparation.userWantsSummary) {
return {
summary: {
summary: "Your summary...",
usage, // optional
details: { /* custom data */ },
},
};
}
});
Settings
Configure compaction in ~/.pi/agent/settings.json or <project-dir>/.pi/settings.json:
{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
}
}
| Setting | Default | Description |
|---|---|---|
enabled |
true |
Master switch for auto-compaction |
reserveTokens |
16384 |
Tokens reserved for the LLM response (the auto-trigger headroom) |
keepRecentTokens |
20000 |
Tokens kept verbatim on the new-request side after a compaction |
Disable auto-compaction with "enabled": false; manual /compact still works. There is no separate branchSummary.* block in the current settings schema — the branch summary uses the same compaction settings, and any future per-branch overrides would land there.
A useful tuning rule of thumb: if your tasks often explode past the model window mid-session, raise keepRecentTokens so the agent keeps more raw history and the summary covers less. If summaries are losing details that matter, raise reserveTokens so compaction fires earlier (less per-pass context means the LLM has more room to summarize thoroughly). If you want predictable cost, lower reserveTokens so compaction fires just-in-time and you can size session budgets off the summary + kept window.
Compaction and prompt cache
Compaction and branch-summary requests use fresh routing session IDs and, where the provider supports it, disable prompt-cache writes for the summarization call. The reason is that these are one-off prompts — they’re not part of any user-visible conversation trajectory, so caching them wastes cache slots without a hit rate to recover the cost. If your extension uses session_before_compact and calls a different model, the same rule applies: pass a fresh session id and consider turning prompt-cache writes off for the summarization request.
Further reading
- Compaction — the canonical upstream doc this guide is built from
- Sessions and history — session entry types and storage; pairs with compaction for the session lifecycle
- Extensions — the event surface, including
session_before_compactandsession_before_tree - Settings —
compaction.*block location and overrides packages/coding-agent/src/core/compaction/compaction.ts— the implementation