fix(compression): make the attempt cap config-driven and enforce it at every compression site - #69315
Merged
Merged
Conversation
….max_attempts) The conversation loop hardcodes max_compression_attempts = 3. Sessions that legitimately need more rounds are stranded: on a restart history reload, incompressible tool schemas can keep the per-request estimate above the compressor threshold even though the message floor compresses correctly, so three rounds cannot clear it and the turn dies with "Context length exceeded: max compression attempts (3) reached" — the same failure class as #62605, where the rough estimate similarly leaves 3 retries short. Make the cap a config key, compression.max_attempts: - default 3 = identical to today, so an unset key is behavior-neutral; - parsed and validated in agent_init alongside the other compression.* keys (>= 1, hard-capped at 10, non-integer values fall back to 3), attached as agent.max_compression_attempts; - the loop reads it via getattr(agent, "max_compression_attempts", 3), so objects without the attribute keep the prior behavior; - documented in the DEFAULT_CONFIG compression block. Tests pin the parse/validate/attach seam: default preserved, custom value honored, floor and ceiling enforced, garbage tolerated, and the loop-side getattr degradation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the salvaged #64010 (Kenmege) and #63870 (dombejar) commits, making one resolved compression.max_attempts cap govern ALL per-turn compression attempt sites: - conversation_loop: resolve max_compression_attempts ONCE at turn start (it was previously re-resolved inside the API-call loop) and route the pre-API pressure gate through it — that gate still hardcoded 'compression_attempts < 3' and logged 'attempt=%s/3'. - conversation_loop: the salvaged post-tool compaction gate now uses the resolved cap instead of a hardcoded 3. - turn_context: the preflight compaction loop was 'for _pass in range(3)'; it now sizes itself from the same resolved cap. - agent_init: harden the max_attempts parser — reject booleans (bool subclasses int; 'true' would coerce to 1), reject fractional floats instead of truncating them, keep accepting integral floats and numeric strings; anything else falls back to 3 (floor 1, ceiling 10 unchanged). - tests: replace #63870's inspect.getsource source-shape test with behavioral loop tests (post-tool compaction fires <= cap times per turn, shares its budget with the pre-API gate, resets between turns); add an e2e test proving a 4th preflight pass runs at config cap=6 while the unset default still stops at 3; extend the #64010 config tests with the bool/float parser semantics. Salvages #64010 by @Kenmege and #63870 by @dombejar.
Contributor
૮ >ﻌ< ა ci reviewrunning on 136deab looks good to me! |
This was referenced Jul 22, 2026
teknium1
added a commit
that referenced
this pull request
Jul 23, 2026
…ght flow Relocates the #20424 wiring: the preflight region moved out of run_agent.py into agent/turn_context.py (and through the compression.max_attempts unification, #69315), so the contributor's elif branch is reapplied at its current home as the else arm of the threshold dispatch chain. Integration contracts: - Byte-identical default: the built-in ContextCompressor inherits ContextEngine.should_compress_preflight() -> False, so the default path performs no compression and touches no turn bookkeeping (pinned by test_builtin_compressor_default_sub_threshold_path_unchanged). - Attempt-cap: the engine gets exactly ONE compress() pass per turn, mutually exclusive with the cap-bounded threshold multi-pass loop, so turn-start passes stay within the resolved compression.max_attempts budget in every case. - No-op blocking (#64382 / 377244f): an engine pass that no-ops (_compress_context returns the input list object) neither sets nor clears preflight_compression_blocked and does not re-baseline the flush history — a sub-threshold maintenance no-op proves nothing about over-threshold compressibility. - Engine exceptions are swallowed at debug level; cooldown/defer/ codex-native gates run before the hook is ever consulted. Salvaged from #20424 by @Beandon13. Fixes #20316.
This was referenced Jul 23, 2026
Merged
Merged
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…ght flow Relocates the NousResearch#20424 wiring: the preflight region moved out of run_agent.py into agent/turn_context.py (and through the compression.max_attempts unification, NousResearch#69315), so the contributor's elif branch is reapplied at its current home as the else arm of the threshold dispatch chain. Integration contracts: - Byte-identical default: the built-in ContextCompressor inherits ContextEngine.should_compress_preflight() -> False, so the default path performs no compression and touches no turn bookkeeping (pinned by test_builtin_compressor_default_sub_threshold_path_unchanged). - Attempt-cap: the engine gets exactly ONE compress() pass per turn, mutually exclusive with the cap-bounded threshold multi-pass loop, so turn-start passes stay within the resolved compression.max_attempts budget in every case. - No-op blocking (NousResearch#64382 / 0e73a78): an engine pass that no-ops (_compress_context returns the input list object) neither sets nor clears preflight_compression_blocked and does not re-baseline the flush history — a sub-threshold maintenance no-op proves nothing about over-threshold compressibility. - Engine exceptions are swallowed at debug level; cooldown/defer/ codex-native gates run before the hook is ever consulted. Salvaged from NousResearch#20424 by @Beandon13. Fixes NousResearch#20316.
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.
Summary
The per-turn compression attempt cap is now config-driven (
compression.max_attempts, default 3) and enforced consistently at every compression site — including the post-tool compaction gate, which previously had no cap at all and could compact after every tool response for the lifetime of a turn.Changes
agent/agent_init.py: parse + validatecompression.max_attempts(floor 1, ceiling 10, default 3); parser hardened to reject booleans (bool subclasses int —truewould coerce to 1) and fractional floats (rejected, not truncated), while accepting integral floats and numeric strings.agent/conversation_loop.py: resolvemax_compression_attemptsonce at turn start; the pre-API pressure gate (previously hardcoded< 3, loggedattempt=%s/3), the overflow/413 retry handlers, and the newly-capped post-tool gate all consume the same counter against the same resolved cap.agent/turn_context.py: turn-start preflight compaction loop wasfor _pass in range(3); now sized from the same resolved cap.hermes_cli/config.py/cli-config.yaml.example:compression.max_attemptsdocumented in DEFAULT_CONFIG (default 3 = behavior-neutral when unset).tests/: replaced fix(compaction): cap post-tool compression attempts per turn #63870'sinspect.getsourcesource-shape test with behavioral loop tests (post-tool compaction fires ≤ cap times per turn, shares its budget with the pre-API gate, budget resets per turn); added an e2e test proving a 4th preflight pass runs at cap=6 while the unset default still stops at 3; extended fix(agent): make the compression retry cap config-driven (compression.max_attempts) #64010's config tests with the bool/float parser semantics.Validation
compression.max_attemptsper turn (shared counter)< 3regardless of configmax_attempts: 6range(3))max_attempts: true/4.7Targeted tests: 67 passed across the five compression-focused files;
-k 'compression and (attempt or cap or retry)'sweep overtests/run_agent/ tests/agent/: 27 passed, 0 failed.Credit
Salvages #64010 by @Kenmege and #63870 by @dombejar; unified so the pre-API gate, retry loop, preflight passes, and the previously-uncapped post-tool gate all honor compression.max_attempts.
Infographic