Skip to content

fix(agent): defer preflight compression by projecting real usage instead of a fixed growth tolerance - #80997

Closed
chesterXalan wants to merge 1 commit into
NousResearch:mainfrom
chesterXalan:preflight-defer-real-usage-projection
Closed

fix(agent): defer preflight compression by projecting real usage instead of a fixed growth tolerance#80997
chesterXalan wants to merge 1 commit into
NousResearch:mainfrom
chesterXalan:preflight-defer-real-usage-projection

Conversation

@chesterXalan

Copy link
Copy Markdown
Contributor

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_rough intentionally 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 reported prompt_tokens=150K for 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:

  • it tolerates only max(4096, 5% of threshold) rough growth since the last proven-fit request — a single heavy tool turn exceeds that; and
  • sessions that never compressed have no baseline at all (last_rough_tokens_when_real_prompt_fit is 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_tokens for that same request, then defer preflight while projected real usage stays under the threshold:

projected_real = last_real_prompt_tokens + (rough_now − rough_at_last_real)
  • 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_usage single-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

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/context_compressor.py — new note_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_tokens reset 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

  1. 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.
  2. Churn repro: run a long CJK-heavy conversation (or any session that accumulates encrypted reasoning replay items) against a ~272K-context model and watch the logs. Before: Preflight compression: ~413,524 tokens >= 244,800 threshold fires 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.
  3. Regression check: force a compaction (/compress or shrink the threshold) and confirm the post-compaction flow is unchanged — one-turn defer while awaiting real usage, then normal operation.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the compression/conversation-loop suites listed above (169 passed); the full tests/ run on my machine has environment-dependent failures (i18n catalogs, models.dev fetch, etc.) that are identical on clean main
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (arm64), Python 3.11
  • Verified live on a production gateway (Codex Responses transport, gpt-5.6-sol @ 272K): deploy, graceful restart, warmup call healthy

Documentation & Housekeeping

  • I've updated relevant documentation (docstrings) — behavior documented in should_defer_preflight_to_real_usage / note_request_rough_estimate
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config changes)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — N/A (pure Python, no platform-specific paths)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

…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.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/compression Context compression and continuation sessions P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
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.
@kshitijk4poor

Copy link
Copy Markdown
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.

@chesterXalan
chesterXalan deleted the preflight-defer-real-usage-projection branch August 7, 2026 14:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants