From 088b98944286e3a775b09562596fceeffb8d7927 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:25:45 -0700 Subject: [PATCH] fix(gateway): scope reset banners' session info to the serving profile (#59048 salvage) (#59329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(gateway): scope reset banners' session info to the serving profile The auto-reset notice and the manual /reset //new banner both appended _format_session_info() outside any profile scope, so a multiplexed gateway advertised the base config's model/provider/context while the session actually ran on the profile's. Route both call sites through a new _reset_notice_session_info(source), which enters _profile_runtime_scope for the source's profile when gateway.multiplex_profiles is on (mirroring _run_agent's gating), so _load_gateway_config()/_resolve_gateway_model() resolve the profile's config.yaml via the existing context-local home override. Single-profile gateways never enter the scope — behavior unchanged. Both call sites invoke the helper via asyncio.to_thread: under the scope, resolution can do blocking work (credential refresh, context-length HTTP probes) that previously failed fast unscoped and must not run on the event loop. Fixes #59003 Co-Authored-By: Claude Fable 5 * chore(release): map irresi author email for PR #59048 salvage --------- Co-authored-by: irresi Co-authored-by: Claude Fable 5 --- gateway/run.py | 24 ++++++++++++++- gateway/slash_commands.py | 8 +++-- scripts/release.py | 1 + tests/gateway/test_session_info.py | 48 ++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) 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