Context
fetchShotContentBlock in src/review/visual/capture.ts (line 130) fetches an already-captured screenshot URL for a vision-capable AI call:
export async function fetchShotContentBlock(url: string): Promise<AiContentBlock | undefined> {
try {
const response = await fetch(url);
...
This fetch(url) call has no AbortSignal.timeout(...) and no other bound on how long it can hang. Every other external fetch() call in src/review/** and src/signals/** is timeout-bounded — including the other fetch in this very same file, resolveShotUrl's redirect-follow at line 192:
const response = await fetch(currentUrl, { redirect: "manual", signal: AbortSignal.timeout(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) });
(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS = 8_000, declared at line 48 of the same file). Other siblings follow the same convention: src/review/enrichment-wire.ts (AbortSignal.timeout(5000) / timeoutMs), src/review/alerts.ts (AbortSignal.timeout(10_000)), src/review/visual/actions-fallback.ts (AbortSignal.timeout(DEFAULT_TIMEOUT_MS), 20s).
fetchShotContentBlock is called from the visual-findings AI wiring to attach a screenshot to a vision model call; if the shot URL's host (a self-host operator's own storage backend, an R2/S3 proxy, etc.) is slow or hangs mid-response, this call has no ceiling and can stall the review pipeline for that PR indefinitely (bounded only by the platform's own outer request limit, if any), unlike every comparable fetch in this file and its siblings.
Requirements
fetchShotContentBlock (src/review/visual/capture.ts) must pass an AbortSignal.timeout(...) to its fetch(url) call, matching the timeout convention already used by the file's own resolveShotUrl fetch (EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) — reuse that same constant rather than inventing a new one, since both fetches target the same class of already-captured-shot URLs.
- On a timeout,
fetchShotContentBlock must continue to return undefined (its existing catch block already degrades to undefined on any fetch/read failure — AbortSignal.timeout rejects with a TimeoutError, which that catch already covers) — no behavior change to the function's public contract beyond bounding the wait.
- This is a bug fix in
src/** only; no change to packages/loopover-engine/** is needed (this function has no engine-package twin).
Deliverables
Test Coverage Requirements
src/review/visual/capture.ts is under src/**, which Codecov's coverage.include measures — this PR must hit 99%+ patch coverage on every changed line and branch, including both the timeout-triggered catch path and the normal success path for fetchShotContentBlock.
Expected Outcome
fetchShotContentBlock's external fetch is bounded by the same timeout the rest of the file (and its review/* siblings) already use, so a slow or hung shot-storage backend can no longer stall this AI-vision wiring indefinitely.
Links & Resources
src/review/visual/capture.ts (fetchShotContentBlock, line 130; EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS, line 48; resolveShotUrl's existing timeout-bounded fetch, line 192)
Context
fetchShotContentBlockinsrc/review/visual/capture.ts(line 130) fetches an already-captured screenshot URL for a vision-capable AI call:This
fetch(url)call has noAbortSignal.timeout(...)and no other bound on how long it can hang. Every other externalfetch()call insrc/review/**andsrc/signals/**is timeout-bounded — including the other fetch in this very same file,resolveShotUrl's redirect-follow at line 192:(
EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS = 8_000, declared at line 48 of the same file). Other siblings follow the same convention:src/review/enrichment-wire.ts(AbortSignal.timeout(5000)/timeoutMs),src/review/alerts.ts(AbortSignal.timeout(10_000)),src/review/visual/actions-fallback.ts(AbortSignal.timeout(DEFAULT_TIMEOUT_MS), 20s).fetchShotContentBlockis called from the visual-findings AI wiring to attach a screenshot to a vision model call; if the shot URL's host (a self-host operator's own storage backend, an R2/S3 proxy, etc.) is slow or hangs mid-response, this call has no ceiling and can stall the review pipeline for that PR indefinitely (bounded only by the platform's own outer request limit, if any), unlike every comparable fetch in this file and its siblings.Requirements
fetchShotContentBlock(src/review/visual/capture.ts) must pass anAbortSignal.timeout(...)to itsfetch(url)call, matching the timeout convention already used by the file's ownresolveShotUrlfetch (EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) — reuse that same constant rather than inventing a new one, since both fetches target the same class of already-captured-shot URLs.fetchShotContentBlockmust continue to returnundefined(its existingcatchblock already degrades toundefinedon any fetch/read failure —AbortSignal.timeoutrejects with aTimeoutError, which thatcatchalready covers) — no behavior change to the function's public contract beyond bounding the wait.src/**only; no change topackages/loopover-engine/**is needed (this function has no engine-package twin).Deliverables
fetchShotContentBlockinsrc/review/visual/capture.tspassessignal: AbortSignal.timeout(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS)on itsfetch(url)call.fetchShotContentBlockto resolve toundefinedwithin the bounded timeout rather than hanging (e.g. by injecting a fetch mock that never resolves and asserting the promise settles once the timeout signal fires, using fake timers or an injected short timeout for the test).AiContentBlock).Test Coverage Requirements
src/review/visual/capture.tsis undersrc/**, which Codecov'scoverage.includemeasures — this PR must hit 99%+ patch coverage on every changed line and branch, including both the timeout-triggeredcatchpath and the normal success path forfetchShotContentBlock.Expected Outcome
fetchShotContentBlock's external fetch is bounded by the same timeout the rest of the file (and its review/* siblings) already use, so a slow or hung shot-storage backend can no longer stall this AI-vision wiring indefinitely.Links & Resources
src/review/visual/capture.ts(fetchShotContentBlock, line 130;EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS, line 48;resolveShotUrl's existing timeout-bounded fetch, line 192)