MusePi

Compaction and Branch Summaries

English | 中文 Compaction and branch summaries are the two mechanisms that keep long sessions usable without losing prior work context.

Both are persisted as session entries and converted back into user-context messages when rebuilding LLM input.

Key implementation files

Session entry model

Compaction and branch summaries are first-class session entries, not plain assistant/user messages.

When context is rebuilt (buildSessionContext):

  1. Latest compaction on the active path is converted to one compactionSummary message.
  2. Kept entries from firstKeptEntryId to the compaction point are re-included.
  3. Later entries on the path are appended.
  4. branch_summary entries are converted to branchSummary messages.
  5. custom_message entries are converted to custom messages.

Those custom roles are then transformed into LLM-facing messages in convertToLlm(): compactionSummary and branchSummary become user messages rendered through the static templates

while custom messages pass through as developer messages with their raw content (no template).

Compaction pipeline

Triggers

Compaction/context maintenance can run in six ways:

  1. Manual context compaction: /compact [instructions] calls AgentSession.compact(...).
  2. Automatic overflow recovery: after a same-model assistant error that matches context overflow.
  3. Automatic incomplete-output recovery: after a same-model assistant message ends with stopReason === "length" (OpenAI/Codex response.incomplete).
  4. Automatic threshold maintenance: after a successful turn when context exceeds the resolved threshold.
  5. Mid-turn threshold maintenance: before the next provider request when a tool-loop turn crosses the threshold and compaction.midTurnEnabled !== false.
  6. Idle maintenance: runIdleCompaction() can invoke the same auto-maintenance path with reason "idle".

Compaction shape (visual)

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

Overflow/incomplete recovery vs threshold/idle maintenance

The automatic paths are intentionally different:

Snapcompact strategy

compaction.strategy: "snapcompact" replaces the LLM summarization call with a local, deterministic archival pass (compact from @musepi/snapcompact):

Display transcript

Compaction no longer visually restarts the conversation. The TUI renders the display transcript (buildSessionContext({ transcript: true }) / AgentSession.buildTranscriptSessionContext()): every path entry in chronological order, with each compaction shown inline as a slim divider — ── 📷 compacted · ctrl+o ── — at the point it fired. Expanding (ctrl+o) reveals the summary. Only the LLM context resets at the compaction boundary; the scrollback above the divider stays intact, including across session resume.

Pre-compaction pruning

Before compaction checks, tool-result pruning may run (pruneToolOutputs).

Default prune policy:

Pruned tool results are replaced with:

If pruning changes entries, session storage is rewritten and agent message state is refreshed before compaction decisions.

Useless-result elision

Tools can flag a finished result as contextually useless — a search with zero matches, a hub wait that timed out with everything still running, an empty hub inbox drain. The flag originates on the tool result (AgentToolResult.useless, set via ToolResultBuilder.useless() or directly on the returned object), is copied by the agent loop onto the persisted ToolResultMessage (never together with isError — errors always win), and is consumed in three places:

The flag never reaches provider wire formats, and flagged pairs are never removed from history (only blanked in place), so tool-call/result pairing and provider-native history replay stay intact.

Boundary and cut-point logic

prepareCompaction() only considers entries since the last compaction entry (if any).

  1. Find previous compaction index.
  2. Compute boundaryStart = prevCompactionIndex + 1.
  3. Adapt keepRecentTokens using measured usage ratio when available.
  4. Run findCutPoint() over the boundary window.

Valid cut points include:

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.

Split-turn handling

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:

Split-turn compaction generates two summaries:

  1. History summary (messagesToSummarize)
  2. Turn-prefix summary (turnPrefixMessages)

Final stored summary is merged as:

<history summary>

---

**Turn Context (split turn):**

<turn prefix summary>

Summary generation

compact(...) builds summaries from serialized conversation text:

  1. Convert messages via convertToLlm().
  2. Serialize with serializeConversation().
  3. Wrap in <conversation>...</conversation>.
  4. Optionally include <previous-summary>...</previous-summary>.
  5. Optionally inject extension hook context and active memory-backend compaction context as <additional-context> entries.
  6. Execute summarization prompt with SUMMARIZATION_SYSTEM_PROMPT.

Prompt selection:

Remote summarization modes:

Handoff generation

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.

File-operation context in summaries

Compaction tracks cumulative file activity using assistant tool calls:

Cumulative behavior:

The file list is a grouped, prefix-folded directory tree (find-tool shape) with a per-file access marker — (Read) for read-only files, (Write) for modified files never read, (RW) for modified files also present in the cumulative read set. Capped at 20 files with an […N files elided…] line. LLM-summary strategies append it as a <files> tag (via upsertFileOperations); snapcompact renders it inside its summary template as a FILES section instead.

<files>
# packages/agent/src/compaction/
compaction.ts (Read)
utils.ts (RW)
## prompts/
file-operations.md (Write)
</files>

Legacy <read-files>/<modified-files> tags from summaries written by earlier versions are stripped (alongside <files>) before re-appending, so old summaries self-heal on the next compaction.

Persist and reload

After summary generation (or hook-provided summary), agent session:

  1. Appends CompactionEntry with appendCompaction(...) for context-full maintenance; handoff strategy creates a new session and injects a handoff custom_message instead.
  2. Rebuilds display context from the active leaf via buildDisplaySessionContext().
  3. Replaces live agent messages with rebuilt context.
  4. Synchronizes active todo phases from the rebuilt branch and closes provider sessions whose history was rewritten.
  5. Emits session_compact hook event.

Branch summarization pipeline

Branch summarization is tied to tree navigation, not token overflow.

Trigger

During navigateTree(...):

  1. Compute abandoned entries from old leaf to common ancestor using collectEntriesForBranchSummary(...).
  2. If caller requested summary (options.summarize), generate summary before switching leaf.
  3. If summary exists, attach it at the navigation target using branchWithSummary(...).

Operationally this is commonly driven by /tree flow when branchSummary.enabled is enabled.

Branch switch shape (visual)

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)

Preparation and token budget

generateBranchSummary(...) computes budget as:

prepareBranchEntries(...) then:

  1. First pass: collect cumulative file ops from all summarized entries, including prior pi-generated branch_summary details.
  2. Second pass: walk newest → oldest, adding messages until token budget is reached.
  3. Prefer preserving recent context.
  4. May still include large summary entries near budget edge for continuity.

Compaction entries are included as messages (compactionSummary) during branch summarization input.

Summary generation and persistence

Branch summarization:

  1. Converts and serializes selected messages.
  2. Wraps in <conversation>.
  3. Uses custom instructions if supplied, otherwise branch-summary.md.
  4. Calls summarization model with SUMMARIZATION_SYSTEM_PROMPT.
  5. Prepends branch-summary-preamble.md.
  6. Appends file-operation tags.

Result is stored as BranchSummaryEntry with optional details (readFiles, modifiedFiles).

Extension and hook touchpoints

session_before_compact

Pre-compaction hook.

Can:

session.compacting

Prompt/context customization hook for default compaction.

Can return:

session_compact

Post-compaction notification with saved compactionEntry and fromExtension flag.

session_before_tree

Runs on tree navigation before default branch summary generation.

Can:

session_tree

Post-navigation event exposing new/old leaf and optional summary entry.

Runtime behavior and failure semantics

Settings and defaults

From settings-schema.ts:

These values are consumed at runtime by AgentSession and compaction/branch summarization modules.