fix(gateway): scope reset banners' session info to the serving profile (#59048 salvage) (#59329)

* 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 <noreply@anthropic.com>

* chore(release): map irresi author email for PR #59048 salvage

---------

Co-authored-by: irresi <blueirobin02@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Teknium 2026-07-07 01:25:45 -07:00 committed by GitHub
parent f1fde49e45
commit 088b989442
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 3 deletions

View file

@ -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.

View file

@ -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 = ""

View file

@ -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)

View file

@ -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