Skip to content

fix(pr-agent): classify timeouts per ATTEMPT, not on total job time - #44

Open
yakimoto wants to merge 1 commit into
mainfrom
fix/pr-agent-per-attempt-timeout
Open

yakimoto wants to merge 1 commit into
mainfrom
fix/pr-agent-per-attempt-timeout

Conversation

@yakimoto

@yakimoto yakimoto commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Re-syncs this repo to wave-foundation-public#72, which landed after the inline pr-agent lane was adopted here. Tracked as wave-pen#417.

The defect

The adopted template stamped AGENT_START once, before attempt 1, then compared total job time — attempt 1 + the 45s backoff + attempt 2 — against STEP_BUDGET_S=360, a budget its own comment calls per-attempt.

Two healthy-but-slow attempts (~180s each, ~405s together) therefore reported:

pr-agent TIMED OUT … A hang, NOT a rate limit.

…sending the next reader to debug a hang that never happened. The else-branch lied the other way, asserting the run was "well inside the budget" from the same misused total.

Found by qodo review on wave-monitor#48 and confirmed against the file before acting.

The fix

Stamp each attempt separately and classify on the longest attempt, with if: always() end stamps so an attempt killed by its step timeout still records one — exactly the case the classifier exists to catch. Total wall time is still reported as context but no longer decides the verdict.

case now before
180s + 180s (405s total) failed after 2 attempts TIMED OUT
attempt killed at ~358s TIMED OUT

Not urgent, not ignorable

The defect is in a message, not behaviour — the lane still retries, still renders NEUTRAL, still never blocks a PR. But that verdict step exists precisely because "a confidently wrong cause is worse than no cause", so a classifier that can misname a hang defeats its own purpose.

Job id pr_agent and every on: trigger unchanged — the job id is the check-run context and branch protection matches on it.

Refs wave-av/wave-pen#417, wave-av/wave-pen#388


Note

Low Risk
CI classifier/messaging only; the job still retries, stays advisory, and never blocks PRs. No application or security logic.

Overview
Fixes the pr-agent verdict step so hang vs rate-limit is decided from per-attempt duration, not wall-clock time across both tries plus backoff.

Previously a single AGENT_START stamp made two slow-but-healthy ~180s attempts (~405s together) look like a step timeout against the 360s per-attempt budget. The workflow now stamps start/end for each attempt (if: always() so a killed step still records an end), classifies on the longest attempt with 15s slack, and still reports total wall time only as context. Retry, NEUTRAL rendering, and job id are unchanged.

Reviewed by Cursor Bugbot for commit 64a39a9. Bugbot is set up for automated code reviews on this repo. Configure here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by Sourcery

Classify pr-agent timeouts using per-attempt duration so retries and backoff no longer cause healthy slow runs to be misreported as hangs.

Bug Fixes:

  • Correct timeout classification in the pr-agent workflow by evaluating the duration of the longest individual attempt instead of total job time.
  • Ensure timed-out attempts are included in classification and distinguish slow attempts from upstream failures.

Enhancements:

  • Continue reporting total wall-clock time as context while basing the verdict on per-attempt budgets and applying timeout slack.

CI:

  • Update pr-agent workflow attempt timing and failure messaging without changing its advisory, non-blocking behavior.

Re-syncs this repo to wave-foundation-public#72, which landed after the inline
lane was adopted here.

THE DEFECT. The adopted template stamped AGENT_START once, before attempt 1,
then compared TOTAL job time — attempt 1 + the 45s backoff + attempt 2 —
against STEP_BUDGET_S=360, a budget its own comment calls PER-ATTEMPT. Two
healthy-but-slow attempts (~180s each, ~405s together) therefore reported

  "pr-agent TIMED OUT ... A hang, NOT a rate limit."

sending the next reader to debug a hang that never happened; the else-branch
lied the other way, asserting the run was "well inside the budget" from the
same misused total.

Found by qodo review on wave-monitor#48 and confirmed against the file before
acting.

THE FIX. Stamp each attempt separately and classify on the LONGEST attempt,
with if: always() end stamps so an attempt killed BY its step timeout still
records one — exactly the case the classifier exists to catch. Total wall time
is still reported as context but no longer decides the verdict.

NOT URGENT, NOT IGNORABLE. The defect is in a MESSAGE, not in behaviour: the
lane still retries, still renders NEUTRAL, still never blocks a PR. But that
verdict step exists precisely because "a confidently wrong cause is worse than
no cause", so shipping a classifier that can misname a hang defeats its purpose.

