Compaction & Branch Summarization

August 16, 2026 · View on GitHub

LLMs have limited context windows. When conversations grow too long, omk uses compaction to summarize older content while preserving recent work. This page covers both auto-compaction and branch summarization.

Source files (omk-mono):

For TypeScript definitions in your project, inspect node_modules/open-multi-agent-kit/dist/.

Overview

OMK has two summarization mechanisms:

MechanismTriggerPurpose
CompactionContext exceeds threshold, or /compactSummarize old messages to free up context
Branch summarization/tree navigationPreserve context when switching branches

Both use the same structured summary format and track file operations cumulatively.

Context Reduction and Prompt Caching

OMK uses three separate layers; their token counts must not be conflated:

  1. Context Budget V2 selects, compresses, points to, or omits loaded context files and skill descriptions before a provider request. Its plan and representation caches avoid repeated local work. Exact representations are content-addressed across queries and budget sizes; query-dependent materialized summaries remain isolated.
  2. Provider prompt caching discounts or reuses an unchanged request prefix. Provider usage is recorded separately as cacheRead and cacheWrite; these are observed provider values, not synthetic compaction savings.
  3. Compaction replaces old conversation turns with a durable summary after context usage crosses the configured threshold.

The system-prompt builder records a stable cache boundary immediately after OMK's base instructions, operator append, and runtime trust boundary. Loaded context files, prompt-selected skills, active-skill bodies, date, and working directory remain in the dynamic suffix. Provider behavior is then:

  • Anthropic Messages, when prompt caching is enabled, sends the stable prefix and dynamic suffix as separate system text blocks and puts cache_control only on the stable block. Tool definitions are canonicalized and sorted, with a cache marker on the final deterministic tool.
  • OpenAI-family transports (Responses, supported Chat Completions, Codex Responses, and Azure Responses) keep the complete prompt text but derive prompt_cache_key affinity from the stable prefix plus canonical tool schemas when their cache settings allow it. Session IDs remain available for request/session affinity. Direct omk-ai callers without boundary metadata retain session-derived cache-key behavior where supported.
  • If an extension replaces the built system prompt, OMK sets an explicit boundary bypass unless the replacement is byte-identical. Anthropic omits the system-prefix cache marker and OpenAI-family requests omit content/session-derived cache affinity for that turn, preventing a dynamic or extension-controlled replacement from being treated as stable content.

/session reports provider cache-read/cache-write tokens, provider cache-hit rate (cacheRead / (input + cacheRead + cacheWrite)), stable-prefix size, key changes, boundary bypasses, and the last local break reason. These diagnostics explain local cache-affinity changes; only provider-returned usage proves an actual cache hit.

This follows the stable-prefix/dynamic-suffix pattern used by OpenClaw's pinned system-prompt-cache-boundary.ts, while carrying the boundary as typed request metadata instead of an in-band marker. OpenClaw's prompt-cache-observability.ts and live-cache-regression-runner.ts are the reference patterns for digest and live-regression diagnostics. OMK unit tests verify payload shape and key stability; no live cache-hit claim is made without provider evidence.

Compaction

When It Triggers

Auto-compaction triggers when:

contextTokens > triggerTokens

where triggerTokens is the smaller of:

  • floor(contextWindow × maxUsageRatio) (default maxUsageRatio = 0.9)
  • contextWindow - reservedBudget (the effective reserve budget)

The reserved budget is computed from:

reservedBudget = reservedOutputTokens + reservedToolResultTokens + safetyMarginTokens + imageReserveTokens + systemPromptTokens

If the reserved budget exceeds the context window, the reserve boundary is ignored and only the usage-ratio boundary applies. All numeric token reserves must be non-negative safe integers; ratios must be finite and in (0, 1]. Invalid values fail session creation instead of silently weakening the policy.

SettingDefaultDescription
reserveTokens16384Legacy/default output reserve; used as reservedOutputTokens when that value is not set
reservedOutputTokensreserveTokensTokens reserved for the LLM response
reservedToolResultTokens0Tokens reserved for pending tool results
safetyMarginTokens0Extra safety margin added to the reserve
imageReserveTokens0Tokens reserved for image content
maxUsageRatio0.9Normal trigger ratio (fraction of context window)
rearmRatio0.75 × maxUsageRatioRatio below which a triggered compaction can rearm
emergencyRatio0.98Emergency compaction ratio

You can also trigger manually with /compact [instructions], where optional instructions focus the summary. If a run is active, manual compaction waits for abort-driven terminal events, including tool results, to persist before capturing the transcript.

Overflow Recovery

