Compaction and branch summaries are the two mechanisms that keep long sessions usable without losing prior work context.
- Compaction rewrites old history into a summary on the current branch.
- Branch summary captures abandoned branch context during
/treenavigation.
Both are persisted as session entries and converted back into user-context messages when rebuilding LLM input.
packages/agent/src/compaction/compaction.ts(context-full summarization and handoff generation)packages/agent/src/compaction/branch-summarization.tspackages/agent/src/compaction/pruning.tspackages/agent/src/compaction/utils.tspackages/agent/src/compaction/openai.tspackages/coding-agent/src/session/session-manager.tspackages/coding-agent/src/session/agent-session.tspackages/coding-agent/src/session/messages.tspackages/coding-agent/src/extensibility/hooks/types.tspackages/coding-agent/src/config/settings-schema.ts
Compaction and branch summaries are first-class session entries, not plain assistant/user messages.
CompactionEntrytype: "compaction"summary, optionalshortSummaryfirstKeptEntryId(compaction boundary)tokensBefore- optional
details,preserveData,fromExtension
BranchSummaryEntrytype: "branch_summary"fromId,summary- optional
details,fromExtension
When context is rebuilt (buildSessionContext):
- Latest compaction on the active path is converted to one
compactionSummarymessage. - Kept entries from
firstKeptEntryIdto the compaction point are re-included. - Later entries on the path are appended.
branch_summaryentries are converted tobranchSummarymessages.custom_messageentries are converted tocustommessages.
Those custom roles are then transformed into LLM-facing user messages in convertToLlm() using the static templates:
packages/agent/src/compaction/prompts/compaction-summary-context.mdpackages/agent/src/compaction/prompts/branch-summary-context.mdpackages/agent/src/compaction/prompts/handoff-document.md
Compaction/context maintenance can run in four ways:
- Manual context compaction:
/compact [instructions]callsAgentSession.compact(...). - Automatic overflow recovery: after a same-model assistant error that matches context overflow.
- Automatic threshold maintenance: after a successful turn when context exceeds the resolved threshold.
- Idle maintenance:
runIdleCompaction()can invoke the same auto-maintenance path with reason"idle".
Before compaction:
entry: 0 1 2 3 4 5 6 7 8 9
┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┐
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┘
└────────┬───────┘ └──────────────┬──────────────┘
messagesToSummarize kept messages
↑
firstKeptEntryId (entry 4)
After compaction (new entry appended):
entry: 0 1 2 3 4 5 6 7 8 9 10
┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┬─────┐
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │ cmp │
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┴─────┘
└──────────┬──────┘ └──────────────────────┬───────────────────┘
not sent to LLM sent to LLM
↑
starts from firstKeptEntryId
What the LLM sees:
┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐
│ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │
└────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘
↑ ↑ └─────────────────┬────────────────┘
prompt from cmp messages from firstKeptEntryId
The automatic paths are intentionally different:
-
Overflow recovery
- Trigger: current-model assistant error is detected as context overflow and the error is not older than the latest compaction.
- The failing assistant error message is removed from active agent state before retry.
- Context promotion is tried first; if a configured larger model is available, the agent switches model and retries without compacting.
- If promotion is unavailable and compaction is enabled, context-full compaction runs with
reason: "overflow"andwillRetry: true; handoff strategy is not used for overflow. - On success, agent auto-continues (
agent.continue()) after compaction.
-
Threshold maintenance
- Trigger: successful, non-error assistant message whose adjusted context tokens exceed
resolveThresholdTokens(...). - Tool-output pruning can reduce the measured token count before threshold comparison.
- Context promotion is tried before compaction.
- If promotion is unavailable, auto maintenance runs with
reason: "threshold"andwillRetry: false. - With
compaction.strategy: "handoff", threshold maintenance starts a new handoff session instead of writing a compaction entry; if handoff returns no document without aborting, it falls back to context-full compaction. - On success, if
compaction.autoContinue !== false, schedules an agent-authored developer prompt fromprompts/system/auto-continue.md; immediately before that prompt executes, live enabled goal/todo/queue/length/workflow state is re-read and the prompt is skipped if no unfinished work remains.
- Trigger: successful, non-error assistant message whose adjusted context tokens exceed
-
Idle maintenance
- Trigger:
runIdleCompaction()when not streaming or already compacting. - Uses
reason: "idle"and does not auto-continue afterward.
- Trigger:
Before compaction checks, tool-result pruning may run (pruneToolOutputs).
Default prune policy:
- Protect newest
40_000tool-output tokens. - Protect the newest
2real user turns (protectRecentTurns; user or bashExecution boundaries) — nothing in those turns is pruned, including stale-classified entries. - Require at least
20_000total estimated savings. - Never prune tool results from
skillorread(areadresult loses immunity only when a later read provably covers it — exact same-target repeats or explicit bounded ranges that contain the earlier explicit ranges; open-ended,:raw,:conflicts, and multi-range selectors never claim range coverage).
Pruned tool results are replaced with a notice that keeps the highest-signal fields, error-first (exit status, error line, path hint, then tail/counts), under an absolute digest budget:
[Output truncated - N tokens; exit=1; error=...](digest form)[Output truncated - N tokens; full output: artifact://<id>] exit=1; error=...(when the session artifact manager is available, the original output is spilled to a session artifact so pruning is reversible — the agent can re-read the full output viaartifact://<id>instead of re-running the tool)
Pruning also returns the pruned originals (PruneResult.originals) so callers can persist them; AgentSession writes them as <id>.<toolName>.log artifact files and only commits a pruned entry that claims an artifact after its artifact write succeeds.
If pruning changes entries, session storage is rewritten and agent message state is refreshed before compaction decisions.
Auto and manual compaction append best-effort session-state lines to the summarization request's <additional-context> (after extension-provided context): the active goal (objective + status), up to 5 active workflow skills with phases, and up to 10 open todos. This makes work-in-progress state survive compaction deterministically instead of relying on the summarizer inferring it from the transcript.
When compaction.autoContinue is enabled, the post-compaction synthetic continue prompt is only scheduled when there is evidence of unfinished work: a goal whose status is exactly active, pending/in-progress todos, queued messages, the most recent assistant turn stopping on length, or a recognized workflow skill in an active nonterminal phase. Paused goals, terminal phases, explicitly continuation-inert integration phases, and unknown skills/phases do not qualify. Generic Ultragoal blocked remains active because blockers may be autonomously resolvable; a verified human wait is represented by a paused inline goal. When no qualifying evidence remains, continuation is skipped with an info notice, avoiding a full cold-context request after already-completed work.
prepareCompaction() only considers entries since the last compaction entry (if any).
- Find previous compaction index.
- Compute
boundaryStart = prevCompactionIndex + 1. - Adapt
keepRecentTokensusing measured usage ratio when available. - Run
findCutPoint()over the boundary window.
Valid cut points include:
- message entries with roles:
user,assistant,bashExecution,hookMessage,branchSummary,compactionSummary custom_messageentriesbranch_summaryentries
Hard rule: never cut at toolResult.
If there are non-message metadata entries immediately before the cut point (model_change, thinking_level_change, labels, etc.), they are pulled into the kept region by moving cut index backward until a message or compaction boundary is hit.
If cut point is not at a user-turn start, compaction treats it as a split turn.
Turn start detection treats these as user-turn boundaries:
message.role === "user"message.role === "bashExecution"custom_messageentrybranch_summaryentry
Split-turn compaction generates two summaries:
- History summary (
messagesToSummarize) - Turn-prefix summary (
turnPrefixMessages)
Final stored summary is merged as:
<history summary>
---
**Turn Context (split turn):**
<turn prefix summary>compact(...) builds summaries from serialized conversation text:
- Convert messages via
convertToLlm(). - Serialize with
serializeConversation(). - Wrap in
<conversation>...</conversation>. - Optionally include
<previous-summary>...</previous-summary>. - Optionally inject hook context as
<additional-context>list. - Execute summarization prompt with
SUMMARIZATION_SYSTEM_PROMPT.
Prompt selection:
- first compaction:
compaction-summary.md - iterative compaction with prior summary:
compaction-update-summary.md - split-turn second pass:
compaction-turn-prefix.md - short UI summary:
compaction-short-summary.md - handoff document:
handoff-document.md(used bygenerateHandoff(...), not serialized compaction)
Remote summarization modes:
- If
compaction.remoteEndpointis set and remote compaction is enabled, local summary generation POSTs:{ systemPrompt, prompt }
- Expects JSON containing at least
{ summary }. - For OpenAI/OpenAI code provider models, compaction first tries the provider-native
/responses/compactendpoint when remote compaction is enabled. It preserves provider replacement history inpreserveData.openaiRemoteCompactionand falls back to local summarization if that native request fails.
packages/agent/src/compaction/compaction.ts also exports generateHandoff(...). Handoff generation uses the same completeSimple(...) oneshot style as summarization, but it preserves the live agent cache prefix by sending the active system prompt, tool array, and real LLM message history, then appending one agent-attributed user message containing the handoff prompt. It forces toolChoice: "none" and returns joined text blocks directly.
Handoff does not write a CompactionEntry. AgentSession.handoff() owns the session transition: it starts a new session, injects the generated document as a visible custom_message with customType: "handoff", and rebuilds agent messages from that new session.
Compaction tracks cumulative file activity using assistant tool calls:
read(path)→ read setwrite(path)→ modified setedit(path)→ modified set
Cumulative behavior:
- Includes prior compaction details only when prior entry is pi-generated (
fromExtension !== true). - In split turns, includes turn-prefix file ops too.
readFilesexcludes files also modified.
Summary text gets file tags appended via prompt template:
<read-files>
...
</read-files>
<modified-files>
...
</modified-files>After summary generation (or hook-provided summary), agent session:
- Appends
CompactionEntrywithappendCompaction(...)for context-full maintenance; handoff strategy creates a new session and injects a handoffcustom_messageinstead. - Rebuilds display context from the active leaf via
buildDisplaySessionContext(). - Replaces live agent messages with rebuilt context.
- Emits
session_compacthook event.
Branch summarization is tied to tree navigation, not token overflow.
During navigateTree(...):
- Compute abandoned entries from old leaf to common ancestor using
collectEntriesForBranchSummary(...). - If caller requested summary (
options.summarize), generate summary before switching leaf. - If summary exists, attach it at the navigation target using
branchWithSummary(...).
Operationally this is commonly driven by /tree flow when branchSummary.enabled is enabled.
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 ─ [summary of B,C,D]
A ───┤
└─ E ─ F (new leaf)
generateBranchSummary(...) computes budget as:
tokenBudget = model.contextWindow - branchSummary.reserveTokens
prepareBranchEntries(...) then:
- First pass: collect cumulative file ops from all summarized entries, including prior pi-generated
branch_summarydetails. - Second pass: walk newest → oldest, adding messages until token budget is reached.
- Prefer preserving recent context.
- May still include large summary entries near budget edge for continuity.
Compaction entries are included as messages (compactionSummary) during branch summarization input.
Branch summarization:
- Converts and serializes selected messages.
- Wraps in
<conversation>. - Uses custom instructions if supplied, otherwise
branch-summary.md. - Calls summarization model with
SUMMARIZATION_SYSTEM_PROMPT. - Prepends
branch-summary-preamble.md. - Appends file-operation tags.
Result is stored as BranchSummaryEntry with optional details (readFiles, modifiedFiles).
Pre-compaction hook.
Can:
- cancel compaction (
{ cancel: true }) - provide full custom compaction payload (
{ compaction: CompactionResult })
Prompt/context customization hook for default compaction.
Can return:
prompt(override base summary prompt)context(extra context lines injected into<additional-context>)preserveData(stored on compaction entry)
Post-compaction notification with saved compactionEntry and fromExtension flag.
Runs on tree navigation before default branch summary generation.
Can:
- cancel navigation
- provide custom
{ summary: { summary, details } }used when user requested summarization
Post-navigation event exposing new/old leaf and optional summary entry.
- Manual compaction aborts current agent operation first.
abortCompaction()cancels both manual and auto-compaction controllers.- Auto compaction emits start/end session events for UI/state updates.
- Auto compaction can try multiple model candidates and retry transient failures; long retry delays prefer the next candidate when one is available.
- Overflow errors are excluded from generic retry path because they are handled by context promotion/compaction.
- If auto-compaction fails:
- overflow path emits
Context overflow recovery failed: ... - threshold path emits
Auto-compaction failed: ...
- overflow path emits
- Branch summarization can be cancelled via abort signal (e.g., Escape), returning canceled/aborted navigation result.
From settings-schema.ts:
compaction.enabled=truecompaction.strategy="context-full"("handoff"and"off"are also supported)compaction.reserveTokens=16384compaction.keepRecentTokens=20000compaction.autoContinue=true(gated on unfinished work; see above)compaction.remoteEnabled=truecompaction.remoteEndpoint=undefinedcompaction.thresholdPercent=-1andcompaction.thresholdTokens=-1; when no positive override is set, the threshold iscontextWindow - max(15% of contextWindow, reserveTokens)compaction.idleEnabled=false(when enabled, idle maintenance rewrites history with reason"idle"and never auto-continues)branchSummary.enabled=falsebranchSummary.reserveTokens=16384
These values are consumed at runtime by AgentSession and compaction/branch summarization modules.