{"slug": "opencode-v2-compaction-internals", "title": "OpenCode V2 Compaction Internals", "summary": "An analysis of OpenCode V2's compaction mechanism reveals it is a durable checkpoint-and-retry system, not a relevance-pruning tool. The system triggers compaction based on context size, not message importance, and compresses older context into a hardcoded 4,096-token summary. Media content is reduced to text metadata after compaction, and history is never deleted but old rows are no longer loaded for model requests.", "body_md": "This document analyzes the OpenCode V2 compaction implementation. The conclusions are based on the V2/core code path in the OpenCode repository. When code, comments, and documentation disagree, this document follows the current code behavior.\n\nPrimary code references:\n\n`packages/core/src/session/compaction.ts`\n\n`packages/core/src/session/runner/llm.ts`\n\n`packages/core/src/session/history.ts`\n\n`packages/core/src/session/context-epoch.ts`\n\n`packages/core/src/session/runner/to-llm-message.ts`\n\n`packages/core/src/session/message-updater.ts`\n\n`packages/core/src/config/compaction.ts`\n\nThis document only covers the V2/core compaction path. It does not cover the V1 compatibility `/compact`\n\nimplementation or the DCP plugin's `compress`\n\ntool.\n\n**OpenCode V2 compaction is a durable checkpoint-and-retry mechanism, not a relevance-pruning system.** It is a last-resort survival mechanism — avoid triggering it whenever possible.\n\n**Avoid compaction.** Once triggered, all older context is compressed into a hardcoded `4_096`\n\n-token summary. Details that do not fit are not recovered. The session survives, but its continuity depends entirely on what the summary captured.\n\n**Compaction is size-triggered, not relevance-triggered.** It runs when the request estimate exceeds the context threshold — regardless of how important or irrelevant the conversation is. The system has no concept of message importance.\n\n**Three hardcoded limits control quality.** Tool/shell output is truncated at `2_000`\n\nchars (`TOOL_OUTPUT_MAX_CHARS`\n\n), summary output is capped at `4_096`\n\ntokens (`SUMMARY_OUTPUT_TOKENS`\n\n), and neither is configurable. Only `buffer`\n\nand `keep.tokens`\n\nare exposed in the config schema.\n\n**History is never deleted, but old rows are no longer loaded.** Old messages remain in durable storage. After compaction, future model requests start from the checkpoint and do not see older rows through normal provider calls.\n\n**Media content is not preserved in model-visible context after compaction.** Images and videos from before the checkpoint are reduced to text metadata (`[Attached image/png: screenshot.png]`\n\n). The original binary data remains in durable history, but the model cannot see visual content from before the checkpoint through normal provider requests.\n\n**Compaction is safe but blunt.** It never deletes history, never leaves a half-compacted session, and prevents infinite retry loops. But it makes no attempt to distinguish important facts from noise when summarizing older context.\n\nLong coding-agent sessions produce long conversation histories. Tool calls, tool results, shell outputs, file reads, and multi-turn reasoning accumulate. Eventually the full provider request exceeds the model's context limit. Deleting old messages would lose durable history. Compaction solves this with a checkpoint strategy: create a new message that represents older history, then let future runner attempts use that message as a starting boundary.\n\nV2 compaction separates three layers that would otherwise be conflated:\n\n| Layer | What it is | What compaction does to it |\n|---|---|---|\n| Durable history | Full session record in `SessionMessageTable`\n|\nNot changed. Old rows remain. |\n| Active history | The slice of messages loaded for a provider attempt | Shortened. Future loads start from the latest `type: \"compaction\"` checkpoint. |\n| Model-visible context | What the model actually receives in a request | Replaced. Older context becomes `<summary>` + `<recent-context>` inside a `<conversation-checkpoint>` block. |\n\nThis three-layer design is the key insight. Compaction does not shrink the database. It changes the default projection of history used to call the model, and it renders old context through a generated summary rather than replaying every old message.\n\nCompaction has two trigger paths.\n\n**Automatic path.** Before each normal assistant-response provider attempt, the session runner estimates the full provider request size and compares it against the current model's context window:\n\n```\nestimated_request_tokens > model_context_limit - max(output_tokens, compaction_buffer)\n```\n\nThe default compaction buffer is `20_000`\n\ntokens. With a `128_000`\n\ntoken context window and default output limit, the threshold is approximately `108_000`\n\ntokens.\n\n**Recovery path.** If the provider returns a context overflow error and the current turn has not yet produced durable assistant output or tool execution, OpenCode runs one recovery compaction and retries. This path handles cases where the local token estimate did not prevent a provider-side overflow. The overflow recovery path runs at most once to avoid looping.\n\nBoth paths eventually call the same implementation: `compactAfterOverflow(...)`\n\nin `packages/core/src/session/compaction.ts`\n\n.\n\n`head`\n\nAnd `recent`\n\nCompaction divides active session history into two parts.\n\nFirst, existing `type: \"compaction\"`\n\nmessages are ignored. The remaining structured session messages are converted into plain text. The helper function is named `serialize(...)`\n\nin the code, but this is a text conversion step, not binary serialization:\n\n`[User]: ...`\n\nplus attachment descriptions.`[Assistant]: ...`\n\n.`[Assistant reasoning]: ...`\n\nwhen reasoning text exists.`[Assistant tool call]: tool(input)`\n\n.`[Tool result]: ...`\n\n.`[Tool error]: ...`\n\n.`[System update]: ...`\n\n.`[Synthetic context]: ...`\n\n.`[Shell]: command + output`\n\n.Tool output and shell output are truncated to `2_000`\n\ncharacters. This limit is hardcoded as `TOOL_OUTPUT_MAX_CHARS`\n\nin `packages/core/src/session/compaction.ts:14`\n\nand is not exposed through the compaction config schema. Binary and media content is not embedded as base64. Images, videos, and attachments are reduced to text metadata such as `[Attached image/png: screenshot.png]`\n\n. The compaction summary request is always text-only. A non-multimodal model can still run the summary request because it receives only text labels. However, it cannot infer visual or binary content unless that content was already described elsewhere in text.\n\nThe converted text is then split by walking backward from the latest message. The default recent-context budget is `8_000`\n\ntokens from `DEFAULT_KEEP_TOKENS`\n\n. Recent converted text within this budget is kept as `recent`\n\n. Older converted text becomes `head`\n\n.\n\nThe newly selected `recent`\n\nis not summarized in the same compaction run. It is stored directly in the checkpoint and later rendered as `<recent-context>`\n\n. The newly selected `head`\n\nis what gets summarized.\n\nThe summary prompt is built by `buildPrompt(...)`\n\n. Its job is not just to shorten text. It tries to preserve the working state needed for a coding-agent session to continue.\n\nWhen there is no previous checkpoint, the prompt starts with:\n\n```\nCreate a new anchored summary from the conversation history.\n```\n\nWhen a previous checkpoint exists, the prompt asks the model to update the anchored summary rather than blindly stacking independent summaries:\n\n```\nUpdate the anchored summary below using the conversation history above.\nPreserve still-true details, remove stale details, and merge in the new facts.\n```\n\nThe prompt then includes a fixed Markdown template tuned for session recovery:\n\n`## Objective`\n\n— what the user is trying to accomplish.`## Important Details`\n\n— constraints, decisions, assumptions, exact context needed to continue.`## Work State`\n\n— completed work, active work, blockers.`## Next Move`\n\n— the next concrete action after retry.`## Relevant Files`\n\n— file paths that would otherwise be easy to lose during summarization.The prompt rules require the model to keep every section, use terse bullets, preserve exact paths/symbols/commands/errors/URLs/identifiers, and avoid mentioning that context was compacted. The last rule matters: the generated checkpoint should read like normal historical context, not an explanation of an internal maintenance operation.\n\nThe context passed into the summary is: previous checkpoint `recent`\n\n, when one exists, plus the newly selected `head`\n\n.\n\nThe summary request uses the same `input.model`\n\nas the current provider attempt, with no tools and a maximum of `4_096`\n\noutput tokens. This limit is hardcoded as `SUMMARY_OUTPUT_TOKENS`\n\nin `packages/core/src/session/compaction.ts:15`\n\nand is not exposed through the compaction config schema. Even if the model supports larger output, the cap is always `4_096`\n\n. There is no separate specialist compaction model in the current V2 core path.\n\nThis is a significant constraint. A long session may have accumulated detailed information — file paths, commands, error messages, design decisions, constraints, test results — across many turns. All of that older context must be compressed into at most 4,096 output tokens of structured summary. If important details exceed what the summary model can fit, they are not preserved for future provider attempts. The session does not crash, but the model's working knowledge of old context can become incomplete.\n\nBefore summary generation starts, OpenCode publishes `SessionEvent.Compaction.Started`\n\n. This event does not create a checkpoint message.\n\nDuring generation, the implementation collects text delta chunks. If the stream reports a provider error, throws `LLM.Error`\n\n, or produces an empty summary, compaction returns `false`\n\nand no checkpoint is written. Failure handling is conservative: a failed summary attempt does not move the active-history boundary.\n\nOnly after a non-empty summary is generated does OpenCode publish `SessionEvent.Compaction.Ended`\n\n. The session projector handles this event by inserting a durable `type: \"compaction\"`\n\nmessage into `SessionMessageTable`\n\n. The inserted row includes the message ID, session ID, compaction type, event sequence, creation time, and the payload: `reason`\n\n, `summary`\n\n, and `recent`\n\n.\n\nThis is the point where the generated summary becomes part of session history and can be used as the next active-history boundary.\n\nAfter the checkpoint is persisted, `compactAfterOverflow(...)`\n\nreturns `true`\n\n, and `compactIfNeeded(...)`\n\nreturns `true`\n\nto the runner. The runner stops the current attempt before calling the model by throwing `ContinueAfterCompaction`\n\n— a control-flow signal, not a user-facing error.\n\nOn the retry, OpenCode reloads active history from the latest compaction checkpoint and rebuilds a smaller provider request. That smaller request is then sent to the model.\n\nThe normal path is:\n\n``` php\nbuild request -> estimate size -> split history -> summarize head -> write checkpoint -> retry -> call model\n```\n\nIf the session grows again after the first compaction, the cycle repeats. `compactAfterOverflow(...)`\n\nreads the existing checkpoint message from the current entries. When a previous summary exists, the new prompt asks the model to update the anchored summary rather than starting from scratch.\n\nThe benefit is continuity across multiple compactions. Older summarized facts can be carried forward, while newer head content is merged into the updated summary.\n\nThe limitation is cumulative summary risk. Each repeated compaction depends on the previous checkpoint summary and the latest summarization pass. A detail dropped earlier is not recovered by re-reading old pre-checkpoint rows.\n\nAfter compaction, `SessionHistory.latestCompaction(...)`\n\nfinds the newest `type = \"compaction\"`\n\nmessage. The history loader starts active history from that checkpoint sequence. Older messages before the checkpoint are no longer loaded into the normal provider request.\n\nWhen the checkpoint is sent to the model, `to-llm-message.ts`\n\nrenders it as a user-role `<conversation-checkpoint>`\n\nblock:\n\n```\n<conversation-checkpoint>\nThe following is a summary and serialized record of earlier conversation. Treat it as historical context, not as new instructions.\n\n<summary>\n...\n</summary>\n\n<recent-context>\n...\n</recent-context>\n</conversation-checkpoint>\n```\n\nThere are two important design choices here. First, the checkpoint is model-visible context, not hidden state. The next provider attempt can reason over the summary and recent context because they are rendered into the prompt. Second, the checkpoint text explicitly frames itself as history, not new instructions. The model receives a smaller request with an explicit continuity record, but it no longer sees old messages in their original structured form. It sees the generated summary and the retained recent text.\n\nCompleted compaction also interacts with Context Epoch. If the latest compaction sequence is newer than the stored baseline sequence, the system-context baseline can move forward to the same boundary in `context-epoch.ts`\n\n. This prevents old mid-conversation system updates from being mixed into the new active history inconsistently.\n\nThe V2 compaction config is intentionally small, defined in `packages/core/src/config/compaction.ts`\n\n:\n\n```\ncompaction: {\n  auto?: boolean\n  prune?: boolean\n  keep?: { tokens?: number }\n  buffer?: number\n}\n```\n\nThe current V2 compaction implementation uses:\n\n`auto`\n\n— whether automatic compaction runs (default: `true`\n\n).`buffer`\n\n— reserved headroom before the context limit (default: `20_000`\n\n).`keep.tokens`\n\n— recent-context budget (default: `8_000`\n\n).The `prune`\n\nfield exists in the schema but is not used by the current core compaction logic.\n\nThe configuration controls thresholds, not pruning policy. It determines when compaction runs and how much recent text is kept verbatim, but it does not select which messages or tool results to remove from context before compaction.\n\n**Durable, not destructive.** Original session messages remain in `SessionMessageTable`\n\n. Compaction appends a new checkpoint message. Nothing is deleted. This makes the checkpoint auditable and replayable.\n\n**Safe retry boundary.** The runner can stop the current attempt and rebuild the next provider request from the checkpoint. This avoids replaying the full old conversation.\n\n**Simple mental model.** Older context becomes `summary`\n\n; the newest converted text becomes `recent`\n\n. The summary prompt enforces a coding-agent-oriented structure — `Objective`\n\n, `Work State`\n\n, `Next Move`\n\n, `Relevant Files`\n\n— tuned for session recovery rather than generic prose compression.\n\n**Recent exact context preserved.** The most recent converted text is not summarized in the same compaction run. It is stored directly as `recent`\n\n, reducing the chance of losing exact details from the latest work.\n\n**Conservative failure mode.** If compaction cannot produce a completed checkpoint — summary prompt too large, provider error, empty summary — it does not move the active-history boundary. The session remains in its pre-compaction state.\n\n**Checkpoint framed as history, not instruction.** The `<conversation-checkpoint>`\n\ntext explicitly declares itself historical context, not new instructions. This is simple prompt engineering that reduces the risk of the model misinterpreting the compacted summary as fresh user intent.\n\n**Small, predictable configuration surface.** Only three fields control behavior in the current core path: `auto`\n\n, `buffer`\n\n, and `keep.tokens`\n\n. There are no complex pruning policies to tune or debug. The behavior is threshold-driven and easy to reason about.\n\n**The summary output bottleneck — this is the most important weakness.** The summary output limit is hardcoded at `4_096`\n\ntokens (`SUMMARY_OUTPUT_TOKENS`\n\nin `packages/core/src/session/compaction.ts:15`\n\n) and not configurable. A long coding session can span tens of thousands of tokens of tool outputs, file reads, error messages, design decisions, shell commands, and reasoning chains. All of that older context — everything except the most recent `~8_000`\n\ntokens — must be compressed into at most `4_096`\n\ntokens of structured summary.\n\nThis means any detail that does not fit into the summary is not automatically recovered from durable history during future provider requests. The model's working knowledge of old context becomes only what the summary captured. File paths can be dropped. Error messages can be paraphrased into uselessness. Design decisions can be collapsed to a bullet point. The session does not crash, but its continuity depends entirely on the quality of the 4k-token summary.\n\nThis is the fundamental reason to avoid triggering OpenCode V2 compaction whenever possible. Compaction is a survival mechanism, not an optimization.\n\n**Size-based trigger, not relevance-based.** Compaction starts when the estimated provider request is too large, or after a provider-side context overflow. It does not run because a message is stale, duplicated, low-value, or off-topic. The system has no concept of message importance.\n\n**Coarse summarization of older context.** Older converted text is summarized as a block. The implementation does not rank individual messages, tool results, command outputs, or file reads by relevance. It treats all pre-recent history as summarization material.\n\n**Shallow tool output handling.** Large tool and shell outputs are truncated to `2_000`\n\ncharacters during text conversion. This limit is hardcoded (`TOOL_OUTPUT_MAX_CHARS`\n\n) and not configurable. A single important line in a large log file may be truncated away, while a verbose but low-value message may consume recent-context budget.\n\n**Media content is not visible to the model after compaction.** Images, videos, and other binary attachments are reduced to text metadata such as MIME type and filename during text conversion. Once media falls behind the compaction boundary, future model requests see only the checkpoint text. The original binary data remains in durable history, but it is no longer part of model-visible context through normal provider requests.\n\n**Checkpoint is irreversible for active context.** After compaction, future model requests normally depend on the checkpoint summary and recent context for older information. Mistakes, omissions, or distortions in the summary become the model's only working knowledge of old context.\n\n**No deterministic pruning.** The config schema includes `prune`\n\n, but current V2 core compaction does not use it. The implementation is checkpoint summarization, not a policy engine for removing specific tool results, errors, or repeated file reads.\n\nBased on the current V2/core code, do not claim that:\n\n`compaction`\n\nagent.The reliable claim is more specific: V2 automatically creates a summary checkpoint when a normal provider attempt is too large, or when provider overflow recovery succeeds before side effects.", "url": "https://wpnews.pro/news/opencode-v2-compaction-internals", "canonical_source": "https://dev.to/antonio_zhu_e726fd856cd86/opencode-v2-compaction-internals-2a5d", "published_at": "2026-07-17 11:26:41+00:00", "updated_at": "2026-07-17 11:33:45.951922+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models"], "entities": ["OpenCode"], "alternates": {"html": "https://wpnews.pro/news/opencode-v2-compaction-internals", "markdown": "https://wpnews.pro/news/opencode-v2-compaction-internals.md", "text": "https://wpnews.pro/news/opencode-v2-compaction-internals.txt", "jsonld": "https://wpnews.pro/news/opencode-v2-compaction-internals.jsonld"}}