OpenAI Codex · Context

After the Window Fills, What Gets Cut and What Stays

Three timings share one dispatcher; three implementations are picked by switches. The same summary, mid-turn, must sit as the last history item.

Course goalAfter this lesson, you can explain three things. First: when context fills, Codex asks timing before implementation; nine combinations share two entry points. Second: a transcript from the server is untrusted by default; instruction wrappers and tool playback are dropped at the door. Third: the same summary sits in different places before sampling and mid-turn; flip the empty-window switch and memory does not come along.
Play first · one history, three pipes each cut their own way
Window at the top: switch timing and implementation, watch what each cuts and what each keeps
Timing
Pre-sample and manual finish first; the next turn reinjects the whole pack. Mid-turn still has to keep sampling, so the summary must sit at the tail.
Impl
A default OpenAI session goes remote. If the provider cannot, it goes local and samples again. Empty window skips the summary; memory does not come along.
Mid-turn × Remote 0 cut 0 kept 3 ver
Kept Cut Fresh-injected env
Filter Summary last Reinject next turn Empty
Waiting to start. Hit Play, or switch a cell first and walk it again.
Nine-cell grid · tap any cell to see that cell's own cut
Pre-sample Mid-turn Manual Remote Local Empty
Logic trail · each animation step maps to a source span
  1. Watch token_limit_reached, or a user-submitted Compactcontext_window.rs L77
  2. Pre-sample runs run_pre_sampling_compactturn.rs L1012
  3. Mid-turn runs should_roll_over, and still needs a follow-upturn.rs L458
  4. Manual spins up CompactTask and interrupts the current turntasks/compact.rs L19
  5. TokenBudget on swaps an empty window; remote and local are both skippedturn.rs L1189
  6. Else dispatch remote v2 or localturn.rs L1201
  7. Remote results pass the should_keep filtercompact_remote.rs L370
  8. InitialContextInjection decides whether to injectcompact.rs L68
  9. Only replace_annotated bumps history_versionhistory.rs L298
Hit Play and watch how the same mixed history is rearranged in this cell.
RemoteWrappers from the server are untrusted. Drop developer and tool playback first, then decide by timing whether to inject environment. Mid-turn must leave the summary last.
LocalFirst append a synthesized prompt and assistant output; the version stays put. After success, swap the whole table for truncated user words plus a summary prefix.
EmptySkip the model and the server. The new window holds only the current environment: no summary, memory does not come along.
Teaching sketch: chip counts and roles are classroom fixtures, to show what each of the three pipes cuts and keeps. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Split timing from implementation
What problem it solves

You let a coding agent edit thirty turns in the same thread. The first twenty it still remembered where the workspace was and which files not to touch. On turn thirty it suddenly asks you “what's the current directory?”. A few turns later, an already-approved npm test gets asked again.

Once usage sits on the window edge, the system must compact history. If when-to-compact and how-to-compact live in the same branch, swapping a provider or adding an empty window that never calls the model forces every entry to change. Mid-turn still has to keep sampling; pre-message compact waits until the next turn to reinject. Those two want the summary in different places.

What the idea is

Codex writes timing as CompactionPhase: PreTurn before a message, MidTurn after tools when it still has to continue, StandaloneTurn when the user asks. Implementation is CompactionImplementation: local resample, the old /responses/compact, the new compaction_trigger. Analytics events have a second pair of labels, Trigger and Reason. The auto path shares run_auto_compact; the manual path shares CompactTask. The nine-cell grid is a multiplication table. Source does not have nine side-by-side functions.

All three auto timings ask the same function first. token_limit_reached is true on a buffered compact limit or a full window, either one. Mid-turn adds another gate: it still has to continue, and the model just asked for a new window or token already hit the top. Full but not continuing, this turn ends naturally; the next user message takes the pre-sample path.

Source: codex-rs/core/src/session/context_window.rs lines 74–91;codex-rs/core/src/session/turn.rs lines 458–483

The dispatcher's first line looks at TokenBudget. On, swap an empty window; remote and local are both skipped. Off, pick by provider capability: knows compaction_trigger and the flag is on, remote v2; knows V2 but the flag is off, the old remote API; Unsupported, local. TokenBudget defaults off; RemoteCompactionV2 defaults on. A default OpenAI session goes remote v2.

Source: codex-rs/core/src/session/turn.rs lines 1178–1201;codex-rs/features/src/lib.rs lines 1428–1433、lines 1542–1547

At cap or /compact Pick phase Same dispatcher Empty TokenBudget First if, skip summary Remote v2 or old API Filter on the way back Local resample Provider Unsupported replace_compacted_history Same install point; the product differs before install
Teaching diagram: pick a phase, enter the same dispatcher, swap the window at the same install point.
Why it lasts

When to swap the window, and what to put in it, are two separate jobs. Rewrite it in another language and you still pick phase before pipe. Even with one provider, you can still split decide from install.

Idea 2 · Remote filters first; summary seat follows phase
What problem it solves