Job id pr_agent and every on: trigger unchanged — the job id is the check-run
context and branch protection matches on it.

Refs wave-av/wave-pen#417, wave-av/wave-pen#388
@codeant-ai

codeant-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 64a39a9 Aug 24, 2026 · 13:38 13:38

@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_a168353a-1914-4225-bc65-a195dc3753ef)

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 28 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 91 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d1c95891-ea61-4307-a0ef-875508a1e200

📥 Commits

Reviewing files that changed from the base of the PR and between 65245aa and 64a39a9.

📒 Files selected for processing (1)
  • .github/workflows/pr-agent.yml

Comment @coderabbitai help to get the list of available commands.

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Aug 24, 2026
@macroscopeapp

macroscopeapp Bot commented Aug 24, 2026

Copy link
Copy Markdown

Approvability

Verdict: Would Approve

Macroscope's review found this PR approvable — This is a one-file CI diagnostic fix that records per-attempt timings and corrects timeout messaging while preserving retry behavior, action configuration, triggers, and neutral non-blocking handling.

Not approved because:

  • Credit balance exhausted. Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix pr-agent timeout classification to be per-attempt (not total job time)

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Track start/end timestamps per pr-agent attempt to avoid false “TIMED OUT” verdicts.
• Classify timeouts using the longest single attempt, still reporting total wall time as context.
• Ensure end stamps are recorded even when an attempt is killed by its step timeout.
Diagram

graph TD
  Attempt1["Attempt 1 (timed)"] --> Decide1{"Attempt 1 failed?"}
  Decide1 -- "no" --> Verdict["Verdict classifier"]
  Decide1 -- "yes" --> Backoff["45s backoff"] --> Attempt2["Attempt 2 (timed)"] --> Verdict
  Attempt1 --> Env[("GITHUB_ENV stamps")]
  Attempt2 --> Env --> Verdict
Loading
High-Level Assessment

The chosen approach is appropriate given GitHub Actions’ lack of a step-level “timed_out” signal. Using per-attempt timestamps plus if: always() end stamps is the most reliable way to distinguish step-timeout kills from upstream failures, while avoiding misclassification caused by combining attempt durations with retry backoff.

Files changed (1) +44 / -6

Bug fix (1) +44 / -6
pr-agent.ymlStamp per-attempt timing and classify timeouts by longest attempt +44/-6

Stamp per-attempt timing and classify timeouts by longest attempt

• Replaces the single job-level start stamp with per-attempt start/end stamps (including 'if: always()' end stamps). Updates the verdict step to compute attempt durations, use the longest attempt (with small slack) to detect step timeouts, and refine warning messages to report per-attempt timings plus total wall time without using wall time for classification.

.github/workflows/pr-agent.yml

@gitar-bot

gitar-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your team has used its included automatic processing for this billing period (headroom scales with your seat count). You can still comment "Gitar review" to run one anytime, and automatic reviews resume on their own by September 1. Add seats for more headroom.
Learn more

Code Review ✅ Approved

Refactors PR-agent timeout classification to measure duration per attempt rather than total job time, preventing false hang diagnoses on slow retries. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Missing pr-agent changelog entry 📘 Rule violation ⚙ Maintainability
Description
This PR changes the pr_agent check’s user-visible warning classification/text for timeout vs
failure in .github/workflows/pr-agent.yml, but CHANGELOG.md has no corresponding entry under `##
[Unreleased]`. This violates the requirement to document user-facing changes in the changelog.
Code

.github/workflows/pr-agent.yml[R216-217]

+          if [ "$LONGEST" -ge $(( STEP_BUDGET_S - SLACK )) ]; then
+            echo "::warning::pr-agent TIMED OUT — the longest attempt ran ${LONGEST}s against a ${STEP_BUDGET_S}s per-attempt budget (attempt 1 ${A1}s, attempt 2 ${A2}s), so it was killed by its step timeout rather than returning an error. A hang, NOT a rate limit. Rendering NEUTRAL: an advisory reviewer must not block the PR (#3128)."
Relevance

●●● Strong

The finding matches the repository’s explicit changelog rule for user-visible changes; no close
rejection precedent was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow now emits different ::warning:: messages and classifies TIMED OUT based on
per-attempt duration (a user-visible change in the PR check output), yet the ## [Unreleased]
section of CHANGELOG.md contains no new bullet describing this change.

