Skip to content

feat(compression): make protect_first_n configurable#13754

Closed
simpolism wants to merge 1 commit into
NousResearch:mainfrom
simpolism:jake/configurable-protect-first-n
Closed

feat(compression): make protect_first_n configurable#13754
simpolism wants to merge 1 commit into
NousResearch:mainfrom
simpolism:jake/configurable-protect-first-n

Conversation

@simpolism

@simpolism simpolism commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The number of head messages preserved verbatim across context compactions was previously hardcoded to 3 in AIAgent.__init__. Expose it as compression.protect_first_n in config, matching the existing protect_last_n pattern.

Motivation: users who rely on rolling compaction for long-running sessions had the opening user/assistant exchange pinned as head forever, which doesn't always match how they want the session framed after many compactions. Lowering to 1 preserves system prompt + first non-system message; lowering to 0 preserves only the system prompt and lets the entire first exchange age out naturally through the summary.

Semantics: protect_first_n counts non-system head messages protected in addition to the system prompt. The system prompt is always implicitly protected when present. Same meaning across both code paths:

protect_first_n=0 → system prompt only (or nothing if no system message)
protect_first_n=2 → system prompt + first 2 non-system messages (default)

This unifies the CLI path (system prompt at position 0 of the message list) and the gateway path (the gateway /compress handler strips the system prompt before calling compress()). Previously these two paths disagreed.

Default chosen as 2 (not 3) so that with a system prompt present, the effective protected head count remains 3 messages — matching the pre-feature behaviour. Sessions without a system prompt will see 2 protected head messages instead of 3, but this is the rare path and the new semantics make the system-prompt-present case the well-defined one.

Related Issue

Fixes #13751

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 🐛 Bug fix (the cross-path semantics inconsistency was a real bug — folded in here since the new config knob makes it user-visible)

Changes Made

  • agent/context_compressor.py: redefine protect_first_n as the count of non-system head messages protected beyond the implicit system-prompt guarantee; both paths converge. Constructor default 2.
  • hermes_cli/config.py: add compression.protect_first_n default (2). show_config label tweaked to "Protect first: N non-system head messages" so the meaning is clear at a glance.
  • run_agent.py: read protect_first_n from config; 0 is now valid.
  • cli-config.yaml.example: document the new key, default, and rationale.
  • tests/agent/test_context_compressor.py: cover default (2), override, protect_first_n=0 and =1 end-to-end behavior, the no-system-prompt (gateway) path regression test, and updated test_default_protect_first_n_is_2 (was _is_3).

How to Test

  1. Set compression.protect_first_n: 1 in ~/.hermes/config.yaml.
  2. Run a long-running session that triggers compaction (or use /compress manually).
  3. Inspect the head of the compacted message list: should contain only the system prompt + first one non-system message.
  4. Set protect_first_n: 0 — head should contain only the system prompt.
  5. Run pytest tests/agent/test_context_compressor.py -q — 80 tests pass.

Checklist

(see existing PR body — same items, still all checked except platform field which should still read "Ubuntu 24.04")

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #11996 (head message fossilization bug this config change addresses).

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #11996 (head message fossilization bug this config change addresses).

The number of head messages preserved verbatim across context compactions
was previously hardcoded to 3 in AIAgent.__init__. Expose it as
`compression.protect_first_n` in config, matching the existing
`protect_last_n` pattern.

Motivation: users who rely on rolling compaction for long-running sessions
had the opening user/assistant exchange pinned as head forever, which
doesn't always match how they want the session framed after many
compactions. Lowering to 1 preserves the system prompt + first non-system
message; lowering to 0 preserves only the system prompt and lets the
entire first exchange age out naturally through the summary.

Semantics: `protect_first_n` counts non-system head messages protected
**in addition to** the system prompt, which is always implicitly protected
when present. Same meaning across both code paths:

  protect_first_n=0 → system prompt only (or nothing if no system message)
  protect_first_n=2 → system prompt + first 2 non-system messages (default)

This unifies the CLI path (which reads messages with the system prompt at
position 0) and the gateway path (where the gateway /compress handler
strips the system prompt before calling compress() — see
gateway/run.py L9150-9154 on the parent fork). Previously these two paths
disagreed:

  CLI path:     protect_first_n=1 → protect system prompt only
  Gateway path: protect_first_n=1 → protect first USER turn forever

