GPT-5.6 all-turns reasoning is double-counted, causing auto-compaction with ~20% context remaining
What version of Codex CLI is running?
codex-cli 0.148.0
What subscription do you have?
ChatGPT Pro
Which model were you using?
gpt-5.6-sol
What platform is your computer?
Microsoft Windows NT 10.0.26200.0 x64
What terminal emulator and version are you using (if applicable)?
_No response_
Codex doctor report
What issue are you seeing?
Summary
In a long-running gpt-5.6-sol rollout whose initial session metadata records Codex CLI 0.144.5, Codex automatically compacted the conversation while approximately 20% of the effective context window was still available. The thread may have subsequently been resumed by newer Codex builds, but the rollout does not record a client version for each resume or turn.
Analysis of the rollout shows that the API-reported token usage already accounted for historical reasoning items. However, Codex behaved as if server_reasoning_included were false and added an estimated size for the same historical reasoning items again.
This caused historical reasoning to be double-counted for the auto-compaction decision.
Across 76 automatic compactions in the analyzed rollout:
- The current
server_reasoning_included = falsecalculation crossed the auto-compaction limit in 76/76 cases. - The
server_reasoning_included = truecalculation crossed the limit in 0/76 cases.
Analyzed rollout
- Model:
gpt-5.6-sol - OS: Windows
- Session creation version: Codex CLI
0.144.5 - This is the version recorded in the rollout's initial
session_meta. - The thread was long-running and may have been resumed by newer Codex builds.
- The rollout does not record the client version for every resume or turn.
- Current source inspection: the same accounting path is still present on
mainat commit3b45c29062ff0e76e71c91b6753290400e7fa8da(2026-08-19). - Effective context window in the analyzed rollout:
272,000tokens - Auto-compaction limit:
244,800tokens - Reasoning context:
all_turns - Rollout ID:
019f74ae-8c3d-77e0-8725-2b2c99546fba
Therefore, this report should not be interpreted as being limited to CLI 0.144.5. That version is directly confirmed by rollout metadata; newer builds remain susceptible at the source-code level when the server already includes historical reasoning but the reasoning-included signal is absent or not propagated.
GPT-5.6 defaults to persisted all_turns reasoning according to the official model guidance:
https://developers.openai.com/api/docs/guides/latest-model
Actual behavior
Codex computes the auto-compaction token usage through:
last_tokens
.saturating_add(self.get_non_last_reasoning_items_tokens())
.saturating_add(items_after_last_model_generated_tokens)
when server_reasoning_included is false.
For this rollout, the historical reasoning represented by get_non_last_reasoning_items_tokens() was already reflected in the API-provided token usage, so this estimate counted the same reasoning a second time.
As a result, automatic compaction was triggered significantly before the real request context reached the configured limit.
Evidence
1. Reconstructing server input-token accounting
I reconstructed the model-visible request history across 1,755 consecutive cross-turn transitions and compared the next response's server-reported input_tokens against two predictions:
| Reconstruction | Median absolute error |
|---|---:|
| Historical reasoning retained/accounted | 25 tokens |
| All historical reasoning excluded | 13,483 tokens |
The server-reported input_tokens closely match the reconstruction that includes historical reasoning.
The alternative hypothesis—that the server excludes previous reasoning—has a median error larger by more than 13,000 tokens.
This indicates that historical reasoning was already included in the service token accounting for this rollout.
2. Replaying every automatic compaction
I replayed all 76 automatic compaction decisions using both branches of get_total_token_usage().
| Calculation | Compactions crossing 244,800 |
|---|---:|
| server_reasoning_included = false | 76/76 |
| server_reasoning_included = true | 0/76 |
There were no preceding new_context_window tool calls or other manual compaction triggers. These were automatic token-limit compactions.
3. Concrete example
One compaction was triggered with:
last server-reported total = 209,336
historical reasoning estimate = 33,371
items after last model response = 4,726
auto-compaction limit = 244,800
Current calculation:
209,336 + 33,371 + 4,726 = 247,433
247,433 > 244,800
Therefore Codex compacts.
Calculation without double-counting historical reasoning:
209,336 + 4,726 = 214,062
214,062 < 244,800
With a 272,000-token effective context window, this leaves approximately:
(272,000 - 214,062) / 272,000 = 21.3%
This matches the observed UI state: auto-compaction occurs with roughly 20% context remaining.
Relevant code path
At the start of every regular turn, Codex resets the flag:
sess.set_server_reasoning_included(/*included*/ false).await;
The flag becomes true only when a ServerReasoningIncluded(true) event is received.
For SSE, this event is emitted only when the response contains the x-reasoning-included header.
For WebSocket transport, the same information is derived from the WebSocket handshake header and emitted for each request made over that connection.
The token calculation then uses the flag here:
pub(crate) fn get_total_token_usage(
&self,
server_reasoning_included: bool,
) -> i64
When the flag remains false, historical reasoning is estimated and added again.
Relevant files:
codex-rs/core/src/tasks/regular.rscodex-rs/core/src/context_manager/history.rscodex-rs/codex-api/src/sse/responses.rscodex-rs/codex-api/src/endpoint/responses_websocket.rs
Likely root cause
For this session, the client behaved as if server_reasoning_included were false even though the returned token usage already accounted for historical reasoning.
This suggests one of the following:
- The backend route did not emit
x-reasoning-includedeven though its usage accounting included historical reasoning. - The header was emitted upstream but was not propagated through the relevant response or WebSocket path.
- The server/client contract for GPT-5.6
all_turnstoken accounting is inconsistent across routes.
I do not have a raw packet capture of the response headers, so the evidence establishes that the client remained on the false accounting branch, but does not distinguish which layer failed to propagate the signal.
What steps can reproduce the bug?
- Start or resume a long-running Codex CLI thread using
gpt-5.6-solwith persistedreasoning.context = all_turns. - Continue the thread across many model sampling turns so that multiple historical encrypted reasoning items accumulate.
- Continue until the context indicator shows approximately 20% remaining.
- Submit another normal user turn.
- Observe that automatic compaction can run even though the server-reported usage plus the newly appended local items remains below the configured auto-compaction limit.
- Inspect the rollout's
token_count,response_item.reasoning, andcompactedrecords and replayget_total_token_usage()withserver_reasoning_includedset to bothfalseandtrue.
This appears to depend on whether the reasoning-included signal is propagated by the selected backend and transport route. The analyzed rollout provides 76 occurrences of the behavior; the numerical replay is included in the issue description.
What is the expected behavior?
If the server-provided token usage already accounts for historical reasoning, Codex should not add get_non_last_reasoning_items_tokens() again.
The auto-compaction decision should use:
server-reported token usage
+ items added locally after the last model response
without re-estimating reasoning that the server has already counted. Automatic compaction should not run while the resulting usage remains below the configured threshold.
Additional information
_No response_
3 Comments
Public source supports a deterministic client-side cause in addition to transport propagation, and this overlaps #32483. On current main (5bcd7b0), RegularTask clears the flag before run_turn performs pre-sampling compaction. That check therefore combines the previous response token usage with a freshly forced false value before any new SSE response or WebSocket request can restore the signal. Header loss can still explain other false branches, but it is not required to explain pre-turn compaction. A regression should assert timing, not only the final compact-request count: consecutive header-bearing responses must not compact before the next request, and the header-clears case should compact only after a headerless response has completed.
@dajiaohuang Thanks — I re-ran the rollout analysis with an independent event-order state machine.
This report overlaps #32483, but it is not fully covered by it:
All 14 pre-sampling cases continued into normal sampling in the same task, with no model or comp-hash change, so they are
genuine pre-turn token-limit compactions. However, the historical rollout does not retain the previous response's raw
X-header. Those 14 cases are therefore compatible with #32483, but I cannot prove whether the previousReasoning-Included
signal was cleared by the startup ordering or was never propagated.
The other 62 cases are outside the scope of the #32483 fix. They occurred after a current-turn response had completed, when
ServerReasoningIncluded(true), if emitted, would already have been processed before the post-sampling token check. Movingthe turn-start reset would not affect those cases.
Across all 76 compactions, the false branch crosses the 244,800-token limit in 76/76 cases, while the true branch crosses it
in 0/76 cases.
Thanks — this split materially narrows the diagnosis. I agree that my earlier public-source finding explains only the 14 pre-sampling cases: those remain consistent with #32483's reset ordering, but the historical rollout cannot prove whether the preceding response ever supplied the signal. It cannot explain the 62 post-sampling cases.
Current
mainat343074d4207d572809bd8cea15f4be1d09d98e0bstill resetsserver_reasoning_includedbeforerun_turn, and neither #32483 nor this issue has a linked PR. For the 62 post-sampling cases, the useful discriminator is response-scoped evidence tying each reported token-usage record to theServerReasoningIncludedstate that qualified it. The current session-level boolean alone cannot distinguish a missing transport signal from a server/accounting-contract mismatch.A focused regression should therefore cover a header-bearing current-turn response followed by the post-sampling token check, plus the intended semantics after an explicitly headerless response. Without the historical response headers or equivalent transport telemetry, I would not infer which route caused those 62 cases.
For account context, I’m using the Pro 20x plan.