The wrapper gives up on a long lens at 30 minutes, and two loops and the forge requests have no limit of their own #56

Open
opened 2026-10-06 05:35:53 +00:00 by jercik · 0 comments
Owner

The wrapper gives up on a lens that is still working after 30 minutes, although the review service allows 60. Separately, two of its loops have no end and its forge requests have no timeout, so a misbehaving service or forge keeps a run going until the workflow's timeout-minutes cancels it.

The coverage window is half the service's lens limit

Stage 1 waits 30 minutes for coverage (src/review/constants.ts:33, src/wrapper/wait.ts:98-113). The review service lets a single lens run for up to 60 minutes. A lens that takes longer than 30 minutes to finish fails the check with did not finish coverage in time; click rerun (src/wrapper/remedies.ts:84-86), although nothing is wrong.

Observed in a run of a consumer repository, on a documentation change touching 33 files (430 additions, 737 deletions). The writing-quality lens ran 35 minutes (2,119 s) and completed with exit 0. The wrapper had given up 5 minutes earlier, and the check failed with the rerun message.

Raising stage 1 to the service's limit plus slack, say 65 minutes, changes the sums. Today one wait is at most 60 minutes (30 + 25 + 5, src/review/constants.ts:33-35). A rerun that re-triages waits a second time (src/wrapper/orchestrator.ts:104-120), so 120 minutes, which fits under the workflow's timeout-minutes: 130 (.forgejo/workflows/review.yml:77). With 65 minutes at stage 1 one wait is 95 minutes, and the same rerun could need 190 in the worst case the code allows. The other direction is to lower the service's lens limit to match. Either number can move, but they need to be set together and written down next to each other, together with the workflow's cap.

Three paths have no limit of their own

  • Forge requests have no timeout (src/forge/client.ts:37-48). The wrapper sets none, so the only bound is the runtime's: on Node 24 a request that gets no response headers fails after about 300 seconds. Service requests are limited to 120 s (dist/index.mjs:5749). Reproduced as d12-forge-hang: a forge that accepted the head read and never answered ended the run with fetch failed: Headers Timeout Error after 300.9 s, exit 1. A run makes many forge requests, since it lists every review and each review's comments twice.
  • The grace deadline resets without limit. waitForSettle sets a new grace deadline every time triage settles, inside a loop with no cycle limit (src/wrapper/wait.ts:117, src/wrapper/wait.ts:130). The reset is deliberate (#10 made the grace run from each settle). poll returns a result before it checks the deadline or sleeps (src/wrapper/wait.ts:28-31). A service whose triage state flips between settled and not settled on successive reads makes the loop run without sleeping and without end. Reproduced as d13-grace-flap: a stand-in service did that for 20 s without the wrapper exiting, at over 2,000 requests a second.
  • Claim paging stops only on a repeated cursor (src/review/claims-walk.ts:27-29). A service that returns a new cursor on every page is walked forever, and the walk runs inside a poll step that cannot check its deadline (src/review/claims-walk.ts:5-8). Reproduced as d14-endless-claims: a stand-in service produced more than 150,000 pages in 15 s with no end.

Even where each path is bounded, nothing limits the sum. Each window is bounded on its own, and the retry ladders and request timeouts add up on top of them.

What it should do instead

  • Give forge requests an AbortSignal.timeout, as the service client has.
  • Set the grace deadline once per wait, or count the regressions and stop after a few. Check the stage deadline on every pass of the loop and sleep a poll interval when the state flipped.
  • Give walkClaims a page limit or a deadline.
  • Pass one run-wide deadline through all of the above, so the job ends with an outcome line before the workflow cancels it.

Why it matters

When the workflow's cap fires, the run is cancelled with no outcome line and a red check that does not say why. While a loop spins, it sends thousands of requests a second to the service, so the security relevance is limited to availability of the service. None of the three has been seen in production. Each needs a service that flips its state or mints cursors without end, or a forge that accepts a connection and never replies.

🤖 Generated with Claude Code

The wrapper gives up on a lens that is still working after 30 minutes, although the review service allows 60. Separately, two of its loops have no end and its forge requests have no timeout, so a misbehaving service or forge keeps a run going until the workflow's `timeout-minutes` cancels it. ## The coverage window is half the service's lens limit Stage 1 waits 30 minutes for coverage ([`src/review/constants.ts:33`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/review/constants.ts#L33), [`src/wrapper/wait.ts:98-113`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/wait.ts#L98-L113)). The review service lets a single lens run for up to 60 minutes. A lens that takes longer than 30 minutes to finish fails the check with `did not finish coverage in time; click rerun` ([`src/wrapper/remedies.ts:84-86`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/remedies.ts#L84-L86)), although nothing is wrong. Observed in a run of a consumer repository, on a documentation change touching 33 files (430 additions, 737 deletions). The writing-quality lens ran 35 minutes (2,119 s) and completed with exit 0. The wrapper had given up 5 minutes earlier, and the check failed with the rerun message. Raising stage 1 to the service's limit plus slack, say 65 minutes, changes the sums. Today one wait is at most 60 minutes (30 + 25 + 5, [`src/review/constants.ts:33-35`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/review/constants.ts#L33-L35)). A rerun that re-triages waits a second time ([`src/wrapper/orchestrator.ts:104-120`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/orchestrator.ts#L104-L120)), so 120 minutes, which fits under the workflow's `timeout-minutes: 130` ([`.forgejo/workflows/review.yml:77`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/.forgejo/workflows/review.yml#L77)). With 65 minutes at stage 1 one wait is 95 minutes, and the same rerun could need 190 in the worst case the code allows. The other direction is to lower the service's lens limit to match. Either number can move, but they need to be set together and written down next to each other, together with the workflow's cap. ## Three paths have no limit of their own - **Forge requests have no timeout** ([`src/forge/client.ts:37-48`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/forge/client.ts#L37-L48)). The wrapper sets none, so the only bound is the runtime's: on Node 24 a request that gets no response headers fails after about 300 seconds. Service requests are limited to 120 s ([`dist/index.mjs:5749`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/dist/index.mjs#L5749)). Reproduced as `d12-forge-hang`: a forge that accepted the head read and never answered ended the run with `fetch failed: Headers Timeout Error` after 300.9 s, exit 1. A run makes many forge requests, since it lists every review and each review's comments twice. - **The grace deadline resets without limit.** `waitForSettle` sets a new grace deadline every time triage settles, inside a loop with no cycle limit ([`src/wrapper/wait.ts:117`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/wait.ts#L117), [`src/wrapper/wait.ts:130`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/wait.ts#L130)). The reset is deliberate (#10 made the grace run from each settle). `poll` returns a result before it checks the deadline or sleeps ([`src/wrapper/wait.ts:28-31`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/wrapper/wait.ts#L28-L31)). A service whose triage state flips between settled and not settled on successive reads makes the loop run without sleeping and without end. Reproduced as `d13-grace-flap`: a stand-in service did that for 20 s without the wrapper exiting, at over 2,000 requests a second. - **Claim paging stops only on a repeated cursor** ([`src/review/claims-walk.ts:27-29`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/review/claims-walk.ts#L27-L29)). A service that returns a new cursor on every page is walked forever, and the walk runs inside a poll step that cannot check its deadline ([`src/review/claims-walk.ts:5-8`](https://code.j4k.dev/j4k-oss/review-wrapper/src/commit/9f272a522e8ed6f748533761de45ee3e94fd7481/src/review/claims-walk.ts#L5-L8)). Reproduced as `d14-endless-claims`: a stand-in service produced more than 150,000 pages in 15 s with no end. Even where each path is bounded, nothing limits the sum. Each window is bounded on its own, and the retry ladders and request timeouts add up on top of them. ## What it should do instead - Give forge requests an `AbortSignal.timeout`, as the service client has. - Set the grace deadline once per wait, or count the regressions and stop after a few. Check the stage deadline on every pass of the loop and sleep a poll interval when the state flipped. - Give `walkClaims` a page limit or a deadline. - Pass one run-wide deadline through all of the above, so the job ends with an outcome line before the workflow cancels it. ## Why it matters When the workflow's cap fires, the run is cancelled with no outcome line and a red check that does not say why. While a loop spins, it sends thousands of requests a second to the service, so the security relevance is limited to availability of the service. None of the three has been seen in production. Each needs a service that flips its state or mints cursors without end, or a forge that accepts a connection and never replies. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
j4k-oss/review-wrapper#56
No description provided.