If a provider rejects a request for context overflow despite OMK's projection, OMK removes the rejected assistant message from retry context, compacts, and retries automatically. Recovery is bounded and staged:

  1. The first recovery uses the configured compaction budgets.
  2. If that retry also overflows, OMK recompacts from the previous kept boundary with reserveTokens, reservedOutputTokens, and keepRecentTokens capped at 4096, then retries once more.
  3. A third overflow stops recovery and reports an actionable error instead of looping.

Compaction cannot shrink a latest user message that alone exceeds the provider's effective context window; split that input or select a model with a larger effective window.

Model Selection

By default, compaction uses the active session model. Set compaction.model to an authenticated canonical provider/model reference when summaries should use a different model. For example, zai/glm-5.2 keeps an interactive Claude session while using GLM only for auto-compaction and /compact.

Summarization Requests

Every compaction and branch-summary LLM call flows through one choke point (completeSummarization):

  • Retries: transient stream drops (terminated, socket close, 5xx, DNS/transport errors) follow the configured retry settings (enabled, maxRetries, baseDelayMs) with exponential backoff instead of failing the whole operation on the first attempt. Quota/billing errors fail fast, and aborts are never retried. Retry progress is emitted as summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished session events (surfaced in the TUI and RPC stream).
  • Isolation: each summarization request runs with prompt caching disabled (cacheRetention: "none") and a fresh routing sessionId, so summaries never write unusable provider cache entries or inherit interactive session affinity.

How It Works

  1. Find cut point: Walk backwards from newest message, accumulating token estimates until keepRecentTokens (default 20k, configurable in ~/.omk/agent/settings.json or <project-dir>/.omk/settings.json) is reached
  2. Extract messages: Collect messages from the previous kept boundary (or session start) up to the cut point
  3. Generate summary: Call LLM to summarize with structured format, passing the previous summary as iterative context when present
  4. Sanitize and append: Deterministically redact sensitive values, then save the CompactionEntry with its summary and firstKeptEntryId. Exact [REDACTED] assignment placeholders are valid; appended data and unredacted credential-shaped literals remain rejected.
  5. Reload: Session reloads, using summary + messages from firstKeptEntryId onwards
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

On repeated compactions, the summarized span starts at the previous compaction's kept boundary (firstKeptEntryId), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. OMK 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.

Split Turns

A "turn" starts with a user message and includes all assistant responses and tool calls until the next user message. Normally, compaction cuts at turn boundaries.

When a single turn exceeds keepRecentTokens, the cut point lands mid-turn at an assistant message. This is a "split turn":

Split turn (one huge turn exceeds budget):

  entry:  0     1     2      3     4      5      6     7      8
        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐
        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │
        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘
                ↑                                     ↑
         turnStartIndex = 1                  firstKeptEntryId = 7
                │                                     │
                └──── turnPrefixMessages (1-6) ───────┘
                                                      └── kept (7-8)

  isSplitTurn = true
  messagesToSummarize = []  (no complete turns before)
  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]

For split turns, omk generates two summaries and merges them:

  1. History summary: Previous context (if any)
  2. Turn prefix summary: The early part of the split turn

Cut Point Rules

Valid cut points are:

  • User messages
  • Assistant messages
  • BashExecution messages
  • Custom messages (custom_message, branch_summary)

Never cut at tool results (they must stay with their tool call).

CompactionEntry Structure

Defined in session-manager.ts:

interface CompactionEntry<T = unknown> {
  type: "compaction";
  id: string;
  parentId: string;
  timestamp: number;
  summary: string;
  firstKeptEntryId: string;
  tokensBefore: number;
  fromHook?: boolean;  // true if provided by extension (legacy field name)
  details?: T;         // implementation-specific data
}

// Default compaction uses this for details (from compaction.ts):
interface CompactionDetails {
  readFiles: string[];
  modifiedFiles: string[];
}

Extensions can store any JSON-serializable data in details. The default compaction tracks file operations, but custom extension implementations can use their own structure.

See prepareCompaction() and compact() for the implementation.

Branch Summarization

When It Triggers

When you use /tree to navigate to a different branch, omk offers to summarize the work you're leaving. This injects context from the left branch into the new branch.

How It Works

  1. Find common ancestor: Deepest node shared by old and new positions
  2. Collect entries: Walk from old leaf back to common ancestor
  3. Prepare with budget: Include messages up to token budget (newest first)
  4. Generate summary: Call LLM with structured format
  5. Append entry: Save BranchSummaryEntry at 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 ─ [summary of B,C,D]
    A ───┤
         └─ E ─ F (new leaf)

Cumulative File Tracking

Both compaction and branch summarization track files cumulatively. When generating a summary, omk extracts file operations from:

  • Tool calls in the messages being summarized
  • Previous compaction or branch summary details (if any)

This means file tracking accumulates across multiple compactions or nested branch summaries, preserving the full history of read and modified files.

BranchSummaryEntry Structure

Defined in session-manager.ts:

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

// Default branch summarization uses this for details (from branch-summarization.ts):
interface BranchSummaryDetails {
  readFiles: string[];
  modifiedFiles: string[];
}

Same as compaction, extensions can store custom data in details.

See collectEntriesForBranchSummary(), prepareBranchEntries(), and generateBranchSummary() for the implementation.

Summary Format

Both compaction and branch summarization use the same structured format:

## 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>

Message Serialization

Before summarization, messages are serialized to text via serializeConversation():

[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 it as a conversation to continue.

Tool results are truncated to 2000 characters during serialization. Content beyond that limit is replaced with a marker indicating how many characters were truncated. This keeps summarization requests within reasonable token budgets, since tool results (especially from read and bash) are typically the largest contributors to context size.

Custom Summarization via Extensions

Extensions can intercept and customize both compaction and branch summarization. See extensions/types.ts for event type definitions.

session_before_compact

Fired before auto-compaction or /compact. Can cancel or provide custom summary. See SessionBeforeCompactEvent and CompactionPreparation in the types file.

omk.on("session_before_compact", async (event, ctx) => {
  const { preparation, branchEntries, customInstructions, signal } = event;

  // preparation.messagesToSummarize - messages to summarize
  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
  // preparation.previousSummary - previous compaction summary
  // preparation.fileOps - extracted file operations
  // preparation.tokensBefore - context tokens before compaction
  // preparation.firstKeptEntryId - where kept messages start
  // preparation.settings - compaction settings

  // branchEntries - all entries on current branch (for custom state)
  // signal - AbortSignal (pass to LLM calls)

  // Cancel:
  return { cancel: true };

  // Custom summary:
  return {
    compaction: {
      summary: "Your summary...",
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      details: { /* custom data */ },
    }
  };
});

Converting Messages to Text

To generate a summary with your own model, convert messages to text using serializeConversation:

import { convertToLlm, serializeConversation } from "open-multi-agent-kit";

omk.on("session_before_compact", async (event, ctx) => {
  const { preparation } = event;
  
  // Convert AgentMessage[] to Message[], then serialize to text
  const conversationText = serializeConversation(
    convertToLlm(preparation.messagesToSummarize)
  );
  // Returns:
  // [User]: message text
  // [Assistant thinking]: thinking content
  // [Assistant]: response text
  // [Assistant tool calls]: read(path="..."); bash(command="...")
  // [Tool result]: output text

  // Now send to your model for summarization
  const summary = await myModel.summarize(conversationText);
  
  return {
    compaction: {
      summary,
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
    }
  };
});

See custom-compaction.ts for a complete example using a different model.

session_before_tree

Fired before /tree navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.

omk.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 user chose to summarize

  // Cancel navigation entirely:
  return { cancel: true };

  // Provide custom summary (only used if userWantsSummary is true):
  if (preparation.userWantsSummary) {
    return {
      summary: {
        summary: "Your summary...",
        details: { /* custom data */ },
      }
    };
  }
});

See SessionBeforeTreeEvent and TreePreparation in the types file.

Settings

Configure compaction in ~/.omk/agent/settings.json or <project-dir>/.omk/settings.json:

{
  "compaction": {
    "enabled": true,
    "model": "zai/glm-5.2",
    "reserveTokens": 16384,
    "reservedToolResultTokens": 8192,
    "safetyMarginTokens": 1024,
    "keepRecentTokens": 20000,
    "maxUsageRatio": 0.9,
    "rearmRatio": 0.7,
    "emergencyRatio": 0.98
  }
}
SettingDefaultDescription
enabledtrueEnable auto-compaction
modelsession modelAuthenticated canonical provider/model used only for compaction
reserveTokens16384Legacy/default output reserve
reservedOutputTokensreserveTokensOptional output-only reserve
reservedToolResultTokens0Reserve tokens for pending tool results
safetyMarginTokens0Extra safety margin added to the reserve
imageReserveTokens0Reserve tokens for image content
keepRecentTokens20000Recent tokens to keep (not summarized)
maxUsageRatio0.9Normal trigger ratio (fraction of context window)
rearmRatio0.75 × maxUsageRatioRatio below which a triggered compaction can rearm
emergencyRatio0.98Emergency compaction ratio

Disable auto-compaction with "enabled": false. You can still compact manually with /compact.