# NousResearch/hermes-agent · Compression autoraise and silent wipe bug reports

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #59814. Verify it addresses your case.
- **Recommended:** read PR #59814 (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: `resolved` · deciding PR(s): `pr-59814`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #48621: gpt-5.3-codex-spark: no compression threshold autoraise + silent context wipe when protect_last_n too large

## Prior & competing PRs

- **#59814: fix(compression): Codex autoraise cluster — gpt-5.4/spark coverage, never-lower, notice dedupe**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `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:
  - `agent/agent_init.py`

    ```diff
    @@ -68,24 +68,118 @@ def _ra():
         return run_agent
     
     
    -def _build_codex_gpt55_autoraise_notice(autoraise: Dict[str, float]) -> str:
    -    """Build the one-time notice shown when Codex gpt-5.5 raises compaction.
    +def _build_codex_gpt5_autoraise_notice(autoraise: Dict[str, Any]) -> str:
    +    """Build the one-time notice shown when Codex gpt-5.x raises compaction.
     
    -    ``autoraise`` is ``{"from": <old_ratio>, "to": <new_ratio>}``. The same
    -    text is printed inline for CLI users and replayed via ``status_callback``
    -    for gateway users, so it must be self-contained and include the exact
    -    opt-back-out command.
    +    ``autoraise`` is ``{"model": <slug>, "from": <old_ratio>, "to": <new_ratio>}``.
    +    The same text is printed inline for CLI users and replayed via
    +    ``status_callback`` for gateway users, so it must be self-contained and
    +    include the exact opt-back-out command.
         """
    +    model = str(autoraise.get("model") or "gpt-5.4/5.5").strip().lower().rsplit("/", 1)[-1]
    +    # gpt-5.3-codex-spark has a native 128K window; the gpt-5.4/5.5 family is
    +    # capped at 272K by the Codex OAuth backend.
    +    cap = "128K" if model.startswith("gpt-5.3-codex-spark") else "272K"
         from_pct = int(round(autoraise["from"] * 100))
         to_pct = int(round(autoraise["to"] * 100))
         return (
    -        f"ℹ Codex gpt-5.5 caps context at 272K, so auto-compaction was raised "
    +        f"ℹ Codex {model} caps context at {cap}, so auto-compaction was raised "
             f"to {to_pct}% (from {from_pct}%) to use more of the window before "
             f"summarizing.\n"
             f"  Opt back out: hermes config set compression.codex_gpt55_autoraise false"
         )
     
     
    +def _resolve_compression_threshold(
    +    global_threshold: float,
    +    model_cthresh: Optional[float],
    +    *,
    +    model: Optional[str] = None,
    +    is_codex_autoraise: bool,
    +) -> tuple[float, Optional[Dict[str, Any]]]:
    +    """Combine the user's global compaction threshold with a per-model override.
    +
    +    Returns ``(effective_threshold, autoraise_notice)``. ``autoraise_notice`` is
    +    ``{"model": <slug>, "from": <old>, "to": <new>}`` only when a Codex
    +    autoraise (gpt-5.4/5.5 272K family or gpt-5.3-codex-spark) actually raises
    +    the threshold, otherwise ``None``.
    +
    +    The Codex overrides are *autoraises*: they must never LOWER a higher
    +    user-configured threshold. A user who already set ``compression.threshold``
    +    above the raised value deliberately keeps more raw context, and silently
    +    dropping them would both waste usable window and contradict the feature's
    +    purpose (use more of the window). Other overrides (e.g. Arcee Trinity)
    +    keep their existing unconditional behaviour.
    +    """
    +    if model_cthresh is None:
    +        return global_threshold, None
    +    if is_codex_autoraise:
    +        if model_cthresh <= global_threshold + 1e-9:
    +            # Autoraise never lowers; keep the user's higher/equal threshold.
    +            return global_threshold, None
    +        return model_cthresh, {
    +            "model": model,
    +            "from": global_threshold,
    +            "to": model_cthresh,
    +        }
    +    return model_cthresh, None
    +
    +
    +def _codex_gpt55_autoraise_notice_marker():
    +    """Path to the per-profile marker recording that the autoraise notice ran.
    +
    +    Lives under ``$HERMES_HOME`` (which is profile-scoped) alongside the other
    +    internal markers like ``.container-mode`` — so it is not a user-facing config
    +    key, and every profile tracks its own notice state independently.
    +    """
    +    return get_hermes_home() / ".codex_gpt55_autoraise_notice"
    +
    +
    +def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str:
    +    """Stable identity for one autoraise notice, keyed on what it displays.
    +
    +    Uses the model slug plus the same from→to percentages the notice text
    +    shows, so an unchanged threshold stays silent across restarts while a
    +    later change (the user edits their global ``threshold``, or switches to a
    +    different autoraised Codex model) re-notifies once.
    +    """
    +    model = str(autoraise.get("model") or "").strip().lower().rsplit("/", 1)[-1]
    +    from_pct = int(round(float(autoraise["from"]) * 100))
    +    to_pct = int(round(float(autoraise["to"]) * 100))
    +    return f"{model}:{from_pct}:{to_pct}"
    +
    +
    +def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool:
    +    """True if this exact autoraise notice was already shown for this profile.
    +
    +    A missing/unreadable marker (or one recording a different threshold) reads
    +    as unseen, so the notice shows.
    +    """
    +    try:
    +        current = _codex_gpt55_autoraise_notice_state(autoraise)
    +        return _codex_gpt55_autoraise_notice_marker().read_text(
    +            encoding="utf-8"
    +        ).strip() == current
    +    except (OSError, KeyError, TypeError, ValueError):
    +        return False
    +
    +
    +def _record_codex_gpt55_autoraise_notice(autoraise: Dict[str, Any]) -> None:
    +    """Persist that the autoraise notice was shown for this profile/config state.
    +
    +    Best-effort: a read-only or missing ``$HERMES_HOME`` just means the notice
    +    may show again next init, which is preferable to breaking agent init.
    +    """
    +    try:
    +        marker = _codex_gpt55_autoraise_notice_marker()
    +        marker.parent.mkdir(parents=True, exist_ok=True)
    +        marker.write_text(
    +            _codex_gpt55_autoraise_notice_state(autoraise), encoding="utf-8"
    +        )
    +    except (OSError, KeyError, TypeError, ValueError):
    +        pass
    +
    +
     def _normalized_custom_base_url(value: Any) -> str:
         if not isinstance(value, str):
             return ""
    ```

    ```diff
    @@ -1409,14 +1503,14 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
         if not isinstance(_compression_cfg, dict):
             _compression_cfg = {}
         compression_threshold = float(_compression_cfg.get("threshold", 0.50))
    -    # Per-model/route compaction-threshold override. Codex gpt-5.5 raises to
    -    # 85% (the Codex backend caps the window at 272K, so the default 50% would
    -    # compact at ~136K — half the usable context). Gated by an opt-out config
    -    # flag so the user can fall back to the global threshold; when the override
    -    # fires we stash a one-time notification (replayed on the first turn) that
    -    # tells the user what changed and how to revert. The notice has its own
    -    # display gate so users can keep the threshold autoraise without getting
    -    # the banner on gateway turns.
    +    # Per-model/route compaction-threshold override. Codex gpt-5.4 / gpt-5.5
    +    # raise to 85% (the Codex backend caps both families at 272K, so the
    +    # default 50% would compact at ~136K — half the usable context). Gated by
    +    # an opt-out config flag so the user can fall back to the global threshold;
    +    # when the override fires we stash a one-time notification (replayed on the
    +    # first turn) that tells the user what changed and how to revert. The
    +    # notice has its own display gate so users can keep the threshold
    +    # autoraise without getting the banner on gateway turns.
         _codex_gpt55_autoraise = str(
             _compression_cfg.get("codex_gpt55_autoraise", True)
         ).lower() in {"true", "1", "yes"}
    ```

    ```diff
    @@ -1427,28 +1521,30 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
         try:
             from agent.auxiliary_client import (
                 _compression_threshold_for_model as _cthresh_fn,
    -            _is_codex_gpt55 as _is_codex_gpt55_fn,
    +            _is_codex_gpt54_or_gpt55 as _is_codex_gpt54_or_gpt55_fn,
    +            _is_codex_spark as _is_codex_spark_fn,
             )
             _model_cthresh = _cthresh_fn(
                 agent.model,
                 agent.provider,
                 allow_codex_gpt55_autoraise=_codex_gpt55_autoraise,
             )
    -        if _model_cthresh is not None:
    -            _prev_threshold = compression_threshold
    -            compression_threshold = _model_cthresh
    -            # Notify only for the Codex gpt-5.5 autoraise (the Arcee Trinity
    -            # override is a long-standing silent default). Skip the notice when
    -            # the user's global threshold already meets/exceeds the raised
    -            # value, since nothing actually changed for them.
    -            if (
    -                _is_codex_gpt55_fn(agent.model, agent.provider)
    -                and _model_cthresh > _prev_threshold + 1e-9
    -            ):
    -                agent._compression_threshold_autoraised = {
    -                    "from": _prev_threshold,
    -                    "to": _model_cthresh,
    -                }
    +        # The Codex autoraises (gpt-5.4/5.5 272K family and gpt-5.3-codex-spark)
    +        # apply only when they RAISE (never lower a user's higher global
    +        # threshold). The notice is populated only when it actually fires, and
    +        # carries the model slug so the banner names the right family. Arcee
    +        # Trinity keeps its long-standing unconditional behaviour.
    +        compression_threshold, agent._compression_threshold_autoraised = (
    +            _resolve_compression_threshold(
    +                compression_threshold,
    +                _model_cthresh,
    +                model=agent.model,
    +                is_codex_autoraise=(
    +                    _is_codex_gpt54_or_gpt55_fn(agent.model, agent.provider)
    +                    or _is_codex_spark_fn(agent.model, agent.provider)
    +                ),
    +            )
    +        )
         except Exception:
             pass
         compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in {"true", "1", "yes"}
    ```

    ```diff
    @@ -1677,6 +1773,12 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
     
         if _selected_engine is not None:
             agent.context_compressor = _selected_engine
    +        # External engines own compaction policy: the host compression
    +        # threshold (including the Codex gpt-5.5 autoraise above) only
    +        # configures the built-in ContextCompressor and never reaches the
    +        # plugin, so the autoraise notice would announce a change that does
    +        # not apply. Drop it. (#44439)
    +        agent._compression_threshold_autoraised = None
             # Resolve context_length for plugin engines — mirrors switch_model() path
             from agent.model_metadata import get_model_context_length
             _plugin_ctx_len = get_model_context_length(
    ```

    ```diff
    @@ -1899,29 +2001,53 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:
                 agent._ollama_num_ctx,
             )
     
    +    # Codex gpt-5.x autoraise notice: show at most once per profile/config
    +    # state. Without the persisted marker the notice re-fires on every agent
    +    # init — and the gateway rebuilds the agent per inbound message, so Discord
    +    # etc. saw it repeatedly (#54432). A change in the raised threshold (or the
    +    # autoraised model) updates the marker state and re-notifies once. The
    +    # config display gate (compression.codex_gpt55_autoraise_notice) still
    +    # suppresses the banner entirely without disabling the threshold autoraise.
    +    _autoraise = getattr(agent, "_compression_threshold_autoraised", None)
    +    _show_autoraise_notice = (
    +        bool(_autoraise)
    +        and compression_enabled
    +        and _codex_gpt55_autoraise_notice
    +        and not _codex_gpt55_autoraise_notice_seen(_autoraise)
    +    )
    +
         if not agent.quiet_mode:
             if compression_enabled:
    -            print(f"📊 Context limit: {agent.context_compressor.context_length:,} tokens (compress at {int(compression_threshold*100)}% = {agent.context_compressor.threshold_tokens:,})")
    +            # Report the active engine's own threshold — for a plugin engine
    +            # the host compression_threshold is not in effect, and mixing the
    +            # two printed a percent that contradicted the token count. (#44439)
    +            _active_threshold_pct = getattr(
    +                agent.context_compressor, "threshold_percent", compression_threshold
    +            )
    +            print(f"📊 Context limit: {agent.context_compressor.context_length:,} tokens (compress at {int(_active_threshold_pct*100)}% = {agent.context_compressor.threshold_tokens:,})")
             else:
                 print(f"📊 Context limit: {agent.context_compressor.context_length:,} tokens (auto-compression disabled)")
    -        # One-time notice when the Codex gpt-5.5 autoraise kicked in, with the
    -        # exact opt-back-out command. Printed inline at startup for CLI users;
    -        # gateway users get the same text replayed via _compression_warning on
    -        # turn 1 (set below, after the warning slot is initialized).
    -        _autoraise = getattr(agent, "_compression_threshold_autoraised", None)
    -        if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:
    -            print(_build_codex_gpt55_autoraise_notice(_autoraise))
    +        # Notice with the exact opt-back-out command. Printed inline at startup
    +        # for CLI users; gateway users get the same text replayed via
    +        # _compression_warning on turn 1 (set below).
    +        if _show_autoraise_notice:
    +            print(_build_codex_gpt5_autoraise_notice(_autoraise))
     
         # Check immediately so CLI users see the warning at startup.
         # Gateway status_callback is not yet wired, so any warning is stored
         # in _compression_warning and replayed in the first run_conversation().
         agent._compression_warning = None
    -    # Gateway parity for the Codex gpt-5.5 autoraise notice: the startup print
    +    # Gateway parity for the Codex gpt-5.x autoraise notice: the startup print
         # above only reaches the CLI, so stash the same text here to be replayed
         # through status_callback on the first turn (Telegram/Discord/Slack/etc.).
    -    _autoraise = getattr(agent, "_compression_threshold_autoraised", None)
    -    if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:
    -        agent._compression_warning = _build_codex_gpt55_autoraise_notice(_autoraise)
    +    if _show_autoraise_notice:
    +        agent._compression_warning = _build_codex_gpt5_autoraise_notice(_autoraise)
    +
    +    # Mark shown so repeated inits in this profile (e.g. every gateway message)
    +    # stay silent. Recorded once, whether the notice went to the CLI print or
    +    # the gateway replay slot.
    +    if _show_autoraise_notice:
    +        _record_codex_gpt55_autoraise_notice(_autoraise)
         # Lazy feasibility check: deferred to the first turn that approaches the
         # compression threshold. Running it eagerly here costs ~400ms cold (network
         # probe of the auxiliary provider chain + /models lookup) on every agent
    ```
  - `agent/auxiliary_client.py`

    ```diff
    @@ -314,33 +314,64 @@ def _is_arcee_trinity_thinking(model: Optional[str]) -> bool:
         return bare == "trinity-large-thinking"
     
     
    -# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.5.
    +# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.4/5.5.
     # The raw OpenAI API and OpenRouter expose 1.05M for the same slug, but the
     # Codex backend hard-caps at 272K (verified live: a ~330K-token request to
     # chatgpt.com/backend-api/codex/responses is rejected with
     # ``context_length_exceeded`` while ~250K succeeds). With a 272K ceiling the
     # default 50% compaction trigger fires at ~136K — wasteful, since the model
     # can hold far more raw context before summarization actually buys anything.
    -# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.5
    -# sessions use the window they actually have.
    -_CODEX_GPT55_COMPACTION_THRESHOLD = 0.85
    +# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.4/
    +# gpt-5.5 sessions use the window they actually have.
    +_CODEX_GPT54_GPT55_COMPACTION_THRESHOLD = 0.85
     
    +# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a
    +# native 128K context window.  The default 50% compaction trigger fires at
    +# ~64K — wasting half the usable window, often before the session has enough
    +# turns to summarize meaningfully.  We raise the trigger to 70% (~90K) so
    +# spark sessions use more of the window before summarization, while still
    +# leaving ~38K headroom for the summary and continued conversation before
    +# the 128K hard limit.
    +_CODEX_SPARK_COMPACTION_THRESHOLD = 0.70
     
    -def _is_codex_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:
    -    """True for gpt-5.5 accessed through the ChatGPT Codex OAuth backend.
    +
    +def _is_codex_gpt54_or_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:
    +    """True for gpt-5.4 / gpt-5.5 on the ChatGPT Codex OAuth backend.
     
         Matches only the Codex OAuth route (provider ``openai-codex``), not the
         direct OpenAI API, OpenRouter, or GitHub Copilot paths — those expose a
         larger context window for the same slug and must keep the user's default
    -    compaction threshold. ``gpt-5.5-pro`` and dated snapshots
    -    (``gpt-5.5-2026-04-23``) are matched via prefix so the override tracks the
    -    family without re-listing every variant.
    +    compaction threshold. ``gpt-5.4-pro`` / ``gpt-5.5-pro`` and dated snapshots
    +    are matched via prefix so the override tracks both 272K-capped families
    +    without re-listing every variant.
         """
         prov = (provider or "").strip().lower()
         if prov != "openai-codex":
             return False
         bare = (model or "").strip().lower().rsplit("/", 1)[-1]
    -    return bare == "gpt-5.5" or bare.startswith("gpt-5.5-") or bare.startswith("gpt-5.5.")
    +    return (
    +        bare == "gpt-5.4"
    +        or bare.startswith("gpt-5.4-")
    +        or bare.startswith("gpt-5.4.")
    +        or bare == "gpt-5.5"
    +        or bare.startswith("gpt-5.5-")
    +        or bare.startswith("gpt-5.5.")
    +    )
    +
    +
    +def _is_codex_spark(model: Optional[str], provider: Optional[str] = None) -> bool:
    +    """True for ``gpt-5.3-codex-spark`` on the ChatGPT Codex OAuth backend.
    +
    +    The model is Codex-OAuth-only (ChatGPT Pro entitlement) with a native
    +    128K context window.  Only the Codex OAuth route (provider
    +    ``openai-codex``) is matched — the slug is not available on other
    +    routes.
    +    """
    +    prov = (provider or "").strip().lower()
    +    if prov != "openai-codex":
    +        return False
    +    bare = (model or "").strip().lower().rsplit("/", 1)[-1]
    +    return bare == "gpt-5.3-codex-spark"
     
     
     def _fixed_temperature_for_model(
    ```

    ```diff
    @@ -379,18 +410,26 @@ def _compression_threshold_for_model(
     
         Per-model/route overrides:
           - Arcee Trinity Large Thinking → 0.75 (preserve reasoning context).
    -      - gpt-5.5 on the Codex OAuth route → 0.85, because Codex caps the window
    -        at 272K and the default 50% trigger would compact at ~136K. Gated by
    -        ``allow_codex_gpt55_autoraise`` so the user can opt back down to the
    -        global default (the caller passes the config flag through here).
    +      - gpt-5.4 / gpt-5.5 on the Codex OAuth route → 0.85, because Codex caps
    +        both families at 272K and the default 50% trigger would compact at
    +        ~136K. Gated by ``allow_codex_gpt55_autoraise`` (historical config-key
    +        name kept for backward compatibility) so the user can opt back down to
    +        the global default (the caller passes the config flag through here).
    +      - gpt-5.3-codex-spark on the Codex OAuth route → 0.70, because the model
    +        has a native 128K window and the default 50% trigger would compact at
    +        ~64K — wasting half the usable context. Not gated by the gpt-5.5
    +        opt-out flag: 128K is the model's native window, so the raise is
    +        unambiguously correct.
     
         Returns a float in (0, 1] to override the global ``compression.threshold``
         config value, or ``None`` to leave the user's config value unchanged.
         """
         if _is_arcee_trinity_thinking(model):
             return 0.75
    -    if allow_codex_gpt55_autoraise and _is_codex_gpt55(model, provider):
    -        return _CODEX_GPT55_COMPACTION_THRESHOLD
    +    if allow_codex_gpt55_autoraise and _is_codex_gpt54_or_gpt55(model, provider):
    +        return _CODEX_GPT54_GPT55_COMPACTION_THRESHOLD
    +    if _is_codex_spark(model, provider):
    +        return _CODEX_SPARK_COMPACTION_THRESHOLD
         return None
     
     # Default auxiliary models for direct API-key providers (cheap/fast for side tasks)
    ```

    ```diff
    @@ -4151,7 +4190,15 @@ def resolve_provider_client(
         # main_model also empty), the branches still hit their own
         # missing-credentials returns and ``_resolve_auto`` falls through to
         # the Step-2 chain as before.
    -    if not model:
    +    #
    +    # Prefer explicit caller model, then provider-scoped aux model, then main model.
    +    # Do NOT pre-fill a blank ``auto`` request from the config/main default here.
    +    # ``auto`` has its own main-runtime resolver below; pre-filling first can pair
    +    # a stale configured model with a live fallback provider (e.g. Claude model
    +    # sent to Codex after the main lane fell back to gpt-5.5). Let _resolve_auto()
    +    # return the actual current runtime model when the caller did not explicitly
    +    # request one. (# compression-current-model)
    +    if not model and provider != "auto":
             model = _get_aux_model_for_provider(provider) or _read_main_model() or model
     
         def _needs_codex_wrap(client_obj, base_url_str: str, model_str: str) -> bool:
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -1362,17 +1362,18 @@ def _ensure_hermes_home_managed(home: Path):
                                           # Default False matches historical behavior; set to
                                           # True if you'd rather pause than silently lose
                                           # context turns when your aux model is flaky.
    -        "codex_gpt55_autoraise": True,  # When True, gpt-5.5 on the ChatGPT Codex OAuth
    -                                      # route raises its compaction trigger to 85% (vs the
    -                                      # global `threshold` above). Codex hard-caps gpt-5.5
    -                                      # at a 272K window, so the default 50% would compact
    -                                      # at ~136K and waste half the usable context. Set to
    -                                      # False to opt back down to the global threshold
    -                                      # (e.g. 0.50) for Codex gpt-5.5 sessions. Only this
    -                                      # exact route is affected — gpt-5.5 on OpenAI's
    -                                      # direct API, OpenRouter, and Copilot keep the
    -                                      # global threshold regardless.
    -        "codex_gpt55_autoraise_notice": True,  # Display the one-time Codex gpt-5.5
    +        "codex_gpt55_autoraise": True,  # Historical key name kept for compatibility.
    +                                      # When True, gpt-5.4 / gpt-5.5 on the ChatGPT Codex
    +                                      # OAuth route raise their compaction trigger to 85%
    +                                      # (vs the global `threshold` above). Codex hard-caps
    +                                      # both families at a 272K window, so the default 50%
    +                                      # would compact at ~136K and waste half the usable
    +                                      # context. Set to False to opt back down to the global
    +                                      # threshold (e.g. 0.50) for those Codex sessions.
    +                                      # Only this exact route is affected — gpt-5.4 / 5.5
    +                                      # on OpenAI's direct API, OpenRouter, and Copilot keep
    +                                      # the global threshold regardless.
    +        "codex_gpt55_autoraise_notice": True,  # Display the one-time Codex gpt-5.4/5.5
                                           # autoraise banner. Set False to keep the
                                           # 85% threshold autoraise but suppress the
                                           # user-facing notice in CLI/gateway output.
    ```
  - `scripts/release.py`

    ```diff
    @@ -285,6 +285,7 @@
         "157689911+itsflownium@users.noreply.github.com": "itsflownium",
         "dirtyren@users.noreply.github.com": "dirtyren",
         "[redacted:email]": "bigstar0920",
    +    "[redacted:email]": "tanmayxchoudhary",
         "285906080+AIalliAI@users.noreply.github.com": "AIalliAI",
         "waseemshahwan@users.noreply.github.com": "waseemshahwan",
         "hellno@users.noreply.github.com": "hellno",
    ```
  - `tests/agent/test_arcee_trinity_overrides.py`

    ```diff
    @@ -13,11 +13,13 @@
     
     import pytest
     
    +from agent.agent_init import _resolve_compression_threshold
     from agent.auxiliary_client import (
         _compression_threshold_for_model,
         _fixed_temperature_for_model,
         _is_arcee_trinity_thinking,
    -    _is_codex_gpt55,
    +    _is_codex_gpt54_or_gpt55,
    +    _is_codex_spark,
     )
     
     
    ```

    ```diff
    @@ -78,14 +80,14 @@ def test_compression_threshold_default_none_for_other_models() -> None:
     
     
     # ---------------------------------------------------------------------------
    -# Codex gpt-5.5 compaction-threshold autoraise
    +# Codex gpt-5.4 / gpt-5.5 compaction-threshold autoraise
     #
    -# ChatGPT's Codex OAuth backend caps gpt-5.5 at a 272K window (verified live:
    -# ~330K-token request rejected with context_length_exceeded, ~250K accepted).
    -# The default 50% compaction trigger would fire at ~136K — half the usable
    -# window — so this route raises the trigger to 85%. Only the Codex OAuth route
    -# is affected; the same slug on OpenAI direct / OpenRouter / Copilot exposes a
    -# larger window and keeps the user's global threshold.
    +# ChatGPT's Codex OAuth backend caps both families at a 272K window (verified
    +# live via the Codex /models resolver and per-slug fallback table). The default
    +# 50% compaction trigger would fire at ~136K — half the usable window — so this
    +# route raises the trigger to 85%. Only the Codex OAuth route is affected; the
    +# same slugs on OpenAI direct / OpenRouter / Copilot expose a larger window and
    +# keep the user's global threshold.
     # ---------------------------------------------------------------------------
     
     
    ```

    ```diff
    @@ -99,32 +101,40 @@ def test_compression_threshold_default_none_for_other_models() -> None:
             "openai/gpt-5.5",  # aggregator-prefixed (still on the codex route)
             "GPT-5.5",  # case-insensitive
             "  gpt-5.5  ",  # whitespace tolerant
    +        "gpt-5.4",  # base 5.4 (272K-capped)
    +        "gpt-5.4-pro",  # pro 5.4 variant (272K-capped)
    +        "gpt-5.4-2026-01-01",  # dated 5.4 snapshot
    +        "openai/gpt-5.4",  # aggregator-prefixed 5.4
         ],
     )
    -def test_is_codex_gpt55_matches_on_codex_provider(model: str) -> None:
    -    assert _is_codex_gpt55(model, "openai-codex") is True
    +def test_is_codex_gpt54_or_gpt55_matches_on_codex_provider(model: str) -> None:
    +    assert _is_codex_gpt54_or_gpt55(model, "openai-codex") is True
     
     
     @pytest.mark.parametrize(
         "provider",
         ["openrouter", "openai", "copilot", "openai-api", "", None],
     )
    -def test_is_codex_gpt55_rejects_non_codex_providers(provider) -> None:
    -    # gpt-5.5 on any non-Codex route keeps the larger window — no override.
    -    assert _is_codex_gpt55("gpt-5.5", provider) is False
    +def test_is_codex_gpt54_or_gpt55_rejects_non_codex_providers(provider) -> None:
    +    # gpt-5.4 / gpt-5.5 on any non-Codex route keep the larger window.
    +    assert _is_codex_gpt54_or_gpt55("gpt-5.5", provider) is False
    +    assert _is_codex_gpt54_or_gpt55("gpt-5.4", provider) is False
     
     
     @pytest.mark.parametrize(
         "model",
    -    ["gpt-5.4", "gpt-5", "gpt-5.55", "gpt-5.50", "", None],
    +    ["gpt-5", "gpt-5.55", "gpt-5.50", "gpt-5.45", "gpt-5.40", "", None],
     )
    -def test_is_codex_gpt55_rejects_non_55_models(model) -> None:
    -    # gpt-5.55 / gpt-5.50 are different families and must NOT match — the
    -    # "gpt-5.5-" / "gpt-5.5." prefix guards require a separator after "5.5".
    -    assert _is_codex_gpt55(model, "openai-codex") is False
    +def test_is_codex_gpt54_or_gpt55_rejects_non_54_55_models(model) -> None:
    +    # Close numeric neighbours must NOT match — the prefix guards require a
    +    # separator after "5.4" / "5.5" so e.g. gpt-5.45 and gpt-5.55 stay out.
    +    assert _is_codex_gpt54_or_gpt55(model, "openai-codex") is False
     
     
     def test_compression_threshold_for_codex_gpt55() -> None:
    +    assert _compression_threshold_for_model("gpt-5.4", "openai-codex") == 0.85
    +    assert _compression_threshold_for_model("gpt-5.4-pro", "openai-codex") == 0.85
    +    assert _compression_threshold_for_model("openai/gpt-5.4", "openai-codex") == 0.85
         assert _compression_threshold_for_model("gpt-5.5", "openai-codex") == 0.85
         assert _compression_threshold_for_model("gpt-5.5-pro", "openai-codex") == 0.85
         assert _compression_threshold_for_model("openai/gpt-5.5", "openai-codex") == 0.85
    ```

    ```diff
    @@ -132,14 +142,24 @@ def test_compression_threshold_for_codex_gpt55() -> None:
     
     def test_compression_threshold_codex_gpt55_other_routes_unaffected() -> None:
         # Same slug, different route → no override (keep the user's config value).
    +    assert _compression_threshold_for_model("gpt-5.4", "openrouter") is None
    +    assert _compression_threshold_for_model("gpt-5.4", "openai") is None
    +    assert _compression_threshold_for_model("gpt-5.4", "copilot") is None
         assert _compression_threshold_for_model("gpt-5.5", "openrouter") is None
         assert _compression_threshold_for_model("gpt-5.5", "openai") is None
         assert _compression_threshold_for_model("gpt-5.5", "copilot") is None
    +    assert _compression_threshold_for_model("openai/gpt-5.4") is None  # no provider
         assert _compression_threshold_for_model("openai/gpt-5.5") is None  # no provider
     
     
     def test_compression_threshold_codex_gpt55_opt_out() -> None:
    -    # allow_codex_gpt55_autoraise=False reverts to the global default (None).
    +    # Historical flag name still governs both Codex families.
    +    assert (
    +        _compression_threshold_for_model(
    +            "gpt-5.4", "openai-codex", allow_codex_gpt55_autoraise=False
    +        )
    +        is None
    +    )
         assert (
             _compression_threshold_for_model(
                 "gpt-5.5", "openai-codex", allow_codex_gpt55_autoraise=False
    ```

    ```diff
    @@ -157,3 +177,140 @@ def test_compression_threshold_opt_out_does_not_disable_trinity() -> None:
             )
             == 0.75
         )
    +
    +
    +# ---------------------------------------------------------------------------
    +# Codex gpt-5.3-codex-spark compaction-threshold autoraise
    +#
    +# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a
    +# native 128K context window.  The default 50% compaction trigger would fire
    +# at ~64K — wasting half the usable window, often before the session has
    +# accumulated enough turns to summarize meaningfully.  This route raises the
    +# trigger to 70% (~90K) to preserve more raw context while leaving ~38K
    +# headroom before the 128K hard limit.
    +# ---------------------------------------------------------------------------
    +
    +
    [redacted:email](
    +    "model",
    +    [
    +        "gpt-5.3-codex-spark",
    +        "openai/gpt-5.3-codex-spark",  # aggregator-prefixed (still on the codex route)
    +        "GPT-5.3-CODEX-SPARK",  # case-insensitive
    +        "  gpt-5.3-codex-spark  ",  # whitespace tolerant
    +    ],
    +)
    +def test_is_codex_spark_matches_on_codex_provider(model: str) -> None:
    +    assert _is_codex_spark(model, "openai-codex") is True
    +
    +
    [redacted:email](
    +    "provider",
    +    ["openrouter", "openai", "copilot", "openai-api", "", None],
    +)
    +def test_is_codex_spark_rejects_non_codex_providers(provider) -> None:
    +    # spark on any non-Codex route is not a real slug — no override.
    +    assert _is_codex_spark("gpt-5.3-codex-spark", provider) is False
    +
    +
    [redacted:email](
    +    "model",
    +    [
    +        "gpt-5.5",  # different family
    +        "gpt-5.3-codex",  # sibling, not spark
    +        "gpt-5.3",  # bare 5.3, not spark
    +        "gpt-5.3-codex-spark-mini",  # hypothetical variant — not matched yet
    +        "", None,
    +    ],
    +)
    +def test_is_codex_spark_rejects_non_spark_models(model) -> None:
    +    assert _is_codex_spark(model, "openai-codex") is False
    +
    +
    +def test_compression_threshold_for_codex_spark() -> None:
    +    assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openai-codex") == 0.70
    +    assert _compression_threshold_for_model("openai/gpt-5.3-codex-spark", "openai-codex") == 0.70
    +
    +
    +def test_compression_threshold_codex_spark_other_routes_unaffected() -> None:
    +    # Same slug, different route → no override (keep the user's config value).
    +    assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openrouter") is None
    +    assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openai") is None
    +    assert _compression_threshold_for_model("gpt-5.3-codex-spark") is None  # no provider
    +
    +
    +def test_compression_threshold_codex_spark_not_gated_by_gpt55_optout() -> None:
    +    # The spark autoraise is independent of the gpt-5.5 opt-out flag — 128K is
    +    # the model's native window, so 70% is unambiguously correct regardless of
    +    # whether the user opted out of the (artificial-cap) gpt-5.5 autoraise.
    +    assert (
    +        _compression_threshold_for_model(
    +            "gpt-5.3-codex-spark", "openai-codex", allow_codex_gpt55_autoraise=False
    +        )
    +        == 0.70
    +    )
    +
    +
    +# ── _resolve_compression_threshold (init_agent application logic) ────────────
    +#
    +# The Codex overrides are *autoraises*: they raise the trigger (0.85 for the
    +# gpt-5.4/5.5 272K family, 0.70 for spark) but must never LOWER a higher
    +# user-configured global threshold.
    +
    +
    +def test_resolve_codex_autoraise_raises_from_default() -> None:
    +    # Default 0.50 global → raised to 0.85, notice emitted.
    +    effective, notice = _resolve_compression_threshold(
    +        0.50, 0.85, model="gpt-5.5", is_codex_autoraise=True
    +    )
    +    assert effective == 0.85
    +    assert notice == {"model": "gpt-5.5", "from": 0.50, "to": 0.85}
    +
    +
    +def test_resolve_codex_autoraise_never_lowers_higher_threshold() -> None:
    +    # Regression: a user who set compression.threshold above 0.85 must keep it.
    +    # The autoraise previously clobbered it down to 0.85 (and silently, since
    +    # the notice was suppressed when nothing "raised").
    +    effective, notice = _resolve_compression_threshold(
    +        0.90, 0.85, model="gpt-5.5", is_codex_autoraise=True
    +    )
    +    assert effective == 0.90
    +    assert notice is None
    +
    +
    +def test_resolve_codex_spark_autoraise_never_lowers_higher_threshold() -> None:
    +    # Same never-lower contract for the spark autoraise (0.70).
    +    effective, notice = _resolve_compression_threshold(
    +        0.80, 0.70, model="gpt-5.3-codex-spark", is_codex_autoraise=True
    +    )
    +    assert effective == 0.80
    +    assert notice is None
    +
    +
    +def test_resolve_codex_autoraise_equal_threshold_is_noop() -> None:
    +    # User already at exactly the raised value: keep it, no notice.
    +    effective, notice = _resolve_compression_threshold(
    +        0.85, 0.85, model="gpt-5.5", is_codex_autoraise=True
    +    )
    +    assert effective == 0.85
    +    assert notice is None
    +
    +
    +def test_resolve_no_override_keeps_global() -> None:
    +    # No per-model override (model_cthresh is None) → global threshold, no notice.
    +    effective, notice = _resolve_compression_threshold(
    +        0.50, None, is_codex_autoraise=False
    +    )
    +    assert effective == 0.50
    +    assert notice is None
    +
    +
    +def test_resolve_non_codex_override_applies_unconditionally() -> None:
    +    # Arcee Trinity (0.75) keeps its long-standing unconditional behaviour: it
    +    # applies even when it lowers the user's global value, and never emits the
    +    # codex autoraise notice.
    +    effective, notice = _resolve_compression_threshold(
    +        0.90, 0.75, is_codex_autoraise=False
    +    )
    +    assert effective == 0.75
    +    assert notice is None
    ```
  - `tests/agent/test_auxiliary_main_first.py`

    ```diff
    @@ -279,6 +279,34 @@ def test_runtime_override_wins_over_config(self, monkeypatch):
             assert mock_resolve.call_args.args[0] == "anthropic"
             assert mock_resolve.call_args.args[1] == "runtime-model"
     
    +    def test_resolve_provider_auto_returns_runtime_model_not_stale_config_default(self):
    +        """Blank auto aux requests must not pair a stale config model with live fallback provider."""
    +        runtime_client = MagicMock()
    +        with patch(
    +            "agent.auxiliary_client._read_main_model",
    +            return_value="claude-opus-4-8",
    +        ) as mock_read_main_model, patch(
    +            "agent.auxiliary_client._resolve_auto",
    +            return_value=(runtime_client, "gpt-5.5"),
    +        ) as mock_resolve_auto:
    +            from agent.auxiliary_client import resolve_provider_client
    +
    +            client, model = resolve_provider_client(
    +                "auto",
    +                main_runtime={
    +                    "provider": "openai-codex",
    +                    "model": "gpt-5.5",
    +                    "base_url": "",
    +                    "api_key": "",
    +                    "api_mode": "codex_responses",
    +                },
    +            )
    +
    +        assert client is runtime_client
    +        assert model == "gpt-5.5"
    +        mock_read_main_model.assert_not_called()
    +        mock_resolve_auto.assert_called_once()
    +
         def test_runtime_base_url_passed_for_named_api_key_provider(self):
             """Named API-key providers inherit the live session endpoint for aux work."""
             token_plan_url = "[redacted:secret-assignment]"
    ```
  - `tests/agent/test_codex_gpt55_autoraise_notice.py`

    ```diff
    @@ -1,4 +1,18 @@
    -"""Regression tests for the Codex gpt-5.5 autoraise notice gate."""
    +"""Regression tests for the Codex gpt-5.x autoraise notice gate.
    +
    +Covers two layers:
    +
    +1. The config display gate (``compression.codex_gpt55_autoraise_notice``) —
    +   suppresses the banner without disabling the threshold autoraise.
    +2. The per-profile dedupe marker (#54432) — the notice must show at most once
    +   per profile/config state. Before the fix it re-fired on every agent init,
    +   and because the gateway rebuilds the agent per inbound message it spammed
    +   Discord etc. The gate persists a marker under ``$HERMES_HOME``
    +   (profile-scoped, isolated to a tempdir by the conftest autouse fixture)
    +   keyed on the model slug + displayed from→to percentages, so an unchanged
    +   threshold stays silent across restarts while a changed threshold (or a
    +   different autoraised Codex model) re-notifies once.
    +"""
     
     from __future__ import annotations
     
    ```

    ```diff
    @@ -6,9 +20,22 @@
     import io
     from pathlib import Path
     
    +import pytest
    +
    +from hermes_constants import get_hermes_home
     from hermes_state import SessionDB
     from run_agent import AIAgent
     
    +from agent.agent_init import (
    +    _codex_gpt55_autoraise_notice_marker,
    +    _codex_gpt55_autoraise_notice_seen,
    +    _codex_gpt55_autoraise_notice_state,
    +    _record_codex_gpt55_autoraise_notice,
    +)
    +
    +# The dict agent_init stashes when the Codex gpt-5.5 override fires.
    +AUTORAISE = {"model": "gpt-5.5", "from": 0.50, "to": 0.85}
    +
     
     def _config(*, show_notice: bool) -> dict:
         return {
    ```

    ```diff
    @@ -57,6 +84,9 @@ def _threshold_ratio(agent: AIAgent) -> float:
         return round(compressor.threshold_tokens / compressor.context_length, 2)
     
     
    +# ── config display gate ──────────────────────────────────────────────────────
    +
    +
     def test_codex_gpt55_autoraise_notice_enabled_by_default(monkeypatch, tmp_path):
         agent, stdout = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)
     
    ```

    ```diff
    @@ -75,3 +105,113 @@ def test_codex_gpt55_autoraise_notice_can_be_suppressed_without_disabling_autora
         assert _threshold_ratio(agent) == 0.85
         assert getattr(agent, "_compression_warning") is None
         assert "auto-compaction was raised" not in stdout
    +
    +
    +def test_codex_gpt55_autoraise_notice_deduped_across_agent_inits(monkeypatch, tmp_path):
    +    # Gateway spam scenario (#54432): the gateway rebuilds the agent per
    +    # inbound message. The first init shows the notice; the second stays
    +    # silent because the per-profile marker was recorded.
    +    agent1, stdout1 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)
    +    assert "auto-compaction was raised" in stdout1
    +    assert getattr(agent1, "_compression_warning") is not None
    +
    +    agent2, stdout2 = _make_codex_agent(monkeypatch, tmp_path, show_notice=True)
    +    assert _threshold_ratio(agent2) == 0.85  # autoraise still applies
    +    assert "auto-compaction was raised" not in stdout2
    +    assert getattr(agent2, "_compression_warning") is None
    +
    +
    +# ── per-profile dedupe marker (#54432) ───────────────────────────────────────
    +
    +
    +def test_marker_lives_under_hermes_home() -> None:
    +    marker = _codex_gpt55_autoraise_notice_marker()
    +    assert marker.parent == get_hermes_home()
    +    assert marker.name == ".codex_gpt55_autoraise_notice"
    +
    +
    +def test_state_keyed_on_model_and_displayed_percentages() -> None:
    +    # Same percentages the notice text renders (int(round(ratio * 100))),
    +    # prefixed with the bare model slug.
    +    assert _codex_gpt55_autoraise_notice_state(AUTORAISE) == "gpt-5.5:50:85"
    +    assert (
    +        _codex_gpt55_autoraise_notice_state(
    +            {"model": "openai/gpt-5.4", "from": 0.75, "to": 0.85}
    +        )
    +        == "gpt-5.4:75:85"
    +    )
    +
    +
    +def test_unseen_before_anything_is_recorded() -> None:
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False
    +
    +
    +def test_seen_after_record() -> None:
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False
    +    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +    # A "restart" is just another call: the marker persists on disk.
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True
    +
    +
    +def test_changed_threshold_renotifies_once() -> None:
    +    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is True
    +    # User raises their global threshold -> "from" changes -> notice re-fires.
    +    changed = {"model": "gpt-5.5", "from": 0.60, "to": 0.85}
    +    assert _codex_gpt55_autoraise_notice_seen(changed) is False
    +    _record_codex_gpt55_autoraise_notice(changed)
    +    assert _codex_gpt55_autoraise_notice_seen(changed) is True
    +    # And the old state is now considered unseen (marker moved forward).
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False
    +
    +
    +def test_changed_model_renotifies_once() -> None:
    +    # Switching to a different autoraised Codex model re-fires the notice
    +    # (the banner names the model, so it displays new information).
    +    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +    other_model = {"model": "gpt-5.4", "from": 0.50, "to": 0.85}
    +    assert _codex_gpt55_autoraise_notice_seen(other_model) is False
    +    _record_codex_gpt55_autoraise_notice(other_model)
    +    assert _codex_gpt55_autoraise_notice_seen(other_model) is True
    +
    +
    +def test_record_is_idempotent() -> None:
    +    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +    _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +    assert (
    +        _codex_gpt55_autoraise_notice_marker().read_text(encoding="utf-8")
    +        == "gpt-5.5:50:85"
    +    )
    +
    +
    +def test_malformed_marker_reads_as_unseen() -> None:
    +    marker = _codex_gpt55_autoraise_notice_marker()
    +    marker.parent.mkdir(parents=True, exist_ok=True)
    +    marker.write_text("not-a-state", encoding="utf-8")
    +    assert _codex_gpt55_autoraise_notice_seen(AUTORAISE) is False
    +
    +
    [redacted:email]("bad", [{}, {"from": 0.5}, {"from": None, "to": None}])
    +def test_seen_tolerates_malformed_autoraise(bad) -> None:
    +    # Never raises even if the stashed dict is missing/garbage keys.
    +    assert _codex_gpt55_autoraise_notice_seen(bad) is False
    +
    +
    +def test_full_init_gate_shows_once_then_stays_silent() -> None:
    +    # Mirror the decision agent_init makes on each build:
    +    #   show = bool(autoraise) and compression_enabled and not seen(autoraise)
    +    def decide(compression_enabled: bool) -> bool:
    +        show = (
    +            bool(AUTORAISE)
    +            and compression_enabled
    +            and not _codex_gpt55_autoraise_notice_seen(AUTORAISE)
    +        )
    +        if show:
    +            _record_codex_gpt55_autoraise_notice(AUTORAISE)
    +        return show
    +
    +    # First init (any surface) shows; every subsequent init in this profile
    +    # stays silent — the gateway spam scenario from the issue.
    +    assert decide(compression_enabled=True) is True
    +    assert decide(compression_enabled=True) is False
    +    assert decide(compression_enabled=True) is False
    ```
  - `tests/run_agent/test_infinite_compaction_loop.py`

    ```diff
    @@ -283,3 +283,58 @@ def test_no_cooldown_allows(self):
             comp.last_prompt_tokens = [redacted:secret-assignment]
             assert comp._summary_failure_cooldown_until == 0.0
             assert comp.should_compress(65_000)
    +
    +
    +# ---------------------------------------------------------------------------
    +# Test: #48621 — gpt-5.3-codex-spark short-session boundary
    +#
    +# Issue #48621 Bug 2 claims that a short high-token session (15-20 messages,
    +# ~90k tokens on a 128k model with protect_last_n=20) hits
    +# compress_start >= compress_end, causing a silent context wipe.  The
    +# raw-budget fallback added in the #40803 fix already mitigates this: the
    +# boundary logic always exposes a minimal compressible window.  This test
    +# locks that behavior in for the exact #48621 parameters.
    +# ---------------------------------------------------------------------------
    +
    +class TestCodexSparkShortSessionBoundary:
    +    """Verify that gpt-5.3-codex-spark's short-session scenario always yields
    +    a non-empty compressible window (no silent wipe)."""
    +
    +    def test_short_high_token_session_has_compressible_window(self):
    +        """16 messages with large tool outputs on a 128k model must leave
    +        a compressible middle (compress_start < compress_end)."""
    +        comp = _make_compressor(
    +            model="gpt-5.3-codex-spark",
    +            threshold_percent=0.70,
    +            protect_first_n=3,
    +            protect_last_n=20,
    +            config_context_length=128000,
    +        )
    +        # Build system + 3 head pairs + 3 tool groups (large outputs) + tail pair
    +        big_tool = "x" * 20000  # ~5k tokens each
    +        messages = [{"role": "system", "content": "You are a helpful agent."}]
    +        for i in range(3):
    +            messages.append({"role": "user", "content": f"Question {i}"})
    +            messages.append({"role": "assistant", "content": f"Answer {i}"})
    +        for i in range(3):
    +            messages.append({"role": "user", "content": f"Run command {i}"})
    +            messages.append({
    +                "role": "assistant", "content": "",
    +                "tool_calls": [{
    +                    "id": f"tc{i}", "type": "function",
    +                    "function": {"name": "terminal", "arguments": "{}"},
    +                }],
    +            })
    +            messages.append({"role": "tool", "tool_call_id": f"tc{i}", "content": big_tool})
    +        messages.append({"role": "user", "content": "Final question"})
    +        messages.append({"role": "assistant", "content": "Final answer"})
    +
    +        head = comp._protect_head_size(messages)
    +        compress_start = comp._align_boundary_forward(messages, head)
    +        compress_end = comp._find_tail_cut_by_tokens(messages, compress_start)
    +
    +        assert compress_start < compress_end, (
    +            f"No compressible window: start={compress_start}, end={compress_end}. "
    +            f"This would cause the silent context wipe described in #48621."
    +        )
    +        assert comp.has_content_to_compress(messages) is True
    ```
  - `tests/run_agent/test_plugin_context_engine_init.py`

    ```diff
    @@ -139,3 +139,101 @@ def test_plugin_engine_update_model_args():
         assert "model" in kw
         assert "provider" in kw
         assert "api_mode" in kw
    +
    +
    +def _codex_agent_kwargs():
    +    return dict(
    +        model="gpt-5.5",
    +        provider="openai-codex",
    +        api_key="[redacted:secret-assignment]",
    +        base_url="https://chatgpt.com/backend-api/codex",
    +        quiet_mode=True,
    +        skip_context_files=True,
    +        skip_memory=True,
    +    )
    +
    +
    +def test_codex_gpt55_autoraise_suppressed_for_plugin_engine():
    +    """Codex gpt-5.5 autoraise must not fire when an external engine is active.
    +
    +    Regression test for #44439 — the host compression threshold (including
    +    the 0.85 autoraise) never reaches a plugin context engine, so the notice
    +    announced a change that did not apply.
    +    """
    +    engine = _StubEngine()
    +    cfg = {
    +        "context": {"engine": "stub"},
    +        "compression": {"enabled": True, "threshold": 0.75},
    +        "agent": {},
    +    }
    +
    +    with (
    +        patch("hermes_cli.config.load_config", return_value=cfg),
    +        patch("plugins.context_engine.load_context_engine", return_value=engine),
    +        patch("agent.model_metadata.get_model_context_length", return_value=272_000),
    +        patch("run_agent.get_tool_definitions", return_value=[]),
    +        patch("run_agent.check_toolset_requirements", return_value={}),
    +        patch("run_agent.OpenAI"),
    +    ):
    +        from run_agent import AIAgent
    +
    +        agent = AIAgent(**_codex_agent_kwargs())
    +
    +    assert agent.context_compressor is engine
    +    assert agent._compression_threshold_autoraised is None
    +    assert agent._compression_warning is None
    +    # The engine's own policy is untouched by the host threshold.
    +    assert engine.threshold_percent == 0.75
    +
    +
    +def test_codex_gpt55_autoraise_still_applies_to_builtin_compressor():
    +    """Stock built-in compressor keeps the 50% → 85% Codex gpt-5.5 autoraise."""
    +    cfg = {
    +        "compression": {"enabled": True, "threshold": 0.50},
    +        "agent": {},
    +    }
    +
    +    with (
    +        patch("hermes_cli.config.load_config", return_value=cfg),
    +        patch("agent.context_compressor.get_model_context_length", return_value=272_000),
    +        patch("run_agent.get_tool_definitions", return_value=[]),
    +        patch("run_agent.check_toolset_requirements", return_value={}),
    +        patch("run_agent.OpenAI"),
    +    ):
    +        from run_agent import AIAgent
    +
    +        agent = AIAgent(**_codex_agent_kwargs())
    +
    +    assert agent._compression_threshold_autoraised == {"model": "gpt-5.5", "from": 0.50, "to": 0.85}
    +    assert agent.context_compressor.threshold_percent == 0.85
    +    # Gateway parity: the notice is stashed for replay on turn 1.
    +    assert agent._compression_warning and "85%" in agent._compression_warning
    +
    +
    +def test_codex_gpt55_autoraise_applies_when_plugin_engine_missing():
    +    """If the configured engine fails to load, the built-in compressor is
    +    active and the autoraise (plus its notice) must still apply."""
    +    cfg = {
    +        "context": {"engine": "no-such-engine"},
    +        "compression": {"enabled": True, "threshold": 0.50},
    +        "agent": {},
    +    }
    +
    +    with (
    +        patch("hermes_cli.config.load_config", return_value=cfg),
    +        patch(
    +            "plugins.context_engine.load_context_engine",
    +            side_effect=ValueError("not found"),
    +        ),
    +        patch("hermes_cli.plugins.get_plugin_context_engine", return_value=None),
    +        patch("agent.context_compressor.get_model_context_length", return_value=272_000),
    +        patch("run_agent.get_tool_definitions", return_value=[]),
    +        patch("run_agent.check_toolset_requirements", return_value={}),
    +        patch("run_agent.OpenAI"),
    +    ):
    +        from run_agent import AIAgent
    +
    +        agent = AIAgent(**_codex_agent_kwargs())
    +
    +    assert agent._compression_threshold_autoraised == {"model": "gpt-5.5", "from": 0.50, "to": 0.85}
    +    assert agent.context_compressor.threshold_percent == 0.85
    ```
  - `website/docs/developer-guide/context-compression-and-caching.md`

    ```diff
    @@ -113,9 +113,13 @@ The ChatGPT Codex OAuth backend hard-caps gpt-5.5 at a **272K** context window
     GitHub Copilot). At the default 50% trigger, compaction would fire at ~136K —
     half the window the model can actually use. When the active route is Codex
     OAuth (`provider: openai-codex`) and the model is gpt-5.5, Hermes raises the
    -trigger to **85%** (~231K) and prints a one-time notice with the opt-out
    -command. Only this exact route is affected; gpt-5.5 on any other provider keeps
    -your global `threshold`. To opt back down to the global value:
    +trigger to **85%** (~231K) and shows a notice with the opt-out command. The
    +notice is shown once per profile — a marker under `$HERMES_HOME`
    +(`.codex_gpt55_autoraise_notice`) records that it ran, so repeated agent/session
    +inits (e.g. every inbound gateway message) don't re-emit it; if the raised
    +threshold later changes it re-notifies once. Only this exact route is affected;
    +gpt-5.5 on any other provider keeps your global `threshold`. To opt back down to
    +the global value:
     
     ```bash
     hermes config set compression.codex_gpt55_autoraise false
    ```
- **#48672: fix(compression): autoraise gpt-5.3-codex-spark threshold to 70% (#48621)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `0b6df665a`, `#59814`, `#48621`
- **#48696: fix(compression): add gpt-5.3-codex-spark to Codex autoraise (#48621)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#59814`, `#48672`

## Fix status

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