Skip to content

perf(video): run enhanced-result persistence off the event loop - #1203

Merged
groupthinking merged 2 commits into
mainfrom
perf/video-save-offloop
Aug 1, 2026
Merged

perf(video): run enhanced-result persistence off the event loop#1203
groupthinking merged 2 commits into
mainfrom
perf/video-save-offloop

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Head: fd5f49ede

Canonical issue

Closes #1202

Outcome

EnhancedVideoProcessor._save_enhanced_result was async def with no await in its body — mkdir, both file writes, and json.dump all ran on the event loop. It is reached in production from process_video (line 189) via service_container.py:253video_processor_factory.get_video_processor().

This change does This change does NOT
Stop the save from stalling the event loop Make saving faster in wall-clock terms
Pay json.dumps off-loop as well as the writes Make concurrent saves parallel
Cost one thread hop for the whole save, not one per syscall Add batching, queuing or coalescing
Make each write atomic, so a reader never sees a partial file Change the on-disk layout or filenames
Clean up the temp file when a write fails Change any public signature

json.dump(obj, f) writes to the file object incrementally, so a large metadata dict became many small write() syscalls. It is now a single json.dumps + one write, performed in the worker thread.

Scope

  • src/youtube_extension/backend/enhanced_video_processor.py — two new static helpers (_atomic_write_text, _write_result_files); _save_enhanced_result now issues one asyncio.to_thread dispatch. Added contextlib and threading imports.
  • tests/unit/test_enhanced_video_processor.py — one new test class, 6 tests. No existing test was modified.

Explicitly out of scope: the other json.loads sites in this module (lines 476/484/547/553/704) parse small model responses on the loop; and enhanced_video_processor.py:329's open() is already inside a nested def _transcribe() dispatched via asyncio.to_thread — correctly off-loop, deliberately untouched.

Design notes

Why one dispatch, not three. to_thread per syscall would cost three thread hops. _write_result_files groups mkdir + both writes so the save costs one.

Why the writes had to become atomic in the same change. Review on #1194 established the rule: moving a write off the event loop removes the loop's implicit serialisation. open(path,'w') truncates first, so once two saves can genuinely overlap, a reader can observe an empty or short file. _atomic_write_text writes "{path}.{pid}.{tid}.tmp" and os.replaces it into place — atomic on POSIX — and unlinks the temp file on any failure. The pid/tid suffix means two writers cannot collide on the temp name.

No caller teardown hazard. Unlike #1190, _save_enhanced_result holds no instance resource across the await; it opens and closes files entirely inside the worker thread, so there is no finally: close() race in any caller.

Risk

Low. The 98 pre-existing tests in this file pass unmodified, including the three that already exercise _save_enhanced_result by patching builtins.open and pathlib.Path.mkdir. The failure contract is unchanged: any exception is still logged and still returns "".

The one behavioural difference a caller could observe: the target file now appears atomically rather than growing incrementally. Nothing in the repo tails these files while they are written.

Verification

104 passed in 3.30s

= 98 pre-existing (zero edits) + 6 new in TestSaveDoesNotBlockEventLoop.

Non-vacuity — both dimensions mutated simultaneously (atomic write → plain truncating open, and to_thread → direct on-loop call):

FAILED ...::test_save_does_not_stall_the_event_loop
FAILED ...::test_json_serialisation_also_runs_off_the_loop
FAILED ...::test_temp_file_is_cleaned_up_when_the_write_fails
FAILED ...::test_reader_never_observes_a_partially_written_file
4 failed, 100 passed

Restoring the source → 104 passed. One failure per dimension, so no new test duplicates another.

Honest split: 4 of the 6 new tests discriminate; 2 are guards (test_save_writes_readable_markdown_and_metadata, test_no_temp_files_remain_after_a_successful_save) that pass under both variants and exist to pin content correctness and cleanliness.

ruff: exact parity with origin/main — the same 6 pre-existing findings (1 × E402, 5 × F841) before and after; zero introduced.

Review round 2 — pair atomicity (fd5f49ede)

@Copilot correctly found that the two writes were individually atomic but not atomic as a pair: with one-second filename precision two saves of the same video in the same second target the same paths and can interleave, pairing one save's markdown with another's metadata. Both writes are now held together under a module-level threading.Lock (_RESULT_WRITE_LOCK), taken inside the worker thread so the event loop is still never blocked.

106 passed  (104 + 2 new in TestConcurrentSavesWriteMatchedPairs)

Non-vacuity — removing the lock:

1 failed, 105 passed   # only test_concurrent_saves_never_mix_markdown_with_foreign_metadata

The single-writer guard test passes under both variants, as a guard should.

Production evidence

enhanced_video_processor and video_processor_factory are both in the transitive import closure of the deployed entrypoints (youtube_extension.main:app, root Dockerfile:93). Call chain: service_container.py:253video_processor_factory.py:63-65EnhancedVideoProcessor()process_video (L140) → _save_enhanced_result (L189).

Agent handoff

Known coverage gap, named deliberately: the heartbeat tests prove the loop keeps running for _save_enhanced_result specifically. They do not prove anything about the json.loads sites elsewhere in this module, which remain on-loop by design.

`EnhancedVideoProcessor._save_enhanced_result` is `async def`, but every
byte of work inside it was synchronous and executed on the event loop:

  1. `save_dir.mkdir(parents=True, exist_ok=True)` - directory syscalls
  2. `open(filepath,'w') / f.write(markdown)`      - the full analysis doc
  3. `json.dump(metadata, f, indent=2)`            - serialises AND writes
     incrementally, so a large metadata dict became many small `write()`
     syscalls rather than one

It is called from `process_video` (line 189), which is reached in
production via `service_container.py:253` -> `video_processor_factory
.get_video_processor()` -> `EnhancedVideoProcessor()`. While a video's
results were being saved, every other in-flight request on that worker
was stalled.

This change hands the whole group - mkdir, both writes, and the
`json.dumps` - to a worker thread in a *single* `asyncio.to_thread`
dispatch, so the save costs one thread hop rather than one per syscall,
and the serialisation cost is paid off-loop too.

Writes are also made atomic. `open(path,'w')` truncates before it
writes, so a crash or a concurrent reader can leave/observe a
half-written analysis on disk. `_atomic_write_text` writes a sibling
temp file and `os.replace`s it into place; the temp name carries the pid
and thread id so two writers cannot collide, and it is unlinked if the
write fails. This pre-empts the lost-serialisation class of bug that
review caught on #1194: moving a write off-loop removes the event
loop's implicit serialisation, so the write must become atomic.

Honest framing: this does NOT make saving faster. It stops saving from
stalling the event loop, and it stops partial files from being visible.

Tests: 104 passed = 98 pre-existing (zero edits) + 6 new.
Non-vacuity proven by reverting both dimensions simultaneously
(atomic write -> plain open, to_thread -> direct call): exactly 4
targeted failures / 100 passed, restored -> 104. 4 of the 6 new tests
discriminate; 2 are guards.
ruff: exact parity with origin/main (identical 6 pre-existing findings).

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

vercel Bot commented Aug 1, 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 Canceled Canceled Aug 1, 2026 10:58pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a9f0c390-5447-4009-8291-8c5b2c82544c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@coderabbitai review

Please review head 4d8d250e5. Specific challenges I would like attacked rather than a general pass:

  1. Atomicity claim. _atomic_write_text writes "{path}.{pid}.{tid}.tmp" then os.replace. Is the pid+thread-id suffix actually sufficient to prevent temp-name collisions, given _save_enhanced_result can be entered concurrently and to_thread reuses threads from a bounded pool? Is there a sequence where two saves for the same video_id within the same one-second timestamp window still corrupt each other?

  2. Cancellation. await asyncio.to_thread(...) is not cancellation-safe — this exact issue was a real regression on perf(cloud-tasks): move blocking gRPC calls off the event loop #1190. I claim it is harmless here because _save_enhanced_result holds no instance resource across the await and no caller does finally: X.close() around process_video. Please verify that claim independently rather than take it.

  3. except BaseException in the cleanup path. I re-raise after unlinking. Is contextlib.suppress(OSError) the right width there, or does it mask a real failure signal? Note I deliberately rejected a suppress(BaseException) hardening on perf(cloud-tasks): move blocking gRPC calls off the event loop #1190 because I could not write a test that discriminated it.

  4. Test honesty. I claim 4 of 6 new tests discriminate and 2 are guards, proven by a simultaneous two-dimension mutation (4 failures / 100 passed). Is test_reader_never_observes_a_partially_written_file genuinely deterministic, or is it timing-dependent and liable to flake in CI? It relies on a reader task sampling a 400KB file while a writer loops 12 times through to_thread.

  5. Did I miss a blocker? _save_enhanced_result still constructs Path objects and formats a timestamp on the loop. Are either of those meaningful, or is datetime.now().strftime genuinely negligible next to what I moved?

