Filed after the fix, to record work that had no ticket. Code is on local main, not pushed: 11501c529 "fix(tui): auto-compact on live mid-turn tokens; Esc stops the turn".
Problem
A long turn could exhaust the context window without auto-compact ever firing, even well past the 80% threshold.
The check was already at the right seam — pre-next-request in crates/tui/src/core/engine/turn_loop.rs (~995), after tools have been appended, the same place GrokBuild calls check_auto_compact_needed and Codex runs CompactionPhase::MidTurn. The signal was wrong.
The gate used max(last billed prompt, /4 estimate of the whole list). After a prompt billed at ~70%, tool results land in the transcript but not in that bill. Whenever the crude estimator undercounted the full list below the last bill, max() returned the stale billed prompt and every new tool result was invisible to the gate. Context then ran out at the next sample.
GrokBuild's formula is exact prior count plus a byte-estimate of the items appended since the last response; Codex additionally compacts-and-retries on ContextWindowExceeded.
Note: this is not a mid-tool-call compaction. The watermark is taken at add_parent_usage, before this step's assistant/tool messages are appended; the pass still runs at the pre-request boundary after tools complete.
Fix (a) — live token signal
| Change |
Why this seam |
core/turn.rs: note_parent_prompt_len + live_input_tokens_for_compaction |
Live tokens = billed prompt + suffix growth |
core/engine/turn_loop.rs, after add_parent_usage |
Watermark is the billed prompt's message count |
core/engine/turn_loop.rs compact gate |
Passes live tokens, not the stale billed figure |
Fix (b) — interrupt semantics
Esc during a compact used to call only try_cancel_compaction and continue, so the turn loop's compaction_cancel arm fired and the turn resumed. Codex and GrokBuild both treat an interrupt as stopping the turn, with the compact as collateral.
| Change |
Why this seam |
tui/ui/compaction_flow.rs: compact_interrupt_should_stop_turn |
Decision table: compact serving a turn → stop the turn |
tui/ui.rs: escape_cancel_request |
The Esc path |
tui/ui/event_loop.rs: Ctrl+C CancelTurn |
Same for Ctrl+C |
Compact-only (a manual /compact with no in-flight turn) still cancels just the pass.
Evidence actually run
live_compaction_tokens_include_tool_results_after_the_billed_prompt — 1 passed
compact_interrupt_stops_the_turn_not_just_the_pass — 1 passed
cargo fmt --all -- --check — pass
cargo clippy -p codewhale-tui --all-targets --all-features --locked -D warnings — pass
Not verified
- No run against a live long turn. The token formula is unit-tested; the engine path end-to-end is not.
- The full
codewhale-tui lib suite (~12k tests) has not been run.
- Commit is local-only, not pushed.
- Codex's compact-and-retry on
ContextWindowExceeded was not adopted — the gate now sees live tokens, but there is still no recovery if a single request overshoots anyway. Worth a follow-up decision.
Filed after the fix, to record work that had no ticket. Code is on local
main, not pushed:11501c529"fix(tui): auto-compact on live mid-turn tokens; Esc stops the turn".Problem
A long turn could exhaust the context window without auto-compact ever firing, even well past the 80% threshold.
The check was already at the right seam — pre-next-request in
crates/tui/src/core/engine/turn_loop.rs(~995), after tools have been appended, the same place GrokBuild callscheck_auto_compact_neededand Codex runsCompactionPhase::MidTurn. The signal was wrong.The gate used
max(last billed prompt, /4 estimate of the whole list). After a prompt billed at ~70%, tool results land in the transcript but not in that bill. Whenever the crude estimator undercounted the full list below the last bill,max()returned the stale billed prompt and every new tool result was invisible to the gate. Context then ran out at the next sample.GrokBuild's formula is exact prior count plus a byte-estimate of the items appended since the last response; Codex additionally compacts-and-retries on
ContextWindowExceeded.Note: this is not a mid-tool-call compaction. The watermark is taken at
add_parent_usage, before this step's assistant/tool messages are appended; the pass still runs at the pre-request boundary after tools complete.Fix (a) — live token signal
core/turn.rs:note_parent_prompt_len+live_input_tokens_for_compactioncore/engine/turn_loop.rs, afteradd_parent_usagecore/engine/turn_loop.rscompact gateFix (b) — interrupt semantics
Esc during a compact used to call only
try_cancel_compactionand continue, so the turn loop'scompaction_cancelarm fired and the turn resumed. Codex and GrokBuild both treat an interrupt as stopping the turn, with the compact as collateral.tui/ui/compaction_flow.rs:compact_interrupt_should_stop_turntui/ui.rs:escape_cancel_requesttui/ui/event_loop.rs: Ctrl+CCancelTurnCompact-only (a manual
/compactwith no in-flight turn) still cancels just the pass.Evidence actually run
live_compaction_tokens_include_tool_results_after_the_billed_prompt— 1 passedcompact_interrupt_stops_the_turn_not_just_the_pass— 1 passedcargo fmt --all -- --check— passcargo clippy -p codewhale-tui --all-targets --all-features --locked -D warnings— passNot verified
codewhale-tuilib suite (~12k tests) has not been run.ContextWindowExceededwas not adopted — the gate now sees live tokens, but there is still no recovery if a single request overshoots anyway. Worth a follow-up decision.