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'.
This commit is contained in:
Ahmed Allam 2026-09-04 18:07:06 +00:00 committed by Ahmed Allam
parent afa7c4a77f
commit 7f46dd17d3
4 changed files with 46 additions and 2 deletions

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)