createrole

14 min read

Multi-turn Context Compression in Codex, Pi and OpenCode: Three Approaches Compared

A source-level comparison of how three open-source agents, Codex, Pi and OpenCode, summarize old history, how much recent verbatim text they keep, and how they recover from overflow. The takeaway is that compression is really the construction of a recoverable state checkpoint, not a summary of a chat log.

  • context compression
  • agent engineering
  • source study

Any agent that runs long enough eventually hits the model's context window. How to shrink the history without interrupting the task is a question every agent runtime has to answer. This post is based on reading the source code of three open-source projects, Codex, Pi and OpenCode, in July 2026. It describes only what the code at that time verifiably did, and leaves out product marketing and older versions.

The shared pattern

None of the three uses "drop the oldest messages" as its main algorithm. They share one skeleton:

  1. Persist the full, or nearly full, raw session.
  2. Detect that the model context is about to overflow, or already has.
  3. Hand the old prefix to a model and ask for a structured summary that work can continue from.
  4. Keep a stretch of recent verbatim text as a high-fidelity tail.
  5. Send later requests only the projection "summary + recent tail + new messages". The raw data stays available for recovery, branching and auditing.

It helps to separate four layers of "context": the persisted history on disk or in a database; the active history projected from the current branch and the latest checkpoint; the request history that actually goes to the provider after filtering unsupported modalities, repairing tool call pairs and truncating large outputs; and the token accounting state, which usually comes from the most recent provider usage. Compression normally replaces only the second and third layers. It does not physically delete the first.

The differences in one table

DimensionCodexPiOpenCode V1OpenCode V2 core
Primary storageLinear rollout + compaction checkpointsTree of entries in the storage backendDatabase messages and partsSequential session entries
Auto thresholdReal server usage or local estimate; measured on total context or on the body after the prefixcontextTokens > contextWindow - reserveTokenstokens >= usableWindowEstimated serialized request exceeds context - max(output, buffer)
Default reserveDecided by model, config and an optional fallback bufferreserveTokens = 16384At most 20k or the max output, whichever is smallerbuffer = 20000
Recent verbatim textLocal path keeps recent user messages up to 20k total; remote v2 keeps up to 64k of message textAbout 20k accumulated backwards from the tail; may cut inside a turnAt most 2 turns by default, with a dynamic budget of 2k to 8kAbout 8k by default; may split a single serialized message by characters
Tool output preprocessingTruncated by policy when written to history; before remote compaction, trailing tool outputs may be replaced with a placeholderEach tool result cut to 2000 characters in the summary inputEach tool output cut to 2000 characters in the summary input; separate background pruneEach tool output cut to 2000 characters when serialized
Recovery after overflowDrops the oldest items one by one if the compact request overflows; remote path has a model fallbackRemoves the failed assistant message, compacts, retries onceCreates a compaction task; may replay the last real user turn, with media downgraded to a text placeholderExplicit compactAfterOverflow
Distinctive featuresFour paths: local, remote, remote v2, token budget; compacts before a turn and mid tool loopSplits a single turn into "prefix summary + verbatim suffix"; supports branch summariesKeeps the most recent whole turns; keeps a message suffix when a turn exceeds the budget; lazy tool result pruningA new standalone core; smaller logic, less expressive

In terms of engineering priorities: Codex cares most about request cache stability, adapting to server capabilities and consistent recovery. Pi cares most about a local tree-shaped session, an explainable cut-point algorithm and splitting very long single-turn tool loops. OpenCode V1 separates the full database history from the projection the model consumes, and uses "recent whole turns" plus tool result pruning as two levels of load shedding. OpenCode V2 core abstracts compression into a more standalone, testable summary + recent.

Codex: compression as a context window lifecycle