A transcript from the server may carry stale developer instructions. Skip the filter and they stack on an environment the local side just rendered from the current world state — same effect as rewriting an old row. Mid-turn still has to keep sampling in the same turn; the model was trained that the summary is the last history item. Inject context after the summary and that training constraint breaks.

What the idea is

The filter is a complete exhaustive match. Drop developer, user wrappers that are not user content, tool calls, and compact triggers. Keep real user messages, persistent hook prompts, assistant, compact items. v2 reuses the same function.

Source: codex-rs/core/src/compact_remote.rs lines 370–397

Then InitialContextInjection decides whether to inject. Pre-sample and manual use DoNotInject: the replacement history has no initial context, reference_context_item is cleared, and the next ordinary turn does a full reinject. Mid-turn uses BeforeLastUserMessage: current environment and permissions sit above the last real user message; the summary stays at the tail. The insert function has a fallback: no real user, inject before the summary; still none, inject before the last compaction item.

Source: codex-rs/core/src/compact.rs lines 59–74

Pre-sample / manual · DoNotInject User words Summary Env stays out of the replacement table Next ordinary turn reinjects the whole pack Mid-turn · BeforeLastUserMessage Current env Last user Summary must be last Same turn keeps sampling; training pins the summary at the tail
Teaching contrast: one summary, two phases, two seats.

All three implementations finally enter replace_compacted_history. live history is replaced wholesale. history_version only bumps then; append does not touch the version. When Guardian reuses a transcript it checks parent_history_version; if the version moved, the old review prefix cannot be reused.

Source: codex-rs/core/src/context_manager/history.rs lines 298–302

Drop wrappers from remote first. Mid-turn, the summary sits at the tail.
Why it lasts

A remote result, unless a local function emitted it, drops instruction wrappers at the door. The insert point guards a training constraint and a reinject timing — two jobs that were already separate. The version only advances on rewrite, because append happens every turn.

Idea 3 · An empty window is compaction too; it just does not call the model
What problem it solves

Sometimes the user just wants a clean window, no summary. If the empty window walked another lifecycle, hooks and ContextCompaction items would never see it. The switch defaults off, so a user does not lose a whole conversation without noticing.

What the idea is

TokenBudget skips model and server summaries, installs a new window, and leaves the summary field an empty string. In the new window the model cannot see the old chat — only the environment and permissions of this moment. That matches the new_context tool's contract: swap the window, do not summarize. It still walks the compact lifecycle; if a pre-compact hook stops it, the window has not swapped yet.

Source: codex-rs/core/src/compact_token_budget.rs lines 21–25

If the local path itself hits a full window, it does not recurse into auto-compact. It drops the oldest item and tries once more. One item left and still over: mark full-window and return an error. Remote failure does not fall back to local either: ordinary full-window pre-compact and mid-turn compact both pass fallback as None; the first remote failure returns at once. Timeout is not on the retry list.

Source: codex-rs/core/src/compact_model_fallback.rs lines 8–20

Why it lasts

Compaction is a window-swap lifecycle. The product can be a summary, or an empty room. Recursion has to stop itself: a manual task does not enter the turn loop; local overflow relies on prune; the mid-turn path relies on a successful compact dropping usage below the limit. If that assumption fails, the loop can enter again.

Side-by-side · Another answer to the same question

DSH: the cut that never calls the model can land first

DSH's compact family lives in packages/compaction/. On pressure it may prune first, then measure again. If prune already dropped below the threshold, the summary does not run. Both paths stay local; there is no Codex-style compact client.

Overflow recovery treats replaceGeneration as a retry permit. Prune lands first, then the summary throws: as long as generation advanced, it may retry from the new surface. generation only bumps when a replace plan commits; append does not. Codex answers the same question with history_version: remote either installs a whole window, or installs nothing.

Source checked on both sides · 2026-08-22 · DSH · Compaction

Claude Code: recursion is stopped by a label; three failures and it quits

The default auto path still calls the model once more to write a summary. Auto-decide blocks recursion first: querySource session_memory and compact return false at once; the comment says they are forked agents, and firing again would deadlock. After 3 consecutive failures the circuit opens. The comment records a session that failed thousands of times in a row.

Codex's Compact task does not enter the turn loop, so that label can be absent. The trade-off: the mid-turn path has no matching consecutive-failure counter. The author wrote the bet in a comment: if compact drops usage below the limit, a deadlock is not a worry, so the counter was skipped.

Source checked on both sides · 2026-08-22
Classroom Exercise
01

What each of the three replacement tables keeps

Take that mixed history above. Walk mid-turn remote, empty window, and pre-sample remote. Write three replacement tables: which blocks each cuts, which it keeps, whether the summary is last, whether initial context is in the table at that moment.

A harder follow-up: on the first remote failure, why the ordinary full-window path does not fall back to local, while model-switch pre-compact may hit remote once more.

Takeaway:Split timing from implementation; nine combinations share two entries. Remote hits the filter first; a mid-turn summary must sit at the tail. An empty window is one compact product; memory does not come along.