NousResearch/hermes-agent

Auto-compression blocked by hardcoded 64K context floor

Cluster 14690 · agent. Data as of

3 open items. Fix merged in #49998. Check it covers your case.

The short answer

1open issue
2open PRs
18items grouped
Known?
Yes. 18 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #49998. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #52053. That is separate from the fix event.
Still open
1 issue and 2 PRs.

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.

Open issues1

  • Issue#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

Open PRs2

  • PR#60154fix: cap config context_length at real local provider window (#60103)
  • PR#60460fix(agent): use real model context when smaller than enforced 64K minimum for compression (#60103)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #49998 fix(compression): auto-compression triggers at minimum context length (#14690)

    merged to main

    files: agent/context_compressor.py, tests/agent/test_context_compressor.py

    Show the diff
    • agent/context_compressor.py
      @@ -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).
      @@ -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,
      @@ -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
      @@ -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
  • #52053 fix(compression): make minimum context floor configurable (#31600)

    merged to main

    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

11 PRs closed without merging

From the close comments: merged via another change 4 · superseded by another PR 2 · won't fix / by design 2 · duplicate 1 · stale or abandoned 1 · other 1

  • #8962 fix(agent): honor model.context_length override below 64K floor · won't fix / by design → #53569
  • #11097 fix(run_agent): honor model.context_length override for sub-64K models · won't fix / by design → #53569
  • #14696 fix(compression): three bugs causing auto-compression to never trigger · stale or abandoned
  • #14878 fix: keep compression reachable at 64k context · merged via another change → #49998
  • #15431 fix(agent): clamp compression threshold below context_length to ensure compression can trigger (#14690) · merged via another change → #49998
Show 6 more
  • #15496 fix(compressor): prevent MINIMUM_CONTEXT_LENGTH floor from blocking compression at small contexts · duplicate → #15431, #14696, #14690
  • #24076 fix(context): avoid 100 percent threshold at minimum context length · merged via another change → #49998
  • #31686 fix(compression): make minimum context floor configurable (#31600) · superseded by another PR → #52053, #31600
  • #33825 fix(agent): compress after repeated tool-call truncation · superseded by another PR → #31600
  • #35888 fix(gateway): clear bloated compression binding on compression-exhaustion auto-reset · merged via another change → #45613
  • #54071 fix(compression): make minimum context floor configurable (#31600) · other → #31600
Closed issues · 2
  • Closed#14690BUG: Context auto-compression never triggers when context_length == MINIMUM_CONTEXT_LENGTH (64000)
  • Closed#31600[Bug]: Hardcoded MINIMUM_CONTEXT_LENGTH = 64_000 deadlocks auto-compression and causes infinite tool loops on high-context models
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#49998, #52053
Closed issues
2
Attempts
11 PRs closed without merging
Fix link checked
21 Jun 2026

For your agent

The same facts, typed.