Problem
AICodeGenerator.fix_build_errors repairs each failing file in a strictly
sequential for loop. For every file it performs, on the event loop thread:
- a blocking
Path.exists() + Path.read_text()
- an
await self.router.generate(...) LLM round-trip
- a blocking
Path.write_text()
Two independent costs follow:
Wall clock scales linearly with the number of broken files. The files are
mutually independent — each gets its own prompt, its own completion and its own
write. Nothing in the loop reads state produced by a previous iteration, so the
serialisation is incidental rather than required. With an LLM round-trip
dominating each iteration, 8 broken files cost ~8× one file.
The event loop is blocked during file I/O. read_text/write_text are
synchronous syscalls executed directly in a coroutine, so every other task on
the loop stalls for their duration.
Production impact
DeploymentManager.verify_and_fix_project
(src/youtube_extension/backend/deployment_manager.py:343) calls
fix_build_errors from inside a retry loop (max_retries, declared at line
301). The serial cost is therefore multiplied by the retry count on every
deployment that fails to build.
Proposed fix
- Move
read_text / write_text off the loop with asyncio.to_thread.
- Fan the per-file work out with
asyncio.gather, bounded by an
asyncio.Semaphore so provider rate limits are respected.
- Keep the bound conservative and configurable via a new optional
max_concurrency parameter, defaulting to a module constant.
- Preserve today's failure semantics exactly: one file failing (missing,
unreadable, or provider error) must not abort its siblings.
- Sort the returned
fixed_files so the result is deterministic
(error_files is a set, so completion order is not stable).
Acceptance criteria
Problem
AICodeGenerator.fix_build_errorsrepairs each failing file in a strictlysequential
forloop. For every file it performs, on the event loop thread:Path.exists()+Path.read_text()await self.router.generate(...)LLM round-tripPath.write_text()Two independent costs follow:
Wall clock scales linearly with the number of broken files. The files are
mutually independent — each gets its own prompt, its own completion and its own
write. Nothing in the loop reads state produced by a previous iteration, so the
serialisation is incidental rather than required. With an LLM round-trip
dominating each iteration, 8 broken files cost ~8× one file.
The event loop is blocked during file I/O.
read_text/write_textaresynchronous syscalls executed directly in a coroutine, so every other task on
the loop stalls for their duration.
Production impact
DeploymentManager.verify_and_fix_project(
src/youtube_extension/backend/deployment_manager.py:343) callsfix_build_errorsfrom inside a retry loop (max_retries, declared at line301). The serial cost is therefore multiplied by the retry count on every
deployment that fails to build.
Proposed fix
read_text/write_textoff the loop withasyncio.to_thread.asyncio.gather, bounded by anasyncio.Semaphoreso provider rate limits are respected.max_concurrencyparameter, defaulting to a module constant.unreadable, or provider error) must not abort its siblings.
fixed_filesso the result is deterministic(
error_filesis aset, so completion order is not stable).Acceptance criteria
max_concurrencyclamps to 1 rather than falling back to the defaultread_text/write_textexecute off the event loop threadfixed_filesis deterministically ordered