Rule 2497946: Update CHANGELOG.md for user-facing changes under Unreleased
.github/workflows/pr-agent.yml[200-220]
CHANGELOG.md[7-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces a user-visible change to PR check output/classification (`pr_agent` timeout vs failure messaging) but does not add an entry under `## [Unreleased]` in `CHANGELOG.md`.

## Issue Context
The compliance checklist requires updating `CHANGELOG.md` for user-facing changes.

## Fix Focus Areas
- CHANGELOG.md[7-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Attempt2 duration inflated 🐞 Bug ≡ Correctness
Description
The workflow stamps ATTEMPT2_END unconditionally but only stamps ATTEMPT2_START when the retry is
scheduled, so A2 can become a large positive value (END-0) if attempt 2 never actually started. That
can drive LONGEST above the timeout threshold and incorrectly emit the "TIMED OUT" classification
for non-timeout failures.
Code

.github/workflows/pr-agent.yml[R207-208]

+          A1=$(( ${ATTEMPT1_END:-0} - ${ATTEMPT1_START:-0} ))
+          A2=$(( ${ATTEMPT2_END:-0} - ${ATTEMPT2_START:-0} ))
Relevance

●●● Strong

Conditional retry start with unconditional end creates a deterministic false duration and can
misclassify failures as timeouts.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow sets ATTEMPT2_START only on retry but always sets ATTEMPT2_END; the verdict then
computes A2 as END-START with 0 defaults, so missing START yields a large positive A2 and can
dominate LONGEST, triggering the timeout branch.

.github/workflows/pr-agent.yml[136-169]
.github/workflows/pr-agent.yml[206-217]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ATTEMPT2_END` is stamped with `if: always()` even when attempt 2 never runs, while `ATTEMPT2_START` is only stamped when the retry is scheduled. In the verdict step, `A2=$((ATTEMPT2_END - ATTEMPT2_START))` then becomes a large positive value when `ATTEMPT2_START` is unset (defaults to 0), which can incorrectly make `LONGEST` exceed the step budget and misclassify a non-timeout failure as `TIMED OUT`.

## Issue Context
- `stamp attempt 2 start` is conditional on `steps.agent.outcome == 'failure'`.
- `stamp attempt 2 end` currently runs unconditionally (`if: always()`), so it can set `ATTEMPT2_END` even when attempt 2 never started.
- Verdict arithmetic defaults missing vars to `0`, which turns the missing-start case into a huge positive duration.

## Fix Focus Areas
- .github/workflows/pr-agent.yml[136-169]
- .github/workflows/pr-agent.yml[206-217]

## Suggested changes
1) Make `stamp attempt 2 end` conditional on attempt 2 being intended (while still using `always()`):
  - Example: `if: always() && steps.agent.outcome == 'failure'`
2) Defensively compute `A2` as `0` unless `ATTEMPT2_START` is set to a non-zero epoch value (or unless `AGENT_RETRY_OUTCOME` is non-empty):
  - Example:
    - `A2=0`
    - `if [ "${ATTEMPT2_START:-0}" -gt 0 ] && [ "${ATTEMPT2_END:-0}" -gt 0 ]; then A2=$((ATTEMPT2_END-ATTEMPT2_START)); fi`
This keeps `LONGEST` meaningful and prevents false timeout classification when the retry didn’t actually run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 1 rule
✅ REVIEW.md
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +216 to +217
if [ "$LONGEST" -ge $(( STEP_BUDGET_S - SLACK )) ]; then
echo "::warning::pr-agent TIMED OUT — the longest attempt ran ${LONGEST}s against a ${STEP_BUDGET_S}s per-attempt budget (attempt 1 ${A1}s, attempt 2 ${A2}s), so it was killed by its step timeout rather than returning an error. A hang, NOT a rate limit. Rendering NEUTRAL: an advisory reviewer must not block the PR (#3128)."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Missing pr-agent changelog entry 📘 Rule violation ⚙ Maintainability

This PR changes the pr_agent check’s user-visible warning classification/text for timeout vs
failure in .github/workflows/pr-agent.yml, but CHANGELOG.md has no corresponding entry under `##
[Unreleased]`. This violates the requirement to document user-facing changes in the changelog.
Agent Prompt
## Issue description
The PR introduces a user-visible change to PR check output/classification (`pr_agent` timeout vs failure messaging) but does not add an entry under `## [Unreleased]` in `CHANGELOG.md`.

## Issue Context
The compliance checklist requires updating `CHANGELOG.md` for user-facing changes.

## Fix Focus Areas
- CHANGELOG.md[7-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +207 to +208
A1=$(( ${ATTEMPT1_END:-0} - ${ATTEMPT1_START:-0} ))
A2=$(( ${ATTEMPT2_END:-0} - ${ATTEMPT2_START:-0} ))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Attempt2 duration inflated 🐞 Bug ≡ Correctness

The workflow stamps ATTEMPT2_END unconditionally but only stamps ATTEMPT2_START when the retry is
scheduled, so A2 can become a large positive value (END-0) if attempt 2 never actually started. That
can drive LONGEST above the timeout threshold and incorrectly emit the "TIMED OUT" classification
for non-timeout failures.
Agent Prompt
## Issue description
`ATTEMPT2_END` is stamped with `if: always()` even when attempt 2 never runs, while `ATTEMPT2_START` is only stamped when the retry is scheduled. In the verdict step, `A2=$((ATTEMPT2_END - ATTEMPT2_START))` then becomes a large positive value when `ATTEMPT2_START` is unset (defaults to 0), which can incorrectly make `LONGEST` exceed the step budget and misclassify a non-timeout failure as `TIMED OUT`.

## Issue Context
- `stamp attempt 2 start` is conditional on `steps.agent.outcome == 'failure'`.
- `stamp attempt 2 end` currently runs unconditionally (`if: always()`), so it can set `ATTEMPT2_END` even when attempt 2 never started.
- Verdict arithmetic defaults missing vars to `0`, which turns the missing-start case into a huge positive duration.

## Fix Focus Areas
- .github/workflows/pr-agent.yml[136-169]
- .github/workflows/pr-agent.yml[206-217]

## Suggested changes
1) Make `stamp attempt 2 end` conditional on attempt 2 being intended (while still using `always()`):
   - Example: `if: always() && steps.agent.outcome == 'failure'`
2) Defensively compute `A2` as `0` unless `ATTEMPT2_START` is set to a non-zero epoch value (or unless `AGENT_RETRY_OUTCOME` is non-empty):
   - Example:
     - `A2=0`
     - `if [ "${ATTEMPT2_START:-0}" -gt 0 ] && [ "${ATTEMPT2_END:-0}" -gt 0 ]; then A2=$((ATTEMPT2_END-ATTEMPT2_START)); fi`
This keeps `LONGEST` meaningful and prevents false timeout classification when the retry didn’t actually run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

✅ Merged (0) · ☑ Fixed (0)

Process

  • No fixes were applied (no_fixes_applied)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @yakimoto, you have reached your weekly rate limit of 250000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the pr-agent workflow to distinguish per-attempt timeouts from failures across the complete retry sequence by timestamping each attempt, classifying on the longest attempt with runner-timeout slack, and retaining total wall time only for context. Retry behavior and advisory, non-blocking job semantics remain unchanged.

Sequence diagram for per-attempt pr-agent timeout classification

sequenceDiagram
    participant Workflow
    participant Agent as PR-Agent
    participant Verdict

    Workflow->>Workflow: stamp attempt 1 start
    Workflow->>Agent: run attempt 1
    Workflow->>Workflow: stamp attempt 1 end
    alt attempt 1 fails
        Workflow->>Workflow: sleep 45s
        Workflow->>Workflow: stamp attempt 2 start
        Workflow->>Agent: run attempt 2
        Workflow->>Workflow: stamp attempt 2 end
    end
    Workflow->>Verdict: compare longest attempt with STEP_BUDGET_S - 15s
    alt longest attempt reaches timeout threshold
        Verdict-->>Workflow: render NEUTRAL: TIMED OUT
    else neither attempt reaches threshold
        Verdict-->>Workflow: render NEUTRAL: failed, likely rate limit
    end
Loading

File-Level Changes

Change Details Files
Track and classify timeout duration independently for each PR-Agent attempt.
  • Replace the single job-level start timestamp with start/end stamps for both attempts.
  • Record end timestamps with if: always() so timeout-killed attempts are measurable.
  • Classify using the longest attempt with 15 seconds of timeout slack, while retaining wall-clock duration as diagnostic context.
.github/workflows/pr-agent.yml
Preserve advisory retry and verdict behavior while correcting timeout messaging.
  • Keep the existing retry, cancellation handling, NEUTRAL result, triggers, and job identity unchanged.
  • Update timeout and upstream-error warnings to report per-attempt durations and the corrected cause.
.github/workflows/pr-agent.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant