fix(agent): defer preflight compression by projecting real usage instead of a fixed growth tolerance - #80997
Closed
chesterXalan wants to merge 1 commit into
Closed
Conversation
…owth tolerance The rough preflight estimate intentionally overestimates, but not by a fixed margin: CJK text is counted at ~1.7x its o200k cost and Responses-mode reasoning replay blobs at several times their billed cost. Heavy sessions show rough estimates 2-3x real usage and compact at 35-55% of the real window, stalling turns for minutes and discarding detail (churn), because the defer guard only tolerated 5% rough growth and sessions that never compressed had no baseline at all. Pair every request's rough estimate (note_request_rough_estimate, recorded in the conversation loop right after the pressure estimate) with the provider's real prompt_tokens in update_from_response(), then defer preflight while projected real usage — last real + rough growth since that reading — stays under the threshold. Rough growth is itself an overestimate of real growth, so the projection is an upper bound and deferring below the threshold is safe; the provider's context-overflow handler remains the backstop. The baseline no longer ratchets on defer: it is refreshed by the response pairing, and advancing it without a matching real reading would shrink apparent growth and defer on stale data.
kshitijk4poor
added a commit
that referenced
this pull request
Aug 7, 2026
…dings) Two docstring corrections on top of the salvaged #80997 fix — behavior unchanged, both verified against the code: - The 'rough growth over-counts every content class' claim is false for Cyrillic/Greek/Thai/Arabic (chars/4 vs ~2-3 chars/token on o200k): growth there can under-count up to ~2x (#62605's direction). Document the real backstops instead: an at/over-threshold real reading clears the baseline (post-response gate fires on real usage within one call) and the provider overflow handler compacts reactively. - Document the two measurement bases (turn-prologue raw messages vs the loop's fully assembled request that seeds the baseline) and why the prologue's smaller basis can only OVER-defer — the loop's pre-API pressure check re-runs the projection with the aligned basis before every provider call, so a prologue over-defer never skips a needed compaction.
Collaborator
|
Merged via #81069 — your commit was cherry-picked onto current main with your authorship preserved (rebase-merge), plus one docs-only follow-up from review correcting the projection's safety claims for non-CJK scripts. Thanks for the excellent analysis and fix — the (rough, real) pairing approach and the churn measurements made this straightforward to validate. |
ma1138569845
pushed a commit
to ma1138569845/dechnicAuditor-agent
that referenced
this pull request
Aug 10, 2026
…dings) Two docstring corrections on top of the salvaged NousResearch#80997 fix — behavior unchanged, both verified against the code: - The 'rough growth over-counts every content class' claim is false for Cyrillic/Greek/Thai/Arabic (chars/4 vs ~2-3 chars/token on o200k): growth there can under-count up to ~2x (NousResearch#62605's direction). Document the real backstops instead: an at/over-threshold real reading clears the baseline (post-response gate fires on real usage within one call) and the provider overflow handler compacts reactively. - Document the two measurement bases (turn-prologue raw messages vs the loop's fully assembled request that seeds the baseline) and why the prologue's smaller basis can only OVER-defer — the loop's pre-API pressure check re-runs the projection with the aligned basis before every provider call, so a prologue over-defer never skips a needed compaction.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…dings) Two docstring corrections on top of the salvaged NousResearch#80997 fix — behavior unchanged, both verified against the code: - The 'rough growth over-counts every content class' claim is false for Cyrillic/Greek/Thai/Arabic (chars/4 vs ~2-3 chars/token on o200k): growth there can under-count up to ~2x (NousResearch#62605's direction). Document the real backstops instead: an at/over-threshold real reading clears the baseline (post-response gate fires on real usage within one call) and the provider overflow handler compacts reactively. - Document the two measurement bases (turn-prologue raw messages vs the loop's fully assembled request that seeds the baseline) and why the prologue's smaller basis can only OVER-defer — the loop's pre-API pressure check re-runs the projection with the aligned basis before every provider call, so a prologue over-defer never skips a needed compaction.
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Stops preflight compression from firing at 35–55% of the model's real context window on sessions where the rough token estimate runs far ahead of provider-reported usage.
estimate_request_tokens_roughintentionally overestimates, but the margin is not a fixed percentage — it depends on content class. Measured against o200k on a live Responses-mode session: CJK text is counted at ~1.7x its tokenizer cost, and encrypted reasoning replay items (codex_reasoning_items, counted at base64-length/4) at several times their billed cost. On a CJK-heavy session with reasoning replay, the preflight estimate reached ~413K tokens while the provider had just reportedprompt_tokens=150Kfor the preceding call — a 2.76x gap. The result is compaction churn: the session compacts minutes-long summary passes at half the real window, discards conversation detail (220 → 71 messages), regrows, and re-fires within the hour.The existing defer guard (#36718) can't help, for two reasons:
max(4096, 5% of threshold)rough growth since the last proven-fit request — a single heavy tool turn exceeds that; andlast_rough_tokens_when_real_prompt_fitis only seeded post-compaction), so a fresh session gets zero deferral — observed as a session force-compacted 24 minutes after it started, at ~35% real usage.The change: pair every request's rough estimate with the provider's real
prompt_tokensfor that same request, then defer preflight while projected real usage stays under the threshold:note_request_rough_estimate()records the request-pressure estimate in the conversation loop right after it is computed;update_from_response()consumes it when real usage arrives and fits, keeping the (rough, real) anchor synchronized on every fitting response — not only right after a compaction;should_defer_preflight_to_real_usage()compares the projection, not a fixed growth allowance, against the threshold. Compression fires when the projection crosses it.Safety: rough growth over-counts every content class relative to the tokenizer, so the projection is an upper bound on real usage and deferring below the threshold cannot skip a needed compaction. The baseline deliberately no longer ratchets inside the defer check — advancing it without a matching real reading would shrink apparent growth and defer on stale data. All existing backstops are untouched: the post-compaction
awaiting_real_usagesingle-turn defer, the anti-thrash verdicts, model-switch state clearing (#23767), and the provider context-overflow error handler as the authoritative last resort.This also improves the opposite failure mode (#62605, rough estimate under-counting for some tokenizers): because the trigger is anchored on provider-reported usage, the projection crosses the threshold earlier than the raw estimate would, instead of trusting an undercount until the server rejects the request.
Related: #36718 (the defer mechanism this generalizes), #62605 (underestimate direction, helped but the output-budget component is out of scope), #58784 (per-class CJK coefficient disputes — moot under this approach, since the trigger self-corrects from real usage regardless of coefficient sign), #14695 (why the rough estimate intentionally overestimates schemas).
Related Issue
No open issue describes the overestimate-churn defect exactly; closest reports are linked above.
Type of Change
Changes Made
agent/context_compressor.py— newnote_request_rough_estimate();update_from_response()pairs the pending estimate with real usage (post-compaction pairing still wins, Bug: Context compression triggers repeatedly after fresh compress — last_prompt_tokens=-1 not updated until next API call #36718);should_defer_preflight_to_real_usage()projects real usage instead of tolerating fixed growth;_pending_request_rough_tokensreset alongside the existing calibration state at all four reset sites (init, session end, cross-session guard, model switch).agent/conversation_loop.py— record the request-pressure estimate right after it is computed (getattr-guarded for minimal test doubles / third-party context engines).tests/agent/test_context_compressor.py— 4 new deferral tests (projection under/over threshold, no-baseline fallback, no-ratchet) and 4 new pairing tests (pairing, post-compaction precedence, usage-less response preservation, over-threshold clearing); 1 test updated to the projection semantics.tests/run_agent/test_413_compression.py— preflight integration test updated: fires when the projection crosses the threshold.How to Test
pytest tests/agent/test_context_compressor.py tests/run_agent/test_413_compression.py tests/agent/test_preflight_lock_defer.py tests/agent/test_engine_preflight_wire.py tests/run_agent/test_compression_lock_defer.py tests/agent/test_compaction_anti_thrash.py tests/agent/test_compression_anti_thrash_recovery.py -q— 169 passed on this branch.Preflight compression: ~413,524 tokens >= 244,800 thresholdfires while the preceding API call reported ~150K real prompt tokens, and a session can be compacted within its first half hour. After: preflight logs show deferral until projected real usage reaches the threshold; compaction frequency drops from several per session to near zero until real usage genuinely approaches the window./compressor shrink the threshold) and confirm the post-compaction flow is unchanged — one-turn defer while awaiting real usage, then normal operation.Checklist
Code
fix(scope):,feat(scope):, etc.)tests/run on my machine has environment-dependent failures (i18n catalogs, models.dev fetch, etc.) that are identical on cleanmainDocumentation & Housekeeping
should_defer_preflight_to_real_usage/note_request_rough_estimatecli-config.yaml.exampleif I added/changed config keys — N/A (no config changes)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A