# NousResearch/hermes-agent · Auto-compression blocked by hardcoded 64K context floor

## Open now

- 1 issue, 2 PRs open on GitHub.
- #60103: \[Bug\]: Compression unreachable on local models whose real context is silently clamped by Ollama — dead zone causes permanent context-shift and multi-minute latency
- PR #60154: fix: cap config context_length at real local provider window (#60103)
- PR #60460: fix(agent): use real model context when smaller than enforced 64K minimum for compression (#60103)

A cluster of bug reports and fixes addressing cases where auto-compression never triggers because the minimum context length constant (64,000) equals or exceeds the actual model context window, particularly affecting local/Ollama models with smaller real contexts. Fixes involve making the floor configurable, clamping thresholds correctly, and honoring model-specific context overrides.

## TL;DR

- 18 member(s): 1 open issue(s), 2 closed issue(s), 2 open PR(s), 2 merged PR(s), 11 PR(s) closed without merging.
- A fix was merged to main in PR #49998. Verify it addresses your case.
- **Recommended:** read PR #49998 (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: `partially_resolved` · deciding PR(s): `pr-52053`. 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.10`: 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.

## Open related issues

- #60103: \[Bug\]: Compression unreachable on local models whose real context is silently clamped by Ollama — dead zone causes permanent context-shift and multi-minute latency

## Closed / duplicate issues

- #14690: BUG: Context auto-compression never triggers when context_length == MINIMUM_CONTEXT_LENGTH (64000)
- #31600: \[Bug\]: Hardcoded MINIMUM_CONTEXT_LENGTH = 64_000 deadlocks auto-compression and causes infinite tool loops on high-context models

## Prior & competing PRs

- **#49998: fix(compression): auto-compression triggers at minimum context length (#14690)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/context_compressor.py`, `tests/agent/test_context_compressor.py`
  - fix hunks:
  - `agent/context_compressor.py`

    ```diff
    @@ -656,9 +656,8 @@ def update_model(
             self.provider = provider
             self.api_mode = api_mode
             self.context_length = context_length
    -        self.threshold_tokens = max(
    -            int(context_length * self.threshold_percent),
    -            MINIMUM_CONTEXT_LENGTH,
    +        self.threshold_tokens = [redacted:secret-assignment]
    +            context_length, self.threshold_percent
             )
             # Recalculate token budgets for the new context length so the
             # compressor stays calibrated after a model switch (e.g. 200K → 32K).
    ```

    ```diff
    @@ -690,6 +689,40 @@ def update_model(
             self.awaiting_real_usage_after_compression = False
             self._ineffective_compression_count = 0
     
    +    # When the MINIMUM_CONTEXT_LENGTH floor meets/exceeds a small context
    +    # window, compacting at the percentage (50% → 32K of a 64K window) wastes
    +    # half the usable context. Trigger near the top of the window instead so a
    +    # minimum-context model uses most of its budget before compacting — same
    +    # rationale as the gpt-5.5/Codex 85% autoraise.
    +    _MIN_CTX_TRIGGER_RATIO = 0.85
    +
    +    @staticmethod
    +    def _compute_threshold_tokens(context_length: int, threshold_percent: float) -> int:
    +        """Compute the compaction trigger threshold in tokens.
    +
    +        The base value is ``context_length * threshold_percent``, floored at
    +        ``MINIMUM_CONTEXT_LENGTH`` so large-context models don't compress
    +        prematurely at 50%. BUT that floor degenerates at small windows: for a
    +        model whose ``context_length`` is at/below the minimum (e.g. a 64K
    +        local model), ``max(0.5*64000, 64000) == 64000`` makes the threshold
    +        equal the ENTIRE window — auto-compression can never fire because the
    +        provider rejects the request before usage reaches 100% (#14690).
    +
    +        When the floor would meet or exceed the context window, trigger at
    +        ``_MIN_CTX_TRIGGER_RATIO`` (85%) of the window — high enough that a
    +        small model uses most of its context before compacting, but below
    +        100% so compaction fires before the provider rejects the request.
    +        """
    +        pct_value = int(context_length * threshold_percent)
    +        floored = max(pct_value, MINIMUM_CONTEXT_LENGTH)
    +        # If flooring pushed the threshold to/over the window it can never be
    +        # reached. Trigger at 85% of the window so a minimum-context model
    +        # rides most of its budget before compacting instead of wasting half.
    +        if context_length > 0 and floored >= context_length:
    +            return max(1, min(int(context_length * ContextCompressor._MIN_CTX_TRIGGER_RATIO),
    +                              context_length - 1))
    +        return floored
    +
         def __init__(
             self,
             model: str,
    ```

    ```diff
    @@ -730,10 +763,11 @@ def __init__(
             # Floor: never compress below MINIMUM_CONTEXT_LENGTH tokens even if
             # the percentage would suggest a lower value.  This prevents premature
             # compression on large-context models at 50% while keeping the % sane
    -        # for models right at the minimum.
    -        self.threshold_tokens = max(
    -            int(self.context_length * threshold_percent),
    -            MINIMUM_CONTEXT_LENGTH,
    +        # for models right at the minimum. _compute_threshold_tokens also
    +        # guards the degenerate case where the floor would equal/exceed the
    +        # window (small models), so auto-compression can still fire (#14690).
    +        self.threshold_tokens = [redacted:secret-assignment]
    +            self.context_length, threshold_percent
             )
             self.compression_count = 0
     
    ```
  - `tests/agent/test_context_compressor.py`

    ```diff
    @@ -204,6 +204,44 @@ def test_fallback_summary_does_not_triplicate_latest_user_ask(self):
                 f"#49307), found {count}x:\n{summary}"
             )
     
    +    def test_threshold_below_window_at_minimum_ctx(self):
    +        """Regression for #14690: at context_length == MINIMUM_CONTEXT_LENGTH
    +        the floored threshold used to equal the whole window, so
    +        auto-compression could never fire. It now triggers at 85% of the
    +        window — high enough not to waste the small budget, below 100% so it
    +        actually fires."""
    +        from agent.context_compressor import MINIMUM_CONTEXT_LENGTH
    +        t = ContextCompressor._compute_threshold_tokens(MINIMUM_CONTEXT_LENGTH, 0.50)
    +        assert t < MINIMUM_CONTEXT_LENGTH
    +        assert t == 54400  # 85% of 64000
    +
    +    def test_threshold_below_window_for_small_ctx(self):
    +        # 32K model: the 64000 floor exceeds the window — trigger at 85%.
    +        t = ContextCompressor._compute_threshold_tokens(32000, 0.50)
    +        assert t == 27200  # 85% of 32000
    +        assert t < 32000
    +
    +    def test_threshold_floored_for_large_ctx(self):
    +        from agent.context_compressor import MINIMUM_CONTEXT_LENGTH
    +        # 200K model at 50% = 100000 (above floor) — unchanged.
    +        assert ContextCompressor._compute_threshold_tokens(200000, 0.50) == 100000
    +        # 100K model at 50% = 50000 (below floor) — floored to MINIMUM.
    +        assert ContextCompressor._compute_threshold_tokens(100000, 0.50) == MINIMUM_CONTEXT_LENGTH
    +
    +    def test_minimum_ctx_model_can_actually_compress(self):
    +        """End-to-end: a model at exactly the minimum context length must have
    +        should_compress() fire below its window (at the 85% trigger), not only
    +        at 100%."""
    +        with patch("agent.context_compressor.get_model_context_length", return_value=64000):
    +            c = ContextCompressor(model="small-64k", quiet_mode=True)
    +            c.context_length = 64000
    +            c.threshold_tokens = [redacted:secret-assignment] c.threshold_percent)
    +        assert c.threshold_tokens == 54400
    +        assert c.threshold_tokens < 64000
    +        # At 85%+ usage compaction fires; below it, it doesn't (no premature compact).
    +        assert c.should_compress(55000) is True
    +        assert c.should_compress(40000) is False
    +
         def test_compression_increments_count(self, compressor):
             msgs = self._make_messages(10)
             # Default config (abort_on_summary_failure=False) — fallback path
    ```
