Context
fetchRepoFocusManifestFile in src/signals/focus-manifest-loader.ts (line 96-114) fetches a repo's .loopover.yml/.loopover.json manifest from GitHub's raw-content CDN, trying up to 4 candidate paths (MANIFEST_FILE_CANDIDATES, line 20) in sequence:
for (const path of MANIFEST_FILE_CANDIDATES) {
const url = `https://github.com/ghraw/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/HEAD/${path}`;
try {
const response = await fetch(url, { headers: { Accept: "application/json", "User-Agent": "loopover" } });
...
None of these fetch() calls (up to 4 sequential attempts per repo) carry an AbortSignal.timeout(...), unlike the established convention elsewhere in src/review/**/src/signals/**: src/review/enrichment-wire.ts uses AbortSignal.timeout(5000)/timeoutMs, src/review/alerts.ts uses AbortSignal.timeout(10_000), src/review/visual/actions-fallback.ts uses AbortSignal.timeout(DEFAULT_TIMEOUT_MS) (20s), and src/review/visual/capture.ts uses AbortSignal.timeout(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) (8s).
fetchRepoFocusManifestFile feeds loadRepoFocusManifestWithCachePolicy (the public entry point used by loadRepoFocusManifest/loadPublicRepoFocusManifest), which is on the cold-cache path for resolving a repo's gate/review policy on webhook and sweep processing — a slow or hanging github.com/ghraw response (or a slow candidate path in the loop before falling through to a working one) has no per-request ceiling here and can stall manifest resolution for that repo.
Requirements
fetchRepoFocusManifestFile (src/signals/focus-manifest-loader.ts) must pass an AbortSignal.timeout(...) to each candidate-path fetch(url, ...) call inside its loop.
- Introduce a named timeout constant local to this file (matching the existing per-file convention, e.g.
src/review/visual/capture.ts's EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) rather than a bare inline number.
- A timeout on one candidate path must continue to the next candidate exactly like today's existing
catch { // try the next candidate path } handles a network/HTTP failure — AbortSignal.timeout rejects with a TimeoutError, which is already caught by the existing broad catch block, so no new catch branch is needed, only the added signal.
- No change to the function's return contract (still resolves to
string | null).
Deliverables
Test Coverage Requirements
src/signals/focus-manifest-loader.ts is under src/** (measured by Codecov). This PR must hit 99%+ patch coverage on every changed line and branch, including the new timeout path exercised by the regression test above.
Expected Outcome
Every candidate-path fetch inside fetchRepoFocusManifestFile is bounded by an explicit timeout, consistent with every other external fetch in src/review/**/src/signals/**, so a slow or hanging github.com/ghraw response can no longer stall repo manifest resolution.
Links & Resources
src/signals/focus-manifest-loader.ts (fetchRepoFocusManifestFile, lines 96-114; MANIFEST_FILE_CANDIDATES, line 20)
- Sibling timeout conventions:
src/review/visual/capture.ts (EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS), src/review/alerts.ts, src/review/visual/actions-fallback.ts (DEFAULT_TIMEOUT_MS)
Context
fetchRepoFocusManifestFileinsrc/signals/focus-manifest-loader.ts(line 96-114) fetches a repo's.loopover.yml/.loopover.jsonmanifest from GitHub's raw-content CDN, trying up to 4 candidate paths (MANIFEST_FILE_CANDIDATES, line 20) in sequence:None of these
fetch()calls (up to 4 sequential attempts per repo) carry anAbortSignal.timeout(...), unlike the established convention elsewhere insrc/review/**/src/signals/**:src/review/enrichment-wire.tsusesAbortSignal.timeout(5000)/timeoutMs,src/review/alerts.tsusesAbortSignal.timeout(10_000),src/review/visual/actions-fallback.tsusesAbortSignal.timeout(DEFAULT_TIMEOUT_MS)(20s), andsrc/review/visual/capture.tsusesAbortSignal.timeout(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS)(8s).fetchRepoFocusManifestFilefeedsloadRepoFocusManifestWithCachePolicy(the public entry point used byloadRepoFocusManifest/loadPublicRepoFocusManifest), which is on the cold-cache path for resolving a repo's gate/review policy on webhook and sweep processing — a slow or hanginggithub.laiyagushi.com/ghrawresponse (or a slow candidate path in the loop before falling through to a working one) has no per-request ceiling here and can stall manifest resolution for that repo.Requirements
fetchRepoFocusManifestFile(src/signals/focus-manifest-loader.ts) must pass anAbortSignal.timeout(...)to each candidate-pathfetch(url, ...)call inside its loop.src/review/visual/capture.ts'sEXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS) rather than a bare inline number.catch { // try the next candidate path }handles a network/HTTP failure —AbortSignal.timeoutrejects with aTimeoutError, which is already caught by the existing broadcatchblock, so no new catch branch is needed, only the addedsignal.string | null).Deliverables
fetchRepoFocusManifestFileinsrc/signals/focus-manifest-loader.tspasses a boundedAbortSignal.timeout(...)on every candidate-path fetch.nullif it was the last candidate) within the bounded timeout, rather than hanging.Test Coverage Requirements
src/signals/focus-manifest-loader.tsis undersrc/**(measured by Codecov). This PR must hit 99%+ patch coverage on every changed line and branch, including the new timeout path exercised by the regression test above.Expected Outcome
Every candidate-path fetch inside
fetchRepoFocusManifestFileis bounded by an explicit timeout, consistent with every other external fetch insrc/review/**/src/signals/**, so a slow or hanginggithub.laiyagushi.com/ghrawresponse can no longer stall repo manifest resolution.Links & Resources
src/signals/focus-manifest-loader.ts(fetchRepoFocusManifestFile, lines 96-114;MANIFEST_FILE_CANDIDATES, line 20)src/review/visual/capture.ts(EXTERNAL_SCREENSHOT_FETCH_TIMEOUT_MS),src/review/alerts.ts,src/review/visual/actions-fallback.ts(DEFAULT_TIMEOUT_MS)