fix(agent): decay protected summaries after restart - #57835
fix(agent): decay protected summaries after restart#57835harjothkhara wants to merge 12 commits into
Conversation
4c01cac to
785783a
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the thorough restart-decay coverage. The core premise is verified on current main: agent/context_compressor.py:2573-2588 derives decay only from in-memory state, while the persisted-handoff rehydration happens after head-boundary computation at agent/context_compressor.py:3066-3108.
Problems
agent/context_compressor.py:3141does not retain the full self-heal scan after an abort. The wide scan populates_previous_summaryat:3153-3157, but an abort returns the original transcript at:3229-3265. The retry then fails thenot self._previous_summarycondition, scans only throughcompress_end, and clears that state at:3169-3175if the fossil was in the protected tail. The existing abort/retry test uses a handoff inside the normal scan window, so it does not cover this path.
Suggested changes
- Scan the full resumed transcript whenever
compression_count < 1, or restore rehydrated state on abort; add the beyond-compress_endabort/retry regression.
Automated hermes-sweeper review.
| summary_search_start = 1 if messages and messages[0].get("role") == "system" else 0 | ||
| summary_idx, summary_body = self._find_latest_context_summary( | ||
| summary_search_end = compress_end | ||
| if self.compression_count < 1 and not self._previous_summary: |
There was a problem hiding this comment.
This condition makes the abort/retry guarantee false for a fossil beyond compress_end: the first wide scan sets _previous_summary at lines 3153-3157, an abort returns the transcript unchanged, and retry skips the wide scan because _previous_summary is no longer empty. Scan on compression_count < 1 alone, or restore the prior state on abort, and add a protected-tail fossil abort/retry regression.
The first-compaction self-heal scan (compression_count < 1) populates _previous_summary from a restart fossil that drifted past the decay probe. When summary generation then aborts (auth / network / abort_on_summary_failure) and returns the transcript unchanged, that rehydrated state was left behind. The retry — still compression_count == 0 but now with a truthy _previous_summary — took the narrow rescan window (through compress_end), missed the beyond-window fossil, and then discarded the rehydrated summary as cross-session leakage, copying the fossil forward as a stacked summary and defeating the restart-decay fix this PR adds. Snapshot _previous_summary before the scan and restore it on the abort path so an aborted compaction is a true no-op. Adds a beyond-window abort/retry regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
785783a to
d556501
Compare
|
Good catch — confirmed and fixed. The abort/retry path did strand the rehydrated state. Root cause: the first-compaction self-heal scan populates Fix ( Added |
protect_first_n decay state (compression_count / _previous_summary) is in-memory only, so a gateway restart re-protected the persisted handoff summary and head fossils, growing the head unboundedly across restart+compaction cycles (#57814). _effective_protect_first_n now probes a bounded resumed-head window for a persisted handoff summary (by metadata or content prefix) and decays protection when one is found, before compress_start is computed. The first post-restart compaction self-heals: stacked summary fossils are folded into the next summary prompt instead of preserved verbatim, rehydration is rolled back on abort, deterministic fallback carries a bounded redacted previous-summary snapshot, and forced user-leading merged summaries keep the live tail request after the summary end marker so they stay rehydratable. Squashed reapply of the 12-commit series from PR #57835 onto current main (branch was 1210 commits behind; single add/add conflict with the task-snapshot grounding helpers resolved by keeping both). Fixes #57814.
protect_first_n decay state (compression_count / _previous_summary) is in-memory only, so a gateway restart re-protected the persisted handoff summary and head fossils, growing the head unboundedly across restart+compaction cycles (#57814). _effective_protect_first_n now probes a bounded resumed-head window for a persisted handoff summary (by metadata or content prefix) and decays protection when one is found, before compress_start is computed. The first post-restart compaction self-heals: stacked summary fossils are folded into the next summary prompt instead of preserved verbatim, rehydration is rolled back on abort, deterministic fallback carries a bounded redacted previous-summary snapshot, and forced user-leading merged summaries keep the live tail request after the summary end marker so they stay rehydratable. Squashed reapply of the 12-commit series from PR #57835 onto current main (branch was 1210 commits behind; single add/add conflict with the task-snapshot grounding helpers resolved by keeping both). Fixes #57814.
…he decay scan Composing #57835's multi-fossil summary scan with #47274's merged-handoff unwrap: when the restart-decay path pulls a merged handoff into the compression window, its genuine prior-tail user content must enter the summarizer input (folded into the fresh summary) rather than being dropped with the summary row. Standalone handoffs still drop. The continuity test now pins the composed contract: recovered verbatim OR via summarizer input, never silently deleted, never duplicated.
protect_first_n decay state (compression_count / _previous_summary) is in-memory only, so a gateway restart re-protected the persisted handoff summary and head fossils, growing the head unboundedly across restart+compaction cycles (#57814). _effective_protect_first_n now probes a bounded resumed-head window for a persisted handoff summary (by metadata or content prefix) and decays protection when one is found, before compress_start is computed. The first post-restart compaction self-heals: stacked summary fossils are folded into the next summary prompt instead of preserved verbatim, rehydration is rolled back on abort, deterministic fallback carries a bounded redacted previous-summary snapshot, and forced user-leading merged summaries keep the live tail request after the summary end marker so they stay rehydratable. Squashed reapply of the 12-commit series from PR #57835 onto current main (branch was 1210 commits behind; single add/add conflict with the task-snapshot grounding helpers resolved by keeping both). Fixes #57814.
…he decay scan Composing #57835's multi-fossil summary scan with #47274's merged-handoff unwrap: when the restart-decay path pulls a merged handoff into the compression window, its genuine prior-tail user content must enter the summarizer input (folded into the fresh summary) rather than being dropped with the summary row. Standalone handoffs still drop. The continuity test now pins the composed contract: recovered verbatim OR via summarizer input, never silently deleted, never duplicated.
|
Merged via #69342 (commit 76e17bc). Your 12-commit series was squash-reapplied with your authorship preserved (the branch was 1210 commits behind — per-commit cherry-pick wasn't feasible). Follow-ups adapted test pins to task-snapshot grounding and composed your decay scan with the merged-handoff unwrap contract. Thanks! |
protect_first_n decay state (compression_count / _previous_summary) is in-memory only, so a gateway restart re-protected the persisted handoff summary and head fossils, growing the head unboundedly across restart+compaction cycles (NousResearch#57814). _effective_protect_first_n now probes a bounded resumed-head window for a persisted handoff summary (by metadata or content prefix) and decays protection when one is found, before compress_start is computed. The first post-restart compaction self-heals: stacked summary fossils are folded into the next summary prompt instead of preserved verbatim, rehydration is rolled back on abort, deterministic fallback carries a bounded redacted previous-summary snapshot, and forced user-leading merged summaries keep the live tail request after the summary end marker so they stay rehydratable. Squashed reapply of the 12-commit series from PR NousResearch#57835 onto current main (branch was 1210 commits behind; single add/add conflict with the task-snapshot grounding helpers resolved by keeping both). Fixes NousResearch#57814.
…he decay scan Composing NousResearch#57835's multi-fossil summary scan with NousResearch#47274's merged-handoff unwrap: when the restart-decay path pulls a merged handoff into the compression window, its genuine prior-tail user content must enter the summarizer input (folded into the fresh summary) rather than being dropped with the summary row. Standalone handoffs still drop. The continuity test now pins the composed contract: recovered verbatim OR via summarizer input, never silently deleted, never duplicated.
protect_first_n decay state (compression_count / _previous_summary) is in-memory only, so a gateway restart re-protected the persisted handoff summary and head fossils, growing the head unboundedly across restart+compaction cycles (NousResearch#57814). _effective_protect_first_n now probes a bounded resumed-head window for a persisted handoff summary (by metadata or content prefix) and decays protection when one is found, before compress_start is computed. The first post-restart compaction self-heals: stacked summary fossils are folded into the next summary prompt instead of preserved verbatim, rehydration is rolled back on abort, deterministic fallback carries a bounded redacted previous-summary snapshot, and forced user-leading merged summaries keep the live tail request after the summary end marker so they stay rehydratable. Squashed reapply of the 12-commit series from PR NousResearch#57835 onto current main (branch was 1210 commits behind; single add/add conflict with the task-snapshot grounding helpers resolved by keeping both). Fixes NousResearch#57814.
…he decay scan Composing NousResearch#57835's multi-fossil summary scan with NousResearch#47274's merged-handoff unwrap: when the restart-decay path pulls a merged handoff into the compression window, its genuine prior-tail user content must enter the summarizer input (folded into the fresh summary) rather than being dropped with the summary row. Standalone handoffs still drop. The continuity test now pins the composed contract: recovered verbatim OR via summarizer input, never silently deleted, never duplicated.
What does this PR do?
Decays
protect_first_nafter a restart when a persisted context-compression handoff summary is already inside the would-be protected head.Before this change,
_effective_protect_first_n()only decayed from in-memorycompression_count/_previous_summary. After a gateway restart those are reset, socompress()could preserve the old handoff summary verbatim before it had a chance to rehydrate_previous_summary, leaving that old summary as a fossilized protected-head message.The fix keeps the same in-memory decay behavior and detects persisted handoff summaries in the resumed-head region. If one is present, the compressor treats the session as already compacted. On the first post-restart compaction, self-heal scans the transcript for actual Hermes handoff summaries, folds any pre-summary live turns into the next summary prompt, and removes stacked summary fossils instead of preserving them verbatim. If summary generation falls back deterministically, the rehydrated previous summary is carried forward in a bounded redacted snapshot. Summary handoffs outside the bounded decay probe can be folded without decaying first-compaction head protection. When a forced user-leading summary must merge into the active user tail, the active request remains below the summary end marker and the merged summary remains rehydratable on the next compaction.
This intentionally implements the issue's Option 2 + Option 3 path instead of persisting a new compaction-state field. The no-system role pinning change is an adjacent pre-existing bug fix for already-decayed transcripts where the gateway path strips the system message before calling
compress().Related Issue
Fixes #57814
Type of Change
Changes Made
agent/context_compressor.py: pass messages into_effective_protect_first_n()so restart/resume state can be inferred from a persisted handoff summary in the protected head.agent/context_compressor.py: force the inserted/merged summary to keep a user-leading visible transcript when resumed no-system histories decay to an empty protected head.agent/context_compressor.py: align summary metadata/content detection and fold non-summary pre-handoff head turns into the next summary prompt.agent/context_compressor.py: limit restart decay inference to a named resumed-head probe window so unrelated summary-looking live-tail messages do not decay first-compaction protection.agent/context_compressor.py: extend first-pass self-heal folding through the transcript so short, zero-protect_first_n, and beyond-probe restart fossils are not preserved as protected tail.agent/context_compressor.py: preserve a bounded redacted previous-summary snapshot in deterministic fallback summaries.agent/context_compressor.py: keep active user tail content after the summary boundary when forced user-leading summaries merge into tail.agent/context_compressor.py: strip summary end markers even when live tail content follows, so merged summaries rehydrate without swallowing that tail.tests/agent/test_context_compressor_summary_continuity.py: add regression tests for protected-head and defaultprotect_first_n=3restart/resume fossilization cases.tests/agent/test_context_compressor_summary_continuity.py: add stacked-summary, metadata-only, protected-tail, fallback-preservation, zero-protect, beyond-probe, active-tail ordering, late-handoff tail preservation, merged-summary rehydration, live-tail head-protection, and restart-probe boundary regression coverage.tests/agent/test_context_compressor.py: add a no-system resumed-handoff role-ordering regression.Duplicate-work checks
gh issue view 57814 --repo NousResearch/hermes-agent --comments: issue is open with no comments at the time of implementation.gh pr list --repo NousResearch/hermes-agent --state all --search "57814": no matching PRs.gh pr list --repo NousResearch/hermes-agent --state all --search "protect_first_n decay state in-memory gateway restart compaction summaries": no matching PRs."compression protect_first_n summary gateway restart"found fix(compressor): shrink protect_first_n on recompaction (#17344) #17349 closed and feat(compression): add configurable warn_after_compressions threshold (#53876) #53958 unrelated to restart decay.How to Test
scripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py -- -k resume_handoff -qscripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py tests/agent/test_context_compressor.py tests/agent/test_resume_stale_active_task.py -- -k "protect_first_n or context_summary_content or previous_summary or resume_handoff or default_protected_head or stacked_handoffs or metadata_summary_decay or without_system_still_starts or inherited_handoff or historical_prefixed or tail_summary_marker or restart_probe_boundary or protected_tail or fallback_preserves or zero_protect_first_n or beyond_restart_probe or double_compaction_user_tail or merge_into_tail or late_handoff or forced_leading" -q -p no:cacheproviderscripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py -- -q -p no:cacheprovider.venv/bin/ruff check agent/context_compressor.py tests/agent/test_context_compressor_summary_continuity.py tests/agent/test_context_compressor.py tests/agent/test_resume_stale_active_task.py.venv/bin/python scripts/check-windows-footguns.py agent/context_compressor.py tests/agent/test_context_compressor_summary_continuity.py tests/agent/test_context_compressor.py tests/agent/test_resume_stale_active_task.pygit diff --checkChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Not applicable.
2026-07-13 shepherding refresh
origin/main, preserving contributor authorship/history. Main advanced by one commit afterward; GitHub still reports the PR cleanly mergeable.785783af4:scripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py tests/agent/test_context_compressor.py tests/agent/test_resume_stale_active_task.py -- -q -p no:cacheprovider— 177 passed..venv/bin/ruff check agent/context_compressor.py tests/agent/test_context_compressor_summary_continuity.py— passed..venv/bin/python scripts/check-windows-footguns.py agent/context_compressor.py tests/agent/test_context_compressor_summary_continuity.py— passed.git diff --check— passed.maxreasoning value.29300630299is fully green: attribution/history, Ruff+ty, Windows, e2e, all eight test slices, supply-chain scans, both Docker architectures, and the aggregate required-check gate passed.