Codex uses one formula for its threshold: it triggers when the active context tokens (or the body after subtracting the current window's prefix) reach auto_compact_limit + fallback_buffer, or when the active context reaches the model's hard window. The prefix baseline is the first server usage sample of the current window. Once a server sample exists, it is no longer overwritten by a local estimate.

There are three trigger points. First, before sampling a new turn, Codex checks whether the old active history has already reached the threshold. Second, mid tool loop: after each sampling it checks at once whether the model needs a follow-up, whether pending input arrived during the run, whether tokens reached the threshold, and whether the model asked for a new window through the new_context tool. It compacts immediately only if sampling has to continue, and after compaction it resumes the original tool loop before accepting input that arrived in the meantime. Third, on model switch: it compacts if the two models declare different comp_hash values, or if switching from a larger window to a smaller one leaves the history over the new threshold. It prefers the old model for the summary, since that model can read the full old context.

There are four execution paths. With the TokenBudget feature enabled, Codex starts a new window directly without requesting a summary; it relies on reminders the model received before rollover to write important state into the workspace. Otherwise it checks whether the provider supports remote compaction: if not, it takes the local summary path; if so, it either appends a compaction trigger to a normal request (v2) or calls a dedicated compact endpoint (v1).

The local path deserves a closer look. After a successful summary, the new history is not a single summary item:

new_history = recent real user messages (up to 20k approximate tokens) + [summary, encoded with role user]

Assistant text, reasoning and the old tool transcript do not enter the new history. Their information survives only through the summary. If the summary request itself overflows, Codex removes the oldest item from the start of history (together with its tool call pair, if any) and retries, and only reports failure when a single input still overflows. Remote v1 behaves differently: if the estimated prompt exceeds the window, it scans backwards from the end of history through consecutive rewritable tool outputs, replaces them with placeholder text, and stops at the first item it cannot rewrite. Remote v2 keeps up to 64k tokens of message text, newest first, and appends the encrypted compaction item returned by the server.

Each time a replacement history is installed, Codex persists a checkpoint containing the summary, the full replacement history and a chain of window numbers and IDs, and resets the usage baseline. On resume it treats the latest checkpoint's replacement history as the new base and replays the items after it.

Pi: a projection over a tree-shaped session

Pi's session is a tree of entries linked by id + parentId. Compaction just appends a child node recording the summary, firstKeptEntryId, the token count before compaction and file operation details. Old nodes are not deleted. When building a request, Pi finds the latest compaction node on the current path and returns "summary + old tail starting at firstKeptEntryId + new messages after the node".

Defaults are reserveTokens = 16384 and keepRecentTokens = 20000; compaction triggers when contextTokens exceeds contextWindow - reserveTokens. The token count prefers the usage of the last successful assistant message, then adds ceil(chars / 4) estimates for messages not yet sampled. Images count as a fixed 4800 characters.

The cut-point algorithm accumulates estimated tokens from newest to oldest and, on reaching 20k, picks the first valid cut point. Valid cut points are user, assistant, bash, branch summary and compaction summary entries, but never a tool result, because a tool result must follow its assistant tool call. The tail is therefore "about 20k" rather than strictly at most 20k.

Pi's most distinctive move is the double summary when the cut lands inside a turn. It generates or updates the standard summary for the history before that turn, generates a shorter dedicated summary for the turn prefix from the user message up to the cut point, joins the two as the new summary, and points firstKeptEntryId at the first item of the verbatim suffix inside the turn. This explicitly handles the case where a single turn's tool chain is itself very long.

The standard summary has a fixed shape with six sections: Goal, Constraints, Progress, Key Decisions, Next Steps and Critical Context. When a previous summary exists, an update prompt merges new progress into it, so repeated compactions roll one summary state forward. Pi does not fully trust the summary model to preserve file changes, so it also scans tool calls for read and write paths, deduplicates them and appends <read-files> and <modified-files> lists to the summary. Summary requests use a separate session ID and write no cache; the output limit is min(0.8 × reserveTokens, maxTokens).

For overflow recovery, Pi recognizes overflow errors from several providers, silent overflow where the call succeeds but input + cacheRead exceeds the window, and silent truncation where stopReason = length with zero output. On a real error it keeps the failed assistant message in the session for auditing, removes it from the active context, compacts and retries once; a second failure stops. When switching branches in the tree, Pi also generates a branch summary for the branch being left, which is separate from ordinary compaction.

OpenCode: a summary/tail view over the full database history

The OpenCode repository has two implementations: V1 on the product's main path and a new V2 session core, with different models and config shapes.

In V1 the usable window is the input limit minus a reserve, which defaults to min(20000, maxOutputTokens); when usage reaches the usable window, compaction is flagged. If the provider reports an overflow error directly, it is also turned into a compaction rather than a termination, as long as auto compaction is not disabled.

Recent text selection first limits itself to the last 2 real user turns, then applies a token budget of clamp(usableWindow × 25%, 2000, 8000). It accumulates from the newest turn backwards, keeping whole turns that fit. If a turn does not fit, it searches from that turn's second message for the earliest suffix that fits. If even the newest turn yields no suffix, it drops the verbatim tail and keeps only the summary. Unlike Pi, the cut-off turn prefix goes straight into the summary input with no separate prefix summary.

Compaction does not delete database messages. It records tail_start_id in a synthetic compaction user part. The order projected to the model is "compaction user, summary assistant, old history tail, new messages after the summary", which is deliberately not monotonic in time, so finding the latest message must use the monotonically increasing message ID rather than array position.

When the provider overflows on the current user turn, V1 creates an overflow compaction task. It finds the last real user message, treats it and everything after it as a replay, generates the summary from the earlier history, and after compaction creates a new user message that replays the original content. Media attachments are downgraded to [Attached mime: filename] text.

V1 also has an independent second layer of compression. After each loop iteration, a background pass scans completed tool parts from newest to oldest, skips the last 2 user turns, stops at a summary boundary, protects the most recent 40k estimated tokens of tool output, and marks older outputs as candidates. It writes them as compacted only when the candidates total more than 20k tokens. This avoids frequent database writes and broken prompt caches for small gains.

V2 core serializes messages into plain text with markers such as [User], [Assistant tool call] and [Tool result], and accumulates 8k tokens by default from the newest message backwards. When the boundary message only partly fits, it splits that message at remainingTokens × 4 characters, sending the prefix into the summary and keeping the suffix as recent text. It can therefore cut inside a single serialized message; the algorithm is simple but does not guarantee structural boundaries. Summary output is capped at 4096 tokens, and before the request it checks that the summary prompt itself fits in context - summaryOutput.

Side-by-side comparison

Cut-point fidelity, from strongest to weakest: Pi (never cuts a tool result and summarizes the cut-off turn prefix separately), OpenCode V1 (prefers whole turns, otherwise keeps a message suffix within a turn), Codex local (restores only recent user text; assistant and tool detail depend entirely on the summary), OpenCode V2 core (can cut inside a serialized string). Codex remote v2 is a different trade-off: it keeps up to 64k of user message text, and the encrypted compaction item carries the old assistant and tool state.

Token precision: none of the three counts with a tokenizer end to end. Codex uses provider usage plus byte and modality heuristics; Pi uses the last valid usage plus chars/4 for the tail; OpenCode uses assistant usage for the threshold and round(chars/4) for tail selection. The reason is that tokenizers are tightly coupled to providers, and tool JSON, images and encrypted reasoning are hard to count precisely on the client. What matters more in practice is leaving enough buffer, having an overflow recovery path, and rebuilding the accounting baseline after compaction.

Prompt cache: compaction necessarily changes the prompt prefix, and all three try to limit further disruption. Codex only appends to history in normal operation; Pi's summary request runs in a separate session and writes no cache; OpenCode's tool prune runs in batches only when more than 20k can be freed.

Cumulative error is a shared weakness. All three use "previous summary + new history → updated summary", so exact parameters, error strings and edge constraints can be lost over successive rounds, and stale conclusions can survive because of "preserve" instructions. Mitigations differ: Codex keeps recent user text or the server-side compaction item; Pi additionally keeps 20k of verbatim text and file lists; OpenCode V1 keeps recent turn text and allows plugin injection.

Failure modeCodexPiOpenCode
Summary request also overflowsLocal: drop oldest items one by one; remote: rewrite trailing tool outputs firstRelies on the 16k reserve and tool output truncation; on failure no compaction is installedV1 strips media and truncates tool outputs, then marks an error and stops; V2 verifies the prompt fits first
Silent provider truncationMostly relies on provider errors and the usage hard windowExplicitly detects usage overflow and length + zero outputUsage reaches the usable threshold; adapter throws an overflow error
Immediate re-trigger after compactionNew window baseline + recomputed usageIgnores usage from before the compaction timestampHides completed compaction pairs, continues on new usage
New user input mid compactionResumes the tool continuation first, then drains pending inputQueue continues under agent queue semantics after compactionSynthetic continue/replay message enters the persistent loop
Resume / restartCheckpoint contains the replacement history and window IDsRaw tree + compaction entry rebuild the projectionRaw database history + compaction part/summary pair

A reusable design

Combining the three, a robust implementation should be split into separately testable components rather than one large compact(): an immutable transcript store where compaction only appends checkpoints; a projector that builds the active history from a checkpoint and repairs tool pairs; an accountant that leads with provider usage, fills in with local estimates and rebuilds the baseline at each checkpoint; a tail selector that keeps whole turns first, cuts at safe message boundaries inside very long turns and never separates a tool call from its result; a summary builder with a fixed schema, bounded tool results and a structured side channel for files and commands; overflow recovery that compacts proactively before the threshold, reactively after a provider overflow, and retries a bounded number of times; and a checkpoint installer that leaves the active history untouched until the summary succeeds, then installs the summary, tail pointer, usage baseline and window ID in one step.

In one sentence

Context compression is not "summarizing the chat log". It is the construction of a state checkpoint that is persistable, recoverable, token-bounded, keeps recent high-fidelity text, and lets execution continue through tool loops and provider overflow. Codex implements it as a context window lifecycle, Pi as a projection over a tree-shaped session, and OpenCode as a summary/tail view over the full database history.

createrole's digital employees also work for long stretches in an agent loop with a terminal and file tools. What we take from this study is the component split above: never delete the raw record, keep the summary and the verbatim tail separate, never split tool pairs, rebuild the accounting baseline with each checkpoint, and bound the retries in overflow recovery.