diff --git a/gateway/run.py b/gateway/run.py index 25820d17934..6162f10b6fb 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -10678,7 +10678,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew f"Adjust reset timing in config.yaml under session_reset." ) try: - session_info = self._format_session_info() + session_info = await asyncio.to_thread( + self._reset_notice_session_info, source + ) if session_info: notice = f"{notice}\n\n{session_info}" except Exception: @@ -11927,6 +11929,26 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # Restore session context variables to their pre-handler state self._clear_session_env(_session_env_tokens) + def _reset_notice_session_info(self, source: SessionSource) -> str: + """Session-info block for the auto-reset notice, profile-scoped. + + When multiplexing, resolve model/provider/context inside the profile + serving ``source`` — otherwise the banner advertises the base config's + model while the session actually runs on the profile's (#59003). + Mirrors ``_run_agent``'s gating so single-profile gateways never + enter the scope. + + Call via ``asyncio.to_thread`` from async handlers: under the scope, + resolution can do blocking work (credential refresh, context-length + HTTP probes) that must not run on the event loop. The scope is entered + inside this method, so contextvars behave correctly in the worker + thread. + """ + if getattr(getattr(self, "config", None), "multiplex_profiles", False): + with _profile_runtime_scope(self._resolve_profile_home_for_source(source)): + return self._format_session_info() + return self._format_session_info() + def _format_session_info(self) -> str: """Resolve current model config and return a formatted info block. diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 14687e07dde..a299e8e9b35 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -231,9 +231,13 @@ class GatewaySlashCommandsMixin: "session_key": session_key, }) - # Resolve session config info to surface to the user + # Resolve session config info to surface to the user, scoped to the + # profile serving this source so a multiplexed /reset //new banner + # reports the profile's model, not the base config's (#59003). try: - session_info = self._format_session_info() + session_info = await asyncio.to_thread( + self._reset_notice_session_info, source + ) except Exception: session_info = "" diff --git a/scripts/release.py b/scripts/release.py index 4dcc16f4d25..d14604a0f8c 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -52,6 +52,7 @@ AUTHOR_MAP = { "AndreasHiltner@users.noreply.github.com": "AndreasHiltner", # PR #56854 salvage (gateway: route multiplex profile responses through the profile's own adapter — 53-site _adapter_for_source sweep) "allenliang2022@users.noreply.github.com": "allenliang2022", # PR #56932 test coverage folded into #56909 salvage (408 → retryable timeout) "m888.braun@hotmail.com": "ManniBr", # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles) + "blueirobin02@gmail.com": "irresi", # PR #59048 salvage (gateway: scope reset banners' session info to the serving profile; #59003) "jashlee+microsoft@microsoft.com": "s905060", # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169) "lohinth25@proton.me": "l0h1nth", # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184) "perkintahmaz50@gmail.com": "devatnull", # PR #58704 salvage (whatsapp: native Baileys polls, clarify-as-poll, location pins, structured quoted replies, PTT/audio split, bridge_helpers extraction) diff --git a/tests/gateway/test_session_info.py b/tests/gateway/test_session_info.py index ec05b31b735..5f93a3820aa 100644 --- a/tests/gateway/test_session_info.py +++ b/tests/gateway/test_session_info.py @@ -107,3 +107,51 @@ class TestFormatSessionInfo: info = runner._format_session_info() assert "4K" in info assert "config" in info + + +class TestResetNoticeSessionInfo: + """#59003: the auto-reset banner must report the serving profile's config, + not the multiplexer's base config.""" + + _RUNTIME = {"provider": "", "base_url": "", "api_key": ""} + + def _source(self): + from gateway.config import Platform + from gateway.session import SessionSource + return SessionSource( + platform=Platform.TELEGRAM, chat_id="123", user_id="u1", + profile="planner", + ) + + def _homes(self, tmp_path): + base = tmp_path / "base" + profile = tmp_path / "profiles" / "planner" + profile.mkdir(parents=True) + base.mkdir() + base.joinpath("config.yaml").write_text( + "model:\n default: base-model\n provider: custom\n context_length: 1000\n") + profile.joinpath("config.yaml").write_text( + "model:\n default: profile-model\n provider: anthropic\n context_length: 2000\n") + return base, profile + + def test_multiplex_uses_profile_config(self, runner, tmp_path): + from types import SimpleNamespace + base, profile = self._homes(tmp_path) + runner.config = SimpleNamespace(multiplex_profiles=True) + with patch("gateway.run._hermes_home", base), \ + patch.object(GatewayRunner, "_resolve_profile_home_for_source", return_value=profile), \ + patch("gateway.run._resolve_runtime_agent_kwargs", return_value=self._RUNTIME): + info = runner._reset_notice_session_info(self._source()) + assert "profile-model" in info + assert "anthropic" in info + assert "base-model" not in info + + def test_single_profile_uses_base_config(self, runner, tmp_path): + from types import SimpleNamespace + base, _profile = self._homes(tmp_path) + runner.config = SimpleNamespace(multiplex_profiles=False) + with patch("gateway.run._hermes_home", base), \ + patch("gateway.run._resolve_runtime_agent_kwargs", return_value=self._RUNTIME): + info = runner._reset_notice_session_info(self._source()) + assert "base-model" in info + assert "profile-model" not in info