Inspired by Claude Code: /compress here [N] — boundary-aware 'summarize up to here' (#35048)
Adds a user-chosen compression boundary to the existing /compress command. /compress here [N] summarizes everything except the most recent N exchanges (default 2), which are preserved verbatim — letting the user pick the compression boundary instead of relying on the automatic token-budget heuristic. Inspired by Claude Code's Rewind 'Summarize up to here' action (v2.1.139, Week 20, May 2026): https://code.claude.com/docs/en/whats-new/2026-w20 - hermes_cli/partial_compress.py: pure split/parse helpers + seam-alternation guard (shared by CLI and gateway). - cli.py / gateway/run.py: route 'here [N]' / '--keep N' to partial compression; compress only the head, re-append the verbatim tail through the seam guard. - Preserves message-flow role alternation (seam guard merges any illegal user->user / assistant->assistant adjacency). - Reuses the existing _compress_context session-rotation/lock machinery — no changes to the compression core. - Bare /compress (full) and /compress <focus> behavior unchanged. Tests: 12 helper unit tests + 5 CLI integration tests + E2E (interleaved tool-call transcript, degenerate/multimodal seams, real handler path).
This commit is contained in:
@@ -9947,10 +9947,20 @@ class HermesCLI:
|
||||
def _manual_compress(self, cmd_original: str = ""):
|
||||
"""Manually trigger context compression on the current conversation.
|
||||
|
||||
Accepts an optional focus topic: ``/compress <focus>`` guides the
|
||||
summariser to preserve information related to *focus* while being
|
||||
more aggressive about discarding everything else. Inspired by
|
||||
Claude Code's ``/compact <focus>`` feature.
|
||||
Two modes:
|
||||
|
||||
* ``/compress [<focus>]`` — compress the *whole* history. An
|
||||
optional focus topic guides the summariser to preserve
|
||||
information related to *focus* while being more aggressive
|
||||
about discarding everything else. Inspired by Claude Code's
|
||||
``/compact <focus>`` feature.
|
||||
* ``/compress here [N]`` — boundary-aware compression. Summarize
|
||||
everything *except* the most recent ``N`` exchanges (default
|
||||
2), which are preserved verbatim. Inspired by Claude Code's
|
||||
Rewind "Summarize up to here" action (v2.1.139, May 2026,
|
||||
https://code.claude.com/docs/en/whats-new/2026-w20). Lets the
|
||||
user pick the compression boundary instead of leaving it to
|
||||
the automatic token-budget heuristic.
|
||||
"""
|
||||
if not self.conversation_history or len(self.conversation_history) < 4:
|
||||
print("(._.) Not enough conversation to compress (need at least 4 messages).")
|
||||
@@ -9964,12 +9974,21 @@ class HermesCLI:
|
||||
print("(._.) Compression is disabled in config.")
|
||||
return
|
||||
|
||||
# Extract optional focus topic from the command (e.g. "/compress database schema")
|
||||
focus_topic = ""
|
||||
from hermes_cli.partial_compress import (
|
||||
parse_partial_compress_args,
|
||||
rejoin_compressed_head_and_tail,
|
||||
split_history_for_partial_compress,
|
||||
)
|
||||
|
||||
# Args after the command word (e.g. "/compress here 3" -> "here 3").
|
||||
raw_args = ""
|
||||
if cmd_original:
|
||||
parts = cmd_original.strip().split(None, 1)
|
||||
if len(parts) > 1:
|
||||
focus_topic = parts[1].strip()
|
||||
_parts = cmd_original.strip().split(None, 1)
|
||||
if len(_parts) > 1:
|
||||
raw_args = _parts[1].strip()
|
||||
|
||||
partial, keep_last, focus_topic = parse_partial_compress_args(raw_args)
|
||||
focus_topic = focus_topic or ""
|
||||
|
||||
original_count = len(self.conversation_history)
|
||||
with self._busy_command("Compressing context..."):
|
||||
@@ -9977,6 +9996,22 @@ class HermesCLI:
|
||||
from agent.model_metadata import estimate_request_tokens_rough
|
||||
from agent.manual_compression_feedback import summarize_manual_compression
|
||||
original_history = list(self.conversation_history)
|
||||
|
||||
# Boundary-aware split: only the head is summarized; the
|
||||
# most recent `keep_last` exchanges ride along verbatim.
|
||||
tail: list = []
|
||||
head = original_history
|
||||
if partial:
|
||||
head, tail = split_history_for_partial_compress(
|
||||
original_history, keep_last
|
||||
)
|
||||
if not tail:
|
||||
# Split degenerated (everything would be kept, or
|
||||
# no head left to compress). Fall back to full
|
||||
# compression so the user still gets an action.
|
||||
partial = False
|
||||
head = original_history
|
||||
|
||||
# Include system prompt + tool schemas in the estimate —
|
||||
# a transcript-only number understates real request pressure
|
||||
# and can even appear to grow after compression because a
|
||||
@@ -9988,7 +10023,11 @@ class HermesCLI:
|
||||
system_prompt=_sys_prompt,
|
||||
tools=_tools,
|
||||
)
|
||||
if focus_topic:
|
||||
if partial:
|
||||
print(f"🗜️ Summarizing up to here: compressing {len(head)} of "
|
||||
f"{original_count} messages (~{approx_tokens:,} tokens), "
|
||||
f"keeping last {keep_last} exchange(s) verbatim...")
|
||||
elif focus_topic:
|
||||
print(f"🗜️ Compressing {original_count} messages (~{approx_tokens:,} tokens), "
|
||||
f"focus: \"{focus_topic}\"...")
|
||||
else:
|
||||
@@ -10001,12 +10040,21 @@ class HermesCLI:
|
||||
# which already contain the agent identity — resulting in the
|
||||
# identity block appearing twice (issue #15281).
|
||||
compressed, _ = self.agent._compress_context(
|
||||
original_history,
|
||||
head,
|
||||
None,
|
||||
approx_tokens=approx_tokens,
|
||||
focus_topic=focus_topic or None,
|
||||
force=True,
|
||||
)
|
||||
# Re-append the verbatim tail after the compressed head.
|
||||
# The split guarantees `tail` begins on a user turn, so the
|
||||
# compressed-head -> tail boundary is normally valid
|
||||
# (the head's compressed output ends on assistant/tool).
|
||||
# rejoin_compressed_head_and_tail() additionally guards the
|
||||
# seam against any illegal user->user / assistant->assistant
|
||||
# adjacency, defending provider role-alternation rules.
|
||||
if partial and tail:
|
||||
compressed = rejoin_compressed_head_and_tail(compressed, tail)
|
||||
self.conversation_history = compressed
|
||||
# _compress_context ends the old session and creates a new child
|
||||
# session on the agent (run_agent.py::_compress_context). Sync the
|
||||
|
||||
Reference in New Issue
Block a user