Skip to content

Commit 4ceab16

Browse files
committed
fix(compression): keep default protect_first_n at 3 + align ABC
Follow-up on the salvaged feat commit: - Keep the constructor / config / yaml-example default at 3 so existing gateway and CLI users see no behavioural change. PR #13754 (which this builds on) had lowered the default to 2 to chase pre-feature parity in the system-prompt-present case, at the cost of quietly halving the protected head for the gateway path (which strips the system prompt before calling compress()). With the new "system prompt is implicit" semantics, default 3 gives every caller a stable head shape. - agent/context_engine.py: bring the ABC's protect_first_n docstring in line with the new semantics so plugin context engines interpret the config key the same way the built-in compressor does. - tests: adjust the default-value test (3, not 2) and a stale comment; per-test protect_first_n=2/3/1 values added in PR #13754 stay as-is since those tests fix concrete head shapes.
1 parent dee71a3 commit 4ceab16

6 files changed

Lines changed: 22 additions & 14 deletions

File tree

agent/context_compressor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def __init__(
405405
self,
406406
model: str,
407407
threshold_percent: float = 0.50,
408-
protect_first_n: int = 2,
408+
protect_first_n: int = 3,
409409
protect_last_n: int = 20,
410410
summary_target_ratio: float = 0.20,
411411
quiet_mode: bool = False,

agent/context_engine.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ def name(self) -> str:
5555
# These control the preflight compression check. Subclasses may
5656
# override via __init__ or property; defaults are sensible for most
5757
# engines.
58+
#
59+
# protect_first_n semantics (since PR #13754): count of non-system head
60+
# messages always preserved verbatim, IN ADDITION to the system prompt
61+
# which is always implicitly protected. Default 3 keeps the
62+
# historical "system + first 3 non-system messages" head shape.
5863

5964
threshold_percent: float = 0.75
6065
protect_first_n: int = 3

cli-config.yaml.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,9 @@ compression:
372372
# opening turns may not match how you want the session framed over time.
373373
# Set to 0 to preserve ONLY the system prompt (plus the rolling summary
374374
# and recent tail) — the cleanest configuration for long-running sessions.
375-
# Default 2 preserves the system prompt plus the first user/assistant
376-
# exchange (≈ 3 messages total when a system prompt is present).
377-
protect_first_n: 2
375+
# Default 3 preserves the system prompt plus the first three non-system
376+
# head messages, matching the pre-feature behaviour.
377+
protect_first_n: 3
378378

379379
# To pin a specific model/provider for compression summaries, use the
380380
# auxiliary section below (auxiliary.compression.provider / model).

hermes_cli/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,13 +731,14 @@ def _ensure_hermes_home_managed(home: Path):
731731
"target_ratio": 0.20, # fraction of threshold to preserve as recent tail
732732
"protect_last_n": 20, # minimum recent messages to keep uncompressed
733733
"hygiene_hard_message_limit": 400, # gateway session-hygiene force-compress threshold by message count
734-
"protect_first_n": 2, # non-system head messages always preserved beyond the system prompt
734+
"protect_first_n": 3, # non-system head messages always preserved
735735
# verbatim, in ADDITION to the system prompt
736736
# (which is always implicitly protected). Set to
737737
# 0 for long-running rolling-compaction sessions
738738
# where you want nothing pinned except the
739739
# system prompt + rolling summary + recent tail.
740740
},
741+
741742
# Anthropic prompt caching (Claude via OpenRouter or native Anthropic API).
742743
# cache_ttl must be "5m" or "1h" (Anthropic-supported tiers); other values are ignored.
743744
"prompt_caching": {
@@ -4867,7 +4868,7 @@ def show_config():
48674868
print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%")
48684869
print(f" Target ratio: {compression.get('target_ratio', 0.20) * 100:.0f}% of threshold preserved")
48694870
print(f" Protect last: {compression.get('protect_last_n', 20)} messages")
4870-
print(f" Protect first: {compression.get('protect_first_n', 2)} non-system head messages")
4871+
print(f" Protect first: {compression.get('protect_first_n', 3)} non-system head messages")
48714872
_aux_comp = config.get('auxiliary', {}).get('compression', {})
48724873
_sm = _aux_comp.get('model', '') or '(auto)'
48734874
print(f" Model: {_sm}")

run_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2122,7 +2122,7 @@ def __init__(
21222122
# is a legitimate (and common) configuration for long-running
21232123
# rolling-compaction sessions.
21242124
compression_protect_first = max(
2125-
0, int(_compression_cfg.get("protect_first_n", 2))
2125+
0, int(_compression_cfg.get("protect_first_n", 3))
21262126
)
21272127

21282128
# Read optional explicit context_length override for the auxiliary

tests/agent/test_context_compressor.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,14 +1299,16 @@ def test_default_protect_last_n_is_20(self):
12991299
c = ContextCompressor(model="test", quiet_mode=True)
13001300
assert c.protect_last_n == 20
13011301

1302-
def test_default_protect_first_n_is_2(self):
1303-
"""Default protect_first_n is 2 (system + 2 extra non-system messages =
1304-
3 protected messages total, preserving the pre-feature behaviour where
1305-
protect_first_n was hardcoded to protect 3 head messages total).
1302+
def test_default_protect_first_n_is_3(self):
1303+
"""Default protect_first_n is 3 (system + 3 extra non-system messages =
1304+
4 protected messages total when a system prompt is present). With the
1305+
new semantics, the constructor default is 3 — the system prompt is
1306+
always implicitly protected ON TOP OF protect_first_n non-system
1307+
messages.
13061308
"""
13071309
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
13081310
c = ContextCompressor(model="test", quiet_mode=True)
1309-
assert c.protect_first_n == 2
1311+
assert c.protect_first_n == 3
13101312

13111313
def test_protect_first_n_override(self):
13121314
"""protect_first_n=0 should be honoured — for users who rely on rolling
@@ -1342,8 +1344,8 @@ def test_protect_first_n_0_preserves_only_system_prompt(self):
13421344
assert result[0]["content"].startswith("System prompt")
13431345
# The first user/assistant exchange (msg 0, msg 1) should NOT be pinned
13441346
# as head verbatim — those would have been summarized or absorbed.
1345-
# Under default protect_first_n=2, result[1] and result[2] would be
1346-
# the literal "msg 0" / "msg 1"; with protect_first_n=0 they aren't.
1347+
# Under default protect_first_n=3, result[1..3] would be the literal
1348+
# "msg 0" / "msg 1" / "msg 2"; with protect_first_n=0 they aren't.
13471349
assert result[1].get("content") != "msg 0"
13481350
# Last 2 messages are tail-protected under protect_last_n=2
13491351
assert result[-1]["content"] == msgs[-1]["content"]

0 commit comments

Comments
 (0)