conductor(plan): flip track from lazy-loading to proactive warmup

Architectural shift driven by user clarification: lazy-loading on first
use causes user-perceptible lag when the user-triggered action (e.g.
provider switch) propagates to a controller method that triggers the
first import. The fix is to pre-import heavy modules on a bg thread
at startup and have functions access them via _require_warmed().

Old design (rejected):
  - from google import genai inside _send_gemini (lazy on first call)
  - First user action that triggers this pays the cost; UI feels laggy

New design (this commit):
  - Top-level heavy imports REMOVED from main-thread-reachable files
  - AppController.__init__ submits warmup jobs to _io_pool (4 threads,
    named 'controller-io-N')
  - Each warmup worker imports its module and updates a thread-safe
    warmup_status dict
  - Functions access modules via _require_warmed(name), which assumes
    the module is in sys.modules (warmed at startup)
  - When all jobs complete, _warmup_done_event is set and registered
    on_warmup_complete callbacks fire
  - GUI shows status indicator + toast when warmup completes
  - Hook API exposes /api/warmup_status and /api/warmup_wait
  - Tests can call controller.wait_for_warmup() before exercising
    warmup-dependent functionality

Phase 2 now bundles job pool + warmup (T2.3+T2.4 add warmup tests +
implementation). Phases 3-5 do 'remove top-level imports' instead of
'lazy-load'. Phase 7 is the notification surface (Hook API + GUI).
Definition of Done includes warmup-completion criteria, the
'no function-body imports' check, and an end-to-end 'provider switch
is INSTANT' smoke test.

