Phase 2 of startup_speedup_20260606 is done.
Tasks:
T2.1 (Red) tests/test_io_pool.py 1354679e 4 tests
T2.2 (Green) src/io_pool.py 1354679e make_io_pool() factory
T2.3 (Red) tests/test_warmup.py 1354679e 10 tests
T2.4 (Green) src/warmup.py 1354679e WarmupManager
T2.5 (Wire) AppController integration 922c5ad9 io_pool + warmup in __init__ + 5 public delegation methods
T2.6 (Plan) this commit
What now exists:
- make_io_pool() returns a 4-worker ThreadPoolExecutor named 'controller-io-N'
- WarmupManager class with submit/status/is_done/wait/on_complete/reset
- AppController creates self._io_pool + self._warmup early in __init__
- Warmup is submitted immediately (jobs run concurrent with the rest of init)
- Public API: controller.warmup_status(), controller.is_warmup_done(),
controller.wait_for_warmup(timeout), controller.on_warmup_complete(cb)
- controller._compute_warmup_list() returns 9 always + 2 conditional (fastapi)
- shutdown() now also shuts down the io_pool
Currently the warmup is a no-op for modules already imported at the top
of app_controller.py (fastapi, requests). Phase 3 will remove those
top-level imports; the warmup infrastructure will then start doing
real work.
18/18 tests passing (4 io_pool + 10 warmup + 4 test_app_controller_*).
Next: Phase 3 (remove top-level SDK imports from src/ai_client.py).
Expected to fix ~3 audit violations (google.genai, anthropic, openai).
16 tasks across 4 phases, each with explicit Red-Green-Refactor TDD steps:
- Phase 1 (1.1-1.16): Library + dry-run. 20 unit tests across categorizer,
batcher, plugin. New run_tests_batched.py has --plan/--audit only.
- Phase 2 (2.1-2.3): Shadow run via CI. Compare new vs old plan output.
- Phase 3 (3.1-3.4): Switch default. Full CLI with --tiers, --durations.
Old script becomes .legacy. Update docs/guide_testing.md.
- Phase 4 (4.1-4.6): Populate registry, gitignore durations, delete
legacy, archive track.
1-space indentation per project style guide. No placeholders. All
test code is concrete.
Default --audit exits non-zero on hard errors only. --strict adds the
'multiple subsystems = probably cross-cutting' heuristic from Section 9
as a CI gate. Two modes, one flag.
Three-tier batching refactor: replace alphabetical 4-at-a-time batching with
fixture-class-isolated tiers (0 opt-in, 1 unit/xdist, 2 mock_app, 3 live_gui
in one session, H headless, P performance).
Hybrid classification: auto-infer from filename + AST fixture scan; hand-curated
tests/test_categories.toml overrides for cross-cutting and ambiguous files.
Opt-in per-test order control via [[files.X.test_order]] sub-tables, gated on
a conftest-loaded pytest plugin (no-op without entries).
Priority order: B (process isolation) > A (subsystem diagnostic) > C (speed).
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.
Fulfills the existing backlog entry at conductor/tracks.md:152
(2026-06-05 root-cause analysis of live_gui wait_for_server timeouts).
Main Thread Purity Invariant: the main thread (entering immapp.run())
must never import a module heavier than imgui_bundle and the lean
gui_2 skeleton. Enforced by:
- static gate: scripts/audit_main_thread_imports.py (CI)
- runtime hook: tests/test_main_thread_purity.py (sys.addaudithook)
Threading constraint: no new threading.Thread(...) calls in src/.
All background work goes through AppController._io_pool
(ThreadPoolExecutor, max_workers=4, thread_name_prefix='controller-io').
9 phases, 57 tasks: audit+baseline, job pool, lazy-load SDKs, lazy-load
FastAPI, lazy-load feature-gated GUI, migrate ad-hoc threads, runtime
enforcement, hook API + diagnostics, verify+checkpoint.
Expected savings: ~2000-2400ms off main-thread import cost.
Target: import src.ai_client < 50ms (from ~1800ms), live_gui fixtures
no longer time out at wait_for_server(timeout=15).
Previously, context (files, screenshots) was always sent with every message,
even on subsequent messages where the AI provider already had the context
from the first message via its history mechanism.
This change:
- Detects if the discussion has any AI responses already
- Only sends md_content (stable_md) on the first message
- Subsequent messages pass empty string for md_content to avoid redundant sending
- Context now properly goes in md_content parameter, not crammed into user_message
The fix is in _api_generate() in src/app_controller.py