- **#8962: fix(agent): honor model.context_length override below 64K floor**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#53569`
- **#11097: fix(run_agent): honor model.context_length override for sub-64K models**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#53569`
- **#14696: fix(compression): three bugs causing auto-compression to never trigger**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#14878: fix: keep compression reachable at 64k context**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#49998`
- **#15431: fix(agent): clamp compression threshold below context_length to ensure compression can trigger (#14690)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#49998`
- **#15496: fix(compressor): prevent MINIMUM_CONTEXT_LENGTH floor from blocking compression at small contexts**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#15431`, `#14696`, `#14690`
- **#24076: fix(context): avoid 100 percent threshold at minimum context length**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#49998`
- **#31686: fix(compression): make minimum context floor configurable (#31600)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52053`, `#31600`
- **#33825: fix(agent): compress after repeated tool-call truncation**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#31600`
- **#35888: fix(gateway): clear bloated compression binding on compression-exhaustion auto-reset**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#45613`
- **#52053: fix(compression): make minimum context floor configurable (#31600)**. merged to main
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_init.py`, `agent/context_compressor.py`, `agent/conversation_compression.py`, `agent/model_metadata.py`, `gateway/run.py`, `tests/gateway/test_agent_cache.py`, `tests/run_agent/test_compression_feasibility.py`, `tests/run_agent/test_compression_minimum_floor.py`
- **#54071: fix(compression): make minimum context floor configurable (#31600)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#31600`
- **#60154: fix: cap config context_length at real local provider window (#60103)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `68a65ed7a`
  - files: `agent/model_metadata.py`
- **#60460: fix(agent): use real model context when smaller than enforced 64K minimum for compression (#60103)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/model_metadata.py`

## Fix status

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