No code changes; this is a planning update only.
This commit is contained in:
ed
2026-06-06 13:45:05 -04:00
parent ca254bac41
commit f2f5ee1197
4 changed files with 597 additions and 269 deletions
+112 -72
View File
@@ -19,11 +19,15 @@
---
## Phase 2: Job Pool Foundation (the "no new threads" rule)
## Phase 2: Job Pool + Warmup Foundation (the "no new threads" + "no lazy-loading" rules)
The user constraint: no new `threading.Thread(...)` per task, per import, per
ad-hoc job. The codebase gets ONE shared `ThreadPoolExecutor` on `AppController`,
named `_io_pool`, used by any subsystem that needs background work.
Two user constraints, addressed together:
1. **No new `threading.Thread(...)`** per task, per import, per ad-hoc job.
2. **No lazy-loading** in function bodies. Heavy imports are warmed on bg
threads at startup, not loaded on first use.
The codebase gets ONE shared `ThreadPoolExecutor` on `AppController` named
`_io_pool`, used for warmup AND any future background work.
- [ ] **T2.1 (Red)** `tests/test_app_controller_io_pool.py`:
- `test_app_controller_has_io_pool`: instantiate `AppController`, assert `hasattr(controller, '_io_pool')` and it's a `ThreadPoolExecutor`
@@ -32,81 +36,94 @@ named `_io_pool`, used by any subsystem that needs background work.
- `test_io_pool_shuts_down_on_close`: call `controller.shutdown()`, assert the pool is shut down
- Confirm FAIL (no `_io_pool` yet)
- [ ] **T2.2 (Green)** In `src/app_controller.py`:
- Add `from concurrent.futures import ThreadPoolExecutor` at top
- Add `from concurrent.futures import ThreadPoolExecutor` and `import importlib` at top
- In `__init__`, after the asyncio loop starts and BEFORE the existing HookServer block: `self._io_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="controller-io")`
- Add warmup state: `self._warmup_lock`, `self._warmup_done_event`, `self._warmup_status` (dict with `pending`/`completed`/`failed` lists), `self._warmup_callbacks`
- Call `self._submit_warmup_jobs()` at the end of `__init__`
- In `shutdown()` (already exists in `App.shutdown` for the GUI; ensure the AppController has a matching shutdown that calls `self._io_pool.shutdown(wait=False)`)
- Add `controller.submit_io(fn, *args)` helper: `return self._io_pool.submit(fn, *args)` (with a docstring saying "use this instead of `threading.Thread` for new background work")
- [ ] **T2.3** Run T2.1 tests; confirm PASS
- [ ] **T2.4** Commit: `feat(app_controller): add shared _io_pool ThreadPoolExecutor` + git note
- [ ] **T2.3 (Red)** `tests/test_warmup_mechanism.py`:
- `test_warmup_jobs_submitted_on_init`: after `AppController.__init__`, assert `len(controller.warmup_status()['pending']) > 0`
- `test_warmup_jobs_complete_within_timeout`: call `controller.wait_for_warmup(timeout=10.0)`, assert True
- `test_warmup_status_reflects_completion`: after `wait_for_warmup`, assert `controller.is_warmup_done() == True` and `len(warmup_status()['pending']) == 0`
- `test_warmup_callback_fires_on_completion`: register a callback via `controller.on_warmup_complete(cb)`, assert it was called once warmup done
- `test_warmup_does_not_block_init`: time `__init__` with a 4-worker pool, assert it returns in < 200ms even though warmup takes longer
- Confirm FAIL (no warmup yet)
- [ ] **T2.4 (Green)** Implement `_submit_warmup_jobs()`, `_compute_warmup_list()`, `_warmup_one()`, `warmup_status()`, `is_warmup_done()`, `wait_for_warmup()`, `on_warmup_complete()` per spec §3.2. Warmup list includes: `google.genai`, `anthropic`, `openai`, `requests`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`, `numpy`. Conditionally adds `fastapi`, `fastapi.security.api_key` if `enable_test_hooks` or `web_host` is set.
- [ ] **T2.5** Run T2.1 and T2.3 tests; confirm PASS
- [ ] **T2.6** Commit: `feat(app_controller): add _io_pool + proactive warmup mechanism` + git note
**Phase 2 checkpoint:** `AppController` owns a 4-thread named pool. `controller.submit_io(fn)` is the sanctioned way to do background work. Existing ad-hoc threads still exist (will be migrated in Phase 5).
**Phase 2 checkpoint:** `AppController` owns a 4-thread named pool. Warmup jobs are submitted in `__init__` and complete in the background. `controller.wait_for_warmup()`, `controller.warmup_status()`, and `controller.on_warmup_complete(cb)` are the public API. Main thread does NOT block waiting for warmup.
---
## Phase 3: Lazy-load AI Provider SDKs (TDD)
## Phase 3: Remove top-level heavy imports from `src/ai_client.py` (TDD)
- [ ] **T3.1 (Red)** Write `tests/test_ai_client_lazy_imports.py`:
- `test_ai_client_does_not_import_genai_at_module_level`: spawn fresh subprocess, `import src.ai_client`, assert `'google.genai' not in sys.modules` (or `google.genai` in modules but `_gemini_client` is `None`)
The current `src/ai_client.py` has `from google import genai` etc. at the top,
which puts the main thread in the import chain. Phase 3 removes these and
swaps to `_require_warmed(name)`.
- [ ] **T3.1 (Red)** Write `tests/test_ai_client_no_top_level_sdk_imports.py`:
- `test_ai_client_does_not_import_genai_at_module_level`: spawn fresh subprocess, `import src.ai_client`, assert `'google.genai' not in sys.modules` (warmup hasn't run in this subprocess)
- `test_ai_client_does_not_import_anthropic_at_module_level`
- `test_ai_client_does_not_import_openai_at_module_level`
- `test_ai_client_does_not_import_requests_at_module_level`
- Confirm tests FAIL (proves the imports are currently eager)
- [ ] **T3.2 (Green)** In `src/ai_client.py`:
- Remove `from google import genai` from top
- Remove `import anthropic` from top
- Remove `import openai` from top
- Remove `import requests` from top
- Add lazy imports inside `_send_gemini`, `_send_anthropic`, `_send_deepseek`, `_send_minimax`
- Provider client globals stay as `None` until first `_ensure_<provider>_client()` call
- [ ] **T3.3** Run existing `tests/test_ai_client.py`; fix any breakage. Most likely issue: tests that rely on top-level import side effects need a fixture that triggers lazy init.
- Add `import sys, importlib, threading` at top
- Remove `from google import genai`, `import anthropic`, `import openai`, `import requests` from top
- Add `_require_warmed(name)` helper: returns `sys.modules[name]` or raises `RuntimeError`
- Each `_send_*` function calls `_require_warmed("google.genai")` etc. instead of using the module directly
- Provider client globals stay as `None` until first `_send_*` initializes them via `_ensure_<provider>_client()` (extracted from current top-level logic, uses the warmed module)
- [ ] **T3.3** Run existing `tests/test_ai_client.py`; fix any breakage. Tests that relied on top-level import side effects need a fixture that warms the modules (or a fallback for test mode).
- [ ] **T3.4** Re-run T3.1 tests, confirm PASS
- [ ] **T3.5** Commit: `git commit -m "refactor(ai_client): lazy-load provider SDKs to defer ~1800ms off main thread"` + git note
- [ ] **T3.5** Commit: `refactor(ai_client): remove top-level SDK imports; use _require_warmed` + git note
- [ ] **T3.6** Update `conductor/tracks.md` T3 row with SHA
**Phase 3 checkpoint:** `import src.ai_client` < 50ms cold. All 273 existing tests still pass.
**Phase 3 checkpoint:** `import src.ai_client` < 50ms cold. When run inside an `AppController` whose warmup has completed, `_send_*` functions find the SDKs in `sys.modules` and execute instantly.
---
## Phase 4: Lazy-load FastAPI in HookServer (TDD)
## Phase 4: Remove top-level FastAPI imports from `src/api_hooks.py` (TDD)
- [ ] **T4.1 (Red)** Write `tests/test_hook_server_lazy_fastapi.py`:
Same pattern as Phase 3, for the FastAPI imports.
- [ ] **T4.1 (Red)** Write `tests/test_hook_server_no_top_level_fastapi.py`:
- `test_hook_server_does_not_import_fastapi_at_module_level`: subprocess test
- `test_hook_server_does_not_import_fastapi_security_at_module_level`
- Confirm FAIL
- [ ] **T4.2 (Green)** In `src/api_hooks.py`:
- Remove `from fastapi import ...` from top
- Remove `from fastapi.security.api_key import APIKeyHeader` from top
- Add lazy imports inside the methods that need them (FastAPI app construction, route registration)
- [ ] **T4.3** Run existing `tests/test_api_hooks.py`; fix breakage
- Remove `from fastapi import ...`, `from fastapi.security.api_key import ...` from top
- Add `_require_warmed(name)` calls inside the methods that need them (FastAPI app construction, route registration)
- [ ] **T4.3** Run existing `tests/test_api_hooks.py`; fix breakage (similar fallback strategy as Phase 3)
- [ ] **T4.4** Confirm T4.1 tests PASS
- [ ] **T4.5** Commit: `git commit -m "refactor(api_hooks): lazy-load fastapi to defer ~470ms off main thread"` + git note
- [ ] **T4.5** Commit: `refactor(api_hooks): remove top-level fastapi imports; use _require_warmed` + git note
**Phase 4 checkpoint:** `from src.api_hooks import HookServer` does not import fastapi.
**Phase 4 checkpoint:** `from src.api_hooks import HookServer` does not import fastapi. The HookServer is fully constructed only after AppController's warmup has loaded fastapi (or after `_require_warmed("fastapi")` triggers the import in test mode).
---
## Phase 5: Lazy-load Feature-gated GUI Modules (TDD per module)
## Phase 5: Remove top-level imports for feature-gated GUI modules (TDD per module)
### 5A: Command Palette
- [ ] **T5A.1 (Red)** `tests/test_command_palette_lazy.py`: `from src.commands import COMMANDS` (or whatever the eager import is) does not import `src.command_palette`. Confirm FAIL.
- [ ] **T5A.2 (Green)** In `src/commands.py`: move `from src.command_palette import ...` inside the command functions that open the palette (`_open_command_palette`, `_toggle_command_palette`).
- [ ] **T5A.1 (Red)** `tests/test_command_palette_no_top_level_import.py`: `from src.commands import COMMANDS` does not import `src.command_palette`. Confirm FAIL.
- [ ] **T5A.2 (Green)** In `src/commands.py`: remove `from src.command_palette import ...` from top. The command functions (`_open_command_palette`, `_toggle_command_palette`) call `_require_warmed("src.command_palette")` to access the module.
- [ ] **T5A.3** Run `tests/test_command_palette.py`; fix.
- [ ] **T5A.4** Commit: `refactor(commands): lazy-load command_palette to defer 244ms`
- [ ] **T5A.4** Commit: `refactor(commands): remove top-level command_palette import; use _require_warmed`
### 5B: NERV Theme
- [ ] **T5B.1 (Red)** `tests/test_theme_nerv_lazy.py`: `from src.theme_2 import *` (or whatever) does not import `src.theme_nerv` or `src.theme_nerv_fx`. Confirm FAIL.
- [ ] **T5B.2 (Green)** In `src/theme_2.py`: move `from src.theme_nerv import ...` and `from src.theme_nerv_fx import ...` inside `apply_nerv_theme()` (or whichever function activates the theme).
- [ ] **T5B.1 (Red)** `tests/test_theme_nerv_no_top_level_import.py`: `from src.theme_2 import *` does not import `src.theme_nerv` or `src.theme_nerv_fx`. Confirm FAIL.
- [ ] **T5B.2 (Green)** In `src/theme_2.py`: remove `from src.theme_nerv import ...` and `from src.theme_nerv_fx import ...` from top. `apply_nerv_theme()` (or whichever function activates the theme) calls `_require_warmed("src.theme_nerv")` and `_require_warmed("src.theme_nerv_fx")`.
- [ ] **T5B.3** Run `tests/test_theme_2.py` and `tests/test_theme_nerv.py`; fix.
- [ ] **T5B.4** Commit: `refactor(theme): lazy-load nerv theme to defer 485ms off non-nerv path`
- [ ] **T5B.4** Commit: `refactor(theme): remove top-level nerv theme imports; use _require_warmed`
### 5C: Markdown Table
- [ ] **T5C.1 (Red)** `tests/test_markdown_helper_lazy.py`: `from src.markdown_helper import MarkdownRenderer` does not import `src.markdown_table`. Confirm FAIL.
- [ ] **T5C.2 (Green)** In `src/markdown_helper.py`: move `from src.markdown_table import ...` inside the table-detection branch of `render()`.
- [ ] **T5C.1 (Red)** `tests/test_markdown_helper_no_top_level_import.py`: `from src.markdown_helper import MarkdownRenderer` does not import `src.markdown_table`. Confirm FAIL.
- [ ] **T5C.2 (Green)** In `src/markdown_helper.py`: remove `from src.markdown_table import ...` from top. The table-detection branch of `render()` calls `_require_warmed("src.markdown_table")`.
- [ ] **T5C.3** Run `tests/test_markdown_helper.py`; fix.
- [ ] **T5C.4** Commit: `refactor(markdown): lazy-load markdown_table to defer 250ms off non-table markdown`
- [ ] **T5C.4** Commit: `refactor(markdown): remove top-level markdown_table import; use _require_warmed`
### 5D: GUI module feature-gated imports
@@ -115,16 +132,14 @@ named `_io_pool`, used by any subsystem that needs background work.
- [ ] **T5D.3** Run full GUI test suite; fix.
- [ ] **T5D.4** Commit per feature group
**Phase 5 checkpoint:** Feature-gated imports are lazy. Default-theme / non-palette / non-table path is lean.
**Phase 5 checkpoint:** All heavy imports removed from main-thread-reachable source files. Default-theme / non-palette / non-table path is lean. Warmup pre-loads all of them in the background.
---
## Phase 6: Migrate Ad-hoc Threads to `_io_pool`
The codebase has several ad-hoc `threading.Thread(...)` calls. Per the user
constraint, these should migrate to `controller.submit_io(fn)`. **This phase
audits and migrates them, but does NOT add new prefetch threads** (the heavy
SDKs are lazy-only per spec §2.2 Layer 3).
constraint, these should migrate to `controller.submit_io(fn)`.
- [ ] **T6.1** Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc thread spawns. Document each in `state.toml` (a new `[ad_hoc_threads]` section).
- [ ] **T6.2** For each ad-hoc thread in `src/log_pruner.py`, `src/project_manager.py`, etc., refactor to use `controller.submit_io(fn)` instead. Wrap the callable body in a try/except (the pool's default behavior is to surface exceptions via the Future; preserve existing error logging).
@@ -135,36 +150,55 @@ SDKs are lazy-only per spec §2.2 Layer 3).
---
## Phase 7: Enforcement (Runtime Audit Hook)
## Phase 7: Warmup Notification (Hook API + GUI)
The user said: *"the app controller should post to test clients or the user
when its threads are warmed up with imports — that way the user knows 'hey
you have the ui first, but now you have all the functionality.'"* This phase
implements the notification surfaces.
### 7A: Hook API endpoints
- [ ] **T7A.1 (Red)** `tests/test_api_hooks_warmup.py`:
- `test_warmup_status_endpoint`: hit `GET /api/warmup_status`, assert response has `pending`/`completed`/`failed` keys
- `test_warmup_wait_endpoint`: hit `GET /api/warmup_wait?timeout=10`, assert response includes the completion state
- Confirm FAIL (endpoints don't exist yet)
- [ ] **T7A.2 (Green)** In `src/api_hooks.py`:
- Add `GET /api/warmup_status` returning `controller.warmup_status()`
- Add `GET /api/warmup_wait` accepting `?timeout=N` (default 30s), calling `controller.wait_for_warmup(timeout)` then returning the final status
- Register `warmup_status` in `_gettable_fields` so the existing Hook API client can fetch it
- [ ] **T7A.3** Run T7A.1 tests; confirm PASS
- [ ] **T7A.4** Commit: `feat(api_hooks): add /api/warmup_status and /api/warmup_wait` + git note
### 7B: GUI status indicator + toast
- [ ] **T7B.1** In `src/gui_2.py` (in the status bar render function), poll `controller.warmup_status()` once per frame. While `pending` is non-empty: show "Warming up... (N/M)" text. When `pending` is empty AND `failed` is empty: show "All imports ready" with a green dot. When `failed` is non-empty: show "Imports: N failed" with a yellow dot.
- [ ] **T7B.2** Register a callback via `controller.on_warmup_complete(cb)` that:
- On transition to done (with no failures): queue a toast notification "All providers ready (M modules)" via the existing toast system
- On transition to done (with failures): queue a warning toast "Warmup finished with N failures — see Diagnostics"
- [ ] **T7B.3** Update `docs/guide_gui_2.md` (or wherever status bar is documented) to describe the new indicator
- [ ] **T7B.4** Commit: `feat(gui_2): warmup status indicator + completion toast` + git note
**Phase 7 checkpoint:** Tests can poll `/api/warmup_status` to know when the system is fully ready. The GUI shows progress during startup and a toast when complete.
---
## Phase 8: Enforcement (Runtime Audit Hook)
The static gate (T1.4) catches known imports at audit time. This phase adds
empirical enforcement: a test that spawns `sloppy.py` and verifies NO heavy
import happens on the main thread at runtime.
- [ ] **T7.1 (Red)** `tests/test_main_thread_purity.py`:
- [ ] **T8.1 (Red)** `tests/test_main_thread_purity.py`:
- `test_headless_startup_no_heavy_imports_on_main`: spawn `uv run python sloppy.py --headless --enable-test-hooks` with a `sitecustomize.py` shim that installs `sys.addaudithook` to log every `import` event with the calling thread. The hook writes to a temp file as JSON-L.
- Wait for headless server ready (5s timeout via `ApiHookClient`).
- Read the audit log. Assert: no event with `thread_name == "MainThread"` for any module in the heavy denylist (`google.genai`, `anthropic`, `openai`, `fastapi`, `requests`, `numpy`, `tkinter`, `psutil`, `pydantic`, `tree_sitter_*`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`, `src.ai_client.send_*`-direct).
- Read the audit log. Assert: no event with `thread_name == "MainThread"` for any module in the heavy denylist (`google.genai`, `anthropic`, `openai`, `fastapi`, `requests`, `numpy`, `tkinter`, `psutil`, `pydantic`, `tree_sitter_*`, `src.command_palette`, `src.theme_nerv`, `src.theme_nerv_fx`, `src.markdown_table`).
- Kill subprocess. Confirm FAIL (current state imports these on main).
- [ ] **T7.2** Once Phase 3-5 land and the static gate passes, this test should start passing. If it doesn't, debug and add more lazy imports.
- [ ] **T7.3** Wire `test_main_thread_purity.py` into CI as a gating test (it'll be slow, ~10s, so mark with `@pytest.mark.slow` and only run in batched CI).
- [ ] **T7.4** Commit: `test: empirical main-thread purity check via sys.audit hook` + git note
- [ ] **T8.2** Once Phase 3-5 land and the static gate passes, this test should start passing. If it doesn't, debug and add more top-level import removals.
- [ ] **T8.3** Wire `test_main_thread_purity.py` into CI as a gating test (it'll be slow, ~10s, so mark with `@pytest.mark.slow` and only run in batched CI).
- [ ] **T8.4** Commit: `test: empirical main-thread purity check via sys.audit hook` + git note
**Phase 7 checkpoint:** CI fails if a future commit re-introduces a heavy main-thread import.
---
## Phase 8: Hook API + Diagnostics
- [ ] **T8.1** Add `/api/startup_profile` endpoint in `src/api_hooks.py` returning `controller.startup_profiler.snapshot()`
- [ ] **T8.2** Register `startup_profile` in `_gettable_fields`
- [ ] **T8.3** Add a "Startup Profile" section to the Diagnostics panel (`src/gui_2.py` `_render_diagnostics` or similar). Show: phase name, duration, % of total.
- [ ] **T8.4** Add `/api/io_pool_status` endpoint returning `{max_workers, active_threads, queued, completed}` so the user can see the job pool is alive.
- [ ] **T8.5** Update `docs/guide_api_hooks.md` with both new endpoints.
- [ ] **T8.6** Tests: extend `tests/test_api_hooks.py` + new `tests/test_startup_profiler.py` + new `tests/test_io_pool_endpoint.py`.
- [ ] **T8.7** Commit: `feat(diagnostics): expose startup profile and io_pool status via Hook API` + git note
**Phase 8 checkpoint:** User can see per-phase startup cost + job-pool liveness in the GUI.
**Phase 8 checkpoint:** CI fails if a future commit re-introduces a heavy main-thread import.
---
@@ -175,30 +209,36 @@ import happens on the main thread at runtime.
- `import src.gui_2` < 500ms
- `import src.app_controller` < 300ms (includes `_io_pool` creation; should still be < 300ms)
- [ ] **T9.2** Re-run `scripts/audit_main_thread_imports.py` (T1.4). Confirm exit 0. No violations.
- [ ] **T9.3** Run `live_gui` test batch (per `conductor/workflow.md:147-150`: max 4 test files per batch, long timeout):
- [ ] **T9.3** Run `tests/test_warmup_mechanism.py` (T2.3); confirm warmup completes and notifications fire
- [ ] **T9.4** Run `live_gui` test batch (per `conductor/workflow.md:147-150`: max 4 test files per batch, long timeout):
- `uv run pytest tests/test_live_gui_*.py --timeout=60 -v` in batches
- Confirm `wait_for_server(timeout=15)` does not time out
- [ ] **T9.4** Manual smoke:
- `uv run sloppy.py` (normal mode): time-to-first-frame
- `uv run sloppy.py --enable-test-hooks` (test mode): time-to-first-frame
- Optionally: tests can call `controller.wait_for_warmup()` before exercising functionality that depends on warmed modules
- [ ] **T9.5** Manual smoke:
- `uv run sloppy.py` (normal mode): time-to-first-frame, observe "Warming up... (N/M)" status, then "All imports ready" toast
- `uv run sloppy.py --enable-test-hooks` (test mode): same observations, plus `/api/warmup_status` returns `completed`
- `uv run sloppy.py --headless` (headless): time-to-server-ready
- [ ] **T9.5** Phase checkpoint commit: `conductor(checkpoint): Phase 9 complete - sloppy.py startup speedup track` + git note with full verification report
- [ ] **T9.6** Update `conductor/tracks.md`: mark track complete, link to archived folder
- Verify a user action that switches provider (or other warmup-dependent operation) is INSTANT, not 1s-delayed
- [ ] **T9.6** Phase checkpoint commit: `conductor(checkpoint): Phase 9 complete - sloppy.py startup speedup track` + git note with full verification report
- [ ] **T9.7** Update `conductor/tracks.md`: mark track complete, link to archived folder
**Phase 9 checkpoint:** All verification criteria in `spec.md:6` met.
**Phase 9 checkpoint:** All verification criteria in `spec.md:6` met. User can switch providers with zero perceptible lag because warmup already loaded the SDK.
---
## Definition of Done
- [ ] All Phase 1-9 tasks checked
- [ ] All tests pass (273+ existing + new TDD tests including `test_main_thread_purity`)
- [ ] All tests pass (273+ existing + new TDD tests including `test_main_thread_purity` and `test_warmup_mechanism`)
- [ ] `uv run ruff check .` and `uv run mypy --explicit-package-bases .` clean (per `mma-tier2-tech-lead` skill)
- [ ] `uv run python scripts/audit_main_thread_imports.py` exits 0
- [ ] `docs/startup_baseline_20260606.txt` and `docs/startup_after_20260606.txt` archived
- [ ] Phase 9 git note contains: baseline diff, audit script result, runtime audit hook result, full test batch results, manual smoke timings, file inventory
- [ ] Track moved to `conductor/tracks/archive/`
- [ ] **NO new `threading.Thread(...)` calls in `src/`** (verified by `grep -rn "threading.Thread(" src/`)
- [ ] **NO `import X` statements in function bodies for heavy modules** — verified by `grep -rn "^\s*import \(google\|anthropic\|openai\|fastapi\|src\.command_palette\|src\.theme_nerv\|src\.markdown_table\)" src/`
- [ ] **Warmup completion notification works** — GUI shows toast, Hook API returns `completed`, `controller.is_warmup_done()` returns True within 10s of startup
- [ ] **User action latency is zero for warmup-dependent operations** — manual smoke test switching providers / opening palette / rendering NERV is instant
---