{"repo":"NousResearch/hermes-agent","public_id":"14690","display_label":"Auto-compression blocked by hardcoded 64K context floor","summary":"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.","subsystem":"agent","counts":{"members":18,"open_issues":1,"open_issues_distinct":1,"open_prs":2,"closed_issues":2,"merged_prs":2,"attempt_prs":13},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":49998},"resolution":{"state":"partially_resolved","by":"pr-52053"},"freshness":{"state":"verified","last_verified_at":"2026-06-21T14:53:14+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.10274509803921568},"fix_edges":[{"issue_number":14690,"pr_number":49998,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":14690,"title":"BUG: Context auto-compression never triggers when context_length == MINIMUM_CONTEXT_LENGTH (64000)","url":null,"state":"closed"},{"kind":"issue","number":31600,"title":"[Bug]: Hardcoded MINIMUM_CONTEXT_LENGTH = 64_000 deadlocks auto-compression and causes infinite tool loops on high-context models","url":null,"state":"closed"},{"kind":"issue","number":60103,"title":"[Bug]: Compression unreachable on local models whose real context is silently clamped by Ollama — dead zone causes permanent context-shift and multi-minute latency","url":null,"state":"open"},{"kind":"pr","number":8962,"title":"fix(agent): honor model.context_length override below 64K floor","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53569","number":53569}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":11097,"title":"fix(run_agent): honor model.context_length override for sub-64K models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53569","number":53569}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":14696,"title":"fix(compression): three bugs causing auto-compression to never trigger","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":14878,"title":"fix: keep compression reachable at 64k context","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49998","number":49998}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":15431,"title":"fix(agent): clamp compression threshold below context_length to ensure compression can trigger (#14690)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49998","number":49998}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":15496,"title":"fix(compressor): prevent MINIMUM_CONTEXT_LENGTH floor from blocking compression at small contexts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#15431","number":15431},{"kind":"pr","value":"#14696","number":14696},{"kind":"pr","value":"#14690","number":14690}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":24076,"title":"fix(context): avoid 100 percent threshold at minimum context length","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49998","number":49998}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":31686,"title":"fix(compression): make minimum context floor configurable (#31600)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#52053","number":52053},{"kind":"pr","value":"#31600","number":31600}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":33825,"title":"fix(agent): compress after repeated tool-call truncation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31600","number":31600}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":35888,"title":"fix(gateway): clear bloated compression binding on compression-exhaustion auto-reset","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#45613","number":45613}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":49998,"title":"fix(compression): auto-compression triggers at minimum context length (#14690)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/context_compressor.py","tests/agent/test_context_compressor.py"],"fix_hunks":[{"file":"agent/context_compressor.py","hunks":["@@ -656,9 +656,8 @@ def update_model(\n         self.provider = provider\n         self.api_mode = api_mode\n         self.context_length = context_length\n-        self.threshold_tokens = max(\n-            int(context_length * self.threshold_percent),\n-            MINIMUM_CONTEXT_LENGTH,\n+        self.threshold_tokens = [redacted:secret-assignment]\n+            context_length, self.threshold_percent\n         )\n         # Recalculate token budgets for the new context length so the\n         # compressor stays calibrated after a model switch (e.g. 200K → 32K).","@@ -690,6 +689,40 @@ def update_model(\n         self.awaiting_real_usage_after_compression = False\n         self._ineffective_compression_count = 0\n \n+    # When the MINIMUM_CONTEXT_LENGTH floor meets/exceeds a small context\n+    # window, compacting at the percentage (50% → 32K of a 64K window) wastes\n+    # half the usable context. Trigger near the top of the window instead so a\n+    # minimum-context model uses most of its budget before compacting — same\n+    # rationale as the gpt-5.5/Codex 85% autoraise.\n+    _MIN_CTX_TRIGGER_RATIO = 0.85\n+\n+    @staticmethod\n+    def _compute_threshold_tokens(context_length: int, threshold_percent: float) -> int:\n+        \"\"\"Compute the compaction trigger threshold in tokens.\n+\n+        The base value is ``context_length * threshold_percent``, floored at\n+        ``MINIMUM_CONTEXT_LENGTH`` so large-context models don't compress\n+        prematurely at 50%. BUT that floor degenerates at small windows: for a\n+        model whose ``context_length`` is at/below the minimum (e.g. a 64K\n+        local model), ``max(0.5*64000, 64000) == 64000`` makes the threshold\n+        equal the ENTIRE window — auto-compression can never fire because the\n+        provider rejects the request before usage reaches 100% (#14690).\n+\n+        When the floor would meet or exceed the context window, trigger at\n+        ``_MIN_CTX_TRIGGER_RATIO`` (85%) of the window — high enough that a\n+        small model uses most of its context before compacting, but below\n+        100% so compaction fires before the provider rejects the request.\n+        \"\"\"\n+        pct_value = int(context_length * threshold_percent)\n+        floored = max(pct_value, MINIMUM_CONTEXT_LENGTH)\n+        # If flooring pushed the threshold to/over the window it can never be\n+        # reached. Trigger at 85% of the window so a minimum-context model\n+        # rides most of its budget before compacting instead of wasting half.\n+        if context_length > 0 and floored >= context_length:\n+            return max(1, min(int(context_length * ContextCompressor._MIN_CTX_TRIGGER_RATIO),\n+                              context_length - 1))\n+        return floored\n+\n     def __init__(\n         self,\n         model: str,","@@ -730,10 +763,11 @@ def __init__(\n         # Floor: never compress below MINIMUM_CONTEXT_LENGTH tokens even if\n         # the percentage would suggest a lower value.  This prevents premature\n         # compression on large-context models at 50% while keeping the % sane\n-        # for models right at the minimum.\n-        self.threshold_tokens = max(\n-            int(self.context_length * threshold_percent),\n-            MINIMUM_CONTEXT_LENGTH,\n+        # for models right at the minimum. _compute_threshold_tokens also\n+        # guards the degenerate case where the floor would equal/exceed the\n+        # window (small models), so auto-compression can still fire (#14690).\n+        self.threshold_tokens = [redacted:secret-assignment]\n+            self.context_length, threshold_percent\n         )\n         self.compression_count = 0\n "]},{"file":"tests/agent/test_context_compressor.py","hunks":["@@ -204,6 +204,44 @@ def test_fallback_summary_does_not_triplicate_latest_user_ask(self):\n             f\"#49307), found {count}x:\\n{summary}\"\n         )\n \n+    def test_threshold_below_window_at_minimum_ctx(self):\n+        \"\"\"Regression for #14690: at context_length == MINIMUM_CONTEXT_LENGTH\n+        the floored threshold used to equal the whole window, so\n+        auto-compression could never fire. It now triggers at 85% of the\n+        window — high enough not to waste the small budget, below 100% so it\n+        actually fires.\"\"\"\n+        from agent.context_compressor import MINIMUM_CONTEXT_LENGTH\n+        t = ContextCompressor._compute_threshold_tokens(MINIMUM_CONTEXT_LENGTH, 0.50)\n+        assert t < MINIMUM_CONTEXT_LENGTH\n+        assert t == 54400  # 85% of 64000\n+\n+    def test_threshold_below_window_for_small_ctx(self):\n+        # 32K model: the 64000 floor exceeds the window — trigger at 85%.\n+        t = ContextCompressor._compute_threshold_tokens(32000, 0.50)\n+        assert t == 27200  # 85% of 32000\n+        assert t < 32000\n+\n+    def test_threshold_floored_for_large_ctx(self):\n+        from agent.context_compressor import MINIMUM_CONTEXT_LENGTH\n+        # 200K model at 50% = 100000 (above floor) — unchanged.\n+        assert ContextCompressor._compute_threshold_tokens(200000, 0.50) == 100000\n+        # 100K model at 50% = 50000 (below floor) — floored to MINIMUM.\n+        assert ContextCompressor._compute_threshold_tokens(100000, 0.50) == MINIMUM_CONTEXT_LENGTH\n+\n+    def test_minimum_ctx_model_can_actually_compress(self):\n+        \"\"\"End-to-end: a model at exactly the minimum context length must have\n+        should_compress() fire below its window (at the 85% trigger), not only\n+        at 100%.\"\"\"\n+        with patch(\"agent.context_compressor.get_model_context_length\", return_value=64000):\n+            c = ContextCompressor(model=\"small-64k\", quiet_mode=True)\n+            c.context_length = 64000\n+            c.threshold_tokens = [redacted:secret-assignment] c.threshold_percent)\n+        assert c.threshold_tokens == 54400\n+        assert c.threshold_tokens < 64000\n+        # At 85%+ usage compaction fires; below it, it doesn't (no premature compact).\n+        assert c.should_compress(55000) is True\n+        assert c.should_compress(40000) is False\n+\n     def test_compression_increments_count(self, compressor):\n         msgs = self._make_messages(10)\n         # Default config (abort_on_summary_failure=False) — fallback path\n"]}]},{"kind":"pr","number":52053,"title":"fix(compression): make minimum context floor configurable (#31600)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true},{"kind":"pr","number":54071,"title":"fix(compression): make minimum context floor configurable (#31600)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31600","number":31600}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":60154,"title":"fix: cap config context_length at real local provider window (#60103)","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"68a65ed7a","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":60460,"title":"fix(agent): use real model context when smaller than enforced 64K minimum for compression (#60103)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}