# NousResearch/hermes-agent · Gateway session message cap behavior and configuration

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #12874: feat(gateway): make session hygiene message-limit configurable and observable

These issues and PRs concern how the gateway counts messages toward its hard session limit, including tool/plumbing rows inflating counts, the cap threshold itself, and making the limit configurable and observable.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 1 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #50194. Verify it addresses your case.
- **Recommended:** read PR #50194 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.34`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #12626: \[Bug\]: Gateway can silently auto-compact at 400 messages even when /usage shows low context pressure
- #15195: Gateway hygiene hard message cap counts tool rows, causing early compaction in tool-heavy Telegram sessions

## Prior & competing PRs

- **#50194: fix(gateway): raise session-hygiene hard message limit 400 → 5000**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/run.py`, `hermes_cli/config.py`, `tests/gateway/test_session_hygiene.py`, `website/docs/user-guide/configuration.md`, `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/configuration.md`
  - fix hunks:
  - `gateway/run.py`

    ```diff
    @@ -9019,7 +9019,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                 _hyg_model = "anthropic/claude-sonnet-4.6"
                 _hyg_threshold_pct = 0.85
                 _hyg_compression_enabled = True
    -            _hyg_hard_msg_limit = 400
    +            _hyg_hard_msg_limit = 5000
                 _hyg_config_context_length = None
                 _hyg_provider = None
                 _hyg_base_url = None
    ```

    ```diff
    @@ -9141,8 +9141,11 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                     # extreme, regardless of token estimates.  This breaks the
                     # death spiral where API disconnects prevent token data
                     # collection, which prevents compression, which causes more
    -                # disconnects.  400 messages is well above normal sessions
    -                # but catches runaway growth before it becomes unrecoverable.
    +                # disconnects.  5000 messages is far above any normal session
    +                # but catches truly runaway growth before it becomes
    +                # unrecoverable.  Set well clear of legitimate large-context
    +                # (1M+) sessions doing thousands of short turns — those
    +                # compress on the token threshold, not this count-based floor.
                     # Threshold is configurable via
                     # compression.hygiene_hard_message_limit.
                     # (#2153)
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -1259,7 +1259,7 @@ def _ensure_hermes_home_managed(home: Path):
             "threshold": 0.50,            # compress when context usage exceeds this ratio
             "target_ratio": 0.20,         # fraction of threshold to preserve as recent tail
             "protect_last_n": 20,         # minimum recent messages to keep uncompressed
    -        "hygiene_hard_message_limit": 400,  # gateway session-hygiene force-compress threshold by message count
    +        "hygiene_hard_message_limit": 5000,  # gateway session-hygiene force-compress threshold by message count
             "protect_first_n": 3,         # non-system head messages always preserved
                                           # verbatim, in ADDITION to the system prompt
                                           # (which is always implicitly protected). Set to
    ```
  - `tests/gateway/test_session_hygiene.py`

    ```diff
    @@ -741,7 +741,7 @@ def _compress_context(self, messages, *_args, **_kwargs):
     async def test_session_hygiene_honors_configurable_hard_message_limit(
         monkeypatch, tmp_path
     ):
    -    """compression.hygiene_hard_message_limit overrides the 400-message default.
    +    """compression.hygiene_hard_message_limit overrides the default.
     
         Regression for user-reported fix: a gateway session with a small
         transcript (12 messages) should not hit hygiene compression by default,
    ```

    ```diff
    @@ -799,7 +799,7 @@ def _compress_context(self, messages, *_args, **_kwargs):
             platform=Platform.TELEGRAM,
             chat_type="private",
         )
    -    # 12 messages: below 400 default → no compression without override,
    +    # 12 messages: below default → no compression without override,
         # but above the configured limit of 10 → should compress.
         runner.session_store.load_transcript.return_value = _make_history(12, content_size=40)
         runner.session_store.has_any_sessions.return_value = True
    ```

    ```diff
    @@ -860,7 +860,7 @@ async def test_session_hygiene_default_hard_message_limit_does_not_fire_at_12_me
         monkeypatch, tmp_path
     ):
         """Sanity check for the companion test above: without config override,
    -    12 messages must NOT trigger the 400-message hard limit.  If this test
    +    12 messages must NOT trigger the default hard limit.  If this test
         passes without changes, the override test's finding is meaningful."""
         fake_dotenv = types.ModuleType("dotenv")
         fake_dotenv.load_dotenv = lambda *args, **kwargs: None
    ```

    ```diff
    @@ -883,7 +883,7 @@ def _compress_context(self, messages, *_args, **_kwargs):
         fake_run_agent.AIAgent = FakeCompressAgent
         monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
     
    -    # No config.yaml — use defaults (hard_limit=400)
    +    # No config.yaml — use defaults (hard_limit=5000)
         gateway_run = importlib.import_module("gateway.run")
         GatewayRunner = gateway_run.GatewayRunner
     
    ```

    ```diff
    @@ -947,7 +947,7 @@ def _compress_context(self, messages, *_args, **_kwargs):
         result = await runner._handle_message(event)
     
         assert result == "ok"
    -    # No compression agent instantiated — 12 messages well under 400 default.
    +    # No compression agent instantiated — 12 messages well under 5000 default.
         assert FakeCompressAgent.last_instance is None, (
    -        "Compression should NOT fire at 12 messages with default hard_limit=400"
    +        "Compression should NOT fire at 12 messages with default hard_limit=5000"
         )
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -730,7 +730,7 @@ compression:
       target_ratio: 0.20                                # Fraction of threshold to preserve as recent tail
       protect_last_n: 20                                # Min recent messages to keep uncompressed
       protect_first_n: 3                                # Non-system head messages pinned across compactions (0 = pin nothing)
    -  hygiene_hard_message_limit: 400                   # Gateway safety valve — see below
    +  hygiene_hard_message_limit: 5000                  # Gateway safety valve — see below
     
     # The summarization model/provider is configured under auxiliary:
     auxiliary:
    ```

    ```diff
    @@ -744,7 +744,7 @@ auxiliary:
     Older configs with `compression.summary_model`, `compression.summary_provider`, and `compression.summary_base_url` are automatically migrated to `auxiliary.compression.*` on first load (config version 17). No manual action needed.
     :::
     
    -`hygiene_hard_message_limit` is a gateway-only **pre-compression safety valve**. Runaway sessions with thousands of messages can hit model context limits before the normal percent-of-context threshold fires; when message count crosses this ceiling, Hermes forces compression regardless of token usage. Default `400` — raise it for platforms where very long sessions are normal, lower it to force more aggressive compression. Editing this value on a running gateway takes effect on the next message (see below).
    +`hygiene_hard_message_limit` is a gateway-only **pre-compression safety valve**. It exists to break a death spiral: when API calls keep disconnecting on an oversized session, the gateway never receives token-usage data, so the token-based threshold can't fire, so the transcript keeps growing and disconnects get worse. This count-based floor fires on message count alone (always known, regardless of API failures) to force compression and recover the session. Default `5000` — far above any normal session, including large-context (1M+) models doing thousands of short turns, which compress on the token threshold long before this. Raise it further for unusual platforms, lower it to force more aggressive compression. Editing this value on a running gateway takes effect on the next message (see below).
     
     `protect_first_n` controls how many **non-system** head messages are pinned across every compaction. Default `3` — the opening user/assistant exchange survives every summarizer pass so the original goal stays visible. On long-running rolling-compaction sessions where the opening turn is no longer relevant, set `protect_first_n: 0` to pin nothing but the system prompt + summary + tail. The system prompt itself is always preserved regardless of this setting.
     
    ```
  - `website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/configuration.md`

    ```diff
    @@ -555,7 +555,7 @@ compression:
       threshold: 0.50                                   # 在上下文限制的此百分比时压缩
       target_ratio: 0.20                                # 保留为最近尾部的阈值分数
       protect_last_n: 20                                # 保持未压缩的最少最近消息数
    -  hygiene_hard_message_limit: 400                   # Gateway 安全阀 —— 见下文
    +  hygiene_hard_message_limit: 5000                  # Gateway 安全阀 —— 见下文
     
     # 摘要模型/provider 在 auxiliary: 下配置：
     auxiliary:
    ```

    ```diff
    @@ -569,7 +569,7 @@ auxiliary:
     带有 `compression.summary_model`、`compression.summary_provider` 和 `compression.summary_base_url` 的旧版配置在首次加载时自动迁移到 `auxiliary.compression.*`（配置版本 17）。无需手动操作。
     :::
     
    -`hygiene_hard_message_limit` 是仅限 gateway 的**预压缩安全阀**。拥有数千条消息的失控会话可能在正常的上下文百分比阈值触发之前就达到模型上下文限制；当消息数超过此上限时，Hermes 强制压缩，无论 token 使用情况如何。默认 `400` —— 对于非常长的会话正常的平台，请调高；要强制更积极的压缩，请降低。在运行中的 gateway 上编辑此值将在下一条消息时生效（见下文）。
    +`hygiene_hard_message_limit` 是仅限 gateway 的**预压缩安全阀**。它的存在是为了打破一个死循环：当超大会话的 API 调用持续断开时，gateway 永远收不到 token 使用数据，基于 token 的阈值因此无法触发，于是 transcript 持续增长、断开愈发严重。这个基于消息数的下限仅凭消息数量触发（无论 API 是否失败，消息数始终已知），强制压缩以恢复会话。默认 `5000` —— 远高于任何正常会话，包括做数千次短轮次的大上下文（1M+）模型，它们早就在 token 阈值处压缩了。对于异常平台可调得更高；要强制更积极的压缩则调低。在运行中的 gateway 上编辑此值将在下一条消息时生效（见下文）。
     
     :::tip Gateway 热重载压缩和上下文长度
     从最近的版本开始，在运行中的 gateway 上编辑 `config.yaml` 中的 `model.context_length` 或任何 `compression.*` 键将在下一条消息时生效 —— 无需 gateway 重启、`/reset` 或会话轮换。缓存的 agent 签名包含这些键，因此 gateway 在检测到更改时会透明地重建 agent。API 密钥和工具/技能配置仍需要通常的重载路径。
    ```
- **#12874: feat(gateway): make session hygiene message-limit configurable and observable**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `03563dabacc144713f9c0827d6045b7a88f13efc`
  - files: `cli-config.yaml.example`, `gateway/run.py`, `gateway/slash_commands.py`, `tests/gateway/test_hygiene_observability.py`
- **#15439: fix(gateway): count only substantive messages for hard message cap, not tool plumbing rows (#15195)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `03563dabacc144713f9c0827d6045b7a88f13efc`, `#50194`, `#15195`

## Fix status

- Merged to main in PR #50194 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-21`.
- Subsystem: `gateway`
