From c201b72f346a75fd121ce1661566697b508d4d32 Mon Sep 17 00:00:00 2001 From: Krowd Date: Fri, 17 Jul 2026 07:56:37 -0700 Subject: [PATCH] fix(auxiliary): sync runtime after fallback restoration --- agent/auxiliary_client.py | 14 +++++++------ agent/turn_context.py | 21 ++++++++++---------- tests/agent/test_turn_context.py | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 642dd37021b..fd86ee2d77d 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -4690,12 +4690,14 @@ def resolve_provider_client( # Normalise aliases provider = _normalize_aux_provider(provider) - # Universal model-resolution fallback chain. Callers (notably title - # generation, vision, session search, and other auxiliary tasks) can - # reach this function without an explicit model — the user picked their - # main provider, didn't bother configuring a per-task ``auxiliary..model``, - # and just expects "use my main model for side tasks too." Resolve in - # this order, stopping at the first non-empty answer: + # Universal model-resolution fallback for concrete providers. ``auto`` is + # intentionally excluded: `_resolve_auto(main_runtime=...)` returns the + # model paired with the provider it actually selected. Pre-filling an auto + # call from `_read_main_model()` can leak a stale process-global runtime + # into a different provider (for example Claude model slug on Codex OAuth) + # and override that correctly resolved model. + # + # Concrete provider resolution order: # # 1. ``model`` argument (caller knew what they wanted) # 2. Provider's catalog default — cheap/fast model the provider diff --git a/agent/turn_context.py b/agent/turn_context.py index 4e390fe4228..ade5d12763d 100644 --- a/agent/turn_context.py +++ b/agent/turn_context.py @@ -151,7 +151,17 @@ def build_turn_context( # null; rebuilding from scratch" warning and a needless first-turn prefix # cache miss. (Issue #45499.) - # Tell auxiliary_client what the live main provider/model are for this turn. + # Tag log records on this thread with the session ID for ``hermes logs``. + set_session_context(agent.session_id) + + # Bind the skill write-origin ContextVar for this thread. + set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool")) + + # Restore the primary runtime if the previous turn activated fallback. + agent._restore_primary_runtime() + + # Tell auxiliary_client what the live main provider/model are for this turn + # after primary restoration has settled the runtime. try: from agent.auxiliary_client import set_runtime_main set_runtime_main( @@ -165,15 +175,6 @@ def build_turn_context( except Exception: pass - # Tag log records on this thread with the session ID for ``hermes logs``. - set_session_context(agent.session_id) - - # Bind the skill write-origin ContextVar for this thread. - set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool")) - - # Restore the primary runtime if the previous turn activated fallback. - agent._restore_primary_runtime() - # Between-turns MCP refresh: an MCP server that finished connecting since # the previous turn (slow HTTP/OAuth servers routinely take 2-6s on a cold # connect, missing the bounded startup wait) lands in THIS turn's tool diff --git a/tests/agent/test_turn_context.py b/tests/agent/test_turn_context.py index e0642c5f11b..1021c9cbdaf 100644 --- a/tests/agent/test_turn_context.py +++ b/tests/agent/test_turn_context.py @@ -259,6 +259,40 @@ def test_pending_cli_message_uses_clean_override_for_api_local_note(): assert agent._pending_cli_user_message is None +def test_runtime_main_sync_happens_after_restore(): + agent = _FakeAgent() + agent.model = "stale-fallback-model" + agent.provider = "openai-codex" + agent.base_url = "https://chatgpt.com/backend-api/codex" + agent.api_key = "fallback-key" + agent.api_mode = "codex_responses" + + def restore_primary(): + agent.model = "primary-model" + agent.provider = "anthropic" + agent.base_url = "https://api.anthropic.com" + agent.api_key = "primary-key" + agent.api_mode = "anthropic_messages" + + agent._restore_primary_runtime = restore_primary + calls = [] + with patch( + "agent.auxiliary_client.set_runtime_main", + side_effect=lambda *args, **kwargs: calls.append((args, kwargs)), + ): + _build(agent) + + assert calls == [( + ("anthropic", "primary-model"), + { + "base_url": "https://api.anthropic.com", + "api_key": "primary-key", + "api_mode": "anthropic_messages", + "auth_mode": "", + }, + )] + + def test_memory_nudge_fires_at_interval(): agent = _FakeAgent() agent._memory_nudge_interval = 1