From 7f46dd17d3395588b9ecc831d6bf05b62169561c Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Fri, 4 Sep 2026 18:07:06 +0000 Subject: [PATCH] fix(cli): wait for the import warm-up before importing the agents SDK on the main thread The warm-up thread imports strix.core.runner while warm_up_llm and preflight_model_connection import agents.models.interface. Both walk the agents SDK graph from different entry points, CPython fails one side to break the import-lock cycle, and the orphan purge then removes agents.* from sys.modules while the main thread is still importing it, crashing strix -n with KeyError: 'agents.models'. --- strix/interface/main.py | 5 +++-- strix/interface/scan_setup.py | 3 +++ strix/llm/warmup.py | 14 ++++++++++++++ tests/test_import_warmup.py | 26 ++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/strix/interface/main.py b/strix/interface/main.py index 297c4838..8f9dae7a 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -41,6 +41,7 @@ from strix.interface.update_check import ( from strix.interface.utils import ( build_final_stats_text, ) +from strix.llm.warmup import start_import_warmup, wait_for_import_warmup from strix.telemetry import posthog, scarf from strix.telemetry.logging import configure_dependency_logging @@ -135,6 +136,8 @@ def _subscription_error_hint(exc: BaseException) -> str | None: async def warm_up_llm(show_model_warning: bool = True) -> None: + wait_for_import_warmup() + from agents.models.interface import ModelTracing from strix.config.models import ( @@ -450,8 +453,6 @@ def main() -> None: sys.exit(run_cloud(sys.argv[2:])) - from strix.llm.warmup import start_import_warmup - start_import_warmup() args = parse_arguments() diff --git a/strix/interface/scan_setup.py b/strix/interface/scan_setup.py index ae7caf2f..d87d41c8 100644 --- a/strix/interface/scan_setup.py +++ b/strix/interface/scan_setup.py @@ -31,6 +31,7 @@ from strix.interface.utils import ( stage_api_specs, write_fetched_collection, ) +from strix.llm.warmup import wait_for_import_warmup from strix.telemetry import posthog, scarf from strix.utils.api_spec import ( SpecParseError, @@ -64,6 +65,8 @@ async def preflight_model_connection( settings: Settings | None = None, ) -> None: """Verify the configured model route before starting a scan.""" + wait_for_import_warmup() + from agents.models.interface import ModelTracing from strix.config.models import StrixProvider, configure_sdk_model_defaults diff --git a/strix/llm/warmup.py b/strix/llm/warmup.py index 0459469d..25d284a0 100644 --- a/strix/llm/warmup.py +++ b/strix/llm/warmup.py @@ -80,3 +80,17 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading. ) _thread.start() return _thread + + +def wait_for_import_warmup(timeout: float | None = None) -> None: + """Block until the warm-up thread has finished, if one was started. + + Call this before the first import of a warmed module on another thread. + Two threads walking the same package graph hold each other's import locks, + CPython breaks the cycle by failing one side, and the failed side's + orphan purge can remove a package the other thread is still importing. + """ + with _lock: + thread = _thread + if thread is not None and thread is not threading.current_thread(): + thread.join(timeout) diff --git a/tests/test_import_warmup.py b/tests/test_import_warmup.py index 64d3bfba..53cfc296 100644 --- a/tests/test_import_warmup.py +++ b/tests/test_import_warmup.py @@ -92,6 +92,32 @@ def test_failed_warm_import_purges_orphaned_submodules() -> None: assert result.returncode == 0, result.stderr +def test_wait_for_import_warmup_lets_main_thread_import_the_agents_graph() -> None: + result = _run( + """ + import sys + + from strix.llm.warmup import start_import_warmup, wait_for_import_warmup + + # Same shape as the CLI: warm-up starts, then the main thread needs a + # module from the middle of the agents graph before it has finished. + start_import_warmup() + wait_for_import_warmup() + + from agents.models.interface import ModelTracing # noqa: F401 + + assert "agents" in sys.modules + assert "agents.models" in sys.modules + assert "strix.core.runner" in sys.modules + """ + ) + assert result.returncode == 0, result.stderr + + +def test_wait_for_import_warmup_is_a_no_op_without_a_thread() -> None: + warmup.wait_for_import_warmup(timeout=0) + + def test_purge_does_not_touch_preexisting_or_healthy_modules() -> None: before = frozenset(sys.modules) - {"strix.llm.warmup"} warmup._purge_orphaned_modules(before)