Skip to content

perf: batch project scaffolding disk writes off the event loop - #1251

Merged
groupthinking merged 3 commits into
mainfrom
perf/scaffolding-writes-off-loop
Aug 2, 2026
Merged

perf: batch project scaffolding disk writes off the event loop#1251
groupthinking merged 3 commits into
mainfrom
perf/scaffolding-writes-off-loop

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1250.

ProjectCodeGenerator performs every scaffolding filesystem call inline inside
async def bodies. A blocked write therefore parks the entire event loop, not
just the requesting coroutine.

Outcome

What this does Moves all 28 scaffolding filesystem calls in code_generator.py off the event loop, batched into one asyncio.to_thread hop per generator.
What this does not do It does not claim a throughput improvement. Payloads are small and fixed-count (13 files, ~15.5 KB across all three project types) — sub-millisecond on a warm SSD.
The actual defect The stall is unbounded on overlayfs, network-backed mounts, and under vm.dirty_ratio writeback throttling, and it is paid by every coroutine on the worker, not just the requester.
Behaviour change Output is byte-for-byte identical, verified by differential execution rather than by inspection. Two deliberate changes, both cleanup-only: a request cancelled mid-scaffold, and a generation that raises, now remove the directory they created instead of stranding it. Neither is reachable by a caller, since the path is only handed out on success. See Risk #4 and #6.

Sites converted:

Location Before After
generate_project tempfile.mkdtemp(...) await asyncio.to_thread(tempfile.mkdtemp, ...)
_generate_react_project 8 inline open()/mkdir() calls 1 batched hop
_generate_vanilla_js_project 4 inline open() calls 1 batched hop
_generate_python_api 3 inline open() calls 1 batched hop

Risk

Low, with three risks named explicitly rather than waved away.

  1. Ordering. The write plan is an ordered list[tuple[Path, Optional[str]]]
    applied strictly in sequence, so intermediate on-disk states match the old
    inline sequence exactly. Directory steps (None content) still precede the
    files that live in them. Covered by
    test_applies_steps_in_order_so_dirs_precede_their_files.

  2. Error semantics. _apply_write_plan suppresses nothing. A failing step
    raises exactly what the inline call would have raised, on the same step, with
    earlier steps already applied — identical to the previous behaviour. Covered
    by test_does_not_suppress_errors_and_leaves_earlier_steps_applied, which
    uses a real NUL-byte path rather than a mock so the stdlib itself produces
    the failure.

  3. json.dumpjson.dumps. The React generator previously used
    json.dump(obj, f, indent=2); content is now built with
    json.dumps(obj, indent=2). These are byte-identical — dump writes exactly
    the chunks dumps joins, and neither appends a trailing newline. Asserted
    directly by test_writes_content_verbatim_without_adding_a_trailing_newline
    and confirmed by the differential run below.

  4. Cancellation atomicity — found in review, fixed here. This is the one real
    behaviour change and it deserves the detail. Before this PR the three leaf
    generators (_generate_react_project, _generate_vanilla_js_project,
    _generate_python_api) contained zero await expressions, so awaiting them
    never suspended the task: everything from mkdtemp to return ran without a
    single cancellation point. Adding await asyncio.to_thread(...) created four
    real suspension points inside a previously uninterruptible region, so a
    cancelled request could unwind while a worker thread was still writing into a
    directory whose path no caller would ever receive.

    Measured on all three variants, cancelling mid-scaffold in an isolated
    TMPDIR:

    Variant Outcome Writer alive at unwind Dirs left behind
    main today returns normally (cancel not delivered) n/a 1 — owned by the caller
    This PR, before the fix CancelledError yes 1 — orphaned, no owner
    This PR, as submitted CancelledError no 0

    The fix drains the scaffolding task through a shield loop, removes the
    directory generate_project created, then re-raises. CancelledError is never
    suppressed and the drain is what makes removal safe — deleting a tree while a
    worker may still be writing into it is its own bug. The pattern mirrors
    _run_sync_rpc in services/cloud/cloud_tasks_queue.py. Cancellation latency
    cost is the remainder of one write batch. Covered by
    test_cancellation_removes_the_project_directory,
    test_cancellation_drains_the_writer_before_unwinding and
    test_cancellation_is_reported_as_cancellation; reverting only the fix fails
    exactly the first two.

  5. Branch selection collapsed 4 → 3. project_type == "web" and the else
    fallback both called _generate_web_project, so they are now one branch.
    Guarded by test_uncancelled_web_request_still_returns_a_project, which
    compares the full on-disk tree produced by an explicit "web" request against
    an unrecognised type.

  6. Failed generations no longer strand a directory. The pre-existing
    generic-exception path (except Exception: logger.error(...); raise) also
    left the directory behind. That predates this change, but generate_project
    is the only holder of the path until it returns, so nothing downstream can
    ever clean it up; the caller in video_processing_service.py reads
    project_path only on the success path (L390). The whole region from branch
    selection through return is now wrapped in try/except Exception: await _discard_project_dir(...); raise. CancelledError derives from BaseException, so the cancellation
    path above is unaffected and cannot double-remove.

    _discard_project_dir suppresses every exception and drains its worker, so
    cleanup can never mask the error that caused it — asserted directly by
    test_original_exception_is_not_masked_by_cleanup, which makes both the
    generation and shutil.rmtree raise and requires the original error to
    surface. test_failed_generation_leaves_no_orphan_directory fails without
    the fix; test_successful_generation_keeps_its_directory guards against the
    cleanup firing on the happy path. This subsumes the separate reports Cancelled video-to-software request leaks the scaffold temp directory #1253
    and Scaffolding failure leaves an orphaned project directory #1254, both now closed as superseded.

Also not claimed: the differential harness compares file contents and return
values, not file metadata, and the new suspension points do let a concurrent
coroutine on the same loop observe intermediate on-disk states that were
previously invisible. Nothing in this repo reads a project directory while it is
being generated, but the window is real and is stated rather than hidden.

Not claimed: this does not bound worst-case completion time. If the filesystem
hangs indefinitely, the worker thread still hangs — it just no longer takes the
event loop with it. The bound is on blast radius, not on latency.

Out of scope: ensure_templates_directory is a plain def, not a coroutine, so
its mkdir never touches the loop and is deliberately left alone.

Verification

Static. An AST scan for blocking filesystem primitives inside async def
bodies reports 0 remaining (was 28). The scan also caught a site the initial
sweep missed — tempfile.mkdtemp — which is included above.

Differential — byte-for-byte output equality. The pre-change module and the
post-change module were loaded side by side in one process and driven through
all three generators with identical input. Every emitted file was SHA-256'd and
every returned dict compared:

react:   8 entries IDENTICAL (return dict identical)
vanilla: 4 entries IDENTICAL (return dict identical)
pyapi:   3 entries IDENTICAL (return dict identical)

BYTE-FOR-BYTE IDENTICAL: True

Tests. 19 new tests, 70 passed89 passed.

Off-loop proofs assert thread identity, never wall-clock elapsed time: a
timing threshold would be flaky under CI contention and would still pass if the
work ran on the loop but happened to be fast.

Three of the new tests guard the batching specifically — a regression that
offloaded each write individually would still satisfy the thread-identity
assertions while paying one context switch per file, so to_thread is
instrumented and asserted to be called exactly once per generator.

Prove-fail, twice, each against the specific semantics rather than the file.

Off-loop hops. The three to_thread hops and the mkdtemp hop were reverted in
place while leaving _apply_write_plan defined, so the failures are genuine
assertion failures rather than import errors:

7 failed, 74 passed

Exactly the 7 off-loop tests failed. The 4 _apply_write_plan contract tests
correctly continued to pass, since the helper itself was unchanged.

Cancellation fix. Reverting only the drain-and-discard change, leaving the
off-loop work intact:

2 failed, 83 passed

Exactly test_cancellation_removes_the_project_directory and
test_cancellation_drains_the_writer_before_unwinding failed.
test_cancellation_is_reported_as_cancellation correctly passed both ways —
the unfixed code did propagate CancelledError, it just leaked the directory —
and the branch-selection guard is a behaviour-preservation test, so it also
passes both ways.

Failed-generation cleanup. Reverting only the try/except Exception block:

1 failed, 88 passed

test_failed_generation_leaves_no_orphan_directory failed.
test_original_exception_is_not_masked_by_cleanup correctly passed both ways —
without cleanup there is nothing to mask — and
test_successful_generation_keeps_its_directory is a happy-path guard, so it
also passes both ways. Both are still worth having: the first is the assertion
that would fail if the guard inside _discard_project_dir were ever narrowed,
and the second is what fails if the cleanup is made unconditional.

The Path.mkdir coverage gap, closed. The Copilot reviewer observed that the
off-loop test hooked only open(), so directory creation was unguarded. Rather
than assert that from inspection, the defect was injected: src_dir.mkdir() and
public_dir.mkdir() were moved out of the write plan and back onto the loop,
then both versions of the test file were run against that same tree.

Test file Result against the injected regression
Before (85 tests) 85 passed — undetected
After (89 tests) 2 failed, 87 passed — caught

The hook is Path.mkdir, recorded alongside open and asserted over the union.
Two ordering details matter: the project root is created before patching, or
the hook fires during setup; and _generate_vanilla_js_project and
_generate_python_api create no subdirectories at all, so the parametrised test
cannot assert the mkdir record is non-empty. A dedicated react-only test does
that, which is what stops the union assertion from being vacuous.

All files were restored after each injection and verified with an empty diff.

Wider sweep. 638 passed across test_code_generator.py,
test_code_generator_agent.py, test_ai_code_generator.py,
test_video_processing_service.py, test_deployment_manager.py and
test_transcript_action_workflow.py.

Byte equality re-confirmed after the cancellation fix — the differential
harness was re-run against the final tree and still reports
BYTE-FOR-BYTE IDENTICAL: True.

Lint. Ruff diagnostic parity against origin/mainPARITY OK on both the
source and the test file. No new diagnostics, no suppressed ones.

Production evidence

Reachable from a live HTTP endpoint, verified at call level rather than by
import graph alone:

POST /api/v1/video-to-software        router.py:778   (mounted main.py:192)
  -> process_video_to_software        video_processing_service.py:317
  -> code_generator.generate_project  video_processing_service.py:380
       -> tempfile.mkdtemp                        (project dir)
       -> _generate_web_project -> _generate_react_project
                                |  _generate_vanilla_js_project
       -> _generate_api_project -> _generate_python_api

This is the same endpoint whose subprocess stalls were accepted and fixed in
#1239 / #1240. That change moved npm install / npm run build / npx tsc off
the loop; this change closes the remaining inline filesystem work on the same
request path, so the endpoint no longer blocks the loop anywhere in its
scaffolding phase.

ProjectCodeGenerator performed every scaffolding filesystem call inline
inside async def bodies, so a blocked write parked the whole event loop
rather than just the requesting coroutine.

Move all 28 filesystem calls off the loop, batched into one
asyncio.to_thread hop per generator (O(1) hops instead of O(files)).
Content generation is pure in-memory string building and stays on the
loop; only the writes are offloaded.

Output is byte-for-byte identical, verified by loading the pre- and
post-change modules side by side and comparing SHA-256 digests of every
emitted file plus every returned dict across all three generators.

Refs #1250

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 2, 2026 17:24
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 2, 2026 6:02pm

@github-actions github-actions Bot added the python label Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Performance

    • Improved project generation responsiveness by moving filesystem operations off the main execution path.
  • Reliability

    • Preserved the order of generated files and consistent error handling across React, vanilla JavaScript, and FastAPI projects.
  • Compatibility

    • Temporary project-directory creation and file generation continue to work across supported project types.

Walkthrough

Project generation now builds artifact contents in memory and performs ordered filesystem operations in worker threads. Temporary project-directory creation also runs off the event loop. React, vanilla JavaScript, and FastAPI generators use the shared write-plan mechanism.

Changes

Async project scaffolding

Layer / File(s) Summary
Write-plan execution and temporary directory setup
src/youtube_extension/backend/code_generator.py
Adds WritePlan and _apply_write_plan for ordered directory and file operations. Offloads temporary-directory creation with asyncio.to_thread.
React project generation
src/youtube_extension/backend/code_generator.py
Builds React artifacts in memory, creates an ordered write plan, and applies it in one worker-thread operation.
Vanilla JavaScript and FastAPI generation
src/youtube_extension/backend/code_generator.py
Replaces inline filesystem writes with ordered worker-thread write plans while preserving generated content and layout.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: copilot

Poem

Files form a plan,
Threads carry writes from the loop,
React blooms in place,
Vanilla and FastAPI
Build without blocking.

🚥 Pre-merge checks | ✅ 4 | ❌ 3

❌ Failed checks (3 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The source changes match issue #1250, but test and lint acceptance criteria cannot be verified because the test file is excluded by path filters. Review tests/unit/test_code_generator.py and the current-head lint results to verify the remaining acceptance criteria.
Enforce Copilot Verification ❓ Inconclusive I need to inspect the pull request metadata for an explicit GitHub Copilot approval. Retrieve the pull request number or its GitHub review records, including reviewer identity and approval state.
Require Ai Unit Tests ❓ Inconclusive Assessment pending repository and pull-request metadata inspection. Need verify the copilot-rabbit label and identify committed AI-generated unit tests.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The reviewed changes are limited to ProjectCodeGenerator filesystem offloading and directly support the objectives in issue #1250.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely describes batching project scaffolding disk writes outside the event loop.
Description check ✅ Passed The description thoroughly documents the outcome, risks, verification, production path, and known scope, despite missing some template sections.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/scaffolding-writes-off-loop
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch perf/scaffolding-writes-off-loop

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped groupthinking/uvai-skills.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@groupthinking

Copy link
Copy Markdown
Owner Author

@linear-code @coderabbitai review

Adversarial framing — the three things most worth attacking here:

  1. Is the byte-for-byte claim actually proven, or just asserted? I ran the
    pre-change and post-change modules side by side in one process and compared
    SHA-256 of every emitted file plus every returned dict. If you think that
    harness has a hole (e.g. it would not catch a difference in file mode, or
    in write ordering as observed by a concurrent reader), say so — I would
    rather find it here.

  2. json.dumpjson.dumps in the React generator. I claim these are
    byte-identical. If there is any input for which they diverge, that is a real
    defect in this PR.

  3. Batching changes failure granularity. Previously each write was its own
    statement; now they run inside one helper. I claim error semantics are
    unchanged — same exception, same step, earlier steps applied. If batching
    creates an observable difference I have missed, that is the finding.

Deliberately not claimed: no throughput improvement. The payloads are small
and fixed-count. The defect being fixed is unbounded tail latency and blast
radius, not average-case speed. Please push back if you think that framing
oversells or undersells the change.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA d178e21.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Changed files vs PR base ==='
git diff --name-status origin/main...HEAD

echo '=== Diff for generator and related tests ==='
git diff --find-renames --unified=80 origin/main...HEAD -- \
  '*code_generator.py' '*test_code_generator.py'

echo '=== Candidate generator files ==='
fd -t f 'code_generator\.py$' .

echo '=== Candidate generator test files ==='
fd -t f 'test_code_generator\.py$' .

echo '=== Source outlines ==='
for f in $(fd -t f 'code_generator\.py$' .); do
  ast-grep outline "$f" --items all --type function,class --view expanded
done

echo '=== Targeted helper and generator references ==='
rg -n -C 5 \
  '(_apply_write_plan|asyncio\.to_thread|json\.(dump|dumps)|def generate_project|async def generate_project|_generate_(react_project|vanilla_js_project|python_api))' \
  -g '*code_generator.py' -g '*test_code_generator.py' .

Length of output: 89533


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== generate_project call sites and nearby cleanup/cancellation handling ==='
rg -n -C 12 \
  '\.generate_project\s*\(|generate_project\s*\(|CancelledError|cancel\(|shutil\.rmtree|rmtree\(|TemporaryDirectory' \
  src tests \
  -g '*.py'

echo '=== Endpoint path identified in the PR objective ==='
fd -t f 'video_processing_service.py|router.py|main.py' src | while read -r f; do
  echo "--- $f"
  rg -n -C 18 \
    '(process_video_to_software|generate_project\s*\(|CancelledError|cancel\(|rmtree\()' \
    "$f" || true
done

echo '=== Existing cancellation-focused tests ==='
rg -n -C 8 \
  '(CancelledError|cancel\(|asyncio\.wait_for|timeout)' \
  tests -g '*.py'

Length of output: 93334


@groupthinking I found one production-relevant behavior change.

Blocking: cancellation can leave an unowned project directory while scaffolding continues.

At Line 182 and Lines 299, 353, and 403, asyncio.to_thread adds cancellation points. Cancelling the awaiting task does not cancel a worker thread that has started. The request can therefore raise CancelledError while mkdtemp or _apply_write_plan continues.

video_processing_service.py awaits generate_project at Line 380. It has no cancellation cleanup for the generated directory. A cancelled request can leave a partial or complete uvai_project_* directory that no caller receives or removes. The prior inline sequence could not be cancelled after it started.

Use the existing pattern in src/youtube_extension/services/cloud/cloud_tasks_queue.py: retain the worker task, shield it, wait for completion after cancellation, then re-raise CancelledError. If cancellation occurs after mkdtemp, remove the project directory after the worker finishes. Add a cancellation test that blocks _apply_write_plan, cancels generate_project, and verifies that no orphan directory remains.

The three stated concerns otherwise check out by inspection:

  • _apply_write_plan keeps filesystem operation order and preserves earlier writes on a later failure.
  • json.dumps(package_json, indent=2) produces the same text as the previous json.dump(..., indent=2) for this fixed JSON-serializable structure.
  • The differential harness proves file bytes and return values. It does not prove metadata or same-event-loop transient visibility. The filesystem calls retain the same modes and ordered operations, but the new await points let concurrent coroutines observe intermediate states that the old no-await sequence did not expose.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Moves project scaffolding filesystem operations off the event loop while preserving generated output and write ordering.

Changes:

  • Adds ordered, batched write plans executed through asyncio.to_thread.
  • Offloads temporary-directory creation.
  • Adds regression tests for batching, threading, ordering, and error propagation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/youtube_extension/backend/code_generator.py Offloads scaffolding filesystem operations.
tests/unit/test_code_generator.py Adds threading and write-plan tests.
Suppressed comments (1)

tests/unit/test_code_generator.py:874

  • The tracked call delegates to the real mkdtemp without placing it under tmp_path, so every test run leaves a generated project in the system temp directory. Set dir=tmp_path so pytest owns and removes the real temporary directory after the test.
        def _tracked(*args, **kwargs):
            seen.append(threading.current_thread().name)
            return real_mkdtemp(*args, **kwargs)

Comment on lines +850 to +855
seen: list[str] = []
monkeypatch.setattr("builtins.open", self._recording_open(seen))

project = tmp_path / "project"
project.mkdir()
await getattr(gen, generator_name)(project, analysis, ["database"])

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and it was not a theoretical gap — I injected the exact defect you described rather than reasoning about it. src_dir.mkdir() and public_dir.mkdir() were moved out of the write plan and back onto the loop, then both versions of the test file were run against that identical tree:

Test file Result against the injected regression
Before (85 tests) 85 passed — undetected
After (89 tests) 2 failed, 87 passed — caught

So the assertions were passing for the wrong reason: directories are created inside _apply_write_plan as (path, None) plan steps, and nothing was watching that seam.

Fixed in d178e210a. The recorder now hooks Path.mkdir alongside open and asserts over the union of both records. Two ordering details turned out to matter:

  • The project root has to be created before patching, otherwise the hook fires during setup and records the calling thread rather than the worker.
  • _generate_vanilla_js_project and _generate_python_api create no subdirectories at all, so the parametrised test cannot assert the mkdir record is non-empty without failing on two of its three cases. A dedicated react-only test (test_directory_creation_never_touches_loop_thread) carries that assertion, which is what stops the union check from being vacuously satisfiable.

Both files were restored afterwards and confirmed with an empty diff.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/youtube_extension/backend/code_generator.py (1)

271-299: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Sanitize generated source before writing and deploying it.

These plans persist artifacts that interpolate analysis-derived values into HTML, JavaScript, and Python source. A malicious video title can break generated Python syntax or inject executable Python when the deployed FastAPI app starts. Raw title, summary, technology, and concept values can also create stored XSS in generated HTML.

Encode each value for its target context before source construction. Use safe Python string literals for main.py, HTML escaping for markup, and safe JavaScript serialization for source values. Parse generated Python with ast.parse and syntax-check generated JavaScript before applying the write plan.

As per coding guidelines, “Implement comprehensive input validation and sanitize outputs for security.” As per path instructions, “Flag any code generation output that reaches users without AST validation or syntax checking.”

Also applies to: 347-353, 398-403

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/youtube_extension/backend/code_generator.py` around lines 271 - 299,
Sanitize all analysis-derived values before constructing generated artifacts in
the code-generation flow, including the paths around _generate_index_html,
_generate_react_app_component, _generate_readme, and the related Python output
generation. Use context-appropriate safe Python literals, HTML escaping, and
JavaScript serialization for titles, summaries, technologies, features, and
concepts. Parse generated Python with ast.parse and syntax-check generated
JavaScript before calling _apply_write_plan, ensuring only validated artifacts
are written or deployed.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/youtube_extension/backend/code_generator.py`:
- Around line 398-403: Update the generated project flow around main_py and
process_video_to_software to stop deploying placeholder FastAPI behavior,
including fixed health data, fake responses, and unimplemented database or
authentication endpoints. Generate real implementations for every requested
feature, or reject unsupported feature combinations before _apply_write_plan
writes and deploys the project; do not emit mock, simulated, or TODO-based
production behavior.
- Around line 182-184: Update generate_project and each asyncio.to_thread
operation, including tempfile.mkdtemp and _apply_write_plan, to retain the
worker task and await it through cancellation so thread work completes safely.
On CancelledError, remove project_path before propagating cancellation,
including cleanup when cancellation occurs during project creation or write-plan
application. Add a cancellation test that blocks _apply_write_plan and verifies
no uvai_project_* directory remains.

---

Outside diff comments:
In `@src/youtube_extension/backend/code_generator.py`:
- Around line 271-299: Sanitize all analysis-derived values before constructing
generated artifacts in the code-generation flow, including the paths around
_generate_index_html, _generate_react_app_component, _generate_readme, and the
related Python output generation. Use context-appropriate safe Python literals,
HTML escaping, and JavaScript serialization for titles, summaries, technologies,
features, and concepts. Parse generated Python with ast.parse and syntax-check
generated JavaScript before calling _apply_write_plan, ensuring only validated
artifacts are written or deployed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2be2b63-07e4-4173-a389-d3a9b68ae098

📥 Commits

Reviewing files that changed from the base of the PR and between 6847a1f and 189d814.

⛔ Files ignored due to path filters (1)
  • tests/unit/test_code_generator.py is excluded by !tests/**
📒 Files selected for processing (1)
  • src/youtube_extension/backend/code_generator.py
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: trivy
  • GitHub Check: Generate and Upload Coverage
  • GitHub Check: Security Scan - python
  • GitHub Check: Security Scan - javascript
  • GitHub Check: test
⚠️ CI failures not shown inline (4)

GitHub Actions: PR Checks / agent-completion_truth-gate: perf: batch project scaffolding disk writes off the event loop

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = '<!-- agent-completion-truth-gate:v1 -->';
const runUrlPrefix = context.serverUrl + '/' + owner + '/' +
  repo + '/actions/runs/';
const runUrl = runUrlPrefix + context.runId;
const gateContext = 'agent-completion/truth-gate/pr-' +
  process.env.PR_NUMBER;
function gateStatusDisposition(
  status,
  expectedPendingId,
  currentRunUrl,
  targetPrefix
) {
  if (!/^\d+$/.test(String(expectedPendingId || '')) ||
      !status || !/^\d+$/.test(String(status.id || ''))) {
    return 'fail_closed';
  }
  const target = String(
    (status && status.target_url) || ''
  );
  const expectedId = BigInt(String(expectedPendingId));
  const statusId = BigInt(String(status.id));
  function validRunTarget(targetUrl) {
    const value = String(targetUrl || '');
    if (!value.startsWith(targetPrefix)) {
      return false;
    }
    const suffix = value.slice(targetPrefix.length);
    return /^\d+$/.test(suffix);
  }
  function statusOwnerId(candidate) {
    if (candidate.state === 'pending') {
      return BigInt(String(candidate.id));
    }
    const owner = String(candidate.description || '').match(
      /^gate-owner:(\d+)(?:\s|$)/
    );
    return owner ? BigInt(owner[1]) : null;
  }
  if (!validRunTarget(currentRunUrl) ||
      !validRunTarget(target)) {
    return 'fail_closed';
  }
  const ownerId = statusOwnerId(status);
  if (ownerId === null) {
    return 'fail_closed';
  }
  if (ownerId === expectedId && target === currentRunUrl) {
    if (statusId === expectedId &&
        status.state === 'pending') {
      return 'current_pending';
    }
    if (['failure', 'error'].includes(status.state)) {
      return 'already_failed';
    }
    if (status.state === 'success') {
      return 'already_succeeded';
    }
    return 'fail_closed';
  }
  if (target === currentRunUrl) {...

GitHub Actions: PR Checks / 0_agent-completion_truth-gate.txt: perf: batch project scaffolding disk writes off the event loop

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const fs = require('fs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const marker = '<!-- agent-completion-truth-gate:v1 -->';
const runUrlPrefix = context.serverUrl + '/' + owner + '/' +
  repo + '/actions/runs/';
const runUrl = runUrlPrefix + context.runId;
const gateContext = 'agent-completion/truth-gate/pr-' +
  process.env.PR_NUMBER;
function gateStatusDisposition(
  status,
  expectedPendingId,
  currentRunUrl,
  targetPrefix
) {
  if (!/^\d+$/.test(String(expectedPendingId || '')) ||
      !status || !/^\d+$/.test(String(status.id || ''))) {
    return 'fail_closed';
  }
  const target = String(
    (status && status.target_url) || ''
  );
  const expectedId = BigInt(String(expectedPendingId));
  const statusId = BigInt(String(status.id));
  function validRunTarget(targetUrl) {
    const value = String(targetUrl || '');
    if (!value.startsWith(targetPrefix)) {
      return false;
    }
    const suffix = value.slice(targetPrefix.length);
    return /^\d+$/.test(suffix);
  }
  function statusOwnerId(candidate) {
    if (candidate.state === 'pending') {
      return BigInt(String(candidate.id));
    }
    const owner = String(candidate.description || '').match(
      /^gate-owner:(\d+)(?:\s|$)/
    );
    return owner ? BigInt(owner[1]) : null;
  }
  if (!validRunTarget(currentRunUrl) ||
      !validRunTarget(target)) {
    return 'fail_closed';
  }
  const ownerId = statusOwnerId(status);
  if (ownerId === null) {
    return 'fail_closed';
  }
  if (ownerId === expectedId && target === currentRunUrl) {
    if (statusId === expectedId &&
        status.state === 'pending') {
      return 'current_pending';
    }
    if (['failure', 'error'].includes(status.state)) {
      return 'already_failed';
    }
    if (status.state === 'success') {
      return 'already_succeeded';
    }
    return 'fail_closed';
  }
  if (target === currentRunUrl) {...

GitHub Actions: PR Checks / agent-completion_truth-gate: perf: batch project scaffolding disk writes off the event loop

Conclusion: failure

View job details

##[group]Run exit 1
 �[36;1mexit 1�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
 ##[error]Process completed with exit code 1.

GitHub Actions: PR Checks / agent-completion_truth-gate: perf: batch project scaffolding disk writes off the event loop

Conclusion: failure

View job details

##[group]Run actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
 with:
   script: const owner = context.repo.owner;
const repo = context.repo.repo;
const runUrlPrefix = context.serverUrl + '/' + owner + '/' +
  repo + '/actions/runs/';
const runUrl = runUrlPrefix + context.runId;
const gateContext = 'agent-completion/truth-gate/pr-' +
  process.env.PR_NUMBER;
function gateStatusDisposition(
  status,
  expectedPendingId,
  currentRunUrl,
  targetPrefix
) {
  if (!/^\d+$/.test(String(expectedPendingId || '')) ||
      !status || !/^\d+$/.test(String(status.id || ''))) {
    return 'fail_closed';
  }
  const target = String(
    (status && status.target_url) || ''
  );
  const expectedId = BigInt(String(expectedPendingId));
  const statusId = BigInt(String(status.id));
  function validRunTarget(targetUrl) {
    const value = String(targetUrl || '');
    if (!value.startsWith(targetPrefix)) {
      return false;
    }
    const suffix = value.slice(targetPrefix.length);
    return /^\d+$/.test(suffix);
  }
  function statusOwnerId(candidate) {
    if (candidate.state === 'pending') {
      return BigInt(String(candidate.id));
    }
    const owner = String(candidate.description || '').match(
      /^gate-owner:(\d+)(?:\s|$)/
    );
    return owner ? BigInt(owner[1]) : null;
  }
  if (!validRunTarget(currentRunUrl) ||
      !validRunTarget(target)) {
    return 'fail_closed';
  }
  const ownerId = statusOwnerId(status);
  if (ownerId === null) {
    return 'fail_closed';
  }
  if (ownerId === expectedId && target === currentRunUrl) {
    if (statusId === expectedId &&
        status.state === 'pending') {
      return 'current_pending';
    }
    if (['failure', 'error'].includes(status.state)) {
      return 'already_failed';
    }
    if (status.state === 'success') {
      return 'already_succeeded';
    }
    return 'fail_closed';
  }
  if (target === currentRunUrl) {
    return 'fail_closed';
  }
  if (ownerId > expectedId) {
    return 'successor';...
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: Always use type hints for Python functions
Use Black formatter with 88 character line length for Python code
Follow PEP 8 conventions for Python code
Use descriptive variable names and add docstrings to all public functions in Python
Group imports in Python: standard library, third-party, local
Use SQLAlchemy ORM and never write raw SQL queries
Use environment variables via os.getenv() or pydantic-settings to access configuration
Wrap database operations in try-except blocks and use context managers or FastAPI dependencies for connection cleanup
Implement comprehensive error handling with proper logging in all functions
Version APIs using /api/v1/ prefix for stability
Use JSON-RPC 2.0 protocol for all MCP communication
Follow the single-flow workflow: YouTube link → context extraction → agent dispatch → outputs
Use context managers for resource management in Python code
Implement comprehensive input validation and sanitize outputs for security
Use parameterized queries and SQLAlchemy ORM to prevent SQL injection
Define API request/response models using Pydantic for FastAPI endpoints
Store the single unified workflow as the only workflow; never introduce alternate flows or manual triggers
Use SQLAlchemy with connection pooling for database connections and manage sessions with context managers
Provide sensible defaults for non-sensitive configuration in Python settings
Maintain backward compatibility and do not break existing API endpoints
Include comprehensive logging for debugging in MCP implementations

Files:

  • src/youtube_extension/backend/code_generator.py

⚙️ CodeRabbit configuration file

Python backend code. Check for type hints, proper exception handling, async context manager usage, and potential blocking calls in async functions. Flag any bare except clauses or missing timeout parameters on network calls. CRITICAL: Flag any file that contains placeholder/stub implementations — especially in unified_ai_sdk. Any class or function that says "TODO: Replace with production implementation" or returns mock/fake data must be flagged as a blocking issue. Flag any code generation output that reaches users without AST validation or syntax checking.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Maintain >80% code coverage for new features

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange

**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the <domain>.<entity>.<action> format.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*

📄 CodeRabbit inference engine (Custom checks)

**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require the copilot-rabbit label and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.

For Vercel-specific work, include https://vercel.com/docs/llms-full.txt in the AI assistant context set.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,pyw}

📄 CodeRabbit inference engine (AGENTS.md)

Write Python code to remain compatible with Linux and Windows where possible, including correct handling of asyncio event loops.

Files:

  • src/youtube_extension/backend/code_generator.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Format Python code with Black using an 88-character line length.
Sort Python imports with isort using the Black profile.
Run Ruff with E, W, F, I, B, C4, and UP rules; E501 is ignored.
Use mypy strict mode; untyped function definitions are disallowed.
Target Python 3.9 or newer.
Validate backend inputs with Pydantic and sanitize subprocess arguments.
Use Anthropic SDK features thinking={"type": "adaptive"} and output_config={"effort": "..."} with anthropic>=0.105.0; do not add TypeError fallbacks for these parameters.

Use the service container dependency-injection pattern in backend/containers/.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit secrets; store keys and credentials in gitignored .env files.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi}: Use Black formatting with an 88-character line length for Python code.
Use Ruff with rules E, W, F, I, B, C4, and UP for Python linting.
Use strict mypy type checking; do not define untyped functions.
Target Python 3.9 or newer.
Validate Python inputs with Pydantic.
Sanitize subprocess arguments before execution.
Do not add mock delays or fake data to production Python code; production runs in REAL_MODE_ONLY.
Keep secrets out of Python source code; load keys and credentials from environment variables instead.
Use absolute imports that resolve with PYTHONPATH=src in the Python backend.

Files:

  • src/youtube_extension/backend/code_generator.py
**/*.{py,pyi,ts,tsx}

📄 CodeRabbit inference engine (GEMINI.md)

**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following <domain>.<entity>.<action>, such as youtube.video.captured.
Make surgical, precise changes and do not delete working code without justification.

Files:

  • src/youtube_extension/backend/code_generator.py
🪛 ast-grep (0.45.0)
src/youtube_extension/backend/code_generator.py

[warning] 49-49: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, "w")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[info] 289-289: use jsonify instead of json.dumps for JSON output
Context: json.dumps(package_json, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🔍 Remote MCP GitHub Copilot

Relevant review context

  • Blocking concern: cancellation cleanup. PR #1251 adds cancellation points around mkdtemp and each batched write. Cancelling the awaiting coroutine does not stop the worker thread, and video_processing_service.py currently has no cleanup for the generated directory. This can leave partial or complete uvai_project_* directories orphaned.

  • The repository already uses a cancellation-safe pattern: create a task for asyncio.to_thread, shield it, wait for completion, then re-raise CancelledError.

  • Shared executor follow-up: asyncio.to_thread uses the shared default executor. Issue #1234 documents that the repository has 63 asyncio.to_thread and 31 run_in_executor(None, ...) call sites sharing that pool; this PR adds four more. This does not invalidate the event-loop fix, but stalled filesystem workers can still contribute to executor starvation.

  • The PR’s diff preserves ordered writes and explicitly tests byte content, ordering/error behavior, thread identity, and one-hop batching. The current review discussion contains no formal review threads, but CodeRabbit identified the cancellation issue above.

Comment thread src/youtube_extension/backend/code_generator.py Outdated
Comment thread src/youtube_extension/backend/code_generator.py

Copy link
Copy Markdown
Owner Author

Context for reviewers: the blocking cancellation-leak finding CodeRabbit raised on this PR (a request cancelled mid-scaffold could leave an orphan uvai_project_* directory once the writes moved off the event loop) is not fixed on this branch — head 189d814 still has it. Vercel VADE independently flagged the same defect.

It is fixed in #1252, which builds on this exact commit (189d814) and adds:

  • _run_offloop (shield-and-wait, mirroring _run_sync_rpc) so cancellation waits for the write worker to settle before cleanup runs;
  • _make_scaffold_dir, closing the mkdtemp cancellation window;
  • scaffold cleanup on cancel/failure in generate_project;
  • prove-fail-verified regression tests for both leak windows (87 focused + 520 suite tests pass), linked to canonical issue Cancelled video-to-software request leaks the scaffold temp directory #1253.

#1252 is a strict superset of this PR, so merging this branch on its own would ship the perf change with the leak still present. Recommend reviewing/merging #1252 instead (and closing this once its commit lands), or landing this first and rebasing #1252 down to just the cancellation fix — reviewer's call. Both PRs are otherwise green except the shared agent-completion/truth-gate provenance gate, which needs human sign-off.

I can't push the fix here directly — this PR is on perf/scaffolding-writes-off-loop, and my designated working branch is claude/determined-maxwell-9t5kou (the branch behind #1252).


Generated by Claude Code

Moving the project writes onto worker threads added suspension points the
inline sequence did not have, so a cancelled request could unwind while a
worker thread was still writing into a directory no caller would ever
receive.

Drain the scaffolding task through a shield loop before propagating the
cancellation, then remove the directory that generate_project created.
Mirrors _run_sync_rpc in services/cloud/cloud_tasks_queue.py.

CancelledError is never suppressed. The pre-existing generic-exception
leak is tracked separately in #1254.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
generate_project is the only holder of the scaffold path until it
returns, so an exception on the way out left a directory that no caller
could name, let alone remove. Wrap the dispatch-to-return region in
try/except Exception and discard the directory before re-raising.

CancelledError derives from BaseException, so the explicit cancellation
path is unaffected and cannot double-remove. _discard_project_dir
suppresses everything and drains its worker, so cleanup can never mask
the original exception.

Also close a coverage gap the Copilot reviewer found: the off-loop test
recorded only open(), so moving Path.mkdir back onto the loop still
passed. It now hooks Path.mkdir as well, and a dedicated test covers the
one generator that creates subdirectories.

Resolves #1253, #1254.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔍 PR Validation

⚠️ Large PR detected (645 lines changed)

@groupthinking

Copy link
Copy Markdown
Owner Author

Correction to my earlier comment above (5159578559), and status of the overlapping work.

That comment claimed the orphan-directory leak "is not fixed on this branch — head 189d814 still has it." That was wrong when posted: the cancellation fix had already landed as 42ed56f57. The head is now d178e210a, which additionally covers the failed-generation path. I'm correcting it here rather than editing it so the record stays intact.

Overlap with #1252. #1252 addresses the same two defects on the same file. Having read its diff in full, it converges independently on the same design — a shield-and-drain helper derived from the existing _run_sync_rpc pattern in services/cloud/cloud_tasks_queue.py. That agreement is worth stating plainly, because two passes arriving at the same structure is decent evidence the structure is right.

The differences:

Aspect #1251 (this PR) #1252
Drain helper _run_to_completion(coro), applied once at the branch-selection boundary _run_offloop(func, *args), applied at each to_thread site
mkdtemp window inline drain plus discard dedicated _make_scaffold_dir(prefix)
Cleanup totality _discard_project_dir catches Exception _safe_rmtree relies on rmtree(ignore_errors=True)
Off-loop conversion of the 28 scaffolding writes yes no

The last two rows are why I'm proposing this branch carry the work. ignore_errors=True is not total — a path containing a NUL byte raises ValueError from rmtree before the error-handling callback is ever consulted, which I verified directly rather than inferring from the docs. In #1252 that lands on the failure path as await _run_offloop(_safe_rmtree, path) inside exception handling, where a raise would mask the original exception. _discard_project_dir suppresses everything and cannot; test_original_exception_is_not_masked_by_cleanup asserts exactly that by making both the generation and shutil.rmtree raise, and requiring the original error to surface.

I want to be fair about the direction of that critique: #1252 was ahead of this branch on the generic-exception path, and that gap was real. It is now closed here in d178e210a, with the safety of doing so checked first — no test asserts on project_path existence after a failure, and video_processing_service.py reads the path only on the success path (L390), so nothing downstream can observe the removal.

Closing #1252, #1253 and #1254 as superseded by this PR. Reopening #1252 is the right move if this branch stalls — the analysis in it is sound, and I'd rather it land than nothing land.

@groupthinking

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@linear-code this one is still awaiting your read — flagging it since you've reviewed the previous four in this series.

Both blocking findings are addressed at d178e210a:

  1. Cancellation safety (yours). Valid, and the window was genuinely introduced by this PR — the pristine leaf generators contain zero awaits, so the pre-PR path was uninterruptible. Fixed by mirroring _run_sync_rpc; three-way probe evidence in the reply on that thread.
  2. Path.mkdir not instrumented (Copilot's). Also valid. The old test file scored 85 passed against an injected on-loop mkdir regression; the new one catches it 2 failed, 87 passed.

The one finding I have declined is the placeholder-FastAPI template at L470 — pre-existing content this PR does not touch, tracked as #1257. Byte-for-byte output equality is proven by differential execution, which is also the evidence that the template is unmodified.

Worth attacking if you want a target: the failed-generation cleanup now removes a directory the caller never sees. I checked that no test asserts on project_path after a failure and that video_processing_service.py reads it only on the success path (L390) — but that is an argument from current callers, not a structural guarantee.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Current PR changes ==='
git diff --name-status origin/main...HEAD
git diff --find-renames --unified=100 origin/main...HEAD -- \
  'src/youtube_extension/backend/code_generator.py' \
  '*test*code_generator*.py' \
  'src/youtube_extension/**/video_processing_service.py'

echo '=== Generator outline ==='
ast-grep outline src/youtube_extension/backend/code_generator.py \
  --items all --type class,function --view expanded

echo '=== Cancellation, cleanup, thread, and generation references ==='
rg -n -C 12 \
  '(_run_to_completion|_discard_project_dir|_make_scaffold_dir|generate_project|_apply_write_plan|asyncio\.to_thread|CancelledError|rmtree|mkdtemp)' \
  src/youtube_extension/backend/code_generator.py \
  src/youtube_extension \
  tests \
  -g '*.py'

echo '=== Direct generator call sites and use of returned project paths ==='
rg -n -C 16 \
  '(await\s+.*generate_project\s*\(|generate_project\s*\(|project_path|output_dir|rmtree\()' \
  src tests \
  -g '*.py'

Length of output: 201305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Changed paths ==='
git diff --name-status origin/main...HEAD

echo '=== Exact generator diff ==='
git diff --unified=35 origin/main...HEAD -- src/youtube_extension/backend/code_generator.py

echo '=== Current generator control flow ==='
nl -ba src/youtube_extension/backend/code_generator.py | sed -n '1,115p;190,290p;330,490p'

echo '=== Locate focused tests ==='
fd -t f -i 'code*generator' tests src | sort

echo '=== Test definitions that exercise scaffold cleanup and cancellation ==='
rg -n -C 10 \
  '(run_to_completion|discard_project_dir|generate_project|CancelledError|cancel\(|rmtree|mkdtemp|write_plan|original_exception|failure)' \
  tests -g '*.py' -g '*code*generator*'

echo '=== Python-version and lint configuration ==='
rg -n -C 3 '(target-version|python_requires|requires-python|python_version)' \
  pyproject.toml setup.cfg setup.py tox.ini .python-version 2>/dev/null || true

Length of output: 24523


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Focused test names ==='
grep -nE \
  '^(async )?def test_.*(cancel|cleanup|failure|exception|thread|write_plan|mkdir|byte|order)|^(class )?Test' \
  tests/unit/test_code_generator.py || true

echo '=== Focused test bodies and imports ==='
grep -n -A 55 -B 10 -E \
  '(test_.*(cancel|cleanup|failure|exception|thread|write_plan|mkdir|byte|order)|_run_to_completion|_discard_project_dir|generate_project)' \
  tests/unit/test_code_generator.py || true

echo '=== Generator helpers and generate_project ==='
sed -n '1,115p;197,288p' src/youtube_extension/backend/code_generator.py

echo '=== Runtime version declarations ==='
for f in pyproject.toml setup.cfg setup.py tox.ini .python-version; do
  if [ -f "$f" ]; then
    echo "--- $f"
    grep -nEi -C 3 'target-version|python_requires|requires-python|python_version' "$f" || true
  fi
done

echo '=== Direct ProjectCodeGenerator calls only ==='
grep -R -n -E -C 8 \
  '(get_code_generator\(\)|ProjectCodeGenerator\(|code_generator\.generate_project\()' \
  src tests --include='*.py' || true

Length of output: 14958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

test_path="tests/unit/test_code_generator.py"

echo '=== Confirm committed test path ==='
git ls-tree -r --name-only HEAD | grep -Fx "$test_path"

echo '=== Relevant test definitions from HEAD ==='
git show "HEAD:$test_path" | grep -nE \
  '^(async )?def test_.*(cancel|cleanup|failure|exception|thread|write_plan|mkdir|byte|order)|^(class )?Test' || true

echo '=== Relevant test bodies from HEAD ==='
git show "HEAD:$test_path" | grep -n -A 70 -B 12 -E \
  '(test_.*(cancel|cleanup|failure|exception|thread|write_plan|mkdir|byte|order)|_run_to_completion|_discard_project_dir|generate_project)' || true

echo '=== Test-file diff ==='
git diff --unified=25 origin/main...HEAD -- "$test_path"

Length of output: 59215


@groupthinking No blocking finding from this review.

The failed-generation cleanup is correct for the current ownership model.

  • generate_project owns project_path until it returns the result.
  • On a generation failure, no caller receives project_path.
  • video_processing_service.py reads generation_result["project_path"] only after generate_project succeeds.
  • _run_to_completion drains the write worker before _discard_project_dir removes the directory. Cleanup cannot race an active writer.
  • _discard_project_dir suppresses cleanup failures. It does not mask the original generation exception or CancelledError.

The committed tests cover the required cases:

  • Cancellation during the write hop removes the directory.
  • Cancellation waits for the worker thread to finish.
  • Cancellation propagates as CancelledError.
  • A write failure removes the directory.
  • A cleanup failure preserves the original write exception.
  • A successful generation retains its directory.

I inspected the committed tests through Git because the sandbox worktree omits tests/. I did not execute the test suite.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@groupthinking
groupthinking merged commit 189c2d2 into main Aug 2, 2026
47 of 48 checks passed
@groupthinking
groupthinking deleted the perf/scaffolding-writes-off-loop branch August 2, 2026 18:11
@linear-code

linear-code Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

GRV-255

groupthinking added a commit that referenced this pull request Aug 3, 2026
* perf: scan processed-video cache off the event loop

GET /api/v2/videos/list is declared async but its whole body was blocking
filesystem work: a stat, a directory glob, and one open()+json.load() per
cached video, with no bound on entry count. The handler never awaited, so
the loop was stalled for the full scan and no other request could be served.

Extract the scan into a module-level _collect_processed_videos_sync() helper
and dispatch it with asyncio.to_thread(), matching the pattern used in #1194,
#1196, #1228, #1233, #1240, #1245 and #1251. The scan logic is moved verbatim,
so the response payload, newest-first ordering, per-entry corrupt-file skip and
empty-list fallbacks are unchanged.

Measured on a 2,000-entry cache, peak event-loop stall drops from ~193 ms to
~2 ms. Scan wall time is unchanged: this is a latency and fairness fix, not a
throughput one.

Closes #1287

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* style: Black-format _collect_processed_videos_sync helper

Normalize string quotes to double and wrap the dict-append and sort
call in _collect_processed_videos_sync to satisfy the 88-char limit,
addressing the CodeRabbit review on #1288. Behaviour-preserving:
diff is confined to the new helper and the reformat is Black's own
AST-equivalent output (verified with --target-version py311).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rG7vUAn6z9tXoEuA3dAqz

* test: prove per-file cache read is off the event loop

The thread-recording cache directory previously asserted only that
exists()/glob() ran off-loop, and relied on the helper extraction to
imply the per-entry open()/json.load() moved with them.

glob() now yields path-like proxies whose __fspath__ records the calling
thread. Because open() resolves a non-str argument through __fspath__,
this captures the thread at the exact moment each blocking read starts,
so the read is proven off-loop rather than inferred.

Verified by reverting only the handler call site to the inline form: the
new assertion fails independently with "blocking cache entry read ran on
the event loop thread".

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: project scaffolding disk writes block the event loop in ProjectCodeGenerator

2 participants