{"repo":"NousResearch/hermes-agent","public_id":"48621","display_label":"Compression autoraise and silent wipe bug reports","summary":"These members concern a reported bug where gpt-5.3-codex-spark lacks a compression threshold autoraise and silently wipes context when protect_last_n is too large, along with several fix attempts addressing autoraise coverage for Codex model variants.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":59814,"merged_at":"2026-07-06T19:46:21+00:00"},"resolution":{"state":"resolved","by":"pr-59814"},"freshness":{"state":"verified","last_verified_at":"2026-07-06T19:46:21+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":42187,"pr_number":59814,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-06T19:46:21+00:00"},{"issue_number":44439,"pr_number":59814,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-06T19:46:21+00:00"},{"issue_number":45817,"pr_number":59814,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-06T19:46:21+00:00"},{"issue_number":48621,"pr_number":59814,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-06T19:46:21+00:00"},{"issue_number":54432,"pr_number":59814,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-06T19:46:21+00:00"}],"members":[{"kind":"issue","number":48621,"title":"gpt-5.3-codex-spark: no compression threshold autoraise + silent context wipe when protect_last_n too large","url":null,"state":"closed"},{"kind":"pr","number":48672,"title":"fix(compression): autoraise gpt-5.3-codex-spark threshold to 70% (#48621)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"0b6df665a","number":0},{"kind":"pr","value":"#59814","number":59814},{"kind":"pr","value":"#48621","number":48621}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":48696,"title":"fix(compression): add gpt-5.3-codex-spark to Codex autoraise (#48621)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59814","number":59814},{"kind":"pr","value":"#48672","number":48672}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":59814,"title":"fix(compression): Codex autoraise cluster — gpt-5.4/spark coverage, never-lower, notice dedupe","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_init.py","agent/auxiliary_client.py","hermes_cli/config.py","scripts/release.py","tests/agent/test_arcee_trinity_overrides.py","tests/agent/test_auxiliary_main_first.py","tests/agent/test_codex_gpt55_autoraise_notice.py","tests/run_agent/test_infinite_compaction_loop.py","tests/run_agent/test_plugin_context_engine_init.py","website/docs/developer-guide/context-compression-and-caching.md"],"fix_hunks":[{"file":"agent/agent_init.py","hunks":["@@ -68,24 +68,118 @@ def _ra():\n     return run_agent\n \n \n-def _build_codex_gpt55_autoraise_notice(autoraise: Dict[str, float]) -> str:\n-    \"\"\"Build the one-time notice shown when Codex gpt-5.5 raises compaction.\n+def _build_codex_gpt5_autoraise_notice(autoraise: Dict[str, Any]) -> str:\n+    \"\"\"Build the one-time notice shown when Codex gpt-5.x raises compaction.\n \n-    ``autoraise`` is ``{\"from\": <old_ratio>, \"to\": <new_ratio>}``. The same\n-    text is printed inline for CLI users and replayed via ``status_callback``\n-    for gateway users, so it must be self-contained and include the exact\n-    opt-back-out command.\n+    ``autoraise`` is ``{\"model\": <slug>, \"from\": <old_ratio>, \"to\": <new_ratio>}``.\n+    The same text is printed inline for CLI users and replayed via\n+    ``status_callback`` for gateway users, so it must be self-contained and\n+    include the exact opt-back-out command.\n     \"\"\"\n+    model = str(autoraise.get(\"model\") or \"gpt-5.4/5.5\").strip().lower().rsplit(\"/\", 1)[-1]\n+    # gpt-5.3-codex-spark has a native 128K window; the gpt-5.4/5.5 family is\n+    # capped at 272K by the Codex OAuth backend.\n+    cap = \"128K\" if model.startswith(\"gpt-5.3-codex-spark\") else \"272K\"\n     from_pct = int(round(autoraise[\"from\"] * 100))\n     to_pct = int(round(autoraise[\"to\"] * 100))\n     return (\n-        f\"ℹ Codex gpt-5.5 caps context at 272K, so auto-compaction was raised \"\n+        f\"ℹ Codex {model} caps context at {cap}, so auto-compaction was raised \"\n         f\"to {to_pct}% (from {from_pct}%) to use more of the window before \"\n         f\"summarizing.\\n\"\n         f\"  Opt back out: hermes config set compression.codex_gpt55_autoraise false\"\n     )\n \n \n+def _resolve_compression_threshold(\n+    global_threshold: float,\n+    model_cthresh: Optional[float],\n+    *,\n+    model: Optional[str] = None,\n+    is_codex_autoraise: bool,\n+) -> tuple[float, Optional[Dict[str, Any]]]:\n+    \"\"\"Combine the user's global compaction threshold with a per-model override.\n+\n+    Returns ``(effective_threshold, autoraise_notice)``. ``autoraise_notice`` is\n+    ``{\"model\": <slug>, \"from\": <old>, \"to\": <new>}`` only when a Codex\n+    autoraise (gpt-5.4/5.5 272K family or gpt-5.3-codex-spark) actually raises\n+    the threshold, otherwise ``None``.\n+\n+    The Codex overrides are *autoraises*: they must never LOWER a higher\n+    user-configured threshold. A user who already set ``compression.threshold``\n+    above the raised value deliberately keeps more raw context, and silently\n+    dropping them would both waste usable window and contradict the feature's\n+    purpose (use more of the window). Other overrides (e.g. Arcee Trinity)\n+    keep their existing unconditional behaviour.\n+    \"\"\"\n+    if model_cthresh is None:\n+        return global_threshold, None\n+    if is_codex_autoraise:\n+        if model_cthresh <= global_threshold + 1e-9:\n+            # Autoraise never lowers; keep the user's higher/equal threshold.\n+            return global_threshold, None\n+        return model_cthresh, {\n+            \"model\": model,\n+            \"from\": global_threshold,\n+            \"to\": model_cthresh,\n+        }\n+    return model_cthresh, None\n+\n+\n+def _codex_gpt55_autoraise_notice_marker():\n+    \"\"\"Path to the per-profile marker recording that the autoraise notice ran.\n+\n+    Lives under ``$HERMES_HOME`` (which is profile-scoped) alongside the other\n+    internal markers like ``.container-mode`` — so it is not a user-facing config\n+    key, and every profile tracks its own notice state independently.\n+    \"\"\"\n+    return get_hermes_home() / \".codex_gpt55_autoraise_notice\"\n+\n+\n+def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str:\n+    \"\"\"Stable identity for one autoraise notice, keyed on what it displays.\n+\n+    Uses the model slug plus the same from→to percentages the notice text\n+    shows, so an unchanged threshold stays silent across restarts while a\n+    later change (the user edits their global ``threshold``, or switches to a\n+    different autoraised Codex model) re-notifies once.\n+    \"\"\"\n+    model = str(autoraise.get(\"model\") or \"\").strip().lower().rsplit(\"/\", 1)[-1]\n+    from_pct = int(round(float(autoraise[\"from\"]) * 100))\n+    to_pct = int(round(float(autoraise[\"to\"]) * 100))\n+    return f\"{model}:{from_pct}:{to_pct}\"\n+\n+\n+def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool:\n+    \"\"\"True if this exact autoraise notice was already shown for this profile.\n+\n+    A missing/unreadable marker (or one recording a different threshold) reads\n+    as unseen, so the notice shows.\n+    \"\"\"\n+    try:\n+        current = _codex_gpt55_autoraise_notice_state(autoraise)\n+        return _codex_gpt55_autoraise_notice_marker().read_text(\n+            encoding=\"utf-8\"\n+        ).strip() == current\n+    except (OSError, KeyError, TypeError, ValueError):\n+        return False\n+\n+\n+def _record_codex_gpt55_autoraise_notice(autoraise: Dict[str, Any]) -> None:\n+    \"\"\"Persist that the autoraise notice was shown for this profile/config state.\n+\n+    Best-effort: a read-only or missing ``$HERMES_HOME`` just means the notice\n+    may show again next init, which is preferable to breaking agent init.\n+    \"\"\"\n+    try:\n+        marker = _codex_gpt55_autoraise_notice_marker()\n+        marker.parent.mkdir(parents=True, exist_ok=True)\n+        marker.write_text(\n+            _codex_gpt55_autoraise_notice_state(autoraise), encoding=\"utf-8\"\n+        )\n+    except (OSError, KeyError, TypeError, ValueError):\n+        pass\n+\n+\n def _normalized_custom_base_url(value: Any) -> str:\n     if not isinstance(value, str):\n         return \"\"","@@ -1409,14 +1503,14 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:\n     if not isinstance(_compression_cfg, dict):\n         _compression_cfg = {}\n     compression_threshold = float(_compression_cfg.get(\"threshold\", 0.50))\n-    # Per-model/route compaction-threshold override. Codex gpt-5.5 raises to\n-    # 85% (the Codex backend caps the window at 272K, so the default 50% would\n-    # compact at ~136K — half the usable context). Gated by an opt-out config\n-    # flag so the user can fall back to the global threshold; when the override\n-    # fires we stash a one-time notification (replayed on the first turn) that\n-    # tells the user what changed and how to revert. The notice has its own\n-    # display gate so users can keep the threshold autoraise without getting\n-    # the banner on gateway turns.\n+    # Per-model/route compaction-threshold override. Codex gpt-5.4 / gpt-5.5\n+    # raise to 85% (the Codex backend caps both families at 272K, so the\n+    # default 50% would compact at ~136K — half the usable context). Gated by\n+    # an opt-out config flag so the user can fall back to the global threshold;\n+    # when the override fires we stash a one-time notification (replayed on the\n+    # first turn) that tells the user what changed and how to revert. The\n+    # notice has its own display gate so users can keep the threshold\n+    # autoraise without getting the banner on gateway turns.\n     _codex_gpt55_autoraise = str(\n         _compression_cfg.get(\"codex_gpt55_autoraise\", True)\n     ).lower() in {\"true\", \"1\", \"yes\"}","@@ -1427,28 +1521,30 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:\n     try:\n         from agent.auxiliary_client import (\n             _compression_threshold_for_model as _cthresh_fn,\n-            _is_codex_gpt55 as _is_codex_gpt55_fn,\n+            _is_codex_gpt54_or_gpt55 as _is_codex_gpt54_or_gpt55_fn,\n+            _is_codex_spark as _is_codex_spark_fn,\n         )\n         _model_cthresh = _cthresh_fn(\n             agent.model,\n             agent.provider,\n             allow_codex_gpt55_autoraise=_codex_gpt55_autoraise,\n         )\n-        if _model_cthresh is not None:\n-            _prev_threshold = compression_threshold\n-            compression_threshold = _model_cthresh\n-            # Notify only for the Codex gpt-5.5 autoraise (the Arcee Trinity\n-            # override is a long-standing silent default). Skip the notice when\n-            # the user's global threshold already meets/exceeds the raised\n-            # value, since nothing actually changed for them.\n-            if (\n-                _is_codex_gpt55_fn(agent.model, agent.provider)\n-                and _model_cthresh > _prev_threshold + 1e-9\n-            ):\n-                agent._compression_threshold_autoraised = {\n-                    \"from\": _prev_threshold,\n-                    \"to\": _model_cthresh,\n-                }\n+        # The Codex autoraises (gpt-5.4/5.5 272K family and gpt-5.3-codex-spark)\n+        # apply only when they RAISE (never lower a user's higher global\n+        # threshold). The notice is populated only when it actually fires, and\n+        # carries the model slug so the banner names the right family. Arcee\n+        # Trinity keeps its long-standing unconditional behaviour.\n+        compression_threshold, agent._compression_threshold_autoraised = (\n+            _resolve_compression_threshold(\n+                compression_threshold,\n+                _model_cthresh,\n+                model=agent.model,\n+                is_codex_autoraise=(\n+                    _is_codex_gpt54_or_gpt55_fn(agent.model, agent.provider)\n+                    or _is_codex_spark_fn(agent.model, agent.provider)\n+                ),\n+            )\n+        )\n     except Exception:\n         pass\n     compression_enabled = str(_compression_cfg.get(\"enabled\", True)).lower() in {\"true\", \"1\", \"yes\"}","@@ -1677,6 +1773,12 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:\n \n     if _selected_engine is not None:\n         agent.context_compressor = _selected_engine\n+        # External engines own compaction policy: the host compression\n+        # threshold (including the Codex gpt-5.5 autoraise above) only\n+        # configures the built-in ContextCompressor and never reaches the\n+        # plugin, so the autoraise notice would announce a change that does\n+        # not apply. Drop it. (#44439)\n+        agent._compression_threshold_autoraised = None\n         # Resolve context_length for plugin engines — mirrors switch_model() path\n         from agent.model_metadata import get_model_context_length\n         _plugin_ctx_len = get_model_context_length(","@@ -1899,29 +2001,53 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:\n             agent._ollama_num_ctx,\n         )\n \n+    # Codex gpt-5.x autoraise notice: show at most once per profile/config\n+    # state. Without the persisted marker the notice re-fires on every agent\n+    # init — and the gateway rebuilds the agent per inbound message, so Discord\n+    # etc. saw it repeatedly (#54432). A change in the raised threshold (or the\n+    # autoraised model) updates the marker state and re-notifies once. The\n+    # config display gate (compression.codex_gpt55_autoraise_notice) still\n+    # suppresses the banner entirely without disabling the threshold autoraise.\n+    _autoraise = getattr(agent, \"_compression_threshold_autoraised\", None)\n+    _show_autoraise_notice = (\n+        bool(_autoraise)\n+        and compression_enabled\n+        and _codex_gpt55_autoraise_notice\n+        and not _codex_gpt55_autoraise_notice_seen(_autoraise)\n+    )\n+\n     if not agent.quiet_mode:\n         if compression_enabled:\n-            print(f\"📊 Context limit: {agent.context_compressor.context_length:,} tokens (compress at {int(compression_threshold*100)}% = {agent.context_compressor.threshold_tokens:,})\")\n+            # Report the active engine's own threshold — for a plugin engine\n+            # the host compression_threshold is not in effect, and mixing the\n+            # two printed a percent that contradicted the token count. (#44439)\n+            _active_threshold_pct = getattr(\n+                agent.context_compressor, \"threshold_percent\", compression_threshold\n+            )\n+            print(f\"📊 Context limit: {agent.context_compressor.context_length:,} tokens (compress at {int(_active_threshold_pct*100)}% = {agent.context_compressor.threshold_tokens:,})\")\n         else:\n             print(f\"📊 Context limit: {agent.context_compressor.context_length:,} tokens (auto-compression disabled)\")\n-        # One-time notice when the Codex gpt-5.5 autoraise kicked in, with the\n-        # exact opt-back-out command. Printed inline at startup for CLI users;\n-        # gateway users get the same text replayed via _compression_warning on\n-        # turn 1 (set below, after the warning slot is initialized).\n-        _autoraise = getattr(agent, \"_compression_threshold_autoraised\", None)\n-        if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:\n-            print(_build_codex_gpt55_autoraise_notice(_autoraise))\n+        # Notice with the exact opt-back-out command. Printed inline at startup\n+        # for CLI users; gateway users get the same text replayed via\n+        # _compression_warning on turn 1 (set below).\n+        if _show_autoraise_notice:\n+            print(_build_codex_gpt5_autoraise_notice(_autoraise))\n \n     # Check immediately so CLI users see the warning at startup.\n     # Gateway status_callback is not yet wired, so any warning is stored\n     # in _compression_warning and replayed in the first run_conversation().\n     agent._compression_warning = None\n-    # Gateway parity for the Codex gpt-5.5 autoraise notice: the startup print\n+    # Gateway parity for the Codex gpt-5.x autoraise notice: the startup print\n     # above only reaches the CLI, so stash the same text here to be replayed\n     # through status_callback on the first turn (Telegram/Discord/Slack/etc.).\n-    _autoraise = getattr(agent, \"_compression_threshold_autoraised\", None)\n-    if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:\n-        agent._compression_warning = _build_codex_gpt55_autoraise_notice(_autoraise)\n+    if _show_autoraise_notice:\n+        agent._compression_warning = _build_codex_gpt5_autoraise_notice(_autoraise)\n+\n+    # Mark shown so repeated inits in this profile (e.g. every gateway message)\n+    # stay silent. Recorded once, whether the notice went to the CLI print or\n+    # the gateway replay slot.\n+    if _show_autoraise_notice:\n+        _record_codex_gpt55_autoraise_notice(_autoraise)\n     # Lazy feasibility check: deferred to the first turn that approaches the\n     # compression threshold. Running it eagerly here costs ~400ms cold (network\n     # probe of the auxiliary provider chain + /models lookup) on every agent"]},{"file":"agent/auxiliary_client.py","hunks":["@@ -314,33 +314,64 @@ def _is_arcee_trinity_thinking(model: Optional[str]) -> bool:\n     return bare == \"trinity-large-thinking\"\n \n \n-# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.5.\n+# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.4/5.5.\n # The raw OpenAI API and OpenRouter expose 1.05M for the same slug, but the\n # Codex backend hard-caps at 272K (verified live: a ~330K-token request to\n # chatgpt.com/backend-api/codex/responses is rejected with\n # ``context_length_exceeded`` while ~250K succeeds). With a 272K ceiling the\n # default 50% compaction trigger fires at ~136K — wasteful, since the model\n # can hold far more raw context before summarization actually buys anything.\n-# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.5\n-# sessions use the window they actually have.\n-_CODEX_GPT55_COMPACTION_THRESHOLD = 0.85\n+# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.4/\n+# gpt-5.5 sessions use the window they actually have.\n+_CODEX_GPT54_GPT55_COMPACTION_THRESHOLD = 0.85\n \n+# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a\n+# native 128K context window.  The default 50% compaction trigger fires at\n+# ~64K — wasting half the usable window, often before the session has enough\n+# turns to summarize meaningfully.  We raise the trigger to 70% (~90K) so\n+# spark sessions use more of the window before summarization, while still\n+# leaving ~38K headroom for the summary and continued conversation before\n+# the 128K hard limit.\n+_CODEX_SPARK_COMPACTION_THRESHOLD = 0.70\n \n-def _is_codex_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:\n-    \"\"\"True for gpt-5.5 accessed through the ChatGPT Codex OAuth backend.\n+\n+def _is_codex_gpt54_or_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:\n+    \"\"\"True for gpt-5.4 / gpt-5.5 on the ChatGPT Codex OAuth backend.\n \n     Matches only the Codex OAuth route (provider ``openai-codex``), not the\n     direct OpenAI API, OpenRouter, or GitHub Copilot paths — those expose a\n     larger context window for the same slug and must keep the user's default\n-    compaction threshold. ``gpt-5.5-pro`` and dated snapshots\n-    (``gpt-5.5-2026-04-23``) are matched via prefix so the override tracks the\n-    family without re-listing every variant.\n+    compaction threshold. ``gpt-5.4-pro`` / ``gpt-5.5-pro`` and dated snapshots\n+    are matched via prefix so the override tracks both 272K-capped families\n+    without re-listing every variant.\n     \"\"\"\n     prov = (provider or \"\").strip().lower()\n     if prov != \"openai-codex\":\n         return False\n     bare = (model or \"\").strip().lower().rsplit(\"/\", 1)[-1]\n-    return bare == \"gpt-5.5\" or bare.startswith(\"gpt-5.5-\") or bare.startswith(\"gpt-5.5.\")\n+    return (\n+        bare == \"gpt-5.4\"\n+        or bare.startswith(\"gpt-5.4-\")\n+        or bare.startswith(\"gpt-5.4.\")\n+        or bare == \"gpt-5.5\"\n+        or bare.startswith(\"gpt-5.5-\")\n+        or bare.startswith(\"gpt-5.5.\")\n+    )\n+\n+\n+def _is_codex_spark(model: Optional[str], provider: Optional[str] = None) -> bool:\n+    \"\"\"True for ``gpt-5.3-codex-spark`` on the ChatGPT Codex OAuth backend.\n+\n+    The model is Codex-OAuth-only (ChatGPT Pro entitlement) with a native\n+    128K context window.  Only the Codex OAuth route (provider\n+    ``openai-codex``) is matched — the slug is not available on other\n+    routes.\n+    \"\"\"\n+    prov = (provider or \"\").strip().lower()\n+    if prov != \"openai-codex\":\n+        return False\n+    bare = (model or \"\").strip().lower().rsplit(\"/\", 1)[-1]\n+    return bare == \"gpt-5.3-codex-spark\"\n \n \n def _fixed_temperature_for_model(","@@ -379,18 +410,26 @@ def _compression_threshold_for_model(\n \n     Per-model/route overrides:\n       - Arcee Trinity Large Thinking → 0.75 (preserve reasoning context).\n-      - gpt-5.5 on the Codex OAuth route → 0.85, because Codex caps the window\n-        at 272K and the default 50% trigger would compact at ~136K. Gated by\n-        ``allow_codex_gpt55_autoraise`` so the user can opt back down to the\n-        global default (the caller passes the config flag through here).\n+      - gpt-5.4 / gpt-5.5 on the Codex OAuth route → 0.85, because Codex caps\n+        both families at 272K and the default 50% trigger would compact at\n+        ~136K. Gated by ``allow_codex_gpt55_autoraise`` (historical config-key\n+        name kept for backward compatibility) so the user can opt back down to\n+        the global default (the caller passes the config flag through here).\n+      - gpt-5.3-codex-spark on the Codex OAuth route → 0.70, because the model\n+        has a native 128K window and the default 50% trigger would compact at\n+        ~64K — wasting half the usable context. Not gated by the gpt-5.5\n+        opt-out flag: 128K is the model's native window, so the raise is\n+        unambiguously correct.\n \n     Returns a float in (0, 1] to override the global ``compression.threshold``\n     config value, or ``None`` to leave the user's config value unchanged.\n     \"\"\"\n     if _is_arcee_trinity_thinking(model):\n         return 0.75\n-    if allow_codex_gpt55_autoraise and _is_codex_gpt55(model, provider):\n-        return _CODEX_GPT55_COMPACTION_THRESHOLD\n+    if allow_codex_gpt55_autoraise and _is_codex_gpt54_or_gpt55(model, provider):\n+        return _CODEX_GPT54_GPT55_COMPACTION_THRESHOLD\n+    if _is_codex_spark(model, provider):\n+        return _CODEX_SPARK_COMPACTION_THRESHOLD\n     return None\n \n # Default auxiliary models for direct API-key providers (cheap/fast for side tasks)","@@ -4151,7 +4190,15 @@ def resolve_provider_client(\n     # main_model also empty), the branches still hit their own\n     # missing-credentials returns and ``_resolve_auto`` falls through to\n     # the Step-2 chain as before.\n-    if not model:\n+    #\n+    # Prefer explicit caller model, then provider-scoped aux model, then main model.\n+    # Do NOT pre-fill a blank ``auto`` request from the config/main default here.\n+    # ``auto`` has its own main-runtime resolver below; pre-filling first can pair\n+    # a stale configured model with a live fallback provider (e.g. Claude model\n+    # sent to Codex after the main lane fell back to gpt-5.5). Let _resolve_auto()\n+    # return the actual current runtime model when the caller did not explicitly\n+    # request one. (# compression-current-model)\n+    if not model and provider != \"auto\":\n         model = _get_aux_model_for_provider(provider) or _read_main_model() or model\n \n     def _needs_codex_wrap(client_obj, base_url_str: str, model_str: str) -> bool:"]},{"file":"hermes_cli/config.py","hunks":["@@ -1362,17 +1362,18 @@ def _ensure_hermes_home_managed(home: Path):\n                                       # Default False matches historical behavior; set to\n                                       # True if you'd rather pause than silently lose\n                                       # context turns when your aux model is flaky.\n-        \"codex_gpt55_autoraise\": True,  # When True, gpt-5.5 on the ChatGPT Codex OAuth\n-                                      # route raises its compaction trigger to 85% (vs the\n-                                      # global `threshold` above). Codex hard-caps gpt-5.5\n-                                      # at a 272K window, so the default 50% would compact\n-                                      # at ~136K and waste half the usable context. Set to\n-                                      # False to opt back down to the global threshold\n-                                      # (e.g. 0.50) for Codex gpt-5.5 sessions. Only this\n-                                      # exact route is affected — gpt-5.5 on OpenAI's\n-                                      # direct API, OpenRouter, and Copilot keep the\n-                                      # global threshold regardless.\n-        \"codex_gpt55_autoraise_notice\": True,  # Display the one-time Codex gpt-5.5\n+        \"codex_gpt55_autoraise\": True,  # Historical key name kept for compatibility.\n+                                      # When True, gpt-5.4 / gpt-5.5 on the ChatGPT Codex\n+                                      # OAuth route raise their compaction trigger to 85%\n+                                      # (vs the global `threshold` above). Codex hard-caps\n+                                      # both families at a 272K window, so the default 50%\n+                                      # would compact at ~136K and waste half the usable\n+                                      # context. Set to False to opt back down to the global\n+                                      # threshold (e.g. 0.50) for those Codex sessions.\n+                                      # Only this exact route is affected — gpt-5.4 / 5.5\n+                                      # on OpenAI's direct API, OpenRouter, and Copilot keep\n+                                      # the global threshold regardless.\n+        \"codex_gpt55_autoraise_notice\": True,  # Display the one-time Codex gpt-5.4/5.5\n                                       # autoraise banner. Set False to keep the\n                                       # 85% threshold autoraise but suppress the\n                                       # user-facing notice in CLI/gateway output."]},{"file":"scripts/release.py","hunks":["@@ -285,6 +285,7 @@\n     \"157689911+itsflownium@users.noreply.github.com\": \"itsflownium\",\n     \"dirtyren@users.noreply.github.com\": \"dirtyren\",\n     \"[redacted:email]\": \"bigstar0920\",\n+    \"[redacted:email]\": \"tanmayxchoudhary\",\n     \"285906080+AIalliAI@users.noreply.github.com\": \"AIalliAI\",\n     \"waseemshahwan@users.noreply.github.com\": \"waseemshahwan\",\n     \"hellno@users.noreply.github.com\": \"hellno\","]},{"file":"tests/agent/test_arcee_trinity_overrides.py","hunks":["@@ -13,11 +13,13 @@\n \n import pytest\n \n+from agent.agent_init import _resolve_compression_threshold\n from agent.auxiliary_client import (\n     _compression_threshold_for_model,\n     _fixed_temperature_for_model,\n     _is_arcee_trinity_thinking,\n-    _is_codex_gpt55,\n+    _is_codex_gpt54_or_gpt55,\n+    _is_codex_spark,\n )\n \n ","@@ -78,14 +80,14 @@ def test_compression_threshold_default_none_for_other_models() -> None:\n \n \n # ---------------------------------------------------------------------------\n-# Codex gpt-5.5 compaction-threshold autoraise\n+# Codex gpt-5.4 / gpt-5.5 compaction-threshold autoraise\n #\n-# ChatGPT's Codex OAuth backend caps gpt-5.5 at a 272K window (verified live:\n-# ~330K-token request rejected with context_length_exceeded, ~250K accepted).\n-# The default 50% compaction trigger would fire at ~136K — half the usable\n-# window — so this route raises the trigger to 85%. Only the Codex OAuth route\n-# is affected; the same slug on OpenAI direct / OpenRouter / Copilot exposes a\n-# larger window and keeps the user's global threshold.\n+# ChatGPT's Codex OAuth backend caps both families at a 272K window (verified\n+# live via the Codex /models resolver and per-slug fallback table). The default\n+# 50% compaction trigger would fire at ~136K — half the usable window — so this\n+# route raises the trigger to 85%. Only the Codex OAuth route is affected; the\n+# same slugs on OpenAI direct / OpenRouter / Copilot expose a larger window and\n+# keep the user's global threshold.\n # ---------------------------------------------------------------------------\n \n ","@@ -99,32 +101,40 @@ def test_compression_threshold_default_none_for_other_models() -> None:\n         \"openai/gpt-5.5\",  # aggregator-prefixed (still on the codex route)\n         \"GPT-5.5\",  # case-insensitive\n         \"  gpt-5.5  \",  # whitespace tolerant\n+        \"gpt-5.4\",  # base 5.4 (272K-capped)\n+        \"gpt-5.4-pro\",  # pro 5.4 variant (272K-capped)\n+        \"gpt-5.4-2026-01-01\",  # dated 5.4 snapshot\n+        \"openai/gpt-5.4\",  # aggregator-prefixed 5.4\n     ],\n )\n-def test_is_codex_gpt55_matches_on_codex_provider(model: str) -> None:\n-    assert _is_codex_gpt55(model, \"openai-codex\") is True\n+def test_is_codex_gpt54_or_gpt55_matches_on_codex_provider(model: str) -> None:\n+    assert _is_codex_gpt54_or_gpt55(model, \"openai-codex\") is True\n \n \n @pytest.mark.parametrize(\n     \"provider\",\n     [\"openrouter\", \"openai\", \"copilot\", \"openai-api\", \"\", None],\n )\n-def test_is_codex_gpt55_rejects_non_codex_providers(provider) -> None:\n-    # gpt-5.5 on any non-Codex route keeps the larger window — no override.\n-    assert _is_codex_gpt55(\"gpt-5.5\", provider) is False\n+def test_is_codex_gpt54_or_gpt55_rejects_non_codex_providers(provider) -> None:\n+    # gpt-5.4 / gpt-5.5 on any non-Codex route keep the larger window.\n+    assert _is_codex_gpt54_or_gpt55(\"gpt-5.5\", provider) is False\n+    assert _is_codex_gpt54_or_gpt55(\"gpt-5.4\", provider) is False\n \n \n @pytest.mark.parametrize(\n     \"model\",\n-    [\"gpt-5.4\", \"gpt-5\", \"gpt-5.55\", \"gpt-5.50\", \"\", None],\n+    [\"gpt-5\", \"gpt-5.55\", \"gpt-5.50\", \"gpt-5.45\", \"gpt-5.40\", \"\", None],\n )\n-def test_is_codex_gpt55_rejects_non_55_models(model) -> None:\n-    # gpt-5.55 / gpt-5.50 are different families and must NOT match — the\n-    # \"gpt-5.5-\" / \"gpt-5.5.\" prefix guards require a separator after \"5.5\".\n-    assert _is_codex_gpt55(model, \"openai-codex\") is False\n+def test_is_codex_gpt54_or_gpt55_rejects_non_54_55_models(model) -> None:\n+    # Close numeric neighbours must NOT match — the prefix guards require a\n+    # separator after \"5.4\" / \"5.5\" so e.g. gpt-5.45 and gpt-5.55 stay out.\n+    assert _is_codex_gpt54_or_gpt55(model, \"openai-codex\") is False\n \n \n def test_compression_threshold_for_codex_gpt55() -> None:\n+    assert _compression_threshold_for_model(\"gpt-5.4\", \"openai-codex\") == 0.85\n+    assert _compression_threshold_for_model(\"gpt-5.4-pro\", \"openai-codex\") == 0.85\n+    assert _compression_threshold_for_model(\"openai/gpt-5.4\", \"openai-codex\") == 0.85\n     assert _compression_threshold_for_model(\"gpt-5.5\", \"openai-codex\") == 0.85\n     assert _compression_threshold_for_model(\"gpt-5.5-pro\", \"openai-codex\") == 0.85\n     assert _compression_threshold_for_model(\"openai/gpt-5.5\", \"openai-codex\") == 0.85","@@ -132,14 +142,24 @@ def test_compression_threshold_for_codex_gpt55() -> None:\n \n def test_compression_threshold_codex_gpt55_other_routes_unaffected() -> None:\n     # Same slug, different route → no override (keep the user's config value).\n+    assert _compression_threshold_for_model(\"gpt-5.4\", \"openrouter\") is None\n+    assert _compression_threshold_for_model(\"gpt-5.4\", \"openai\") is None\n+    assert _compression_threshold_for_model(\"gpt-5.4\", \"copilot\") is None\n     assert _compression_threshold_for_model(\"gpt-5.5\", \"openrouter\") is None\n     assert _compression_threshold_for_model(\"gpt-5.5\", \"openai\") is None\n     assert _compression_threshold_for_model(\"gpt-5.5\", \"copilot\") is None\n+    assert _compression_threshold_for_model(\"openai/gpt-5.4\") is None  # no provider\n     assert _compression_threshold_for_model(\"openai/gpt-5.5\") is None  # no provider\n \n \n def test_compression_threshold_codex_gpt55_opt_out() -> None:\n-    # allow_codex_gpt55_autoraise=False reverts to the global default (None).\n+    # Historical flag name still governs both Codex families.\n+    assert (\n+        _compression_threshold_for_model(\n+            \"gpt-5.4\", \"openai-codex\", allow_codex_gpt55_autoraise=False\n+        )\n+        is None\n+    )\n     assert (\n         _compression_threshold_for_model(\n             \"gpt-5.5\", \"openai-codex\", allow_codex_gpt55_autoraise=False","@@ -157,3 +177,140 @@ def test_compression_threshold_opt_out_does_not_disable_trinity() -> None:\n         )\n         == 0.75\n     )\n+\n+\n+# ---------------------------------------------------------------------------\n+# Codex gpt-5.3-codex-spark compaction-threshold autoraise\n+#\n+# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a\n+# native 128K context window.  The default 50% compaction trigger would fire\n+# at ~64K — wasting half the usable window, often before the session has\n+# accumulated enough turns to summarize meaningfully.  This route raises the\n+# trigger to 70% (~90K) to preserve more raw context while leaving ~38K\n+# headroom before the 128K hard limit.\n+# ---------------------------------------------------------------------------\n+\n+\n[redacted:email](\n+    \"model\",\n+    [\n+        \"gpt-5.3-codex-spark\",\n+        \"openai/gpt-5.3-codex-spark\",  # aggregator-prefixed (still on the codex route)\n+        \"GPT-5.3-CODEX-SPARK\",  # case-insensitive\n+        \"  gpt-5.3-codex-spark  \",  # whitespace tolerant\n+    ],\n+)\n+def test_is_codex_spark_matches_on_codex_provider(model: str) -> None:\n+    assert _is_codex_spark(model, \"openai-codex\") is True\n+\n+\n[redacted:email](\n+    \"provider\",\n+    [\"openrouter\", \"openai\", \"copilot\", \"openai-api\", \"\", None],\n+)\n+def test_is_codex_spark_rejects_non_codex_providers(provider) -> None:\n+    # spark on any non-Codex route is not a real slug — no override.\n+    assert _is_codex_spark(\"gpt-5.3-codex-spark\", provider) is False\n+\n+\n[redacted:email](\n+    \"model\",\n+    [\n+        \"gpt-5.5\",  # different family\n+        \"gpt-5.3-codex\",  # sibling, not spark\n+        \"gpt-5.3\",  # bare 5.3, not spark\n+        \"gpt-5.3-codex-spark-mini\",  # hypothetical variant — not matched yet\n+        \"\", None,\n+    ],\n+)\n+def test_is_codex_spark_rejects_non_spark_models(model) -> None:\n+    assert _is_codex_spark(model, \"openai-codex\") is False\n+\n+\n+def test_compression_threshold_for_codex_spark() -> None:\n+    assert _compression_threshold_for_model(\"gpt-5.3-codex-spark\", \"openai-codex\") == 0.70\n+    assert _compression_threshold_for_model(\"openai/gpt-5.3-codex-spark\", \"openai-codex\") == 0.70\n+\n+\n+def test_compression_threshold_codex_spark_other_routes_unaffected() -> None:\n+    # Same slug, different route → no override (keep the user's config value).\n+    assert _compression_threshold_for_model(\"gpt-5.3-codex-spark\", \"openrouter\") is None\n+    assert _compression_threshold_for_model(\"gpt-5.3-codex-spark\", \"openai\") is None\n+    assert _compression_threshold_for_model(\"gpt-5.3-codex-spark\") is None  # no provider\n+\n+\n+def test_compression_threshold_codex_spark_not_gated_by_gpt55_optout() -> None:\n+    # The spark autoraise is independent of the gpt-5.5 opt-out flag — 128K is\n+    # the model's native window, so 70% is unambiguously correct regardless of\n+    # whether the user opted out of the (artificial-cap) gpt-5.5 autoraise.\n+    assert (\n+        _compression_threshold_for_model(\n+            \"gpt-5.3-codex-spark\", \"openai-codex\", allow_codex_gpt55_autoraise=False\n+        )\n+        == 0.70\n+    )\n+\n+\n+# ── _resolve_compression_threshold (init_agent application logic) ────────────\n+#\n+# The Codex overrides are *autoraises*: they raise the trigger (0.85 for the\n+# gpt-5.4/5.5 272K family, 0.70 for spark) but must never LOWER a higher\n+# user-configured global threshold.\n+\n+\n+def test_resolve_codex_autoraise_raises_from_default() -> None:\n+    # Default 0.50 global → raised to 0.85, notice emitted.\n+    effective, notice = _resolve_compression_threshold(\n+        0.50, 0.85, model=\"gpt-5.5\", is_codex_autoraise=True\n+    )\n+    assert effective == 0.85\n+    assert notice == {\"model\": \"gpt-5.5\", \"from\": 0.50, \"to\": 0.85}\n+\n+\n+def test_resolve_codex_autoraise_never_lowers_higher_threshold() -> None:\n+    # Regression: a user who set compression.threshold above 0.85 must keep it.\n+    # The autoraise previously clobbered it down to 0.85 (and silently, since\n+    # the notice was suppressed when nothing \"raised\").\n+    effective, notice = _resolve_compression_threshold(\n+        0.90, 0.85, model=\"gpt-5.5\", is_codex_autoraise=True\n+    )\n+    assert effective == 0.90\n+    assert notice is None\n+\n+\n+def test_resolve_codex_spark_autoraise_never_lowers_higher_threshold() -> None:\n+    # Same never-lower contract for the spark autoraise (0.70).\n+    effective, notice = _resolve_compression_threshold(\n+        0.80, 0.70, model=\"gpt-5.3-codex-spark\", is_codex_autoraise=True\n+    )\n+    assert effective == 0.80\n+    assert notice is None\n+\n+\n+def test_resolve_codex_autoraise_equal_threshold_is_noop() -> None:\n+    # User already at exactly the raised value: keep it, no notice.\n+    effective, notice = _resolve_compression_threshold(\n+        0.85, 0.85, model=\"gpt-5.5\", is_codex_autoraise=True\n+    )\n+    assert effective == 0.85\n+    assert notice is None\n+\n+\n+def test_resolve_no_override_keeps_global() -> None:\n+    # No per-model override (model_cthresh is None) → global threshold, no notice.\n+    effective, notice = _resolve_compression_threshold(\n+        0.50, None, is_codex_autoraise=False\n+    )\n+    assert effective == 0.50\n+    assert notice is None\n+\n+\n+def test_resolve_non_codex_override_applies_unconditionally() -> None:\n+    # Arcee Trinity (0.75) keeps its long-standing unconditional behaviour: it\n+    # applies even when it lowers the user's global value, and never emits the\n+    # codex autoraise notice.\n+    effective, notice = _resolve_compression_threshold(\n+        0.90, 0.75, is_codex_autoraise=False\n+    )\n+    assert effective == 0.75\n+    assert notice is None"]},{"file":"tests/agent/test_auxiliary_main_first.py","hunks":["@@ -279,6 +279,34 @@ def test_runtime_override_wins_over_config(self, monkeypatch):\n         assert mock_resolve.call_args.args[0] == \"anthropic\"\n         assert mock_resolve.call_args.args[1] == \"runtime-model\"\n \n+    def test_resolve_provider_auto_returns_runtime_model_not_stale_config_default(self):\n+        \"\"\"Blank auto aux requests must not pair a stale config model with live fallback provider.\"\"\"\n+        runtime_client = MagicMock()\n+        with patch(\n+            \"agent.auxiliary_client._read_main_model\",\n+            return_value=\"claude-opus-4-8\",\n+        ) as mock_read_main_model, patch(\n+            \"agent.auxiliary_client._resolve_auto\",\n+            return_value=(runtime_client, \"gpt-5.5\"),\n+        ) as mock_resolve_auto:\n+            from agent.auxiliary_client import resolve_provider_client\n+\n+            client, model = resolve_provider_client(\n+                \"auto\",\n+                main_runtime={\n+                    \"provider\": \"openai-codex\",\n+                    \"model\": \"gpt-5.5\",\n+                    \"base_url\": \"\",\n+                    \"api_key\": \"\",\n+                    \"api_mode\": \"codex_responses\",\n+                },\n+            )\n+\n+        assert client is runtime_client\n+        assert model == \"gpt-5.5\"\n+        mock_read_main_model.assert_not_called()\n+        mock_resolve_auto.assert_called_once()\n+\n     def test_runtime_base_url_passed_for_named_api_key_provider(self):\n         \"\"\"Named API-key providers inherit the live session endpoint for aux work.\"\"\"\n         token_plan_url = \"[redacted:secret-assignment]\""]},{"file":"tests/agent/test_codex_gpt55_autoraise_notice.py","hunks":["@@ -1,4 +1,18 @@\n-\"\"\"Regression tests for the Codex gpt-5.5 autoraise notice gate.\"\"\"\n+\"\"\"Regression tests for the Codex gpt-5.x autoraise notice gate.\n+\n+Covers two layers:\n+\n+1. The config display gate (``compression.codex_gpt55_autoraise_notice``) —\n+   suppresses the banner without disabling the threshold autoraise.\n+2. The per-profile dedupe marker (#54432) — the notice must show at most once\n+   per profile/config state. Before the fix it re-fired on every agent init,\n+   and because the gateway rebuilds the agent per inbound message it spammed\n+   Discord etc. The gate persists a marker under ``$HERMES_HOME``\n+   (profile-scoped, isolated to a tempdir by the conftest autouse fixture)\n+   keyed on the model slug + displayed from→to percentages, so an unchanged\n+   threshold stays silent across restarts while a changed threshold (or a\n+   different autoraised Codex model) re-notifies once.\n+\"\"\"\n \n from __future__ import annotations\n ","@@ -6,9 +20,22 @@\n import io\n from pathlib import Path\n \n+import pytest\n+\n+from hermes_constants import get_hermes_home\n from hermes_state import SessionDB\n from run_agent import AIAgent\n \n+from agent.agent_init import (\n+    _codex_gpt55_autoraise_notice_marker,\n+    _codex_gpt55_autoraise_notice_seen,\n+    _codex_gpt55_autoraise_notice_state,\n+    _record_codex_gpt55_autoraise_notice,\n+)\n+\n+# The dict agent_init stashes when the Codex gpt-5.5 override fires.\n+AUTORAISE = {\"model\": \"gpt-5.5\", \"from\": 0.50, \"to\": 0.85}\n+\n \n def _config(*, show_notice: bool) -> dict:\n     return {","@@ -57,6 +84,9 @@ def _threshold_ratio(agent: AIAgent) -> float:\n     return round(compressor.threshold_tokens / compressor.context_length, 2)\n \n \n+# ── config display gate ──────────────────────────────────────────────────────\n+\n+\n def test_codex_gpt55_autoraise_notice_enabled_by_default(monkeypatch, tmp_path):\n     agent, stdout = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)\n ","@@ -75,3 +105,113 @@ def test_codex_gpt55_autoraise_notice_can_be_suppressed_without_disabling_autora\n     assert _threshold_ratio(agent) == 0.85\n     assert getattr(agent, \"_compression_warning\") is None\n     assert \"auto-compaction was raised\" not in stdout\n+\n+\n+def test_codex_gpt55_autoraise_notice_deduped_across_agent_inits(monkeypatch, tmp_path):\n+    # Gateway spam scenario (#54432): the gateway rebuilds the agent per\n+    # inbound message. The first init shows the notice; the second stays\n+    # silent because the per-profile marker was recorded.\n+    agent1, stdout1 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)\n+    assert \"auto-compaction was raised\" in stdout1\n+    assert getattr(agent1, \"_compression_warning\") is not None\n+\n+    agent2, stdout2 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)\n+    assert _threshold_ratio(agent2) == 0.85  # autoraise still applies\n+    assert \"auto-compaction was raised\" not in stdout2\n+    assert getattr(agent2, \"_compression_warning\") is None\n+\n+\n+# ── per-profile dedupe marker (#54432) ───────────────────────────────────────\n+\n+\n+def test_marker_lives_under_hermes_home() -> None:\n+    marker = _codex_gpt55_autoraise_notice_marker()\n+    assert marker.parent == get_hermes_home()\n+    assert marker.name == \".codex_gpt55_autoraise_notice\"\n+\n+\n+def test_state_keyed_on_model_and_displayed_percentages() -> None:\n+    # Same percentages the notice text renders (int(round(ratio * 100))),\n+    # prefixed with the bare model slug.\n+    assert _codex_gpt55_autoraise_notice_state(AUTORAISE) == \"gpt-5.5:50:85\"\n+    assert (\n+        _codex_gpt55_autoraise_notice_state(\n+            {\"model\": \"openai/gpt-5.4\", \"from\": 0.75, \"to\": 0.85}\n+        )\n+        == \"gpt-5.4:75:85\"\n+    )\n+\n+\n+def test_unseen_before_anything_is_recorded() -> None:\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False\n+\n+\n+def test_seen_after_record() -> None:\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False\n+    _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+    # A \"restart\" is just another call: the marker persists on disk.\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True\n+\n+\n+def test_changed_threshold_renotifies_once() -> None:\n+    _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True\n+    # User raises their global threshold -> \"from\" changes -> notice re-fires.\n+    changed = {\"model\": \"gpt-5.5\", \"from\": 0.60, \"to\": 0.85}\n+    assert _codex_gpt55_autoraise_notice_seen(changed) is False\n+    _record_codex_gpt55_autoraise_notice(changed)\n+    assert _codex_gpt55_autoraise_notice_seen(changed) is True\n+    # And the old state is now considered unseen (marker moved forward).\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False\n+\n+\n+def test_changed_model_renotifies_once() -> None:\n+    # Switching to a different autoraised Codex model re-fires the notice\n+    # (the banner names the model, so it displays new information).\n+    _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+    other_model = {\"model\": \"gpt-5.4\", \"from\": 0.50, \"to\": 0.85}\n+    assert _codex_gpt55_autoraise_notice_seen(other_model) is False\n+    _record_codex_gpt55_autoraise_notice(other_model)\n+    assert _codex_gpt55_autoraise_notice_seen(other_model) is True\n+\n+\n+def test_record_is_idempotent() -> None:\n+    _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+    _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+    assert (\n+        _codex_gpt55_autoraise_notice_marker().read_text(encoding=\"utf-8\")\n+        == \"gpt-5.5:50:85\"\n+    )\n+\n+\n+def test_malformed_marker_reads_as_unseen() -> None:\n+    marker = _codex_gpt55_autoraise_notice_marker()\n+    marker.parent.mkdir(parents=True, exist_ok=True)\n+    marker.write_text(\"not-a-state\", encoding=\"utf-8\")\n+    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False\n+\n+\n[redacted:email](\"bad\", [{}, {\"from\": 0.5}, {\"from\": None, \"to\": None}])\n+def test_seen_tolerates_malformed_autoraise(bad) -> None:\n+    # Never raises even if the stashed dict is missing/garbage keys.\n+    assert _codex_gpt55_autoraise_notice_seen(bad) is False\n+\n+\n+def test_full_init_gate_shows_once_then_stays_silent() -> None:\n+    # Mirror the decision agent_init makes on each build:\n+    #   show = bool(autoraise) and compression_enabled and not seen(autoraise)\n+    def decide(compression_enabled: bool) -> bool:\n+        show = (\n+            bool(AUTORAISE)\n+            and compression_enabled\n+            and not _codex_gpt55_autoraise_notice_seen(AUTORAISE)\n+        )\n+        if show:\n+            _record_codex_gpt55_autoraise_notice(AUTORAISE)\n+        return show\n+\n+    # First init (any surface) shows; every subsequent init in this profile\n+    # stays silent — the gateway spam scenario from the issue.\n+    assert decide(compression_enabled=True) is True\n+    assert decide(compression_enabled=True) is False\n+    assert decide(compression_enabled=True) is False"]},{"file":"tests/run_agent/test_infinite_compaction_loop.py","hunks":["@@ -283,3 +283,58 @@ def test_no_cooldown_allows(self):\n         comp.last_prompt_tokens = [redacted:secret-assignment]\n         assert comp._summary_failure_cooldown_until == 0.0\n         assert comp.should_compress(65_000)\n+\n+\n+# ---------------------------------------------------------------------------\n+# Test: #48621 — gpt-5.3-codex-spark short-session boundary\n+#\n+# Issue #48621 Bug 2 claims that a short high-token session (15-20 messages,\n+# ~90k tokens on a 128k model with protect_last_n=20) hits\n+# compress_start >= compress_end, causing a silent context wipe.  The\n+# raw-budget fallback added in the #40803 fix already mitigates this: the\n+# boundary logic always exposes a minimal compressible window.  This test\n+# locks that behavior in for the exact #48621 parameters.\n+# ---------------------------------------------------------------------------\n+\n+class TestCodexSparkShortSessionBoundary:\n+    \"\"\"Verify that gpt-5.3-codex-spark's short-session scenario always yields\n+    a non-empty compressible window (no silent wipe).\"\"\"\n+\n+    def test_short_high_token_session_has_compressible_window(self):\n+        \"\"\"16 messages with large tool outputs on a 128k model must leave\n+        a compressible middle (compress_start < compress_end).\"\"\"\n+        comp = _make_compressor(\n+            model=\"gpt-5.3-codex-spark\",\n+            threshold_percent=0.70,\n+            protect_first_n=3,\n+            protect_last_n=20,\n+            config_context_length=128000,\n+        )\n+        # Build system + 3 head pairs + 3 tool groups (large outputs) + tail pair\n+        big_tool = \"x\" * 20000  # ~5k tokens each\n+        messages = [{\"role\": \"system\", \"content\": \"You are a helpful agent.\"}]\n+        for i in range(3):\n+            messages.append({\"role\": \"user\", \"content\": f\"Question {i}\"})\n+            messages.append({\"role\": \"assistant\", \"content\": f\"Answer {i}\"})\n+        for i in range(3):\n+            messages.append({\"role\": \"user\", \"content\": f\"Run command {i}\"})\n+            messages.append({\n+                \"role\": \"assistant\", \"content\": \"\",\n+                \"tool_calls\": [{\n+                    \"id\": f\"tc{i}\", \"type\": \"function\",\n+                    \"function\": {\"name\": \"terminal\", \"arguments\": \"{}\"},\n+                }],\n+            })\n+            messages.append({\"role\": \"tool\", \"tool_call_id\": f\"tc{i}\", \"content\": big_tool})\n+        messages.append({\"role\": \"user\", \"content\": \"Final question\"})\n+        messages.append({\"role\": \"assistant\", \"content\": \"Final answer\"})\n+\n+        head = comp._protect_head_size(messages)\n+        compress_start = comp._align_boundary_forward(messages, head)\n+        compress_end = comp._find_tail_cut_by_tokens(messages, compress_start)\n+\n+        assert compress_start < compress_end, (\n+            f\"No compressible window: start={compress_start}, end={compress_end}. \"\n+            f\"This would cause the silent context wipe described in #48621.\"\n+        )\n+        assert comp.has_content_to_compress(messages) is True"]},{"file":"tests/run_agent/test_plugin_context_engine_init.py","hunks":["@@ -139,3 +139,101 @@ def test_plugin_engine_update_model_args():\n     assert \"model\" in kw\n     assert \"provider\" in kw\n     assert \"api_mode\" in kw\n+\n+\n+def _codex_agent_kwargs():\n+    return dict(\n+        model=\"gpt-5.5\",\n+        provider=\"openai-codex\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://chatgpt.com/backend-api/codex\",\n+        quiet_mode=True,\n+        skip_context_files=True,\n+        skip_memory=True,\n+    )\n+\n+\n+def test_codex_gpt55_autoraise_suppressed_for_plugin_engine():\n+    \"\"\"Codex gpt-5.5 autoraise must not fire when an external engine is active.\n+\n+    Regression test for #44439 — the host compression threshold (including\n+    the 0.85 autoraise) never reaches a plugin context engine, so the notice\n+    announced a change that did not apply.\n+    \"\"\"\n+    engine = _StubEngine()\n+    cfg = {\n+        \"context\": {\"engine\": \"stub\"},\n+        \"compression\": {\"enabled\": True, \"threshold\": 0.75},\n+        \"agent\": {},\n+    }\n+\n+    with (\n+        patch(\"hermes_cli.config.load_config\", return_value=cfg),\n+        patch(\"plugins.context_engine.load_context_engine\", return_value=engine),\n+        patch(\"agent.model_metadata.get_model_context_length\", return_value=272_000),\n+        patch(\"run_agent.get_tool_definitions\", return_value=[]),\n+        patch(\"run_agent.check_toolset_requirements\", return_value={}),\n+        patch(\"run_agent.OpenAI\"),\n+    ):\n+        from run_agent import AIAgent\n+\n+        agent = AIAgent(**_codex_agent_kwargs())\n+\n+    assert agent.context_compressor is engine\n+    assert agent._compression_threshold_autoraised is None\n+    assert agent._compression_warning is None\n+    # The engine's own policy is untouched by the host threshold.\n+    assert engine.threshold_percent == 0.75\n+\n+\n+def test_codex_gpt55_autoraise_still_applies_to_builtin_compressor():\n+    \"\"\"Stock built-in compressor keeps the 50% → 85% Codex gpt-5.5 autoraise.\"\"\"\n+    cfg = {\n+        \"compression\": {\"enabled\": True, \"threshold\": 0.50},\n+        \"agent\": {},\n+    }\n+\n+    with (\n+        patch(\"hermes_cli.config.load_config\", return_value=cfg),\n+        patch(\"agent.context_compressor.get_model_context_length\", return_value=272_000),\n+        patch(\"run_agent.get_tool_definitions\", return_value=[]),\n+        patch(\"run_agent.check_toolset_requirements\", return_value={}),\n+        patch(\"run_agent.OpenAI\"),\n+    ):\n+        from run_agent import AIAgent\n+\n+        agent = AIAgent(**_codex_agent_kwargs())\n+\n+    assert agent._compression_threshold_autoraised == {\"model\": \"gpt-5.5\", \"from\": 0.50, \"to\": 0.85}\n+    assert agent.context_compressor.threshold_percent == 0.85\n+    # Gateway parity: the notice is stashed for replay on turn 1.\n+    assert agent._compression_warning and \"85%\" in agent._compression_warning\n+\n+\n+def test_codex_gpt55_autoraise_applies_when_plugin_engine_missing():\n+    \"\"\"If the configured engine fails to load, the built-in compressor is\n+    active and the autoraise (plus its notice) must still apply.\"\"\"\n+    cfg = {\n+        \"context\": {\"engine\": \"no-such-engine\"},\n+        \"compression\": {\"enabled\": True, \"threshold\": 0.50},\n+        \"agent\": {},\n+    }\n+\n+    with (\n+        patch(\"hermes_cli.config.load_config\", return_value=cfg),\n+        patch(\n+            \"plugins.context_engine.load_context_engine\",\n+            side_effect=ValueError(\"not found\"),\n+        ),\n+        patch(\"hermes_cli.plugins.get_plugin_context_engine\", return_value=None),\n+        patch(\"agent.context_compressor.get_model_context_length\", return_value=272_000),\n+        patch(\"run_agent.get_tool_definitions\", return_value=[]),\n+        patch(\"run_agent.check_toolset_requirements\", return_value={}),\n+        patch(\"run_agent.OpenAI\"),\n+    ):\n+        from run_agent import AIAgent\n+\n+        agent = AIAgent(**_codex_agent_kwargs())\n+\n+    assert agent._compression_threshold_autoraised == {\"model\": \"gpt-5.5\", \"from\": 0.50, \"to\": 0.85}\n+    assert agent.context_compressor.threshold_percent == 0.85"]},{"file":"website/docs/developer-guide/context-compression-and-caching.md","hunks":["@@ -113,9 +113,13 @@ The ChatGPT Codex OAuth backend hard-caps gpt-5.5 at a **272K** context window\n GitHub Copilot). At the default 50% trigger, compaction would fire at ~136K —\n half the window the model can actually use. When the active route is Codex\n OAuth (`provider: openai-codex`) and the model is gpt-5.5, Hermes raises the\n-trigger to **85%** (~231K) and prints a one-time notice with the opt-out\n-command. Only this exact route is affected; gpt-5.5 on any other provider keeps\n-your global `threshold`. To opt back down to the global value:\n+trigger to **85%** (~231K) and shows a notice with the opt-out command. The\n+notice is shown once per profile — a marker under `$HERMES_HOME`\n+(`.codex_gpt55_autoraise_notice`) records that it ran, so repeated agent/session\n+inits (e.g. every inbound gateway message) don't re-emit it; if the raised\n+threshold later changes it re-notifies once. Only this exact route is affected;\n+gpt-5.5 on any other provider keeps your global `threshold`. To opt back down to\n+the global value:\n \n ```bash\n hermes config set compression.codex_gpt55_autoraise false\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}