Skip to content

fix(pr-patrol): reduce compute waste with issue-type budgets and timeout abandonment#1826

Merged
OAGr merged 2 commits intomainfrom
claude/stupefied-chaum
Mar 7, 2026
Merged

fix(pr-patrol): reduce compute waste with issue-type budgets and timeout abandonment#1826
OAGr merged 2 commits intomainfrom
claude/stupefied-chaum

Conversation

@OAGr
Copy link
Contributor

@OAGr OAGr commented Mar 7, 2026

Summary

PR Patrol was spending 90%+ of compute time on failed attempts — only 35 minutes out of 6.2 hours produced successful fixes. The rest was timeouts and max-turns failures, often on the same unfixable PRs repeatedly.

Root causes and fixes:

  • Timeouts didn't count toward abandonment — a PR could timeout at 30 minutes indefinitely without being abandoned. Now both timeouts and max-turns increment the same failure counter (abandoned at 2).
  • All issue types got 40 turns / 30 minmissing-issue-ref (a 10-second gh pr edit) got the same budget as resolving merge conflicts. Now each issue type has a scaled budget:
    • conflict: 40 turns / 30 min
    • ci-failure, bot-review-major: 25 turns / 15 min
    • stale: 10 turns / 5 min
    • missing-testplan, bot-review-nitpick: 8 turns / 5 min
    • missing-issue-ref: 5 turns / 3 min
  • No early-exit guidance — Claude would burn all turns trying to fix unfixable CI checks (like check-protected-paths needing a human label). Prompt now has explicit "when to stop early" section listing unfixable scenarios.
  • Reflection used Sonnet for simple JSONL log analysis — switched to Haiku with 5-min timeout.

Test plan

  • All 34 PR Patrol tests pass (including 6 new computeBudget tests)
  • TypeScript compiles clean
  • Added pr-patrol/**/*.test.ts to vitest config (was missing)

Related to #1791

🤖 Generated with Claude Code

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Warning

Rate limit exceeded

@OAGr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4846c2f-d239-4127-b10a-7ba860e79b7d

📥 Commits

Reviewing files that changed from the base of the PR and between 3e54e82 and 0221084.

📒 Files selected for processing (2)
  • crux/pr-patrol/index.test.ts
  • crux/pr-patrol/index.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/stupefied-chaum

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@OAGr
Copy link
Contributor Author

OAGr commented Mar 7, 2026

⚠️ PR Overlap Warning

This PR shares 1 file(s) with PR #1825:

  • crux/pr-patrol/index.ts

Coordinate to avoid merge conflicts.

Posted by PR Patrol — informational only.

@OAGr
Copy link
Contributor Author

OAGr commented Mar 7, 2026

⚠️ PR Overlap Warning

This PR shares 2 file(s) with PR #1824:

  • crux/pr-patrol/index.ts
  • crux/vitest.config.ts

Coordinate to avoid merge conflicts.

Posted by PR Patrol — informational only.

…out abandonment

PR Patrol was burning 90%+ of compute on failed attempts:
- Timeouts (30 min each) didn't count toward abandonment, causing infinite retries
- All issue types got the same 40-turn / 30-min budget regardless of complexity
- No early-exit guidance in prompts, so Claude kept trying unfixable issues
- Reflection used expensive Sonnet model for simple log analysis

Changes:
- Unify failure tracking: timeouts now count toward abandonment (2 failures = abandoned)
- Add per-issue-type budgets: missing-issue-ref gets 5 turns/3 min, ci-failure gets 25/15, etc.
- Add "when to stop early" section to prompts with clear unfixable-scenario detection
- CI failure prompt now explicitly lists human-required checks to skip immediately
- Reflection uses haiku model with 5-min timeout instead of sonnet/30-min
- Include pr-patrol tests in vitest config
- Add computeBudget tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@OAGr OAGr force-pushed the claude/stupefied-chaum branch from 2990c70 to a7a01cd Compare March 7, 2026 01:59
@vercel
Copy link

vercel bot commented Mar 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
longterm-wiki-agent3 Error Error Mar 7, 2026 2:39am

@OAGr
Copy link
Contributor Author

OAGr commented Mar 7, 2026

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@OAGr OAGr added the claude-working Claude Code is actively working on this label Mar 7, 2026
@OAGr
Copy link
Contributor Author

