diff --git a/gateway/run.py b/gateway/run.py index e71fe26ba8e..f2722c8cf61 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5008,6 +5008,42 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew pass return None + def _refresh_fallback_model(self) -> list | None: + """Re-read fallback_providers from disk for the next agent create/reuse. + + Cron already does this per job via ``get_fallback_chain``; the gateway + previously froze ``self._fallback_model`` at process start, so a chain + configured (or changed) after ``hermes gateway`` was running never + reached messaging sessions even though the same process's cron jobs + fell back correctly. Fixes #60955. + """ + self._fallback_model = self._load_fallback_model() + return self._fallback_model + + @staticmethod + def _apply_fallback_chain_to_agent(agent: Any, chain: list | None) -> None: + """Keep a cached agent's fallback chain aligned with current config. + + Skips rewrite while a cooldown is holding the agent on an already- + activated fallback provider — ``restore_primary_runtime`` owns that + turn-scoped lifecycle. When primary is active (or cooldown expired), + replace the chain so mid-uptime ``fallback_providers`` edits take + effect without requiring a gateway restart (#60955). + """ + if agent is None: + return + new_chain = list(chain or []) + rate_limited_until = getattr(agent, "_rate_limited_until", 0) or 0 + if ( + getattr(agent, "_fallback_activated", False) + and rate_limited_until > time.monotonic() + ): + return + agent._fallback_chain = new_chain + agent._fallback_model = new_chain[0] if new_chain else None + if not getattr(agent, "_fallback_activated", False): + agent._fallback_index = 0 + def _snapshot_running_agents(self) -> Dict[str, Any]: return { session_key: agent @@ -13227,7 +13263,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew chat_type=source.chat_type, thread_id=source.thread_id, session_db=getattr(self._session_db, "_db", self._session_db), - fallback_model=self._fallback_model, + # Reload from disk — do not reuse the startup snapshot (#60955). + fallback_model=self._refresh_fallback_model(), ) try: return agent.run_conversation( @@ -18045,6 +18082,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # Refresh agent max_iterations from current config # (cached agent may have been created with old config) agent.max_iterations = max_iterations + # Refresh fallback chain the same way — a chain + # configured after this agent was cached (or after + # gateway start) must reach the next turn (#60955). + self._apply_fallback_chain_to_agent( + agent, self._refresh_fallback_model(), + ) logger.debug("Reusing cached agent for session %s", session_key) reused_cached_agent = True @@ -18100,7 +18143,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew thread_id=source.thread_id, gateway_session_key=session_key, session_db=getattr(self._session_db, "_db", self._session_db), - fallback_model=self._fallback_model, + # Reload from disk — do not reuse the startup snapshot (#60955). + fallback_model=self._refresh_fallback_model(), ) if _cache_lock and _cache is not None: with _cache_lock: