bug: Compaction can overwrite concurrent history writes and loop indefinitely
Summary
The context compaction system has two correctness issues:
- Race condition: history is rebuilt from a stale snapshot then fully replaced, silently dropping any items recorded during the compaction window.
- No retry cap: mid-turn compaction can loop indefinitely if it fails to reduce token usage below the threshold.
Finding 1: Clone-then-replace race condition
Local compaction path
File: codex-rs/core/src/compact.rs
line 109: history = sess.clone_history() // snapshot ①
line 136: drain_to_completed(sess, ...) // model API call (2-10 seconds)
line 198: history_snapshot = sess.clone_history() // snapshot ②
line 230: sess.replace_compacted_history(new_history) // full replacement
Between snapshot ② (line 198) and replacement (line 230), any new items added to the session history (e.g., subagent notifications via inject_user_message_without_turn, or background events) are silently overwritten by replace_compacted_history.
Remote compaction path (wider window)
File: codex-rs/core/src/compact_remote.rs
line 93: history = sess.clone_history() // snapshot
line 124: compact_conversation_history(...) // remote API call (2-10+ seconds)
line 165: sess.replace_compacted_history(new_history) // full replacement
The remote API call adds significantly more latency, making the race window wider.
The replacement is not atomic with persistence
File: codex-rs/core/src/codex.rs, lines 2850-2865
pub(crate) async fn replace_compacted_history(...) {
self.replace_history(items, ...).await; // step 1: replace in memory
self.persist_rollout_items(&[Compacted(...)]).await; // step 2: persist compaction
self.persist_rollout_items(&[TurnContext(...)]).await; // step 3: persist context
}
A crash between steps leaves memory and disk out of sync.
Concurrent write sources during compaction
codex-rs/core/src/agent/control.rs:421— completion watcher injects subagent notificationcodex-rs/core/src/codex_thread.rs:103—inject_user_message_without_turncodex-rs/core/src/codex.rs:3222—notify_background_event
Finding 2: Mid-turn compaction infinite loop
File: codex-rs/core/src/codex.rs, line 4951
The run_turn function (line 4869) is a loop. When mid-turn compaction triggers and a tool follow-up continues, there is no maximum compaction attempt counter. The code comment acknowledges this:
"as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop."
If compaction produces a summary nearly as large as the original (e.g., dense conversation with many tool results), the loop will trigger compaction on every subsequent iteration indefinitely.
Secondary note
File: codex-rs/core/src/codex.rs, line 4720 — an existing TODO acknowledges that pre-turn compaction does not account for the upcoming user message size, which can cause context-window-exceeded errors immediately after compaction.
File: codex-rs/core/src/codex.rs, lines 3099-3130 — recompute_token_usage after compaction zeroes out input_tokens, cached_input_tokens, and output_tokens, filling only total_tokens via byte estimation. This degrades subsequent auto-compact threshold accuracy.
Suggested improvements
- Add a version counter or generation ID to session history;
replace_compacted_historyshould fail-and-retry (or merge) if the generation has advanced since the snapshot. - Add a
max_compact_attemptscounter to the mid-turn loop to prevent unbounded retries. - Make replacement + persistence a single atomic operation (or at least log a warning on partial failure).
I have a fix ready and can submit a PR if invited.
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I traced both issues and have a proposed fix. Sharing here since external PRs are restricted.
Root cause analysis
Finding 1: Race condition in
compact.rsandcompact_remote.rsThe window between the final snapshot and
replace_compacted_history()is larger than just snapshot ②→replacement — the compaction task itself takes 2–10 seconds (model API call). Any items appended during that entire window (e.g. subagent notifications viainject_user_message_without_turn, background events) are silently dropped.Fix: take one more snapshot immediately before calling
replace_compacted_history()and merge any items that appeared since the original snapshot:This assumes append-only semantics during compaction (new items only grow the tail), which matches the current architecture.
Finding 2: Mid-turn compaction infinite loop in
codex.rsrun_turn()retries compaction in a loop with no cap. If the summary itself is large enough to keep token usage aboveauto_compact_limit, the loop never exits.Fix: add a counter and abort after 3 attempts:
Working implementation
Full implementation with both fixes: https://github.com/Lordanakun/codex/commit/a460195acabbcaa7238ef886a1264c654e5544ef
3 files changed, 25 insertions. Happy to adjust the approach if maintainers prefer a different strategy (e.g. holding a write lock vs. the merge-on-replace approach).
Update: Just validated by building from source on Apple M4 (Rust 1.93.0 / aarch64). One borrow checker fix needed in
compact_remote.rs— the temporary clone needs to be bound to a named variable:The same split is not needed in
compact.rsbecause that file already useshistory_snapshot(a prior clone) rather than movinghistorydirectly.Updated branch: https://github.com/Lordanakun/codex/tree/fix/compaction-race-and-infinite-loop
Hey there was a bug in the Responses API
/compactendpoint that did not properly truncatereplacement_history, causing compaction to barely reduce token % in longrunning conversations. This was causing lots of reports of compaction looping or happening very frequently. This should be fixed by tomorrowHowever I think the 2 code issues you flagged here are probably still work fixing
https://github.com/openai/codex/pull/14563 would be the PR if we want to fix these edge cases
Thanks for reporting this problem. Until recently, Codex used a separate endpoint and server-side logic for compaction. We recently switched to a more robust approach where the compaction logic is moved locally using the same endpoint as normal turns. This eliminates "remote compaction" errors and increases the stability and reliability of compaction operations. If you see further problems with compaction, please use
/feedbackand open a new bug report.