Bug: `trim_function_call_history_to_fit_context_window` breaks early on non-rewriteable items, leaving context overflows untrimmed

Open 💬 0 comments Opened Jul 26, 2026 by afjal1

What version of Codex CLI is running?

Reproduced with codex-cli 0.145.0. The same implementation is still present in main at e4fb5311d7468839def62eabda4b268f4a54cf11.

What platform is your computer?

Darwin 26.5.2 arm64 arm (macOS).

What issue are you seeing?

trim_function_call_history_to_fit_context_window breaks its truncation loop as soon as it encounters a non-rewriteable item, even if earlier (older) tool outputs could still be trimmed. This leaves the context window over its limit when non-rewriteable items (assistant messages, reasoning, etc.) sit between trimmable tool outputs.

Root cause: The function walks history from newest to oldest and breaks at the first non-rewriteable item:

// compact_remote.rs:390-401
for (item, item_tokens) in original_items.iter().zip(item_token_estimates).rev() {
    if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window {
        break;
    }
    let Some(rewritten_item) = rewritten_output_for_context_window(item) else {
        break;  // <-- stops here even if earlier outputs could be trimmed
    };
    ...
}

The rewritten_output_for_context_window function (line 416) only rewrites FunctionCallOutput, CustomToolCallOutput, and ToolSearchOutput. Any other item type (assistant messages, reasoning, user messages) causes the loop to break.

Example scenario:

History structure (newest first):

[FunctionCallOutput_3, AssistantMessage_2, FunctionCallOutput_1, UserMessage]

If the context window is exceeded and only FunctionCallOutput_3 is trimmed, the overflow persists because AssistantMessage_2 blocks trimming of FunctionCallOutput_1.

Impact: The context window remains over its limit after trimming, which can cause:

  1. The compaction request to fail with ContextWindowExceeded
  2. Subsequent turns to trigger aggressive compaction that removes history items
  3. User-visible "context window exceeded" errors

What steps can reproduce the bug?

  1. Build a conversation with alternating assistant messages and tool outputs
  2. Continue until the context window is near capacity
  3. Trigger a compaction or context trim
  4. The trim will only affect trailing tool outputs, leaving earlier overflows unaddressed

What is the expected behavior?

The function should skip non-rewriteable items and continue trimming earlier tool outputs until the context window is within limits or no more trimmable items remain.

Suggested fix

Change the break to continue for non-rewriteable items, but track whether we've already trimmed something to avoid infinite loops:

for (item, item_tokens) in original_items.iter().zip(item_token_estimates).rev() {
    if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window {
        break;
    }
    let Some(rewritten_item) = rewritten_output_for_context_window(item) else {
        continue;  // Skip non-rewriteable items, keep looking
    };
    estimated_tokens = estimated_tokens
        .saturating_sub(i128::from(item_tokens))
        .saturating_add(i128::from(estimate_item_token_count(&rewritten_item)));
    rewritten_items.push(rewritten_item);
}

Note: This requires updating the rewritten_items logic since items are now collected out of order. The final assembly would need to replace items at their original positions rather than appending.

Related

  • #32556 — Context window usage appears to grow unexpectedly during long sessions
  • #23643 — Window/Context fills up enormously fast

Scope

One function in one file (compact_remote.rs:365-414). The fix is ~15 lines. No behavioral change for正常 trimming — only affects the case where non-rewriteable items block trimming.

View original on GitHub ↗