Track + metadata + state + tracks.md registration for the Fleury-pattern
error handling refactor.
Key design decisions (per user approval):
- Option A for _send_<vendor>() handling: rename to _send_<vendor>_result()
and change return type to Result[str] (contained to internal callers).
- send() is marked @typing_extensions.deprecated; send_result() is the new
public API.
- ProviderError exception is FULLY REPLACED by ErrorInfo dataclass
(a value, not an exception).
- 5 phases: foundation, mcp_client, ai_client, rag_engine, deprecation+archive.
- Post-tracks baseline check (Phase 1 Task 1.1) verifies the 3 pending
tracks have merged before proceeding.
- 9 Open Questions, 7 Risks, 5 verification criteria, follow-up track
public_api_migration_20260606 planned in spec §12.1.
Blocked by: startup_speedup_20260606, test_batching_refactor_20260606,
qwen_llama_grok_integration_20260606. Blocks: public_api_migration_20260606.
This track executes after startup_speedup, test_batching_refactor, and
qwen_llama_grok_integration land. Section 10 documents the expected
post-tracks codebase state and answers 6 critical coordination questions:
- Q1: Existing _send_<vendor>() functions (returning str) are renamed
to _send_<vendor>_result() and changed to return Result[str] (Option A:
clean rename, contained to internal callers).
- Q2: send_openai_compatible in src/openai_compatible.py STAYS as-is
(it raises at the SDK boundary; correct per Fleury). The new
_send_<vendor>_result() functions catch and convert to ErrorInfo.
- Q3: Deprecation warning on send() will produce Python warnings in
tests; filterwarnings in conftest.py silences them during transition.
- Q4: The except ProviderError clauses in src/ai_client.py become
dead code after the refactor and are removed in Phase 3.
- Q5: ProviderError is FULLY REPLACED by ErrorInfo (a value, not an
exception). ProviderError removed entirely; ErrorInfo is the new
error type.
- Q6: ProviderError.ui_message() moves to ErrorInfo.ui_message().
Phase 1 also adds a baseline verification task to confirm the 3 pending
tracks have merged before proceeding.
Also renumbered Out of Scope (11) and See Also (12) sections to
preserve monotonic section numbers.
The capability matrix v1 has no 'audio' field (audio_input is deferred to v2).
Qwen-Audio's vision flag was incorrectly marked true. Changed to false and
clarified that v1 uses Qwen-Audio as text-only; audio attachment UI is
hidden via the absent audio capability check.
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