In practice on long-running gateway sessions the old semantics pinned
whatever stale aside happened to be the first user message, reinserting
it into every compaction summary indefinitely.

Default chosen as 2 (not 3) so that the effective protected head count
remains 3 messages in the common case — assuming a system prompt is
present, default protection becomes system + 2 non-system = 3 total,
matching the pre-feature behaviour where `protect_first_n` was hardcoded
to protect 3 messages total. Sessions without a system prompt will see a
small behaviour change (2 protected head messages instead of 3), but this
is the rare path and the new semantics make the system-prompt-present
case the well-defined one.

Changes:

- agent/context_compressor.py: redefine protect_first_n as the count of
  non-system head messages protected beyond the implicit system-prompt
  guarantee; both paths converge. Constructor default updated to 2.
- hermes_cli/config.py: add `compression.protect_first_n` default (2),
  matching the new semantics. `show_config` label tweaked to
  'Protect first: N non-system head messages' for clarity.
- run_agent.py: read protect_first_n from config; 0 is now valid (system
  prompt is always implicitly protected).
- cli-config.yaml.example: document the new key and rationale.
- tests/agent/test_context_compressor.py: cover default, override, the
  end-to-end `protect_first_n=0` and `protect_first_n=1` behaviour,
  the no-system-prompt (gateway) path, and the new shared-semantics
  regression test.

Fixes NousResearch#13751
Tested on Ubuntu 24.04.
@simpolism

Copy link
Copy Markdown
Contributor Author

will be remaking shortly

@simpolism simpolism closed this May 14, 2026
@simpolism

Copy link
Copy Markdown
Contributor Author

Actually -- going to update my branch + description

@simpolism simpolism reopened this May 14, 2026
@simpolism
simpolism force-pushed the jake/configurable-protect-first-n branch from 46b7596 to 609e341 Compare May 14, 2026 00:34
@simpolism

Copy link
Copy Markdown
Contributor Author

Rebased on current main (was ~3 weeks behind and conflicting), and folded in a follow-up semantics correction so the PR presents the final-form definition of protect_first_n from a single commit. Changes since previous review:

  1. Now rebased on current upstream/main.
  2. protect_first_n redefined as the count of non-system head messages protected in addition to the system prompt. System prompt is always implicitly protected when present. Same meaning in both the CLI path and the gateway path (the previous version disagreed across these — gateway /compress strips the system prompt before calling compress(), so protect_first_n=1 had different meaning in each path).
  3. Numeric default changed from 3 to 2 so the effective protected-head count remains the same as the previous hardcoded behaviour when a system prompt is present (system + 2 non-system = 3 total). Sessions without a system prompt see one fewer protected head message — small behaviour shift on a rare code path, called out here so it's not invisible.
  4. protect_first_n=0 is now valid (drops the max(1, ...) floor in run_agent.py).
  5. Tests updated to cover the new semantics + new regression test for the no-system-prompt path.
  6. Removed the unrelated scripts/release.py AUTHOR_MAP change from the previous PR — that's now in its own PR (chore(release): map [email protected] and [email protected] to @simpolism #25308).

Ready for re-review.

teknium1 added a commit that referenced this pull request May 14, 2026
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.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged via PR #25447 — merged into main as commits dee71a3 (your original feat commit, authorship preserved) and 4ceab16 (small follow-up keeping the default at 3 instead of 2, plus aligning the ContextEngine ABC docstring with the new semantics). Thanks @simpolism!

@teknium1 teknium1 closed this May 14, 2026
jsboige pushed a commit to jsboige/hermes-agent that referenced this pull request May 14, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
AlexFoxD pushed a commit to AlexFoxD/hermes-agent that referenced this pull request May 21, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jul 3, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
kulikman pushed a commit to kulikman/hermes-agent that referenced this pull request Jul 16, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
waym0reom3ga added a commit to waym0reom3ga/autolycus-agent that referenced this pull request Jul 21, 2026
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 NousResearch#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 NousResearch#13754 stay as-is
  since those tests fix concrete head shapes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: expose protect_first_n in config

3 participants