Context: this is the 9th in a series of event-loop-blocking fixes. Reviewers have caught real correctness bugs in #1186, #1188, #1190 and #1194 — including two regressions I introduced myself — so please be adversarial.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== changed paths and diff ==='
git diff --find-renames origin/main...HEAD -- \
  src/youtube_extension/backend/enhanced_video_processor.py \
  tests/unit/test_enhanced_video_processor.py

echo '=== enhanced processor outline ==='
ast-grep outline src/youtube_extension/backend/enhanced_video_processor.py \
  --items all --view expanded

echo '=== focused enhanced processor source ==='
nl -ba src/youtube_extension/backend/enhanced_video_processor.py | sed -n '130,230p'
nl -ba src/youtube_extension/backend/enhanced_video_processor.py | sed -n '1,80p'

echo '=== save and process callers/usages ==='
rg -n -C 5 --glob '*.py' \
  '(?:\._save_enhanced_result\s*\(|\bprocess_video\s*\(|get_video_processor\s*\()' \
  src tests

echo '=== new test class and nearby fixtures/helpers ==='
rg -n -C 8 --glob '*.py' \
  '(?:class TestSaveDoesNotBlockEventLoop\b|test_reader_never_observes_a_partially_written_file\b|test_save_does_not_stall_the_event_loop\b|test_json_serialisation_also_runs_off_the_loop\b)' \
  tests/unit/test_enhanced_video_processor.py

Length of output: 14841


The first inspection reached the diff, but the environment lacks nl. I will use sed and rg to inspect the missing ranges and cancellation boundaries.

You are interacting with an AI system.

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 enhanced-result serialization and atomic file persistence off the event loop.

Changes:

  • Dispatches directory creation, JSON serialization, and writes through one worker thread.
  • Adds atomic temporary-file replacement and cleanup.
  • Adds six persistence and responsiveness tests.

Reviewed changes

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

File Description
src/youtube_extension/backend/enhanced_video_processor.py Adds off-loop, atomic result persistence.
tests/unit/test_enhanced_video_processor.py Tests responsiveness, content, cleanup, and atomic reads.

Comment on lines +769 to +772
EnhancedVideoProcessor._atomic_write_text(filepath, markdown)
EnhancedVideoProcessor._atomic_write_text(
metadata_file, json.dumps(metadata, indent=2, default=str)
)
Review finding (@Copilot, enhanced_video_processor.py:772): the two writes
in `_write_result_files` are individually atomic but were not held together.
Result filenames carry only one-second precision, so two saves of the same
video inside the same second resolve to the same two paths and can interleave
as A-markdown, B-markdown, B-metadata, A-metadata -- leaving B's markdown
paired with A's metadata.

Hold both writes under a module-level `_RESULT_WRITE_LOCK`. The lock is a
`threading.Lock` because the writes execute in the worker thread, and it is
held only across two file writes, so it never blocks the event loop. Saves
are a once-per-video-completion operation, so global serialisation of the
write pair costs nothing measurable.

- 106 tests pass (104 + 2 new in `TestConcurrentSavesWriteMatchedPairs`)
- Non-vacuity: removing the lock yields exactly 1 targeted failure /
  105 passed; the single-writer guard test still passes

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

Copy link
Copy Markdown
Owner Author

@copilotvalid finding, accepted and fixed in fd5f49ede.

You were right that atomicity per-file is not the same as atomicity of the pair. With one-second filename precision, two saves of the same video in the same second resolve to the same two paths, and the interleaving you described (A-markdown, B-markdown, B-metadata, A-metadata) leaves B's markdown next to A's metadata.

Fix: both writes are now held together under a module-level _RESULT_WRITE_LOCK.

  • It is a threading.Lock, not an asyncio.Lock, because the writes execute inside the to_thread worker — an asyncio lock would not exclude a second worker thread.
  • It is held across only the two file writes, and only inside a worker thread, so it never blocks the event loop — the point of the PR is preserved.
  • I chose a single global lock over a path-keyed map: saves are a once-per-video-completion operation, so global serialisation of two file writes costs nothing measurable, and a path-keyed dict would grow unboundedly.

Proof it discriminates — new test test_concurrent_saves_never_mix_markdown_with_foreign_metadata deterministically drives exactly your interleaving (writer B waits for A to land its markdown; A stalls between its own two writes):

Code Result
with _RESULT_WRITE_LOCK 106 passed
lock removed 1 failed, 105 passed — only the pairing test

A second test (test_pair_write_still_produces_both_files) guards that serialising the pair did not change the single-writer result; it passes under both variants, as a guard should.

