Skip to content

bug: Compaction can overwrite concurrent history writes and loop indefinitely #13946

Closed

Description

@William17738

Summary

The context compaction system has two correctness issues:

  1. Race condition: history is rebuilt from a stale snapshot then fully replaced, silently dropping any items recorded during the compaction window.
  2. 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 notification
  • codex-rs/core/src/codex_thread.rs:103 — inject_user_message_without_turn
  • codex-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

  1. Add a version counter or generation ID to session history; replace_compacted_history should fail-and-retry (or merge) if the generation has advanced since the snapshot.
  2. Add a max_compact_attempts counter to the mid-turn loop to prevent unbounded retries.
  3. 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.

Activity

  1. github-actions commented on Mar 8, 2026

    @github-actions
    Contributor

    Potential duplicates detected. Please review them and close your issue if it is a duplicate.

    Powered by Codex Action

  2. added
    bugSomething isn't working
    contextIssues related to context management (including compaction)
    on Mar 8, 2026
  3. Lordanakun commented on Mar 13, 2026

    @Lordanakun

    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.rs and compact_remote.rs

    The 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 via inject_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:

    // compact.rs (same pattern applies to compact_remote.rs)
    // add before the CompactedItem { ... } block
    let latest_history_snapshot = sess.clone_history().await;
    let latest_history_items = latest_history_snapshot.raw_items();
    if latest_history_items.len() > history_items.len() {
        new_history.extend_from_slice(&latest_history_items[history_items.len()..]);
    }

    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.rs

    run_turn() retries compaction in a loop with no cap. If the summary itself is large enough to keep token usage above auto_compact_limit, the loop never exits.

    Fix: add a counter and abort after 3 attempts:

    // add before the mid-turn compaction block in run_turn()
    let mut mid_turn_compaction_attempts: u32 = 0;
    
    // inside the if token_limit_reached && needs_follow_up block:
    mid_turn_compaction_attempts += 1;
    if mid_turn_compaction_attempts > 3 {
        error!(
            turn_id = %turn_context.sub_id,
            mid_turn_compaction_attempts,
            "mid-turn compaction exceeded retry limit"
        );
        return None;
    }

    Working implementation

    Full implementation with both fixes: Lordanakun@a460195

    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).

  4. Lordanakun commented on Mar 13, 2026

    @Lordanakun

    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:

    // compact_remote.rs — corrected version
    let history_clone = history.clone();
    let history_items = history_clone.raw_items();
    let ghost_snapshots: Vec<ResponseItem> = history_items
        .iter()
        .filter(|item| matches!(item, ResponseItem::GhostSnapshot { .. }))
        .cloned()
        .collect();
    
    // ... (later, before replace_compacted_history)
    let latest_history_snapshot = sess.clone_history().await;
    let latest_history_items = latest_history_snapshot.raw_items();
    if latest_history_items.len() > history_items.len() {
        new_history.extend_from_slice(&latest_history_items[history_items.len()..]);
    }

    The same split is not needed in compact.rs because that file already uses history_snapshot (a prior clone) rather than moving history directly.

    Updated branch: https://github.com/Lordanakun/codex/tree/fix/compaction-race-and-infinite-loop

  5. charley-oai commented on Mar 13, 2026

    @charley-oai
    Contributor

    Hey there was a bug in the Responses API /compact endpoint that did not properly truncate replacement_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 tomorrow

    However I think the 2 code issues you flagged here are probably still work fixing

  6. charley-oai commented on Mar 13, 2026

    @charley-oai
    Contributor

    #14563 would be the PR if we want to fix these edge cases

  7. etraut-openai commented on Jun 29, 2026

    @etraut-openai
    Contributor

    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 /feedback and open a new bug report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcontextIssues related to context management (including compaction)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions