Auto-compaction never triggers
What version of Codex CLI is running?
codex-cli 0.117.0
What subscription do you have?
Pro
Which model were you using?
gpt-5.3-codex
What platform is your computer?
macOS Darwin 25.3.0 (Apple Silicon)
What terminal emulator and version are you using (if applicable)?
Headless mode via codex exec (spawned programmatically)
What issue are you seeing?
Auto-compaction never fires when running Codex in headless exec mode. The session accumulates tool outputs until it hits the context window limit, then crashes with "Codex ran out of room in the model's context window." The watcher retries, each retry adds to the session, and it enters a permanent crash loop.
Config:
model_context_window = 400000
model_auto_compact_token_limit = 350000
Evidence from Codex session file:
- 86 shell commands executed, producing ~242K tokens of tool output
token_countevents in session file all show emptyinput_tokens_used— token tracking appears broken in exec mode- Zero
compactevents in the session — compaction never triggered - Session crashes at context limit without any compaction attempt
Evidence from traces:
turn_contextevents appear (which I believe are compaction triggers) but they are followed immediately bytask_completewithlast_agent_message: null— suggesting compaction was attempted but failed silently- After first crash, every retry fails within 4-5 seconds with the same context overflow error
Codex session ID: 019d30b8-c263-7190-9072-14650024a1ae
This is blocking automated workflows where agents need to read large codebases. The agent reads files incrementally (200-300 line chunks via sed), but after ~86 commands the accumulated context exceeds the window. In interactive mode, compaction would presumably trigger and summarize earlier context. In exec mode, it never fires.
What steps can reproduce the bug?
- Set
model_auto_compact_token_limit = 350000andmodel_context_window = 400000in config.toml - Run codex in headless exec mode:
codex exec --resume <session-id> "Read a large codebase by running sed on many files" - The agent will read files until it accumulates ~350K+ tokens
- Expected: auto-compact triggers at 350K, summarizes context, continues
- Actual: no compaction, crashes at 400K, permanent crash loop on retry
What is the expected behavior?
model_auto_compact_token_limit should trigger auto-compaction in exec mode the same way it does in interactive mode. Token counting should work (currently reports empty values in exec mode).
Additional information
Related issues: #14860, #14913, #13279 (compaction death spiral)
This may be the same underlying issue as #14860 (TCP timeout on compaction endpoint), but the symptom is different — in our case compaction never even attempts to fire, rather than timing out mid-compact.
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Update: confirmed this persists after completely nuking
~/.codex/, fresh installing 0.117.0, and re-authenticating (Pro subscription). No old threads or sessions carried over.Error on every retry:
Running via
codex execwith--resume. Config:model_context_window = 400000,model_auto_compact_token_limit = 300000.Root cause found in source code (
codex-rs/core/src/codex.rs):Auto-compaction only checks token count between turns, not between tool calls within a turn.
Pre-sampling compact check (line 6080) runs once at turn start. But within a single turn, the agent can make 80+ tool calls that accumulate 240K+ tokens of output. No compaction check runs between tool calls — the token counter only updates from API response headers after the model responds.
So the sequence is:
get_total_token_usage()returns 0 (fresh session, no prior API response) → skip compactContextWindowExceededThere is a TODO in the code acknowledging this (line 5584-5586):
The fix would be checking accumulated token count after each tool output (or periodically mid-turn), not just between turns.
Additional finding: compaction also fails to trigger in interactive mode (not just exec), suggesting this is not exec-specific.
Also noting that #16042 confirms a regression in >=0.115 where compaction fires but the model stops afterward with a "Handoff" summary instead of continuing. That reporter confirms 0.114.0 works correctly with no issues in multi-day runs. We will test downgrading to 0.114.0.
Our session traces show token_count events with real values climbing from 17K → 302K → 1.3M with model_context_window=380000 and auto_compact_token_limit derived as 342K (90% of 380K), yet zero compaction events fire. The compaction condition at codex.rs:5895 (
token_limit_reached && needs_follow_up) is never satisfied despite both conditions appearing to be independently true at different points in the turn.Important update: the issue does not reproduce when
model_context_windowandmodel_auto_compact_token_limitare NOT set in config.toml. With defaults (256K context window), compaction works correctly and agents run for 10+ minutes with zero failures on the same workload.The bug only triggers when these variables are explicitly set in config.toml (e.g.
model_context_window = 400000,model_auto_compact_token_limit = 300000). Removing both lines and letting Codex use the default 256K window resolves the issue completely.Looking into this, I found an accounting mutation on the
ContextWindowExceededrecovery path that looks relevant. It overwrites the cumulative token usage in a way that conflicts with how the same value is read back for the limit-reached check.Where.
codex-rs/protocol/src/protocol.rs,TokenUsageInfo::fill_to_context_window(L2100 atf57467275cb0; unchanged on currentmain, now ~L2115):Two things happen here:
total_token_usageis replaced wholesale — the accumulated input/output/cache breakdown is zeroed andtotal_tokensis set to the full window.last_token_usage.total_tokensbecomesmax(0, context_window - previous_total).previous_totalis cumulative across turns, so once it crosses the window this is0.The recovery path reaches this on overflow:
core/src/session/turn.rs:1185callsset_total_tokens_full(&turn_context)onErr(ContextWindowExceeded), which routes intofill_to_context_window(core/src/compact.rs:295does the same).Downstream,
core/src/context_manager/history.rs:328(get_total_token_usage) readsinfo.last_token_usage.total_tokensas the active context size, andcore/src/session/context_window.rs:23(context_window_token_status) uses that return value to computetoken_limit_reached/full_context_window_limit_reached(L75-79). In the common case where cumulative usage already exceeds the window, the fill leaveslast_token_usage.total_tokensat0, so the next reached-check would read as not-reached.Observed behavior (synthetic turn sizes, in-crate test). I drove the real function with fabricated usage:
fill_to_context_window(200_000):total.{input,output,cached}all0,total.total = 200_000,last.total = 168_500— a turn nobody made.last.total = 0, and the 217_000 lifetime spend gets reported down to 200_000.Reproduce (needs only a Rust toolchain):
Save this as
codex-rs/protocol/tests/zz_fill_usage.rs:Scope. This is a crate-level test of the accounting function — it shows the mutation and the arithmetic, not an end-to-end run that observes the next compaction decision after an overflow. So I'd treat it as a candidate contributor to the symptoms in this issue rather than a confirmed root cause. If it checks out, the fix looks like:
last_token_usageshould carry the actual current request size (notwindow - cumulative), and the cumulativetotal_token_usageshouldn't be overwritten — the fabricated baseline belongs in a separate field.