Private
Public Access
0
0

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:
2026-06-06 13:45:05 -04:00
parent ca254bac41
commit f2f5ee1197
4 changed files with 597 additions and 269 deletions
@@ -11,12 +11,14 @@
"src/startup_profiler.py",
"scripts/audit_main_thread_imports.py",
"scripts/audit_gui2_imports.py",
"tests/test_ai_client_lazy_imports.py",
"tests/test_hook_server_lazy_fastapi.py",
"tests/test_ai_client_no_top_level_sdk_imports.py",
"tests/test_hook_server_no_top_level_fastapi.py",
"tests/test_app_controller_io_pool.py",
"tests/test_command_palette_lazy.py",
"tests/test_theme_nerv_lazy.py",
"tests/test_markdown_helper_lazy.py",
"tests/test_warmup_mechanism.py",
"tests/test_command_palette_no_top_level_import.py",
"tests/test_theme_nerv_no_top_level_import.py",
"tests/test_markdown_helper_no_top_level_import.py",
"tests/test_api_hooks_warmup.py",
"tests/test_main_thread_purity.py",
"tests/test_startup_profiler.py",
"tests/test_io_pool_endpoint.py"
@@ -42,19 +44,26 @@
"estimated_phases": 9,
"spec": "spec.md",
"plan": "plan.md",
"architectural_invariant": "The main thread (the one that enters immapp.run()) must NEVER import a module heavier than imgui_bundle and the lean gui_2 skeleton. Enforced by scripts/audit_main_thread_imports.py (static CI gate) and tests/test_main_thread_purity.py (runtime audit-hook test).",
"threading_constraint": "NO new threading.Thread(...) calls in src/. All background work must go through AppController._io_pool (ThreadPoolExecutor, max_workers=4, thread_name_prefix='controller-io').",
"architectural_invariant": "The main thread (the one that enters immapp.run()) must NEVER import a module heavier than imgui_bundle and the lean gui_2 skeleton. Heavy modules are removed from main-thread-reachable files entirely and accessed via _require_warmed(name) at use sites, which assumes the module is in sys.modules because AppController's warmup pre-loaded it on the _io_pool. Enforced by scripts/audit_main_thread_imports.py (static CI gate) and tests/test_main_thread_purity.py (runtime audit-hook test).",
"threading_constraint": "NO new threading.Thread(...) calls in src/. All background work must go through AppController._io_pool (ThreadPoolExecutor, max_workers=4, thread_name_prefix='controller-io'). The _io_pool is also the home of the heavy-module warmup jobs submitted in AppController.__init__.",
"warmup_mechanism": "AppController.__init__ submits one job per heavy module to _io_pool. Each job imports its module and updates a thread-safe warmup_status dict. When the last job completes, _warmup_done_event is set and registered on_warmup_complete callbacks fire. The GUI polls warmup_status() each frame for a status-bar indicator. /api/warmup_status and /api/warmup_wait expose the state to tests and external clients. The user is notified via a toast on completion: 'All providers ready (M modules).'",
"verification_criteria": [
"import src.ai_client < 50ms cold start (from ~1800ms)",
"import src.gui_2 < 500ms cold start (from ~3000ms)",
"import src.app_controller < 300ms cold start (from ~700ms)",
"uv run sloppy.py --enable-test-hooks reaches immapp.run() in < 1.5s",
"live_gui.wait_for_server(timeout=15) passes for all tests",
"scripts/audit_main_thread_imports.py exits 0 (no main-thread heavy imports)",
"scripts/audit_main_thread_imports.py exits 0 (no heavy imports on main)",
"tests/test_main_thread_purity.py passes (runtime audit hook confirms invariant)",
"controller.wait_for_warmup(timeout=10) returns True",
"All warmup modules in sys.modules after warmup completes",
"User-triggered provider switch is INSTANT (proves warmup worked)",
"GUI shows 'Warming up... (N/M)' then 'All imports ready' with green dot, then a toast",
"GET /api/warmup_status returns {pending: [], completed: [...], failed: []}",
"NO `import X` statements inside function bodies for heavy modules (grep-verified)",
"No regressions in 273+ existing tests",
"ZERO new threading.Thread(...) calls in src/ (after Phase 6 migration)",
"Startup profile + io_pool status visible via /api/startup_profile and /api/io_pool_status"
"Startup profile + io_pool status visible via /api/startup_profile, /api/io_pool_status"
],
"links": {
"backlog_entry": "conductor/tracks.md:152",
+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
---
+403 -144
View File
@@ -88,9 +88,11 @@ mechanism that does not run on the main thread.
### 2.2 Four layers of protection
#### Layer 1 — Pure lazy loading (the load-bearing wall, non-negotiable)
#### Layer 1 — Explicit warmup-aware module access (the load-bearing wall, non-negotiable)
Move heavy imports from module top-level into the function body that needs them:
Remove heavy imports from the top of source files reachable from the main
thread. Functions that need them use a `_require_warmed(name)` helper that
assumes the module is already in `sys.modules` (because warmup put it there):
```python
# BEFORE (src/ai_client.py, current)
@@ -100,18 +102,44 @@ import openai
# ... 5 provider SDKs loaded unconditionally
# AFTER
def _send_gemini(md_content, user_message, ...):
from google import genai # 955ms, paid once, on the first call's thread
...
import sys
import importlib
from typing import Any
def _send_anthropic(...):
import anthropic
...
def _require_warmed(name: str) -> Any:
"""Get a module that AppController's warmup should have loaded.
Raises RuntimeError if the module is not in sys.modules. This is the
explicit contract: heavy modules MUST be warmed at startup. No lazy
loading on first use — the import is paid upfront on a bg thread.
"""
mod = sys.modules.get(name)
if mod is None:
raise RuntimeError(
f"Module {name!r} is not warmed. "
f"AppController.__init__ must have run first (which submits warmup jobs)."
)
return mod
def _send_gemini(md_content, user_message, ...):
genai = _require_warmed("google.genai")
# ... use genai ...
```
**Main-thread cost: zero.** First call still pays the latency, but it happens on
the asyncio worker thread (per `guide_architecture.md:215-234`), so the GUI never
sees it.
**Why no `import X` inside the function body?** Because that would be lazy
loading on first use. If the first use is triggered by a user UI action
(e.g. switching the provider from MiniMax to Gemini, the controller enqueues
an action that propagates to the first call), the user sees a 955ms lag
between their click and any visible response. That's the bad case the user
called out: *"lazy loading introduces latencies when interacting with the UI
state vs the bg state."*
By warming proactively, the first user-triggered call is instant. The cost
is paid during startup on a bg thread, before the user can interact.
**Main-thread cost: zero.** The main thread's import chain is fully lean
(none of the heavy modules are imported top-level). The warmup jobs run on
`_io_pool` workers in parallel with the main thread's remaining init.
#### Layer 2 — Shared job pool on AppController (no new threads per task)
@@ -124,22 +152,22 @@ The codebase already has these dedicated / shared threads:
- Ad-hoc `threading.Thread` calls — used for one-off tasks; the user wants to
**MINIMIZE** these
**User constraint:** no new daemon threads per import prefetch, per I/O task, per
**User constraint:** no new daemon threads per import warmup, per I/O task, per
log-prune. We add ONE shared `ThreadPoolExecutor` to `AppController` named
`_io_pool`, and any subsystem that needs background work submits jobs to it.
This includes:
- Initial RAG index warm-up (if applicable)
- Log pruning (currently a one-shot thread — refactor to use the pool)
- Disk-bound subsystem initialization (e.g., TOML re-read on persona switch)
- Any other ad-hoc I/O
- **Heavy module warmup (the primary use case for this track)**
```python
# In AppController.__init__
from concurrent.futures import ThreadPoolExecutor
self._io_pool = ThreadPoolExecutor(
max_workers=4,
thread_name_prefix="controller-io",
max_workers=4,
thread_name_prefix="controller-io",
)
```
@@ -147,25 +175,95 @@ self._io_pool = ThreadPoolExecutor(
import, not 1 per subsystem. Just 4 long-lived threads that all background work
shares. Future work that needs a bg thread should `controller._io_pool.submit(fn)`.
#### Layer 3 — NO prefetch of the heaviest SDKs (deliberate)
#### Layer 3 — Proactive warmup + completion notification (the new mechanism)
The original Phase 5 of this plan proposed a `import-prefetch` daemon thread that
warms `google.genai` (~955ms) on a background thread. **This has been explicitly
rejected** for the heavy SDKs, and the reasoning is sound:
This is the core of the track. In `AppController.__init__`, immediately after
`_io_pool` is created, the controller submits a job to the pool for each heavy
module that needs warming. The main thread does NOT wait for these to complete.
- A 955ms import on a background thread holds the GIL for ~10-50ms at a time
during C extension init. Each hold stalls the main thread's render loop.
- The user pays 955ms total either way: prefetch = 955ms of background stutter
+ instant first call; lazy-only = 955ms of stutter on the first call only,
with the GUI fully interactive in between.
- Prefetching wastes the import cost when the user never uses that provider
(e.g., default is Gemini but the user actually only uses Anthropic).
```python
# In AppController.__init__, right after self._io_pool is created
self._warmup_status: dict[str, list[str]] = {
"pending": [], "completed": [], "failed": [],
}
self._warmup_lock = threading.Lock()
self._warmup_done_event = threading.Event()
self._warmup_callbacks: list[Callable] = []
self._submit_warmup_jobs()
```
**Rule: heavy SDKs (`google.genai`, `anthropic`, `openai`, `fastapi`) are
lazy-only, never prefetched.** Lighter modules (themes, command palette,
markdown table) MAY be optionally warmed on the `_io_pool` if profiling shows
they're commonly used, but it's not a hard requirement and the default is
"don't warm."
```python
def _submit_warmup_jobs(self) -> None:
"""Submit bg jobs to import heavy modules. Notifies subscribers on completion."""
heavy = self._compute_warmup_list()
with self._warmup_lock:
self._warmup_status["pending"] = list(heavy)
self._warmup_status["completed"] = []
self._warmup_status["failed"] = []
self._warmup_done_event.clear()
for module_name in heavy:
self._io_pool.submit(self._warmup_one, module_name)
def _compute_warmup_list(self) -> list[str]:
result = [
# AI provider SDKs
"google.genai", "anthropic", "openai", "requests",
# Feature-gated GUI (used by main thread but not on first frame)
"src.command_palette",
"src.theme_nerv", "src.theme_nerv_fx",
"src.markdown_table",
]
if self._enable_test_hooks or self._web_host:
result.extend(["fastapi", "fastapi.security.api_key"])
return result
def _warmup_one(self, module_name: str) -> None:
try:
importlib.import_module(module_name)
with self._warmup_lock:
self._warmup_status["pending"].remove(module_name)
self._warmup_status["completed"].append(module_name)
except Exception as e:
with self._warmup_lock:
self._warmup_status["pending"].remove(module_name)
self._warmup_status["failed"].append(module_name)
finally:
with self._warmup_lock:
done = not self._warmup_status["pending"]
callbacks = list(self._warmup_callbacks) if done else []
if done:
self._warmup_done_event.set()
for cb in callbacks:
try:
cb(self._warmup_status)
except Exception:
pass
```
**Completion notification** is critical for the user-visible UX. Three surfaces:
1. **GUI status indicator** — the status bar shows "Warming up... (5/8)" while
the bg jobs run, then "All imports ready" with a green dot when complete.
The GUI never blocks waiting; the indicator is updated by polling
`controller.warmup_status()` once per frame (cheap, lock-guarded).
2. **GUI toast notification** — when warmup completes, show a toast:
"All providers ready" with the count of modules loaded. User can dismiss.
3. **Hook API endpoint**`GET /api/warmup_status` returns the current state;
`GET /api/warmup_wait?timeout=N` blocks until done (for tests).
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 is
exactly what the notification surfaces achieve.
**Why this beats lazy-loading:** if a user clicks "switch to Gemini" and the
controller lazy-loads `google.genai` on that action, the user sees ~1s of
nothing happening between the click and the visible response. With warmup,
the click is instant because `google.genai` is already in `sys.modules`. The
1s of cost was paid during startup, when the user was looking at a splash or
otherwise not waiting on input.
#### Layer 4 — Worker-process isolation (future, out of scope)
@@ -181,18 +279,19 @@ the GUI's thread?"* The answer is:
| Scenario | Blocks GUI? |
|---|---|
| Module top-level import of heavy X, then main imports X | **YES** (X's import is in main's chain) |
| Lazy import of X inside a function called from the asyncio thread | **NO** (asyncio thread blocks, not main) |
| Lazy import of X inside a function called from the main thread | **YES** (first call only; the function caller blocks) |
| `_io_pool` worker importing X while main thread renders | **NO direct block, but GIL contention causes micro-stutters** (~5-50ms each). Acceptable because the pool is capped at 4 threads. |
| `_io_pool` worker imports X; main thread later imports X (same module) | **YES** (main blocks on per-module import lock until worker finishes). This is why Layer 1 must come first. |
| Spawning a new `threading.Thread` for each import prefetch | **Wasteful** (thread creation ~1-5ms each; thread count explodes). Use the `_io_pool` instead. |
| Module top-level import of heavy X, then main imports X | **YES** (X's import is in main's chain). This is why we remove heavy imports from main-thread-reachable files. |
| `_io_pool` worker warming X while main thread renders | **NO direct block, but GIL contention causes micro-stutters** (~5-50ms each). Acceptable because the pool is capped at 4 threads and the main thread is mostly idle in `immapp.run()`. |
| `_io_pool` worker warms X; main thread later calls `_require_warmed("X")` (X already in `sys.modules`) | **NO** (the lookup is a `dict.get()` — instant, no import lock contention). |
| User-triggered UI action (e.g. provider switch) propagates to controller which calls `_require_warmed` on a warmed module | **NO** (lookup is instant). This is the win the user explicitly called out: no user-perceptible lag. |
| `wait_for_warmup()` blocks the asyncio thread waiting for warmup | **NO direct block on GUI** (different thread). Asyncio thread waits; main thread renders. Acceptable but rarely needed if user waits for warmup notification first. |
| Spawning a new `threading.Thread` for each import warmup | **Wasteful** (thread creation ~1-5ms each; thread count explodes). Use the `_io_pool` instead. |
This means: **Layer 1 is non-negotiable.** Even with the `_io_pool`, if the
heavy import is also in the main thread's import chain, the main thread will
block on the import lock the moment it tries to use the module. Layer 1 removes
the heavy imports from the main thread's chain; Layer 2 reuses threads
efficiently; Layer 3 deliberately avoids prefetching the heaviest.
This means: **Layer 1 is non-negotiable.** Even with warmup on `_io_pool`, if
the heavy import is also in the main thread's import chain, the main thread
will block on the import lock the moment it tries to use the module. Layer 1
removes the heavy imports from the main thread's chain; Layer 2 reuses
threads efficiently; Layer 3 proactively warms on bg threads so the FIRST
user-triggered use is instant.
### 2.4 Enforcement: the "main thread purity" audit
@@ -236,95 +335,119 @@ not just at static analysis time.
### 3.1 Per-file import plan
For each source file reachable from the main thread's import chain, we
**remove top-level heavy imports** and have functions access them via
`_require_warmed(name)`. The warmup jobs (§3.2) put the modules in
`sys.modules` before any function is called.
#### `src/ai_client.py` (the biggest win: ~1800ms)
Top-level today: `from google import genai`, `import anthropic`, `import openai`,
`import requests` (used by deepseek/minimax).
After:
- Drop `from google import genai` from top — lazy in `_send_gemini()`
- Drop `import anthropic` from top — lazy in `_send_anthropic()`
- Drop `import openai` from top — lazy in `_send_deepseek()` and `_send_minimax()`
- Drop `import requests` from top — lazy in those two providers' HTTP code
- Provider client objects (`_gemini_client`, `_anthropic_client`, etc.) stay as
module globals but are now `None` until first use
- The `_send_*` functions check their provider client is initialized and call a
new `_ensure_<provider>_client()` lazy initializer (extracted from the current
top-level logic)
- **Drop all four heavy imports from the top.** Add `_require_warmed(name)`
helper at the top.
- `_send_gemini()` calls `_require_warmed("google.genai")` to get the module
- `_send_anthropic()` calls `_require_warmed("anthropic")`
- `_send_deepseek()` and `_send_minimax()` call `_require_warmed("openai")` and `_require_warmed("requests")`
- Provider client objects (`_gemini_client`, `_anthropic_client`, etc.) stay
as module globals but are now `None` until `_send_*` initializes them
(extracted from current top-level logic into a new
`_ensure_<provider>_client()` that uses the warmed module)
- The warmup list in `AppController._compute_warmup_list()` includes
`google.genai`, `anthropic`, `openai`, `requests` (always warmed)
**Result:** ~1800ms off the main thread. First AI call still pays it, but on
the asyncio worker.
**Result:** ~1800ms off the main thread. The bg threads pay this cost during
startup. By the time the first AI call happens (which is always async, on
the asyncio thread), the modules are in `sys.modules` and the lookup is
instant. No user-perceptible lag.
#### `src/app_controller.py` (FastAPI in headless/web only)
#### `src/api_hooks.py` (FastAPI in headless/web only)
Top-level today: `from fastapi import ...`, `from fastapi.security.api_key import ...`
(only needed if `--enable-test-hooks` or `--web-host`).
After:
- Drop these from top — lazy inside `HookServer.__init__` (which is itself lazy
in the controller: `if enable_test_hooks: from src.api_hooks import HookServer; ...`)
- **Drop these from top.** Add `_require_warmed(name)` calls inside the
methods that need them.
- The warmup list in `AppController._compute_warmup_list()` includes
`fastapi`, `fastapi.security.api_key` **conditionally** — only when
`enable_test_hooks` or `web_host` is set
**Result:** ~470ms off the main thread for non-test, non-web launches. Critical
because `live_gui` tests launch with `--enable-test-hooks` but the FastAPI work
can be deferred until the asyncio loop is ready.
**Result:** ~470ms off the main thread for non-test, non-web launches.
For `live_gui` tests (`--enable-test-hooks`), the warmup loads fastapi
during the same startup window, so the hook server is ready when the
process announces readiness.
#### `src/commands.py` and `src/command_palette.py` (command palette lazy)
#### `src/commands.py` (command palette warmup-aware)
Top-level today: `from src.command_palette import ...` at `src/commands.py:1`.
After:
- Lazy in each `_*_command()` function in `src/commands.py` that actually
opens the palette
- The CommandRegistry decorator can keep module-level function references, but
the *body* of the command does the heavy import
- **Drop the top-level import.** The command functions call
`_require_warmed("src.command_palette")` to access the module
- The warmup list includes `src.command_palette`
**Result:** ~244ms off if user doesn't open palette during the first session.
**Result:** ~244ms off the main thread's import chain. The bg thread
warms it during startup; the first `Ctrl+Shift+P` is instant.
#### `src/theme_2.py` and `src/theme_nerv.py` / `src/theme_nerv_fx.py` (NERV theme lazy)
#### `src/theme_2.py` (NERV theme warmup-aware)
Top-level today: NERV modules imported at `src/theme_2.py` module top.
Top-level today: `from src.theme_nerv import ...`, `from src.theme_nerv_fx import ...`
at the top of `src/theme_2.py`.
After:
- Lazy in `apply_nerv_theme()` (the function that activates NERV)
- The default theme path stays lean (uses only `src/theme_2.py` + `src/theme_models.py`)
- **Drop the top-level imports.** `apply_nerv_theme()` (or the function
that activates NERV) calls `_require_warmed("src.theme_nerv")` and
`_require_warmed("src.theme_nerv_fx")`
- The warmup list includes both NERV modules
**Result:** ~485ms off if user doesn't pick NERV theme (the default path).
**Result:** ~485ms off the main thread's import chain (the default
non-NERV path is lean). User pays the cost during startup; theme switch
is instant when they pick NERV.
#### `src/markdown_helper.py` (markdown table lazy)
#### `src/markdown_helper.py` (markdown table warmup-aware)
Top-level today: `from src.markdown_table import ...` at `src/markdown_helper.py:1`.
After:
- Lazy in `_render_table_block()` (or wherever GFM table detection happens)
- The first markdown render that hits a table pays the 250ms; subsequent hits are
cached in `sys.modules`
- **Drop the top-level import.** The table-detection branch of `render()`
calls `_require_warmed("src.markdown_table")`
- The warmup list includes `src.markdown_table`
**Result:** ~250ms off the first markdown render that lacks tables (typical).
**Result:** ~250ms off the main thread's import chain. First markdown
table render is instant.
#### `src/imgui_scopes.py`, `src/gui_2.py`, `src/bg_shader.py` (KEEP `imgui_bundle`)
These MUST keep `import imgui_bundle` at top — the ImGui render loop is the hot
path and needs the module on first frame. There is no way to defer this without
breaking the render loop.
These MUST keep `import imgui_bundle` at top — the ImGui render loop is the
hot path and needs the module on first frame. There is no way to defer
this without breaking the render loop.
What CAN be deferred inside `src/gui_2.py`:
- `import numpy` (only needed for `bg_shader`; the GUI itself doesn't need numpy
on the first frame)
- Other feature-gated imports
- `import numpy` (only needed for `bg_shader`; the GUI itself doesn't
need numpy on the first frame) — move to `_require_warmed("numpy")` in
the bg shader call site, add `numpy` to the warmup list
- Other feature-gated imports — same pattern
#### `src/gui_2.py` direct heavy imports (audit)
We will use AST to audit which `import X` statements at `src/gui_2.py` top-level
are reachable from the first-frame render path (`render_main_window`,
`render_main_menu_bar`, etc.) and which are feature-gated. Feature-gated ones
move inside the function that gates them.
We will use AST to audit which `import X` statements at `src/gui_2.py`
top-level are reachable from the first-frame render path
(`render_main_window`, `render_main_menu_bar`, etc.) and which are
feature-gated. First-frame imports stay top-level. Feature-gated ones
move to `_require_warmed(...)` calls at the use site, with the module
added to the warmup list.
### 3.2 Job pool scaffolding
### 3.2 Job pool + warmup scaffolding
New code in `src/app_controller.py`:
```python
from concurrent.futures import ThreadPoolExecutor
import importlib
import threading
# In AppController.__init__, after the asyncio loop starts:
self._io_pool = ThreadPoolExecutor(
@@ -332,16 +455,108 @@ self._io_pool = ThreadPoolExecutor(
thread_name_prefix="controller-io",
)
def submit_io(self, fn, *args, **kwargs):
"""Submit a background job to the shared I/O pool. Use this instead of
threading.Thread for new background work.
Returns a concurrent.futures.Future. Caller can .result() if they need
to block, or .add_done_callback for fire-and-forget with error handling.
"""
return self._io_pool.submit(fn, *args, **kwargs)
# Warmup state
self._warmup_lock = threading.Lock()
self._warmup_done_event = threading.Event()
self._warmup_status: dict[str, list[str]] = {
"pending": [], "completed": [], "failed": [],
}
self._warmup_callbacks: list[Callable] = []
self._submit_warmup_jobs()
```
`_submit_warmup_jobs()` computes the warmup list and submits one job per
module to the pool:
```python
def _submit_warmup_jobs(self) -> None:
heavy = self._compute_warmup_list()
with self._warmup_lock:
self._warmup_status["pending"] = list(heavy)
self._warmup_status["completed"] = []
self._warmup_status["failed"] = []
self._warmup_done_event.clear()
for name in heavy:
self._io_pool.submit(self._warmup_one, name)
def _compute_warmup_list(self) -> list[str]:
result = [
"google.genai", "anthropic", "openai", "requests",
"src.command_palette",
"src.theme_nerv", "src.theme_nerv_fx",
"src.markdown_table",
"numpy", # used by bg_shader; warmed for first invocation
]
if self._enable_test_hooks or self._web_host:
result.extend(["fastapi", "fastapi.security.api_key"])
return result
```
Each warmup worker imports the module, updates the status, and on the
last one fires the completion callbacks (so the GUI status indicator and
toast notification can react):
```python
def _warmup_one(self, name: str) -> None:
try:
importlib.import_module(name)
with self._warmup_lock:
self._warmup_status["pending"].remove(name)
self._warmup_status["completed"].append(name)
except Exception:
with self._warmup_lock:
self._warmup_status["pending"].remove(name)
self._warmup_status["failed"].append(name)
finally:
with self._warmup_lock:
done = not self._warmup_status["pending"]
cbs = list(self._warmup_callbacks) if done else []
if done:
self._warmup_done_event.set()
for cb in cbs:
try:
cb(dict(self._warmup_status))
except Exception:
pass
```
Public API on `AppController`:
```python
def warmup_status(self) -> dict[str, list[str]]:
"""Snapshot the current warmup state. Cheap (lock-guarded copy)."""
with self._warmup_lock:
return {k: list(v) for k, v in self._warmup_status.items()}
def is_warmup_done(self) -> bool:
return self._warmup_done_event.is_set()
def wait_for_warmup(self, timeout: float | None = None) -> bool:
"""Block until warmup completes. Returns True on done, False on timeout."""
return self._warmup_done_event.wait(timeout=timeout)
def on_warmup_complete(self, callback: Callable[[dict], None]) -> None:
"""Register a callback for warmup completion. If already done, fires immediately."""
with self._warmup_lock:
if self._warmup_done_event.is_set():
snap = {k: list(v) for k, v in self._warmup_status.items()}
if "snap" in dir(): # already done
callback(snap)
else:
with self._warmup_lock:
self._warmup_callbacks.append(callback)
```
Hook API endpoints (added in `src/api_hooks.py`):
- `GET /api/warmup_status``controller.warmup_status()`
- `GET /api/warmup_wait?timeout=N` → blocks until done, returns final status
GUI integration (in `src/gui_2.py`):
- Status bar: "Warming up... (5/8)" while in flight, "All imports ready" + green dot when done. Polled once per frame from `controller.warmup_status()` (cheap, ~microseconds).
- On transition to done: show a toast notification "All providers ready (8 modules)" for 5 seconds.
In `AppController.shutdown()` (or wherever lifecycle cleanup lives):
`self._io_pool.shutdown(wait=False)`. Non-blocking because the pool's
workers are daemon threads and will die with the process anyway.
@@ -381,37 +596,50 @@ Used at every major init step in `AppController.__init__` and `App.__init__`.
- T1.4: Add `scripts/audit_main_thread_imports.py` (static gate)
- T1.5: Commit baseline + audit script
### Phase 2: Job Pool Foundation (Day 1) — the "no new threads" rule
- T2.1 (TDD Red): Write `tests/test_app_controller_io_pool.py` asserting
`AppController` has a `_io_pool: ThreadPoolExecutor` with 4 workers, named
`controller-io-*`
- T2.2 (Green): Add `self._io_pool = ThreadPoolExecutor(max_workers=4,
thread_name_prefix="controller-io")` to `AppController.__init__`. Add
`submit_io(fn, *args)` helper. Wire shutdown into `controller.shutdown()`.
- T2.3: Verify T2.1 tests pass + full suite still passes
### Phase 2: Job Pool + Warmup Foundation (Day 1)
- T2.1 (TDD Red): `tests/test_app_controller_io_pool.py` assert
`AppController` has a 4-worker `_io_pool` named `controller-io-*`
- T2.2 (Green): Add `_io_pool` to `AppController.__init__` with named threads
- T2.3 (TDD Red): `tests/test_warmup_mechanism.py` — assert warmup jobs are
submitted in `__init__`, complete within 10s, fire the done event, support
callbacks, don't block init
- 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
- T2.5: Run T2.1 + T2.3 tests, confirm PASS
- T2.6: Commit
### Phase 3: Lazy-load AI provider SDKs (Day 2)
- T3.1 (TDD Red): Write `tests/test_ai_client_lazy_imports.py` asserting
`import src.ai_client` does NOT import any provider SDK
- T3.2 (Green): Move `from google import genai` / `import anthropic` /
`import openai` / `import requests` into their respective `_send_*` functions
- T3.3: Verify existing `tests/test_ai_client.py` still passes
- T3.4: Commit, re-run benchmark, expect `import src.ai_client` < 50ms
### Phase 3: Remove top-level heavy SDK imports from `src/ai_client.py` (Day 2)
- T3.1 (TDD Red): `tests/test_ai_client_no_top_level_sdk_imports.py` assert
`import src.ai_client` does NOT load `google.genai` / `anthropic` / `openai` /
`requests` (warmup hasn't run in the subprocess)
- T3.2 (Green): Remove the four heavy imports from the top of `ai_client.py`.
Add `_require_warmed(name)` helper. Each `_send_*` uses
`_require_warmed("google.genai")` etc.
- T3.3: Run existing `tests/test_ai_client.py`; fix any breakage (tests
relying on top-level import side effects need a fixture that warms or a
fallback for test mode)
- T3.4: Confirm T3.1 tests PASS
- T3.5: Commit
### Phase 4: Lazy-load FastAPI in `HookServer` (Day 2)
- T4.1 (TDD Red): Write `tests/test_hook_server_lazy_fastapi.py` asserting
### Phase 4: Remove top-level FastAPI imports from `src/api_hooks.py` (Day 2)
- T4.1 (TDD Red): `tests/test_hook_server_no_top_level_fastapi.py` assert
`from src.api_hooks import HookServer` does NOT import fastapi
- T4.2 (Green): Move `from fastapi import ...` inside the methods that need them
- T4.3: Verify existing `tests/test_api_hooks.py` still passes
- T4.2 (Green): Remove the fastapi imports from top. Use `_require_warmed`
inside the methods that need them
- T4.3: Run existing `tests/test_api_hooks.py`; fix
- T4.4: Commit
### Phase 5: Lazy-load feature-gated GUI modules (Day 3)
- T5.1: Lazy-load `src.command_palette` in `src/commands.py`
- T5.2: Lazy-load `src.theme_nerv` and `src.theme_nerv_fx` in `src/theme_2.py`
- T5.3: Lazy-load `src.markdown_table` in `src/markdown_helper.py`
- T5.4: Audit and lazy-load feature-gated imports in `src/gui_2.py`
- T5.5: Run all GUI tests; fix any circular imports
- T5.6: Commit per task
### Phase 5: Remove top-level imports for feature-gated GUI modules (Day 3)
- T5A: Command Palette`tests/test_command_palette_no_top_level_import.py`
+ remove from `src/commands.py` + use `_require_warmed("src.command_palette")`
- T5B: NERV Theme — `tests/test_theme_nerv_no_top_level_import.py` + remove
from `src/theme_2.py` + use `_require_warmed("src.theme_nerv")` etc.
- T5C: Markdown Table — `tests/test_markdown_helper_no_top_level_import.py` +
remove from `src/markdown_helper.py` + use `_require_warmed("src.markdown_table")`
- T5D: GUI feature-gated — audit `src/gui_2.py` via the T1.2 script, apply
same pattern. `numpy` migrates to `_require_warmed` in `bg_shader` call site.
- T5E: Commit per module (4 atomic commits)
### Phase 6: Migrate ad-hoc threads to `_io_pool` (Day 4)
- T6.1: Audit: `grep -rn "threading.Thread(" src/` to find all ad-hoc
@@ -419,30 +647,49 @@ Used at every major init step in `AppController.__init__` and `App.__init__`.
- T6.2: Refactor each ad-hoc thread to use `controller.submit_io(fn)` instead
- T6.3: Per-migration commit
- T6.4: Final `grep -rn "threading.Thread(" src/` shows ZERO new spawns
(the grep result should be identical to the T6.1 audit list, no new entries)
### Phase 7: Enforcement — Runtime Audit Hook (Day 4)
- T7.1 (TDD Red): `tests/test_main_thread_purity.py` — spawn `sloppy.py
### Phase 7: Warmup Notification (Hook API + GUI) (Day 4)
- T7A.1 (TDD Red): `tests/test_api_hooks_warmup.py`assert
`GET /api/warmup_status` and `GET /api/warmup_wait` work
- T7A.2 (Green): Add the two endpoints in `src/api_hooks.py` and register
`warmup_status` in `_gettable_fields`
- T7B.1: In `src/gui_2.py`, add a status-bar indicator that polls
`controller.warmup_status()` each frame: "Warming up... (N/M)" while
pending, "All imports ready" with green dot on completion
- T7B.2: Register a callback via `controller.on_warmup_complete(cb)` that
shows a toast "All providers ready (M modules)" on success
- T7B.3: Update docs (status bar, toast, hook API)
- T7B.4: Commit
### Phase 8: Enforcement — Runtime Audit Hook (Day 4)
- T8.1 (TDD Red): `tests/test_main_thread_purity.py` — spawn `sloppy.py
--headless --enable-test-hooks` with a `sys.addaudithook` shim, verify no
heavy import happens on the main thread
- T7.2: Once Phase 3-5 land, this test should start passing. Wire into CI.
- T7.3: Commit
### Phase 8: Hook API + Diagnostics (Day 5)
- T8.1: Add `/api/startup_profile` endpoint
- T8.2: Add `/api/io_pool_status` endpoint
- T8.3: Add to `_gettable_fields` and the Diagnostics panel
- T8.4: Document in `docs/guide_api_hooks.md`
- T8.5: Tests + commit
- T8.2: Once Phase 3-5 land, this test should start passing. Wire into CI
as a gating test (`@pytest.mark.slow`).
- T8.3: Commit
### Phase 9: Verify + Checkpoint (Day 5)
- T9.1: Re-run `scripts/benchmark_imports.py`; confirm `import src.gui_2` and
`import src.ai_client` are now < 100ms each
- T9.1: Re-run `scripts/benchmark_imports.py --runs=3`; confirm
`import src.ai_client` < 50ms, `import src.gui_2` < 500ms,
`import src.app_controller` < 300ms
- T9.2: Re-run `scripts/audit_main_thread_imports.py`; exit 0
- T9.3: Run `tests/test_main_thread_purity.py`; pass
- T9.4: Run full `live_gui` test batch; `wait_for_server(timeout=15)` no
longer times out
- T9.5: Manual smoke test: `uv run sloppy.py` and
- T9.3: Run `tests/test_warmup_mechanism.py`; warmup completes and notifications fire
- T9.4: Run `tests/test_main_thread_purity.py`; pass
- T9.5: Run full `live_gui` test batch; `wait_for_server(timeout=15)` no
longer times out. Tests can call `controller.wait_for_warmup()` before
exercising warmup-dependent functionality.
- T9.6: Manual smoke:
- `uv run sloppy.py`: time-to-first-frame < 1.5s, observe status indicator
"Warming up... (N/M)" → "All imports ready" + toast
- `uv run sloppy.py --enable-test-hooks`: same, plus `/api/warmup_status`
returns `completed` after a brief wait
- `uv run sloppy.py --headless`: time-to-server-ready
- **Provider switch test**: switch from MiniMax to Gemini in the GUI
after warmup. The action must be INSTANT, not 1s-delayed (proves
warmup did its job)
- T9.7: Phase checkpoint commit + git note with full verification report
- T9.8: Update `conductor/tracks.md`; archive track
`uv run sloppy.py --enable-test-hooks` both feel snappier
- T9.6: Phase checkpoint commit with full verification report
@@ -486,9 +733,22 @@ The track is complete when:
- [ ] `scripts/audit_main_thread_imports.py` exits 0 (no heavy imports on main)
- [ ] `tests/test_main_thread_purity.py` passes (runtime audit hook confirms invariant)
- [ ] `scripts/benchmark_imports.py` shows no new red entries in the top-20
- [ ] First AI call latency on the asyncio thread is < 1500ms (pays the SDK load once,
then the user has a snappy first call forever after). Main thread sees ZERO
of this cost.
- [ ] **`controller.wait_for_warmup(timeout=10.0)` returns True** — warmup completed
within 10s of `AppController.__init__`
- [ ] **All modules in the warmup list are in `sys.modules` after warmup** —
`controller.warmup_status()['pending']` is empty, `'completed'` contains
all expected module names
- [ ] **User-triggered actions on warmed modules are instant** — manual test
switching providers (e.g. MiniMax → Gemini) after warmup completes shows
NO perceptible lag (was ~1s with lazy-loading)
- [ ] **GUI status indicator transitions** — observe "Warming up... (N/M)" in
the status bar, then "All imports ready" with green dot, then a toast
notification fires via `controller.on_warmup_complete(...)`
- [ ] **Hook API exposes warmup state** — `GET /api/warmup_status` returns
`{pending: [], completed: [...], failed: []}`; `GET /api/warmup_wait?timeout=10`
returns the final state
- [ ] **NO `import X` statements inside 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/`
- [ ] No regressions in the existing 272/273 passing tests
- [ ] `grep -rn "threading.Thread(" src/` shows ZERO new spawns after Phase 6
migration (only the existing project scaffolding threads like `HookServer`
@@ -505,9 +765,8 @@ The track is complete when:
- Importing on the main thread for the lean `gui_2` skeleton (~300ms unavoidable)
- `pydantic` lazy loading (used by `src/models.py` which is imported by 16 files;
the cost is already amortized and deferring it would cascade)
- Prefetch / warm-up of the heavy SDKs in the background (Layer 3 in §2.2 is
deliberately the "do nothing" layer; the user pays the import cost once on
first use, on the asyncio thread, not in the background)
- Lazy-loading heavy modules in function bodies (Layer 1 in §2.2 — explicitly
rejected by the user; warmup is the only mechanism)
---
@@ -10,13 +10,13 @@ last_updated = "2026-06-06"
[phases]
phase_1 = { status = "in_progress", checkpoint_sha = "", name = "Audit + Benchmark + Foundation" }
phase_2 = { status = "pending", checkpoint_sha = "", name = "Job Pool Foundation (no new threads)" }
phase_3 = { status = "pending", checkpoint_sha = "", name = "Lazy-load AI provider SDKs" }
phase_4 = { status = "pending", checkpoint_sha = "", name = "Lazy-load FastAPI in HookServer" }
phase_5 = { status = "pending", checkpoint_sha = "", name = "Lazy-load feature-gated GUI modules" }
phase_2 = { status = "pending", checkpoint_sha = "", name = "Job Pool + Warmup Foundation" }
phase_3 = { status = "pending", checkpoint_sha = "", name = "Remove top-level SDK imports (ai_client)" }
phase_4 = { status = "pending", checkpoint_sha = "", name = "Remove top-level FastAPI imports" }
phase_5 = { status = "pending", checkpoint_sha = "", name = "Remove top-level feature-gated GUI imports" }
phase_6 = { status = "pending", checkpoint_sha = "", name = "Migrate ad-hoc threads to _io_pool" }
phase_7 = { status = "pending", checkpoint_sha = "", name = "Enforcement: runtime audit hook" }
phase_8 = { status = "pending", checkpoint_sha = "", name = "Hook API + Diagnostics" }
phase_7 = { status = "pending", checkpoint_sha = "", name = "Warmup Notification (Hook API + GUI)" }
phase_8 = { status = "pending", checkpoint_sha = "", name = "Enforcement: runtime audit hook" }
phase_9 = { status = "pending", checkpoint_sha = "", name = "Verify + Checkpoint" }
[tasks]
@@ -26,42 +26,41 @@ t1_2 = { status = "pending", commit_sha = "", description = "Audit src/gui_2.py
t1_3 = { status = "pending", commit_sha = "", description = "Add StartupProfiler and instrument init" }
t1_4 = { status = "pending", commit_sha = "", description = "Write scripts/audit_main_thread_imports.py (static CI gate)" }
t1_5 = { status = "pending", commit_sha = "", description = "Commit baseline + audit script" }
# Phase 2: Job Pool Foundation
# Phase 2: Job Pool + Warmup Foundation
t2_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_app_controller_io_pool.py" }
t2_2 = { status = "pending", commit_sha = "", description = "Green: add _io_pool ThreadPoolExecutor + submit_io helper to AppController" }
t2_3 = { status = "pending", commit_sha = "", description = "Confirm T2.1 tests pass + full suite still passes" }
t2_4 = { status = "pending", commit_sha = "", description = "Commit T2" }
# Phase 3: Lazy-load AI Provider SDKs
t3_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_ai_client_lazy_imports.py" }
t3_2 = { status = "pending", commit_sha = "", description = "Green: move provider SDK imports into _send_* funcs" }
t2_2 = { status = "pending", commit_sha = "", description = "Green: add _io_pool ThreadPoolExecutor to AppController" }
t2_3 = { status = "pending", commit_sha = "", description = "Red: tests/test_warmup_mechanism.py" }
t2_4 = { status = "pending", commit_sha = "", description = "Green: implement _submit_warmup_jobs, _compute_warmup_list, _warmup_one, warmup_status, is_warmup_done, wait_for_warmup, on_warmup_complete" }
t2_5 = { status = "pending", commit_sha = "", description = "Confirm T2.1 + T2.3 tests pass" }
t2_6 = { status = "pending", commit_sha = "", description = "Commit T2" }
# Phase 3: Remove top-level SDK imports
t3_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_ai_client_no_top_level_sdk_imports.py" }
t3_2 = { status = "pending", commit_sha = "", description = "Green: remove top-level SDK imports from src/ai_client.py; add _require_warmed helper; update _send_* to use it" }
t3_3 = { status = "pending", commit_sha = "", description = "Fix existing test_ai_client.py breakage" }
t3_4 = { status = "pending", commit_sha = "", description = "Confirm T3.1 tests PASS" }
t3_5 = { status = "pending", commit_sha = "", description = "Commit T3" }
t3_6 = { status = "pending", commit_sha = "", description = "Update tracks.md T3 row" }
# Phase 4: Lazy-load FastAPI
t4_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_hook_server_lazy_fastapi.py" }
t4_2 = { status = "pending", commit_sha = "", description = "Green: move fastapi imports into HookServer methods" }
# Phase 4: Remove top-level FastAPI imports
t4_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_hook_server_no_top_level_fastapi.py" }
t4_2 = { status = "pending", commit_sha = "", description = "Green: remove fastapi imports from src/api_hooks.py; use _require_warmed" }
t4_3 = { status = "pending", commit_sha = "", description = "Fix existing test_api_hooks.py breakage" }
t4_4 = { status = "pending", commit_sha = "", description = "Confirm T4.1 tests PASS" }
t4_5 = { status = "pending", commit_sha = "", description = "Commit T4" }
# Phase 5A: Command Palette
t5a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_command_palette_lazy.py" }
t5a_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/commands.py" }
# Phase 5: Remove top-level feature-gated GUI imports
t5a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_command_palette_no_top_level_import.py" }
t5a_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/commands.py; use _require_warmed" }
t5a_3 = { status = "pending", commit_sha = "", description = "Fix existing test_command_palette.py" }
t5a_4 = { status = "pending", commit_sha = "", description = "Commit T5A" }
# Phase 5B: NERV Theme
t5b_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_theme_nerv_lazy.py" }
t5b_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/theme_2.py" }
t5b_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_theme_nerv_no_top_level_import.py" }
t5b_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/theme_2.py; use _require_warmed" }
t5b_3 = { status = "pending", commit_sha = "", description = "Fix existing test_theme_2.py + test_theme_nerv.py" }
t5b_4 = { status = "pending", commit_sha = "", description = "Commit T5B" }
# Phase 5C: Markdown Table
t5c_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_markdown_helper_lazy.py" }
t5c_2 = { status = "pending", commit_sha = "", description = "Green: lazy-load in src/markdown_helper.py" }
t5c_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_markdown_helper_no_top_level_import.py" }
t5c_2 = { status = "pending", commit_sha = "", description = "Green: remove from src/markdown_helper.py; use _require_warmed" }
t5c_3 = { status = "pending", commit_sha = "", description = "Fix existing test_markdown_helper.py" }
t5c_4 = { status = "pending", commit_sha = "", description = "Commit T5C" }
# Phase 5D: gui_2 feature-gated imports
t5d_1 = { status = "pending", commit_sha = "", description = "Run audit_gui2_imports.py and collect feature-gated list" }
t5d_2 = { status = "pending", commit_sha = "", description = "Apply TDD pattern per feature-gated import" }
t5d_2 = { status = "pending", commit_sha = "", description = "Apply same pattern per feature-gated import in src/gui_2.py; numpy in bg_shader" }
t5d_3 = { status = "pending", commit_sha = "", description = "Run full GUI test suite; fix" }
t5d_4 = { status = "pending", commit_sha = "", description = "Commit per feature group" }
# Phase 6: Migrate ad-hoc threads
@@ -69,26 +68,29 @@ t6_1 = { status = "pending", commit_sha = "", description = "Audit threading.Thr
t6_2 = { status = "pending", commit_sha = "", description = "Refactor each ad-hoc thread to use controller.submit_io" }
t6_3 = { status = "pending", commit_sha = "", description = "Run full test suite; fix" }
t6_4 = { status = "pending", commit_sha = "", description = "Commit per migration; final grep shows zero new spawns" }
# Phase 7: Enforcement - Runtime Audit Hook
t7_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_main_thread_purity.py" }
t7_2 = { status = "pending", commit_sha = "", description = "Confirm test passes after Phase 3-5" }
t7_3 = { status = "pending", commit_sha = "", description = "Wire into CI as @pytest.mark.slow gating test" }
t7_4 = { status = "pending", commit_sha = "", description = "Commit T7" }
# Phase 8: Hook API + Diagnostics
t8_1 = { status = "pending", commit_sha = "", description = "Add /api/startup_profile endpoint" }
t8_2 = { status = "pending", commit_sha = "", description = "Add /api/io_pool_status endpoint" }
t8_3 = { status = "pending", commit_sha = "", description = "Add startup profile + io_pool status to Diagnostics panel" }
t8_4 = { status = "pending", commit_sha = "", description = "Update docs/guide_api_hooks.md" }
t8_5 = { status = "pending", commit_sha = "", description = "Tests for endpoints + profiler round-trip" }
t8_6 = { status = "pending", commit_sha = "", description = "Commit T8" }
# Phase 7: Warmup Notification (Hook API + GUI)
t7a_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_api_hooks_warmup.py" }
t7a_2 = { status = "pending", commit_sha = "", description = "Green: add /api/warmup_status and /api/warmup_wait endpoints" }
t7a_3 = { status = "pending", commit_sha = "", description = "Register warmup_status in _gettable_fields" }
t7a_4 = { status = "pending", commit_sha = "", description = "Commit T7A" }
t7b_1 = { status = "pending", commit_sha = "", description = "Add status-bar indicator in src/gui_2.py that polls warmup_status each frame" }
t7b_2 = { status = "pending", commit_sha = "", description = "Register on_warmup_complete callback that shows toast on success" }
t7b_3 = { status = "pending", commit_sha = "", description = "Update docs for status bar + hook API" }
t7b_4 = { status = "pending", commit_sha = "", description = "Commit T7B" }
# Phase 8: Enforcement - Runtime Audit Hook
t8_1 = { status = "pending", commit_sha = "", description = "Red: tests/test_main_thread_purity.py" }
t8_2 = { status = "pending", commit_sha = "", description = "Confirm test passes after Phase 3-5" }
t8_3 = { status = "pending", commit_sha = "", description = "Wire into CI as @pytest.mark.slow gating test" }
t8_4 = { status = "pending", commit_sha = "", description = "Commit T8" }
# Phase 9: Verify + Checkpoint
t9_1 = { status = "pending", commit_sha = "", description = "Re-run benchmark; diff vs baseline" }
t9_2 = { status = "pending", commit_sha = "", description = "Re-run audit_main_thread_imports.py; exit 0" }
t9_3 = { status = "pending", commit_sha = "", description = "Run test_main_thread_purity.py; pass" }
t9_4 = { status = "pending", commit_sha = "", description = "Run live_gui test batch; confirm wait_for_server passes" }
t9_5 = { status = "pending", commit_sha = "", description = "Manual smoke (normal, test-hooks, headless modes)" }
t9_6 = { status = "pending", commit_sha = "", description = "Phase checkpoint commit + git note" }
t9_7 = { status = "pending", commit_sha = "", description = "Update tracks.md; archive track" }
t9_3 = { status = "pending", commit_sha = "", description = "Run test_warmup_mechanism.py; warmup completes and notifications fire" }
t9_4 = { status = "pending", commit_sha = "", description = "Run test_main_thread_purity.py; pass" }
t9_5 = { status = "pending", commit_sha = "", description = "Run live_gui test batch; confirm wait_for_server passes" }
t9_6 = { status = "pending", commit_sha = "", description = "Manual smoke: provider-switch is INSTANT after warmup" }
t9_7 = { status = "pending", commit_sha = "", description = "Phase checkpoint commit + git note" }
t9_8 = { status = "pending", commit_sha = "", description = "Update tracks.md; archive track" }
[verification]
# To be filled at Phase 9
@@ -98,13 +100,31 @@ baseline_gui_2_ms = 0
after_gui_2_ms = 0
baseline_app_controller_ms = 0
after_app_controller_ms = 0
warmup_completes_within_seconds = 0
warmup_modules_in_sys_modules = 0
provider_switch_latency_ms_after_warmup = 0
live_gui_passed = 0
live_gui_failed = 0
audit_main_thread_violations = 0
io_pool_max_workers = 4
io_pool_thread_name_prefix = "controller-io"
new_threading_thread_calls = 0
function_body_heavy_imports = 0
[ad_hoc_threads]
# Filled in Phase 6 T6.1 audit
# Format: {file = "src/foo.py", line = 42, current_target = "lambda", proposed_target = "controller.submit_io(...)"}
[warmup_list]
# Filled in Phase 2 T2.4 implementation
google_genai = true
anthropic = true
openai = true
requests = true
src_command_palette = true
src_theme_nerv = true
src_theme_nerv_fx = true
src_markdown_table = true
numpy = true
fastapi = "conditional" # only when enable_test_hooks or web_host
fastapi_security_api_key = "conditional"