diff --git a/tests/tui_gateway/test_compression_config_hot_reload.py b/tests/tui_gateway/test_compression_config_hot_reload.py index b9e540535f..19f00b2b0d 100644 --- a/tests/tui_gateway/test_compression_config_hot_reload.py +++ b/tests/tui_gateway/test_compression_config_hot_reload.py @@ -163,6 +163,7 @@ def _neutral_session(**compression_ctor): agent = SimpleNamespace( model="unset-test-model", provider="", + base_url="", context_compressor=compressor, compression_enabled=True, compression_idle_compact_after_seconds=0, diff --git a/tests/tui_gateway/test_live_config_context_pin_mirror.py b/tests/tui_gateway/test_live_config_context_pin_mirror.py index d12bd77851..a2b8b729a0 100644 --- a/tests/tui_gateway/test_live_config_context_pin_mirror.py +++ b/tests/tui_gateway/test_live_config_context_pin_mirror.py @@ -24,6 +24,7 @@ def _session(config_context_length=PIN): agent = SimpleNamespace( model="model-a", provider="custom:acme", + base_url="http://127.0.0.1:8123/v1", _config_context_length=config_context_length, context_compressor=compressor, compression_enabled=True, @@ -55,8 +56,28 @@ def test_changed_context_length_refreshes_the_agent_pin_too(monkeypatch): """A new ``model.context_length`` must land on both cached copies.""" session, compressor = _session() - _sync(monkeypatch, session, {"model": {"context_length": 400_000}, "compression": {}}) + _sync(monkeypatch, session, {"model": {"default": "model-a", "provider": "custom:acme", + "context_length": 400_000}, "compression": {}}) assert compressor._config_context_length == 400_000 assert compressor.context_length == 400_000 assert session["agent"]._config_context_length == 400_000 + + +def test_hot_reload_does_not_pin_a_session_on_another_route(monkeypatch): + """The pin describes the configured default route; a session that switched elsewhere must not + inherit it on the next config save (same scoping as the switch path).""" + session, compressor = _session(config_context_length=None) + session["agent"].base_url = "https://openrouter.ai/api/v1" + session["agent"].provider = "openrouter" + monkeypatch.setattr(cc_mod, "get_model_context_length", lambda *a, **k: 256_000) + + _sync(monkeypatch, session, { + "model": {"default": "model-b", "provider": "custom:acme", "base_url": "http://127.0.0.1:8123/v1", + "context_length": PIN}, + "custom_providers": [{"name": "acme", "base_url": "http://127.0.0.1:8123/v1", "models": {}}], + "compression": {}, + }) + + assert compressor._config_context_length is None + assert session["agent"]._config_context_length is None diff --git a/tui_gateway/session_compression.py b/tui_gateway/session_compression.py index 6e7c8c7541..f4db291eed 100644 --- a/tui_gateway/session_compression.py +++ b/tui_gateway/session_compression.py @@ -85,7 +85,7 @@ def _apply_live_compression_config(agent: Any, cfg: dict | None) -> None: cfg = cfg if isinstance(cfg, dict) else {} compression = cfg.get("compression") if isinstance(cfg.get("compression"), dict) else {} model_cfg = cfg.get("model") if isinstance(cfg.get("model"), dict) else {} - from agent.agent_init import set_config_context_length + from agent.agent_init import config_context_length_for_runtime, set_config_context_length enabled_raw = compression.get("enabled", True) agent.compression_enabled = enabled_raw if isinstance(enabled_raw, bool) else str(enabled_raw).lower() in {"true", "1", "yes"} agent.codex_responses_native_compaction = is_truthy_value(compression.get("codex_responses_native", False)) @@ -137,16 +137,17 @@ def _apply_live_compression_config(agent: Any, cfg: dict | None) -> None: cc.threshold_percent = cc._effective_threshold_percent(cc.context_length, base) except Exception: cc.threshold_percent = pct - raw_ctx = model_cfg.get("context_length") - if raw_ctx is not None: - with contextlib.suppress(TypeError, ValueError): - if (new_ctx := int(raw_ctx)) > 0: - # Both cached copies: the compressor's (its own re-resolution) and the agent's - # (switch/fallback + every display surface). Writing one left the other stale, so the - # session showed a pinned ceiling while compressing against a different window (#116467). - set_config_context_length(agent, new_ctx) - with contextlib.suppress(Exception): - cc.context_length = new_ctx + # Same scoping rule as construction and the switch path: the pin describes the configured default + # route, so a session that /model-switched elsewhere must not have it re-applied on a config save + # (None = absent, invalid, or scoped out). + new_ctx = config_context_length_for_runtime(agent, cfg) + if new_ctx is not None: + # Both cached copies: the compressor's (its own re-resolution) and the agent's + # (switch/fallback + every display surface). Writing one left the other stale, so the + # session showed a pinned ceiling while compressing against a different window (#116467). + set_config_context_length(agent, new_ctx) + with contextlib.suppress(Exception): + cc.context_length = new_ctx elif getattr(cc, "_config_context_length", None) is not None: # model.context_length removed: drop the override and force re-inference from model metadata on # next access (construction's deferred resolution); re-applies the small-context floor too.