Thank you — this is the second correctness bug review has caught in my to_thread refactors here (after the lost-serialisation race in #1194). The pattern is consistent: moving work off the loop removes the loop's implicit serialisation, and I need to ask what was serialising a shared resource before every such change.

@groupthinking groupthinking left a comment

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.

Independent review of 4d8d250e5 — verified against the 5 challenges

Re-ran the branch locally (PYTHONPATH=src): 104 passed in 3.08s, matching the claim. I attacked each numbered point rather than restating the PR body.

1. Atomicity / temp-name collision — holds per file; one honest gap on the pair.

  • Temp-name collision is genuinely prevented: to_thread runs each _write_result_files on a distinct pool thread, and a thread executes only one write at a time, so two concurrent saves always have distinct threading.get_ident() → distinct .{pid}.{tid}.tmp names. No collision even for the same video_id/timestamp.
  • Each individual file is atomic (os.replace), so no reader ever observes a partial .md or .json. Solid.
  • The gap: the .md and the .json are two independent os.replaces, not one atomic unit. Two saves for the same video_id that land in the same one-second timestamp bucket and truly overlap can interleave as A.md → B.md → B.json → A.json, leaving markdown from generation B beside metadata from generation A — each file internally complete, but the pair mismatched. This is newly possible because the previous on-loop version was implicitly serialized by the event loop; moving to to_thread removes that serialization (the same reasoning that forced the atomic-write change here). It sits inside the stated non-goal ("does NOT make concurrent saves parallel") and is low-severity (needs same video, same second, real overlap), so not a blocker — but it's the one property this change regresses versus the serialized original. Worth a one-line comment in _save_enhanced_result naming it as accepted, or bumping timestamp granularity if you want to close it.

2. Cancellation — claim verified. The surrounding handler is except Exception as e (line 803). asyncio.CancelledError is a BaseException, not Exception, so a cancelled await asyncio.to_thread(...) is not swallowed into a "" return — it propagates. The worker thread finishes its write (a completed, atomic save the caller no longer awaits — benign), and no instance resource is held across the await, so there is no finally: close() hazard like #1190.

3. except BaseException + suppress(OSError) — right width. Catching BaseException in the cleanup unlinks the temp file even on KeyboardInterrupt/CancelledError mid-write, and re-raising preserves the original signal. unlink can only raise OSError-family, so suppress(OSError) is exactly wide enough to stop a cleanup failure from masking the real error, and no wider. No change needed.

4. Test determinism — not flaky. Ran the three timing-sensitive tests (..._partially_written_file, ..._does_not_stall_the_event_loop, ..._json_serialisation_also_runs_off_the_loop) 25× in a loop: 0 failures. The partial-file assertion is not timing-dependent — os.replace guarantees every observation is a complete generation regardless of scheduling; the only timing-sensitive part (reader sampled ≥ once) is asserted separately with wide headroom (sleep(0) reader vs. 12 × to_thread writer). The heartbeat tests carry ~20× margin (0.10s block vs. 0.005s ticks). CI-safe.

5. Residual on-loop work — negligible. datetime.now().strftime and the Path joins are O(1) microsecond-scale string ops; they don't scale with payload size the way json.dumps + the writes do. Leaving them on the loop is correct.

Net: no blocking findings. Core CI is green (test, lint-python, build, bandit, guards). The red checks are the repo-wide governance/scan gates (truth-gate, gitleaks working-tree, trivy, Security Scan, validate, dependency-review, label, Agent completion enforcement) and a canceled Vercel deploy — none implicate this diff. The only actionable item is the optional cross-file pair-atomicity note in (1).

(Review posted from the author account, so this registers as a comment rather than a formal approval.)


Generated by Claude Code

@groupthinking
groupthinking merged commit af77fb4 into main Aug 1, 2026
23 of 38 checks passed
@groupthinking
groupthinking deleted the perf/video-save-offloop branch August 1, 2026 23:10
@linear-code

linear-code Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

GRV-232

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

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

Workflow evidence

groupthinking added a commit that referenced this pull request Aug 1, 2026
Review round 4, finding 2 (CodeRabbit, Stability & Availability, Major).

This PR moved 14 blocking boto3 calls onto the *shared* default asyncio
executor via asyncio.to_thread. botocore's defaults leave a request
effectively unbounded, so a stalled AWS call would now pin one of that
pool's limited worker threads indefinitely and starve every other
to_thread user in the process -- including the metrics persistence
(#1194), sqlite access (#1196) and result writes (#1203) already merged
onto that same pool. _wait_for_job_completion can issue up to 120 such
calls per job, so the exposure is real rather than theoretical.

Both the Rekognition and S3 clients are now constructed with an explicit
botocore Config carrying connect_timeout, read_timeout and a bounded
standard-mode retry policy. Values are overridable via
AWS_REKOGNITION_CONNECT_TIMEOUT / _READ_TIMEOUT / _MAX_ATTEMPTS and are
validated with math.isfinite, raising rather than clamping so that
'inf', 'nan', '0' and negatives are rejected outright.

Parsing happens before initialize()'s try block: that method ends in a
catch-all `except Exception -> CloudAIError`, which would otherwise bury
a precise ConfigurationError message behind a generic init failure.

Tests: 127 pass (105 pre-existing + 22 new). Non-vacuity proven by
mutating both dimensions simultaneously -- making the env helpers ignore
the environment and dropping `config=` from both client constructions
yields exactly 18 targeted failures / 109 passed, matching the predicted
count (1 client-config + 1 override + 12 timeout rejections + 4
max-attempts rejections). ruff parity with origin/main unchanged (8 = 8).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
groupthinking added a commit that referenced this pull request Aug 1, 2026
…1205)

* perf(cloud-ai): run AWS Rekognition boto3 calls off the event loop

boto3 is a synchronous SDK. Every Rekognition call in AWSRekognition was
issued directly inside an `async def`, so each one blocked the event loop
for a full network round-trip. `_wait_for_job_completion` is the worst
case: it polls every 5s for up to 600s, so a single video analysis could
stall the loop up to 120 times.

All 14 boto3 calls now dispatch via `await asyncio.to_thread(...)`, and
the local-image read in `_prepare_image_input` goes through a new
module-level `_read_file_bytes` helper on the same path.

- 89 pre-existing tests pass with zero edits
- 6 new heartbeat tests (`TestRekognitionDoesNotBlockEventLoop`); 5 of the
  6 discriminate, proven by reverting both dimensions simultaneously
  (5 targeted failures / 90 passed)
- ruff: exact parity with origin/main (8 pre-existing findings, 0 added)

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

* test(rekognition): assert the local read leaves the loop thread, not elapsed ticks

The heartbeat form of this one test failed on CI. Unlike the four boto3 tests,
which drive a controllable 0.12s mock, the local file read is a few
microseconds of real work, so "did the loop tick while it ran" is a
load-sensitive proxy rather than a property.

Assert the property directly instead: record `threading.get_ident()` inside
`_read_file_bytes` and require it to differ from the thread running the event
loop. That is exactly what "dispatched off the loop" means, needs no sleeps,
and cannot flake under runner contention.

- 95 tests pass (89 pre-existing, unmodified, + 6 new)
- Non-vacuity: calling `_read_file_bytes` directly instead of via
  `asyncio.to_thread` yields exactly 1 targeted failure / 94 passed
- Suite runtime for the file drops to 0.85s (the 0.12s sleep is gone)

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

* test(rekognition): cover every blocking SDK operation

* test(rekognition): make off-loop read test robust to module eviction

* perf(rekognition): bound AWS client requests with botocore timeouts

Review round 4, finding 2 (CodeRabbit, Stability & Availability, Major).

This PR moved 14 blocking boto3 calls onto the *shared* default asyncio
executor via asyncio.to_thread. botocore's defaults leave a request
effectively unbounded, so a stalled AWS call would now pin one of that
pool's limited worker threads indefinitely and starve every other
to_thread user in the process -- including the metrics persistence
(#1194), sqlite access (#1196) and result writes (#1203) already merged
onto that same pool. _wait_for_job_completion can issue up to 120 such
calls per job, so the exposure is real rather than theoretical.

Both the Rekognition and S3 clients are now constructed with an explicit
botocore Config carrying connect_timeout, read_timeout and a bounded
standard-mode retry policy. Values are overridable via
AWS_REKOGNITION_CONNECT_TIMEOUT / _READ_TIMEOUT / _MAX_ATTEMPTS and are
validated with math.isfinite, raising rather than clamping so that
'inf', 'nan', '0' and negatives are rejected outright.

Parsing happens before initialize()'s try block: that method ends in a
catch-all `except Exception -> CloudAIError`, which would otherwise bury
a precise ConfigurationError message behind a generic init failure.

Tests: 127 pass (105 pre-existing + 22 new). Non-vacuity proven by
mutating both dimensions simultaneously -- making the env helpers ignore
the environment and dropping `config=` from both client constructions
yields exactly 18 targeted failures / 109 passed, matching the predicted
count (1 client-config + 1 override + 12 timeout rejections + 4
max-attempts rejections). ruff parity with origin/main unchanged (8 = 8).

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.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: _save_enhanced_result blocks the event loop with mkdir, two file writes and json.dump

2 participants