Repository navigation
Pre-turn compaction clears reasoning-included state before accounting #32483
Description
What version of Codex CLI is running?
0.144.1; also reproduced from source on main@c4318c386de365bd0dd9595a08d55a30bb142d11
What subscription do you have?
API / custom Responses provider
Which model were you using?
Any Responses model/provider that returns X-Reasoning-Included: true
What platform is your computer?
Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
What terminal emulator and version are you using (if applicable)?
Codex CLI in WSL2
Codex doctor report
{
"note": "The bug has a deterministic source-level integration test and is not dependent on local installation state."
}What issue are you seeing?
Codex can trigger premature pre-turn auto-compaction after a response reports
X-Reasoning-Included: true.
server_reasoning_included tells context accounting that the server's input
usage already includes replayed encrypted reasoning. When true,
History::get_total_token_usage() does not add a local encrypted-reasoning
estimate on top of server usage.
The regular-task startup path currently clears that flag before run_turn():
sess.send_event(ctx.as_ref(), event).await;
sess.set_server_reasoning_included(/*included*/ false).await;However, run_turn() performs pre-turn compaction before it sends the new
sampling request:
run_pre_sampling_compact(&sess, &turn_context, &mut client_session).awaitAs a result, pre-turn accounting loses the previous response's reasoning
contract and can double-count historical encrypted reasoning. Threads can then
compact substantially before their configured token limit.
This is an ordering issue, not a request-header parsing issue. The response
event correctly sets the flag to true, but the next regular task clears it
before the code that still needs it.
What steps can reproduce the bug?
The following integration-test shape reproduces it deterministically:
- Configure an auto-compaction limit of 300 tokens.
- Return two Responses streams containing encrypted reasoning items.
- Set
X-Reasoning-Included: trueon both responses. - Report low server usage (
10, then80tokens), below the compaction limit. - Submit a third user turn.
- Assert that
/v1/responses/compactwas not called.
On unmodified main, the assertion fails twice under nextest:
thread 'suite::compact::auto_compact_preserves_reasoning_header_for_next_pre_turn_check'
panicked:
pre-turn accounting should retain the previous response's reasoning-included signal
Summary: 1 test run: 0 passed, 1 failed
The complete regression test and minimal fix are available in:
The fix moves the reset to immediately after run_pre_sampling_compact(), so
pre-turn accounting uses the previous response's flag while the upcoming
response still starts cleared and must reassert the header.
Focused verification on current main:
auto_compact_preserves_reasoning_header_for_next_pre_turn_check ... ok
auto_compact_runs_when_reasoning_header_clears_between_turns ... ok
auto_compact_counts_encrypted_reasoning_before_last_user ... ok
The existing omitted-header test is important: it proves the change preserves
the per-turn behavior introduced by #9402. If the next response omits the
header, Codex still re-enables local encrypted-reasoning accounting.
What is the expected behavior?
Pre-turn compaction should account for the existing history using the previous
response's X-Reasoning-Included value. The flag should be reset only after
pre-turn compaction and before the new response stream starts.
Additional information
The tested source change is intentionally small:
- remove the reset from regular-task startup;
- reset immediately after successful pre-turn compaction;
- add an integration regression test for consecutive header-bearing turns.
Validation completed on current main:
- three focused
codex-corecompaction tests passed; just fix -p codex-corecompleted without diagnostics;just fmtcompleted successfully.
Per the external contribution policy, I am opening the issue first rather than
an unsolicited pull request. The tested branch is ready if a maintainer would
like to invite the PR.
Activity
shtratos commentedon Jul 11, 2026
Follow-up: cold resume had the same accounting gap because
server_reasoning_includedwas previously in-memory only.I added a second regression:
It runs two reasoning responses with
X-Reasoning-Included: true, shuts down,resumes from the persisted rollout, and submits a third turn. Before the
follow-up change, the test failed twice because resume defaulted the bit to
false and issued an unwanted compact request.
The branch now persists this bit as a dedicated rollout-only
ServerReasoningIncluded(bool)marker and restores the latest marker onresume/fork. The marker is deliberately ignored by thread history, search,
memory export, and app-server materialization; it does not change
TokenCountEventor other client-visible event JSON.Backward compatibility:
falsedefault;
persisted and future resumes restore the exact accounting contract.
Current branch:
https://github.com/shtratos/codex/commits/fix/preserve-reasoning-header-preturn
New commit:
shtratos@0da44b4a6
Fresh verification on current
main:The branch is ready for an invited PR if this approach matches the intended
fix.
coinman-dev commentedon Oct 4, 2026
Same cause here on Codex 0.159.2 with
gpt-6.1-solunder WSL2. A small client patch that stops the double count is described in #39767 (comment): with it the same chat went from being compacted at 734,065 tokens to 922,856 with no early compaction, and its regression test fails on unpatched 0.160.0. Patch and notes: https://github.com/coinman-dev/codex-context-accounting-fixghzlatarev commentedon Oct 12, 2026
Still reproducible on stock 0.162.1 (macOS arm64) and an unmodified source build at
7c9bfb910ef65c8f4a957c53affbd43d5fac2882. This adds a binary-level, credential-free app-server reproduction to the source-level test in the issue.The fixture uses a fresh temporary HOME/CODEX_HOME/workspace and a localhost Responses SSE mock. It submits three turns. Every accepted response reports input=100, output=10, total=110. The first response optionally contains a 120,000-character opaque synthetic reasoning item (
Arepeated; not real encrypted model data). Post-turn compaction is disabled; the pre-turn threshold is 20,000.truetruefalsetrueIn the
truecase, request 3 is local compaction (tools: []), despite the previous response explicitly asserting that its usage includes replayed reasoning. The no-reasoning and higher-limit controls isolate the accounting effect. The mock deliberately gives a small reported usage relative to the opaque payload so the client-side estimate becomes observable; these numbers are a controlled contract fixture, not production token measurements.The tested source still clears the flag at regular-task startup, before pre-turn compaction. Accounting then adds older reasoning estimates when the flag is false.
tasks/regular.rsis byte-identical on inspected main7e9c061ceb388c67097acac069d6b7f8a60e5011; that newer commit was not rebuilt.The fix direction already proposed here—preserve the previous response's flag through pre-turn accounting, then clear it before the upcoming response—is consistent with these observations. This probe does not test cold resume or the proposed patch.
Run the standard-library Python script below:
The script prints structural evidence and turn outcomes. Exit 0 means the probe completed, not that Codex passed a regression assertion. Temporary fixture data is retained under the printed directory for inspection.
Standalone reproduction