Skip to content

fix(compression): skip empty post-handoff summary windows (salvage #59526) - #70251

Merged
teknium1 merged 2 commits into
mainfrom
salv/59526-empty-window-guard
Jul 23, 2026
Merged

fix(compression): skip empty post-handoff summary windows (salvage #59526)#70251
teknium1 merged 2 commits into
mainfrom
salv/59526-empty-window-guard

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Skips the wasted aux-LLM summary call (and its noisy abort) when the newest handoff summary consumes the entire compressible window — compress() now early-returns the transcript unchanged instead of dispatching _generate_summary on empty input.

Salvages PR #59526 by @WOLIKIMCHENG — cherry-picked onto current main with authorship preserved (fix(compression): skip empty post-handoff summary windows, including the regression test). @ms-alan's #59737 independently identified the same bug and the same correct early-return site — credit to both; #59737 was opened 7h later without a test, so #59526 is the salvage vehicle.

Fixes the #59496 shape.

The bug (verified on main @ c2c2449)

After the post-handoff slice recompute (turns_to_summarize = pre_summary_turns + messages[summary_idx+1:compress_end], ~L4490-4503), an empty turns_to_summarize still reached _generate_summary(...) unconditionally (~L4548). Post-#69297 a standalone handoff strips to None in _window_row, so a window containing only the handoff row genuinely produces an empty summarizer input — burning an aux LLM call that aborts noisily for zero benefit, every turn the shape recurs.

Not covered by the existing guards: the compress_start >= compress_end guard (#40803, ~L4396) fires at an earlier, different site; the semantic no-op check in conversation_compression.py runs after the wasted call.

Adaptations from the original diff (main moved under it)

  • Ineffective-counter strike routed through _record_ineffective_compression_verdict() instead of the PR's stale bare _ineffective_compression_count += 1fix(compression): persist anti-thrash state across process restarts #69872 made the anti-thrash counter durable, and all strike/clear sites must write through the helper so the persisted copy stays in sync.
  • The skip DOES count as an ineffective strike (deliberate choice, documented here): this mirrors the sibling compress_start >= compress_end guard's behavior exactly — the empty-window shape cannot shrink, so without a strike should_compress() keeps saying yes and every subsequent turn re-fires the same no-op loop. That is the precise failure [Bug]: Infinite Context Compaction Loop (messages=N->N) on low context_length / limit configurations #40803's guard exists to stop; this guard inherits the same contract.
  • Guard placement: after the region recording / chunk_count telemetry, well below the top-of-function per-call state reset — so the fix(compression): reset in-place compaction state on every abort path #69853 _last_compression_attempt_* per-attempt abort-state semantics are untouched (verified by tests/run_agent/test_compression_abort_state_reset.py, all green).
  • _previous_summary rehydration is deliberately kept (not rolled back as the summary-abort path does for fix(agent): decay protected summaries after restart #57835): the rehydrated summary came from a handoff genuinely present in the transcript, which is returned unchanged.
  • New telemetry failure_class = "empty_post_handoff_window" distinguishes this skip from no_compressible_window / insufficient_messages in the attempt log.
  • Regression test adapted to current main: the original fixture no longer produced an empty window on today's boundary logic (a live probe showed _generate_summary being called and a normal compaction succeeding). The fixture was reshaped so the standalone handoff is the only window row, and the test now also pins the failure_class and the strike-through-helper behavior. Kept in the contributor's commit alongside their guard.

Note: the skip returns the input transcript per the standard caller contract for no-op skips (callers gate post-compaction bookkeeping on result is not input; Phase-1 tool-result pruning may still return a pruned copy, exactly as on the sibling guard paths — the contract is unchanged by this PR).

Validation

Check Result
Counterfactual: regression test vs pre-fix origin/main source FAILS (_generate_summary called once on empty window) — bug reproduced
Regression test on this branch PASS
tests/agent/test_context_compressor*.py + tests/run_agent/test_compression_abort_state_reset.py 246 tests, 0 failed
Full sibling sweep: tests/agent/test_compress*.py + test_context_compressor*.py + abort-state suite (rebased tip) 498 tests, 0 failed
ruff check on changed files clean
ast.parse after conflict resolution clean

Attribution

Infographic

empty-window-compression-guard

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 25769d2

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/compression Context compression and continuation sessions sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 23, 2026
@isak-ialogics

Copy link
Copy Markdown
Contributor

CI triage: the blocking slice-4 failure appears unrelated to this compression diff. The same exact test and assertion (tests/gateway/test_config.py::TestApiServerEnvOverride::test_env_key_does_not_reenable_explicitly_disabled_api_server, None == 'secret-key') also fail on current main at c4f5a45d (main run); #70249 and #70247 hit it independently as well. Suggested next action: rebase/rerun after the gateway-config regression on main is fixed rather than changing this patch for that failure.

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 P3 Low — cosmetic, nice to have 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