fix(agent): make the compression retry cap config-driven (compression.max_attempts) - #64010
fix(agent): make the compression retry cap config-driven (compression.max_attempts)#64010Kenmege wants to merge 2 commits into
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 NousResearch#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>
68eff97 to
c040037
Compare
|
CI triage on the slice 8/8 failure: all 7 failed tests are in This PR touches no dependencies ( |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment
30 PRs reviewed (batch: 64044-63999). See aggregate summary at PR 64044.
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing a real current-head compression limit. The configuration approach fits the existing compression section and the patch applies cleanly, but one shared retry path remains hardcoded.
Problems
agent/conversation_loop.py:1046still gates pre-API compaction withcompression_attempts < 3, andagent/conversation_loop.py:1054still logs/3. The proposedmax_compression_attemptslocal is initialized only later in the loop. This is the pre-API path described by #62605, socompression.max_attempts: 6would not permit more than three preflight compactions.- The added parser uses
int(...), which accepts booleans and truncates fractional numeric input. That does not match the PR's stated fallback behavior for non-integer values.
Suggested changes
- Resolve the cap before the pre-API guard and use it for both the guard and attempt logging; add an end-to-end loop test that proves a cap above three reaches a fourth pre-API compression.
- Reject booleans and non-integral numeric values explicitly, with regression coverage.
Automated hermes-sweeper review.
| max_compression_attempts = 3 | ||
| # Config-driven via compression.max_attempts (parsed + validated in | ||
| # agent_init). Default 3 preserves the prior hardcoded behavior. | ||
| max_compression_attempts = getattr(agent, "max_compression_attempts", 3) |
There was a problem hiding this comment.
This local is initialized after the pre-API compaction branch, which still uses compression_attempts < 3 and logs /3 on current main (agent/conversation_loop.py:1046-1054). Please resolve the configured cap before that branch and use it there too; otherwise compression.max_attempts: 6 still allows only three preflight compressions on the #62605 path.
…pression-attempts
|
Updated this branch to current |
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.
Follow-up to the salvaged NousResearch#64010 (Kenmege) and NousResearch#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 NousResearch#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 NousResearch#64010 config tests with the bool/float parser semantics. Salvages NousResearch#64010 by @Kenmege and NousResearch#63870 by @dombejar.
What does this PR do?
Makes the conversation loop's compression retry cap configurable:
compression.max_attempts(default 3 — identical to today's hardcoded value, so an unset key is behavior-neutral).Why: the cap is currently
max_compression_attempts = 3, hardcoded in the loop. Sessions that legitimately need more rounds are stranded with no recourse: 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 — three rounds cannot clear it and the turn dies withContext length exceeded: max compression attempts (3) reached. This is the same failure class as #62605, where the rough token estimate leaves 3 retries short of converging. Observed live on a 272K-context lane with a large tool surface: raising the cap to 6 resolved the dead-end with no other change.How:
agent_initalongside the othercompression.*keys:>= 1enforced, hard-capped at 10, non-integer values fall back to 3; attached asagent.max_compression_attempts.getattr(agent, "max_compression_attempts", 3), so any object without the attribute keeps today's behavior.DEFAULT_CONFIGcompression block.All existing exhaustion/retry semantics (413 payload path, context-length path,
/compress–/newadvice) are unchanged — only the bound becomes configurable.Related Issue
Related: #62605 (auto-compression fails after 3 retries when the estimate underruns) — this knob is the direct operator-side mitigation for that class; it does not claim to fix the estimator itself.
Type of Change
Changes Made
agent/agent_init.py— parse + validatecompression.max_attempts(default 3, floor 1, cap 10, garbage-tolerant), attach to the agent.agent/conversation_loop.py— replace the hardcoded3with thegetattrpickup (default preserved).hermes_cli/config.py—DEFAULT_CONFIGdocumentation for the new key.cli-config.yaml.example— the new key documented in thecompression:block.tests/agent/test_compression_max_attempts_config.py— six tests pinning the parse/validate/attach seam (default, custom, ceiling, floor, garbage, loop-side getattr degradation), following the existingtest_codex_gpt55_autoraise_notice.pypattern of building a realAIAgentunder a monkeypatched config.How to Test
pytest tests/agent/test_compression_max_attempts_config.py -q— 6 new tests pass.agent.max_compression_attempts == 3and the loop bound is unchanged.compression.max_attempts: 6in config.yaml and restart — the additional rounds allow the preflight to converge (verified live on a tool-schema-heavy 272K lane).Checklist
Code
fix(scope):,feat(scope):, etc.)scripts/run_tests.sh, ~38k tests). Result reported honestly: the failures present are pre-existing at upstreammainand platform/environment-dependent on macOS — identical failure set re-verified at the base commit vs this branch with the same invocation.Documentation & Housekeeping
DEFAULT_CONFIGcomment block documents the key, when to raise it, and its bounds) — README/docs/N/Acli-config.yaml.example(newmax_attemptsentry in thecompression:block, matching the surrounding key documentation style)CONTRIBUTING.md/AGENTS.md— N/A (no architecture/workflow change)Screenshots / Logs
Live dead-end this resolves (gateway restart, 272K lane, large tool surface):
With
compression.max_attempts: 6, the same reload converges and the turn completes; sessions without the key see byte-identical behavior to today.🤖 Generated with Claude Code