From 47d1bf9ed8801ecf6984def0b5adbb623aa32e3b Mon Sep 17 00:00:00 2001 From: Samir Hanna Verza Date: Wed, 26 Aug 2026 08:52:11 -0300 Subject: [PATCH] fix(warmup): pre-import the agents SDK to avoid a thread-race crash The background import warm-up thread imports strix.core.runner (and thus the openai-agents SDK) while, on the scan path, the main thread independently imports the same package via strix.report -> strix.report.dedupe -> `from agents.models.interface import ModelTracing`. openai-agents has an internal import cycle (agents.agent_output <-> agents.agent). A single thread resolves it, but when two threads import the `agents` package concurrently, CPython's per-module import locks + deadlock-avoidance can hand one thread a partially initialized module, crashing the run with: ImportError: cannot import name 'AgentOutputSchemaBase' from partially initialized module 'agents.agent_output' (most likely due to a circular import) --version/--help never hit it because they don't import agents; it only shows up on a real scan, which is why it looked intermittent. Import the `agents` package once, synchronously, on the caller's thread before the daemon warm-up thread starts, so it is fully initialized before any concurrent import. Also start the warm-up after parse_arguments() so --version/--help/argparse errors exit before the now-partly-synchronous warm-up runs; it still overlaps the Docker checks and image pull that follow. Adds tests/test_warmup.py asserting start_import_warmup() leaves `agents` in sys.modules synchronously. Co-Authored-By: Claude Opus 4.8 --- strix/interface/main.py | 7 ++++-- strix/llm/warmup.py | 19 ++++++++++++++++ tests/test_warmup.py | 49 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/test_warmup.py diff --git a/strix/interface/main.py b/strix/interface/main.py index 964599787..e5fa7ecd4 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -433,10 +433,13 @@ def main() -> None: from strix.llm.warmup import start_import_warmup - start_import_warmup() - + # parse_arguments() first so --version/--help/argparse errors exit before + # the (heavier) import warm-up runs. Warm-up then overlaps with the I/O + # startup below (update check, Docker checks, image pull). args = parse_arguments() + start_import_warmup() + start_background_check() if not args.non_interactive and prompt_update_if_available(Console()): if is_binary_install() and sys.platform != "win32": diff --git a/strix/llm/warmup.py b/strix/llm/warmup.py index 98da959d7..5fd049409 100644 --- a/strix/llm/warmup.py +++ b/strix/llm/warmup.py @@ -38,6 +38,24 @@ def _warm(modules: tuple[str, ...]) -> None: logger.debug("Import warm-up for %r failed", name, exc_info=True) +def _preimport_thread_unsafe_sdk() -> None: + """Import the ``agents`` SDK once, on the caller's thread, before the daemon + warm-up starts. + + ``openai-agents`` has an internal import cycle (``agents.agent_output`` <-> + ``agents.agent``) that is not thread-safe: when two threads import the + ``agents`` package concurrently, CPython's deadlock-avoidance can hand one + of them a partially initialized module, raising ``ImportError: cannot + import name 'AgentOutputSchemaBase' ... (circular import)``. Fully importing + the package single-threaded here closes that race window before the daemon + warm-up thread (and the main thread's later report import) touch it. + """ + try: + importlib.import_module("agents") + except Exception: # noqa: BLE001 - a failed warm-up must never fail the run. + logger.debug("Pre-import of agents SDK failed", exc_info=True) + + def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.Thread: """Start importing the heavy scan dependencies in the background, once. @@ -48,6 +66,7 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading. with _lock: if _thread is not None: return _thread + _preimport_thread_unsafe_sdk() _thread = threading.Thread( target=_warm, args=(modules,), name="strix-import-warmup", daemon=True ) diff --git a/tests/test_warmup.py b/tests/test_warmup.py new file mode 100644 index 000000000..9ba8da16a --- /dev/null +++ b/tests/test_warmup.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import subprocess +import sys +import textwrap +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def test_start_import_warmup_preimports_agents_sdk_synchronously() -> None: + """``start_import_warmup`` must import the thread-unsafe ``agents`` SDK on + the caller's thread before spawning the daemon warm-up thread. + + ``openai-agents`` has an internal import cycle that is not thread-safe: if + the warm-up thread and the main thread import the ``agents`` package + concurrently, CPython can hand one of them a partially initialized module + (``ImportError: cannot import name 'AgentOutputSchemaBase' ... circular + import``). Warming ``agents`` synchronously closes that race window. + + Run in a fresh interpreter with an empty ``modules`` set so the daemon + thread warms nothing: ``agents`` can then only be in ``sys.modules`` because + the synchronous pre-import ran. + """ + child = textwrap.dedent( + """ + import sys + + assert "agents" not in sys.modules + from strix.llm.warmup import start_import_warmup + + # Importing the warm-up module alone must not pull the agents SDK. + assert "agents" not in sys.modules + start_import_warmup(modules=()) + assert "agents" in sys.modules, "agents SDK was not pre-imported synchronously" + print("OK") + """ + ) + result = subprocess.run( # noqa: S603 + [sys.executable, "-c", child], + cwd=PROJECT_ROOT, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "OK" in result.stdout