Skip to content

fix(agent): decay protected summaries after restart - #57835

Closed
harjothkhara wants to merge 12 commits into
NousResearch:mainfrom
harjothkhara:fix/compression-restart-summary-decay
Closed

fix(agent): decay protected summaries after restart#57835
harjothkhara wants to merge 12 commits into
NousResearch:mainfrom
harjothkhara:fix/compression-restart-summary-decay

Conversation

@harjothkhara

@harjothkhara harjothkhara commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Decays protect_first_n after 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-memory compression_count / _previous_summary. After a gateway restart those are reset, so compress() 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor (no behavior change)
  • New skill (bundled or hub)

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 default protect_first_n=3 restart/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

How to Test

  1. Red proof on current main before the fix:
    scripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py -- -k resume_handoff -q
    • The new regression test failed because the old summary remained in the compressed result.
  2. Green focused proof after the fix:
    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 -- -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:cacheprovider
    • 29 tests passed.
  3. Full focused continuity file:
    scripts/run_tests.sh tests/agent/test_context_compressor_summary_continuity.py -- -q -p no:cacheprovider
    • 16 tests passed.
  4. Static / compatibility checks:
    • .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.py
    • git diff --check

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 pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Not applicable.

2026-07-13 shepherding refresh

  • Rebased the contributor-owned branch from 349 commits behind onto then-current origin/main, preserving contributor authorship/history. Main advanced by one commit afterward; GitHub still reports the PR cleanly mergeable.
  • Repaired a protected-tail boundary bug found by the private maintainer review: live tail turns before a fossil handoff now remain verbatim instead of entering the lossy summarizer input, fossil summaries cannot satisfy the surviving-user guard, and fallback telemetry counts only messages actually summarized.
  • Added deterministic regressions for protected-tail preservation, user-role fossils, an all-fossil tail, and abort-then-retry wide self-healing. The restart probe's containment invariant, retry behavior, reserved marker semantics, and emitted-tail telemetry are now documented in code.
  • Focused proof on final head 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:cacheprovider177 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.
  • Private cross-review: grogu confirmed the central bounded-probe/self-heal design and its load-bearing containment invariant; all concrete edge-case hardening requested in the completed pass was added. Mario was unavailable because the local Codex runner rejected its configured max reasoning value.
  • GitHub Actions run 29300630299 is 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.
  • Duplicate-work status is unchanged: issue [Bug]: protect_first_n decay state is in-memory — a gateway restart re-protects old compaction summaries, head grows unboundedly #57814 remains open and the timeline still points only to this implementation.

@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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P2 Medium — degraded but workaround exists labels Jul 3, 2026
@harjothkhara
harjothkhara marked this pull request as ready for review July 3, 2026 18:34
@harjothkhara
harjothkhara force-pushed the fix/compression-restart-summary-decay branch 3 times, most recently from 4c01cac to 785783a Compare July 14, 2026 02:15

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:3141 does not retain the full self-heal scan after an abort. The wide scan populates _previous_summary at :3153-3157, but an abort returns the original transcript at :3229-3265. The retry then fails the not self._previous_summary condition, scans only through compress_end, and clears that state at :3169-3175 if 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_end abort/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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 15, 2026
harjothkhara and others added 12 commits July 15, 2026 11:45
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>
@harjothkhara
harjothkhara force-pushed the fix/compression-restart-summary-decay branch from 785783a to d556501 Compare July 15, 2026 18:52
@harjothkhara

Copy link
Copy Markdown
Contributor Author

Good catch — confirmed and fixed. The abort/retry path did strand the rehydrated state.

Root cause: the first-compaction self-heal scan populates _previous_summary from a beyond-window fossil, but the auth/network/abort_on_summary_failure abort returned the transcript unchanged without rolling that back. The retry (still compression_count == 0, now with a truthy _previous_summary) took the narrow rescan through compress_end, missed the tail fossil, and then discarded the rehydrated summary as cross-session leakage — copying the fossil forward as a stacked summary, exactly defeating this PR's restart-decay fix.

Fix (d556501): snapshot _previous_summary before the scan and restore it on the abort branch, so an aborted compaction is a true no-op and the retry re-runs the full first-compaction scan. Went with the "restore rehydrated state on abort" option since it also closes the discard path, not just the scan window.

Added test_restart_fossil_survives_summary_abort_then_retry — beyond-window fossil, first attempt aborts, asserts _previous_summary is rolled back and the retry still folds the fossil to a single summary. Red before the fix, green after; full compressor suite green. Also rebased onto current main.

teknium1 pushed a commit that referenced this pull request Jul 22, 2026
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.
teknium1 pushed a commit that referenced this pull request Jul 22, 2026
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.
teknium1 added a commit that referenced this pull request Jul 22, 2026
…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.
teknium1 pushed a commit that referenced this pull request Jul 22, 2026
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.
teknium1 added a commit that referenced this pull request Jul 22, 2026
…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.
@teknium1

Copy link
Copy Markdown
Contributor

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!

@teknium1 teknium1 closed this Jul 22, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
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.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…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.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
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.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…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.
teknium1 pushed a commit that referenced this pull request Aug 15, 2026
…discard

A degenerate compress_end can hide an in-window handoff past the cut; the
#57835 guard then cleared a valid same-session _previous_summary (#83248).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) 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.

[Bug]: protect_first_n decay state is in-memory — a gateway restart re-protects old compaction summaries, head grows unboundedly

3 participants