Skip to content

fix(codex): context meter tracks the full-history resend + recover from context-window rejections (#174) - #175

Merged
justrach merged 1 commit into
mainfrom
fix/174-codex-context-meter
Jul 12, 2026
Merged

fix(codex): context meter tracks the full-history resend + recover from context-window rejections (#174)#175
justrach merged 1 commit into
mainfrom
fix/174-codex-context-meter

Conversation

@justrach

Copy link
Copy Markdown
Owner

The bug (#174)

A long gpt-5.6-sol session at Extra high reads 95k/270k ctx (35% · compact@216k) and then every request dies with Your input exceeds the context window of this model — permanently wedged.

The meter (last_context_tokens) copies the WS-chained usage.total_tokens, which tracks a conversation the server has pruned (prior-turn reasoning is discarded from chained context). But runTurn closes the WS per turn, so every turn's first request resends the full local history — including every retained encrypted reasoning item. That resend is what the backend measures, it can be ~3x the chained number, and it's the request that gets rejected — so no corrective usage ever arrives, the meter never crosses compact@216k, and neither the pre-turn compaction nor the ApiError recovery ever fires.

codex CLI avoids this by correcting its meter with an estimate of reasoning items the server accounting excluded (ContextManager::get_non_last_reasoning_items_tokens, confirmed against openai/codex).

The fix

  1. Meter: recordUsageResponses takes max(server total, full-input estimate) — serialized input bytes / 4 via a zero-alloc counting discard writer. compact@80% now fires before the wall.
  2. Recovery: an exceeds the context window rejection pins the meter to the window, so the existing ApiError compact-and-recover path engages — rescues already-wedged sessions on their next input.
  3. Reasoning-aware trim: compact() first drops Responses reasoning items older than the last user message (dropPriorTurnReasoning) — the backend discards those from chained context anyway, and they dominate resend bloat — so the compaction request itself fits under the wall it's escaping. Current-turn reasoning stays (required between a function_call and its output).

Verification (scripted Responses mock: fat reasoning items, chained usage lying at total_tokens=100)

scenario main (pre-fix) this branch
meter over 6 turns stuck at 100 100 → 2k → 4k → … → 10k (tracks the real resend)
one mid-session context rejection turn dies, meter stays stale compacts + continues (context_tokens 4115 → 100)
backend rejects EVERY oversized body all 6 turns die identically — wedged (the reported symptom, reproduced) every rejection compacts and the session keeps working

Unit tests for the estimator and the reasoning trim; zig build + zig build test clean.

Closes #174. Related: #165 (this is the accounting consequence of the delta transport).

…om context-window rejections (#174)

Three-part fix for the wedged-session failure (meter reads 95k/270k while
the backend rejects every request for exceeding the window):

- recordUsageResponses takes max(server total, full-input estimate): the
  WS-chained total_tokens tracks a conversation the server has pruned
  (prior-turn reasoning is discarded from chained context), while every
  turn's FIRST request resends the full local history including all
  retained encrypted reasoning items. The meter now reflects what that
  resend costs (serialized input bytes / 4 via a counting discard writer,
  zero-alloc), so compact@80% fires before the wall — the same correction
  codex CLI applies (ContextManager get_non_last_reasoning_items_tokens).
- 'exceeds the context window' rejections pin the meter to the window:
  the rejected request never returns usage to correct a stale meter, so
  without this the ApiError compact-and-recover gate can never pass and
  the session wedges permanently (every retry resends the same oversized
  history).
- compact() drops prior-turn Responses reasoning items before the summary
  request (dropPriorTurnReasoning): the backend discards them from chained
  context anyway, and they dominate resend bloat on long high-effort
  sessions — this keeps the compaction request itself under the wall it
  is escaping. Current-turn reasoning stays (required between a
  function_call and its output).

Verified against a scripted Responses mock (fat reasoning items, chained
usage lying at total_tokens=100): meter grows 100 -> 10k over 6 turns
instead of sitting at 100; a mid-session context rejection compacts and
the session continues (history collapse visible as context_tokens
4115 -> 100); with the mock rejecting EVERY oversized body, every
rejection now recovers instead of wedging. Unit tests for the estimator
and the reasoning trim.

Closes #174

Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

codex: context meter reads the WS-chained total, so full-history resends blow past the window at "35% ctx" and the session wedges (no auto-compact)

1 participant