OAGr commented Mar 7, 2026

🤖 PR Patrol ran for 98s (40 max turns, model: sonnet).

Issues detected: missing-issue-ref

Result:
PR #1826 body updated successfully. Added Related to #1791 to the PR description — the PR partially addresses issue #1791 (compute waste from retrying failed PRs) by making timeouts and max-turns increment the same failure counter, but doesn't fully close it since #1791 specifically mentions hard-errors (exit code 1 with elapsed_s: 0) which aren't handled by this PR.

@OAGr OAGr removed the claude-working Claude Code is actively working on this label Mar 7, 2026
@OAGr
Copy link
Contributor Author

OAGr commented Mar 7, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crux/pr-patrol/index.ts (1)

1325-1336: ⚠️ Potential issue | 🟠 Major

Don't record stop-early / no-op runs as fixed.

Any exitCode === 0 lands in the success path, even when the new prompt tells Claude to stop because the issue needs human action, is pre-existing on main, or requires no change. Since nothing verifies the issue disappeared, those PRs just cool down and then get queued again unchanged. Please give “stopped early” a distinct outcome and persist that state so patrol can skip or abandon it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crux/pr-patrol/index.ts` around lines 1325 - 1336, When an exitCode === 0 run
actually stopped early/no-op and did not fix the issue, don't mark it as
'fixed'—add a distinct outcome value (e.g., outcome = 'stopped_early') and
branch before the existing success branch (the block that checks result.exitCode
=== 0 && !result.hitMaxTurns) to detect the early-stop/no-op condition (e.g.,
result.stoppedEarly or a heuristic on result.output/result.issues) and set
outcome accordingly; update the PR comment logic (the githubApi POST that
currently posts the success summary using result.output, effectiveMaxTurns and
config.model) to reflect the stopped_early state and persist that outcome where
PR statuses are saved (the same place other outcomes are recorded so patrol can
skip/abandon these PRs on subsequent runs).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crux/pr-patrol/index.ts`:
- Around line 188-208: The abandonment counter
(getFailCount/recordFailure/isAbandoned) is treated as "consecutive failures"
but is monotonic and never cleared; update the logic so the counter is reset or
scoped to the current run context: either clear the failures-<key> file after a
successful/no-op run (call an explicit reset that removes or writes 0 for
failures-<key> when checkMainBranch or the PR processing path succeeds), or
incorporate a run identifier (e.g., current run SHA or current relevant issue
set) into the key used by getFailCount/recordFailure/isAbandoned (e.g., use
MAIN_BRANCH_KEY+runSha) so counts are per-run; modify checkMainBranch and any
success path to call the reset function or to pass the scoped key to
isAbandoned/recordFailure.
- Around line 1446-1451: The current flow treats any resolved spawnClaude()
result as a successful reflection; modify the code after calling
spawnClaude(prompt, {...}) to treat runs where result.timedOut or
result.hitMaxTurns are true as incomplete/failed: do not append a normal
"Reflection complete" record or mark success in persistent state when those
flags are set, instead append/mark an incomplete result and log a
timeout/max-turns message. Check result.timedOut and result.hitMaxTurns
immediately after spawnClaude returns and branch accordingly before calling
whatever record-append or success-log logic currently emits "✓ Reflection
complete".

---

Outside diff comments:
In `@crux/pr-patrol/index.ts`:
- Around line 1325-1336: When an exitCode === 0 run actually stopped early/no-op
and did not fix the issue, don't mark it as 'fixed'—add a distinct outcome value
(e.g., outcome = 'stopped_early') and branch before the existing success branch
(the block that checks result.exitCode === 0 && !result.hitMaxTurns) to detect
the early-stop/no-op condition (e.g., result.stoppedEarly or a heuristic on
result.output/result.issues) and set outcome accordingly; update the PR comment
logic (the githubApi POST that currently posts the success summary using
result.output, effectiveMaxTurns and config.model) to reflect the stopped_early
state and persist that outcome where PR statuses are saved (the same place other
outcomes are recorded so patrol can skip/abandon these PRs on subsequent runs).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a11e43b3-172f-4c86-a97b-087e53bd04f4

📥 Commits

Reviewing files that changed from the base of the PR and between 3e54e82 and a7a01cd.

📒 Files selected for processing (2)
  • crux/pr-patrol/index.test.ts
  • crux/pr-patrol/index.ts

Comment on lines +188 to +208
function getFailCount(key: number | string): number {
// Check both new and legacy file names for backwards compat
const newFile = join(STATE_DIR, `failures-${key}`);
const legacyFile = join(STATE_DIR, `max-turns-${key}`);
if (existsSync(newFile)) {
return parseInt(readFileSync(newFile, 'utf-8').trim(), 10) || 0;
}
if (existsSync(legacyFile)) {
return parseInt(readFileSync(legacyFile, 'utf-8').trim(), 10) || 0;
}
return 0;
}

function recordMaxTurnsFailure(key: number | string): number {
const count = getMaxTurnsFailCount(key) + 1;
writeFileSync(join(STATE_DIR, `max-turns-${key}`), String(count));
function recordFailure(key: number | string): number {
const count = getFailCount(key) + 1;
writeFileSync(join(STATE_DIR, `failures-${key}`), String(count));
return count;
}

function isAbandoned(key: number | string): boolean {
return getMaxTurnsFailCount(key) >= 2;
return getFailCount(key) >= 2;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Reset or scope abandonment state instead of keeping it forever.

This counter is monotonic, but the rest of the flow treats it as “2 consecutive failures”. Once MAIN_BRANCH_KEY reaches 2, checkMainBranch() stops querying GitHub and returns isRed: false, so a later red-main incident can be skipped and normal merge processing continues. The same stale state also makes a later unrelated PR failure look like the second consecutive miss. Please clear the counter after a successful/no-op run, or scope it to the current run SHA / issue set before calling isAbandoned().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crux/pr-patrol/index.ts` around lines 188 - 208, The abandonment counter
(getFailCount/recordFailure/isAbandoned) is treated as "consecutive failures"
but is monotonic and never cleared; update the logic so the counter is reset or
scoped to the current run context: either clear the failures-<key> file after a
successful/no-op run (call an explicit reset that removes or writes 0 for
failures-<key> when checkMainBranch or the PR processing path succeeds), or
incorporate a run identifier (e.g., current run SHA or current relevant issue
set) into the key used by getFailCount/recordFailure/isAbandoned (e.g., use
MAIN_BRANCH_KEY+runSha) so counts are per-run; modify checkMainBranch and any
success path to call the reset function or to pass the scoped key to
isAbandoned/recordFailure.

Comment on lines 1446 to 1451
const result = await spawnClaude(prompt, {
...config,
maxTurns: 10, // Reflection needs fewer turns
model: 'haiku', // Reflection is log analysis — doesn't need sonnet
timeoutMinutes: 5, // Should complete quickly
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle timed-out reflections as incomplete, not successful.

spawnClaude() resolves on timeout/max-turns, so with the new 5-minute cap this path can still append a normal record and log ✓ Reflection complete after a killed run. Check result.timedOut / result.hitMaxTurns before recording success.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crux/pr-patrol/index.ts` around lines 1446 - 1451, The current flow treats
any resolved spawnClaude() result as a successful reflection; modify the code
after calling spawnClaude(prompt, {...}) to treat runs where result.timedOut or
result.hitMaxTurns are true as incomplete/failed: do not append a normal
"Reflection complete" record or mark success in persistent state when those
flags are set, instead append/mark an incomplete result and log a
timeout/max-turns message. Check result.timedOut and result.hitMaxTurns
immediately after spawnClaude returns and branch accordingly before calling
whatever record-append or success-log logic currently emits "✓ Reflection
complete".

- Reset failure counter on successful fix (was monotonic/never cleared)
- Handle timed-out reflections as incomplete instead of success
- Add 'no-op' outcome for stop-early runs (heuristic detection)
- No-op runs increment failure counter to avoid infinite retries
- 8 new tests for looksLikeNoOp detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

🛑 CodeRabbit Security Gate

Action required: This PR has 1 unresolved Critical/Major CodeRabbit finding(s). Either resolve each thread in the GitHub review interface, or add the coderabbit-security-ok label after reviewing and acknowledging each finding.

Unresolved Critical/Major Findings (1)

  • crux/pr-patrol/index.ts (line 235): ⚠️ Potential issue | 🔴 Critical

    ⚠️ Potential issue | 🔴 Critical Reset or scope abandonment state instead of keeping it forever. This counter is monotonic, but the rest of the flow treats it as “2 consecutive failures”. Once MAIN_BRANCH_KEY reaches 2, checkMainBranch() stops querying GitHub and returns isRed: false


How to resolve: Mark each CodeRabbit thread as resolved in the "Files changed" tab, or add the coderabbit-security-ok label if the findings are false positives. See #1649.

@OAGr OAGr merged commit c076de5 into main Mar 7, 2026
15 of 17 checks passed
@OAGr OAGr deleted the claude/stupefied-chaum branch March 7, 2026 03:13
OAGr added a commit that referenced this pull request Mar 7, 2026
- Split 1,560-line index.ts into 8 focused modules:
  types.ts, state.ts, detection.ts, scoring.ts, merge.ts,
  prompts.ts, execution.ts, reflection.ts
- Add pr-patrol/**/*.test.ts to vitest config (tests were silently ignored)
- Fix empty catch blocks violating error-handling rules
- Add missing stale PR section in buildPrompt()
- Fix summary comment mid-line truncation (take last ~500 chars by line)
- Fix overlap comment replace() to use replaceAll()
- Add staleHours CLI option (was env-var only)
- Move state persistence from /tmp to ~/.cache/pr-patrol/state/
  with backward-compatible migration from legacy location
- Parallelize overlap detection file fetches (Promise.allSettled)
- Include PR #1826 budget/timeout improvements in the decomposed modules
- Add 46 new tests (scoring, prompts, detection) → 74 total

Closes #1830
Closes #1831

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OAGr added a commit that referenced this pull request Mar 7, 2026
- Split 1,560-line index.ts into 8 focused modules:
  types.ts, state.ts, detection.ts, scoring.ts, merge.ts,
  prompts.ts, execution.ts, reflection.ts
- Add pr-patrol/**/*.test.ts to vitest config (tests were silently ignored)
- Fix empty catch blocks violating error-handling rules
- Add missing stale PR section in buildPrompt()
- Fix summary comment mid-line truncation (take last ~500 chars by line)
- Fix overlap comment replace() to use replaceAll()
- Add staleHours CLI option (was env-var only)
- Move state persistence from /tmp to ~/.cache/pr-patrol/state/
  with backward-compatible migration from legacy location
- Parallelize overlap detection file fetches (Promise.allSettled)
- Include PR #1826 budget/timeout improvements in the decomposed modules
- Add 46 new tests (scoring, prompts, detection) → 74 total

Closes #1830
Closes #1831

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OAGr added a commit that referenced this pull request Mar 7, 2026
- Split 1,560-line index.ts into 8 focused modules:
  types.ts, state.ts, detection.ts, scoring.ts, merge.ts,
  prompts.ts, execution.ts, reflection.ts
- Add pr-patrol/**/*.test.ts to vitest config (tests were silently ignored)
- Fix empty catch blocks violating error-handling rules
- Add missing stale PR section in buildPrompt()
- Fix summary comment mid-line truncation (take last ~500 chars by line)
- Fix overlap comment replace() to use replaceAll()
- Add staleHours CLI option (was env-var only)
- Move state persistence from /tmp to ~/.cache/pr-patrol/state/
  with backward-compatible migration from legacy location
- Parallelize overlap detection file fetches (Promise.allSettled)
- Include PR #1826 budget/timeout improvements in the decomposed modules
- Add 46 new tests (scoring, prompts, detection) → 74 total

Closes #1830
Closes #1831

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OAGr added a commit that referenced this pull request Mar 7, 2026
- Split 1,560-line index.ts into 8 focused modules:
  types.ts, state.ts, detection.ts, scoring.ts, merge.ts,
  prompts.ts, execution.ts, reflection.ts
- Add pr-patrol/**/*.test.ts to vitest config (tests were silently ignored)
- Fix empty catch blocks violating error-handling rules
- Add missing stale PR section in buildPrompt()
- Fix summary comment mid-line truncation (take last ~500 chars by line)
- Fix overlap comment replace() to use replaceAll()
- Add staleHours CLI option (was env-var only)
- Move state persistence from /tmp to ~/.cache/pr-patrol/state/
  with backward-compatible migration from legacy location
- Parallelize overlap detection file fetches (Promise.allSettled)
- Include PR #1826 budget/timeout improvements in the decomposed modules
- Add 46 new tests (scoring, prompts, detection) → 74 total

Closes #1830
Closes #1831

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant