Uh oh!
There was an error while loading. Please reload this page.
fix(review-pr): enforce a coherent time budget across agent retries - #48
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The time-budget enforcement logic is sound. The two medium-severity hypotheses raised during analysis were both dismissed after careful code tracing:
"Budget guard subtracts already-doubled delay" — DISMISSED. At line 395,
currentDelayholds the post-doubled value, which is exactly the duration of the upcoming sleep on the next iteration. The guard correctly asks: "after sleepingcurrentDelayseconds, will at least 60 s remain?" — this is the intended conservative check and there is no off-by-one."Negative remaining budget spawns agent with 1-second timeout" — DISMISSED. The guard at line 395 (checked after each failed attempt) guarantees
remainingAfterDelay >= 60 sbefore allowing a retry. After sleepingcurrentDelay, roughly 60 s remain when line 352 is reached, soMath.max((deadline - Date.now()) / 1000, 1)never clamps to 1 s in practice.
The two low-severity configuration-clarity observations (feedback step max-retries vs total-timeout=300; review step identical timeout and total-timeout) were noted in YAML wiring not changed by this PR's diff and are therefore out of scope.
derekmisler
left a comment
There was a problem hiding this comment.
🟢 APPROVE
solid fix, the core logic in exec.ts is clean and the budget math holds up. one nitpick below about parseInt vs parseFloat (super low-risk in practice, nobody passes 0.5 seconds as a real budget, but worth fixing for consistency with the test layer). also, with timeout: "2700" and total-timeout: "2700" both set to the same value, only a quick-fail retry can actually start; a timed-out attempt exhausts the budget completely, so max-retries: "1" only kicks in for the != 124 path. that's the intended behavior per the PR description, just worth a quick comment confirming it's deliberate.
Lower-confidence findings (not posted inline):
max-retries: "1"is inert for timeout-failures (both budgets are identical, soremainingAfterDelay < 60always fires after a full 2700s run). this is intentional and correct per the description, but a short comment in the workflow would help future readers understand why the retry is only for transient API failures, not timeouts.
Uh oh!
There was an error while loading. Please reload this page.
derekmisler
left a comment
There was a problem hiding this comment.
parseFloat fix looks good, all checks green. lgtm!
The review agent ran with timeout 2700s, max-retries 1 and retry-on-timeout 1 inside a 50-minute job: a first attempt timing out at 45 min launched a retry that GitHub always killed at the job ceiling, skipping the composite's always() steps (lock release, PR summary, reactions) and sometimes posting a duplicate review. - add total-timeout input: wall-clock budget across all attempts and retry delays, enforced by the runner (last attempt capped to the remaining budget, retry needs >= 60s left), so the agent step always ends before the job-level timeout and cleanup steps run - add no-retry-pattern input: skip remaining retries once the verbose log proves a side effect landed (pullrequestreview-N), preventing duplicate reviews on retry - review-pr: cap review at 2700s total, feedback processing at 300s, drop the doomed retry-on-timeout, raise job ceiling to 60 min as a safety net that should never fire - fix stale comments (1800s, 35-min budget) in action.yml and AGENTS.md
Co-authored-by: Derek Misler <derek.misler@docker.com> Signed-off-by: Sayt0 <138035894+Sayt-0@users.noreply.github.com>
dc450f3 to
eebdccaCompareUh oh!
There was an error while loading. Please reload this page.
Summary
The review agent could consume up to 2 x 2700 s (plus up to ~9 min of feedback-processing retries) inside a job capped at 50 minutes. A first attempt timing out at 45 min launched a retry that GitHub always killed at the job ceiling: wasted retry, cancelled run, and the composite's
always()steps (lock release, summary, reactions) never ran, leaving a residual lock and no feedback on the PR. A retry after a partial failure could also post a duplicate review.Changes
Runner: two new root-action inputs (
src/main/exec.ts,action.yml)total-timeout(seconds, 0 = unlimited)timeout-minuteskill never fires and cleanup steps stay alive.no-retry-pattern(regex)Budget rewiring (
review-pr/action.yml,review-pr.yml)total-timeout: 300(5 min hard)max-retries+retry-on-timeout(90+ min)total-timeout: 2700(45 min hard)retry-on-timeout: 1removed: a second 45 min pass can never fit the budget; timeouts are already surfaced on the PR for a manual re-request.no-retry-pattern: pullrequestreview-[0-9]+(the same marker the "Post clean summary" step greps to detect a posted review): no duplicate review on retry.review-pr/action.yml), "35-min job budget" (review-pr/action.yml), "1800s (30 min)" (AGENTS.md).Issue expectations mapping
total-timeout: 2700on the review step,retry-on-timeoutremovedalways()steps skipped (residual lock, no PR feedback)review-pr/action.yml,AGENTS.mdno-retry-patternguardImpact on other open PRs and consumers
review-pr/action.ymlreferences the root action by pinned SHA (v2.0.2): the new inputs stay inactive until the next release re-pins the chain (release.yml 3-pass), so no mixed state is possible.concurrencygroup andcancel-in-progress: falseuntouched: one PR's budget never affects another PR's review.review-pr.yml(itsenv:block replaces thepermissions:block right below thetimeout-minutesline); resolution keeps both sides verbatim, whichever lands second.Validation
pnpm buildpnpm test(753 tests, 8 new for total-timeout / no-retry-pattern)pnpm test:integrationpnpm lint(Biome + tsc + actionlint)tests/test-job-summary.sh,tests/test-output-extraction.sh