fix(gateway): keep /model + /reasoning overrides on topic recovery & compression splits

Session-scoped /model and /reasoning overrides were silently lost on
Telegram DM/forum topics and after compression session splits (#30479).

Root cause: _handle_message_with_agent rewrites source.thread_id via
_recover_telegram_topic_thread_id (lobby/stripped reply -> the user's
bound topic) before deriving the session key. The /model and /reasoning
handlers derived their override key from the raw inbound event.source,
skipping that recovery, so the override was stored under one key and the
next message turn read a different key.

Fix: add _normalize_source_for_session_key (applies the same recovery a
message turn does) and use it in both handlers before deriving the key.
session_id rotation on compression was never the cause — overrides are
keyed by the durable session_key; the split path preserves it.

Author: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
teknium1
2026-06-07 22:10:32 -07:00
committed by Teknium
parent b8469a81e3
commit 2a10da3a16
2 changed files with 148 additions and 1 deletions
+38 -1
View File
@@ -2618,6 +2618,34 @@ class GatewayRunner:
return None
return None
def _normalize_source_for_session_key(
self,
source: SessionSource,
) -> SessionSource:
"""Apply Telegram DM topic recovery to a source for session-key purposes.
``_handle_message_with_agent`` rewrites ``source.thread_id`` via
``_recover_telegram_topic_thread_id`` *before* deriving the session
key for a normal message turn (a lobby/stripped reply gets pinned to
the user's last-active topic). Session-scoped command handlers like
``/model`` and ``/reasoning`` derive their override key from the raw
inbound ``event.source``, which skips that recovery so the override
is stored under a different key than the next message turn reads,
and the override is silently dropped on Telegram forum topics and
after compression session splits (#30479).
Returns a recovery-normalized copy when a rewrite applies, otherwise
the original source unchanged. Always derive the override storage key
from the result so storage and read use an identical key.
"""
try:
recovered = self._recover_telegram_topic_thread_id(source)
except Exception:
return source
if recovered is None:
return source
return dataclasses.replace(source, thread_id=recovered)
def _resolve_session_agent_runtime(
self,
*,
@@ -11175,6 +11203,11 @@ class GatewayRunner:
# Check for session override
source = event.source
# Normalize the source the same way a normal message turn does
# (Telegram DM topic recovery) before deriving the override key, so
# the override is stored under the key the next message turn reads
# (#30479).
source = self._normalize_source_for_session_key(source)
session_key = self._session_key_for_source(source)
override = self._session_model_overrides.get(session_key, {})
if override:
@@ -12923,7 +12956,11 @@ class GatewayRunner:
raw_args = event.get_command_args().strip()
args, persist_global = self._parse_reasoning_command_args(raw_args)
config_path = _hermes_home / "config.yaml"
session_key = self._session_key_for_source(event.source)
# Normalize the source (Telegram DM topic recovery) before deriving
# the override key so storage matches the key the next message turn
# reads — same fix as /model (#30479).
_reasoning_source = self._normalize_source_for_session_key(event.source)
session_key = self._session_key_for_source(_reasoning_source)
self._show_reasoning = self._load_show_reasoning()
self._reasoning_config = self._resolve_session_reasoning_config(
source=event.source,