mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
fix(compression): skip empty post-handoff summary windows
This commit is contained in:
parent
eebc2286fc
commit
34678d2f2e
2 changed files with 78 additions and 0 deletions
|
|
@ -4521,6 +4521,39 @@ This compaction should PRIORITISE preserving all information related to the focu
|
|||
)
|
||||
telemetry["chunk_count"] = 1 if turns_to_summarize else 0
|
||||
|
||||
if not turns_to_summarize:
|
||||
# The newest handoff summary consumed the entire compressible
|
||||
# window (every window row was a standalone handoff that strips
|
||||
# to None, and nothing follows it before compress_end) — there
|
||||
# is nothing new to summarize. Skip the summary call entirely:
|
||||
# without this guard the empty window still reached
|
||||
# _generate_summary, wasting an aux LLM call that aborts
|
||||
# noisily on empty input (#59496). Mirrors the sibling
|
||||
# "no compressable window" guard above (#40803): record an
|
||||
# ineffective strike through the durable write-through helper
|
||||
# so the anti-thrash breaker in should_compress() can stop the
|
||||
# loop — this shape cannot shrink, so every subsequent turn
|
||||
# would otherwise re-fire the same no-op. The rehydrated
|
||||
# _previous_summary is deliberately KEPT (not rolled back as
|
||||
# the summary-abort path does for #57835): it came from a
|
||||
# handoff genuinely present in this transcript, which is
|
||||
# returned unchanged.
|
||||
telemetry["failure_class"] = "empty_post_handoff_window"
|
||||
self._record_ineffective_compression_verdict(
|
||||
self._ineffective_compression_count + 1,
|
||||
)
|
||||
self._last_compression_savings_pct = 0.0
|
||||
if not self.quiet_mode:
|
||||
logger.warning(
|
||||
"Compression skipped: latest context summary leaves no "
|
||||
"new turns to summarize in window %d-%d. "
|
||||
"ineffective_compression_count=%d",
|
||||
compress_start,
|
||||
compress_end,
|
||||
self._ineffective_compression_count,
|
||||
)
|
||||
return messages
|
||||
|
||||
if not self.quiet_mode:
|
||||
logger.info(
|
||||
"Context compression triggered (%d tokens >= %d threshold)",
|
||||
|
|
|
|||
|
|
@ -725,3 +725,48 @@ def test_metadata_summary_decay_also_rehydrates_previous_summary():
|
|||
assert "metadata-only prior summary" in prompt
|
||||
# Grounding may prepend a task-snapshot section; pin the fresh body.
|
||||
assert (compressor._previous_summary or "").endswith("fresh summary")
|
||||
|
||||
|
||||
def test_empty_post_handoff_window_noops_without_summary_call():
|
||||
"""A latest handoff that consumes the window must not trigger an empty summary.
|
||||
|
||||
Regression test from PR #59526 (#59496), fixture adapted to current main:
|
||||
the standalone handoff sits alone in the compressible window, strips to
|
||||
None via _strip_context_summary_handoff_message, and leaves
|
||||
turns_to_summarize empty — the guard must skip _generate_summary
|
||||
entirely instead of wasting an aux LLM call on empty input.
|
||||
"""
|
||||
compressor = _compressor()
|
||||
old_summary = "WINDOW-END-SUMMARY durable facts already captured"
|
||||
messages = [
|
||||
{"role": "system", "content": "system prompt"},
|
||||
{"role": "user", "content": f"{SUMMARY_PREFIX}\n{old_summary}"},
|
||||
{"role": "assistant", "content": "recent tail response"},
|
||||
{"role": "user", "content": "tail request"},
|
||||
{"role": "assistant", "content": "tail answer"},
|
||||
{"role": "user", "content": "latest tail request"},
|
||||
{"role": "assistant", "content": "latest tail answer"},
|
||||
]
|
||||
|
||||
with (
|
||||
patch.object(compressor, "_find_tail_cut_by_tokens", return_value=2),
|
||||
patch.object(compressor, "_generate_summary") as mock_generate_summary,
|
||||
):
|
||||
result = compressor.compress(messages, current_tokens=90_000)
|
||||
|
||||
mock_generate_summary.assert_not_called()
|
||||
assert result == messages
|
||||
# The rehydrated summary state is deliberately kept: the handoff is
|
||||
# genuinely present in the returned (unchanged) transcript.
|
||||
assert compressor._previous_summary == old_summary
|
||||
assert compressor.compression_count == 0
|
||||
# Mirrors the sibling no-compressible-window guard (#40803): the shape
|
||||
# cannot shrink, so it counts as an ineffective strike (routed through
|
||||
# the durable write-through helper) to arm the anti-thrash breaker.
|
||||
assert compressor._ineffective_compression_count == 1
|
||||
assert compressor._last_compression_savings_pct == 0.0
|
||||
assert compressor._last_summary_dropped_count == 0
|
||||
assert compressor._last_summary_fallback_used is False
|
||||
assert compressor._last_compress_aborted is False
|
||||
telemetry = compressor._last_compression_telemetry or {}
|
||||
assert telemetry.get("failure_class") == "empty_post_handoff_window"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue