Skip to content

fix(pr-pipeline): treat an auth failure as a limit stop and carry the lane record on read-lane checks - #10242

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/resolve-conflicts-dead-runs
Oct 26, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
fix/resolve-conflicts-dead-runs

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Refs: #9826

Summary

From about 21:35Z to 22:44Z on 2026-10-25, PR_UNBLOCK_CLAUDE_API_KEY and PR_UNBLOCK_CLAUDE_CODE_OAUTH_TOKEN both returned 401. Each resolve-conflicts run's key leg handed off, so its scan and report jobs were skipped as designed. The subscription leg then ended class=auth (annotation: resolve-conflicts ended class=auth on PR_UNBLOCK_CLAUDE_CODE_OAUTH_TOKEN api_error_status=401, job 121299868428), and no-model-turn / "Fail without a result" followed from that.

The posted check runs carried no lane record (summary The activity step failed.): pr-run-activity-read.yml never passed the record to report-check-run. limit-stop.mjs therefore read each check as a plain failure, and dispatch.mjs retried at once. #9826's head ab760e9 spent all three runs between 21:44Z and 22:07Z and was recorded at the cap before the secrets were re-set. #10121 and #10118 went the same way.

Fix

  • pr-run-activity-read.yml: the run job exposes the lane record's failure-class and resets-at, and the report job passes them to report-check-run as lane-class and lane-resets, the same way pr-run-activity-write.yml does.
  • limit-stop.mjs: class=auth is a limit stop. It names no reset, so it resets limits.fallback-reset hours after the run, and it still counts toward limit-stops-per-head. New pendingStop returns the unreset stop that resets last.
  • dispatch.mjs: while a head waits only on an unreset stop, it logs ::notice::#N: head <sha>'s pr-unblock / resolve-conflicts stopped on class=<class>; no resolve-conflicts before <time>. The cap warning now ends with ; the last run stopped on class=<class> when the newest run was a stop.
  • Forged records: the run job that sets these outputs can run head code, so its record can be forged. That was already true of the write lanes. A forged stop can only restart the lane after a clamped reset, at most limit-stops-per-head times. For address-feedback and review-dependency, whose dispatchers do not retry failures, that is up to three restarts where there used to be none. This is the same bounded risk the write lanes already accept.
  • README Limits and resolve-conflicts dispatch text updated.

Under this rule, each head would have spent one run during the outage and retried after the reset.

Verification

  • node --test over limit-stop, dispatch, feedback-dispatch and dependency-dispatch tests: 196 pass, 0 fail.
  • resolve-config/callers.test.mjs: 183 pass, 0 fail. It includes a new wiring test for the read workflow's lane record.
  • report-check-run tests: 101 pass, 0 fail.
  • actionlint .github/workflows/pr-run-activity-read.yml: clean.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 2 commits October 25, 2026 23:55
From 21:35Z to 22:44Z on 2026-10-25 both PR_UNBLOCK secrets returned 401.
Each resolve-conflicts run's key leg handed off (scan and report skipped
by design), the subscription leg ended class=auth, and its check carried
no lane record, because pr-run-activity-read.yml never passed the record
to report-check-run. limit-stop.mjs read each as a plain failure and
dispatch.mjs retried at once, so #9826's head spent its three runs in 23
minutes and was recorded at the cap before the secrets were re-set.

The read workflow now posts the lane record's class and reset, as the
write workflow does. limit-stop.mjs treats class=auth as a limit stop
that resets fallback-reset hours after the run, still counted toward
limit-stops-per-head so a forged record cannot buy unbounded runs, and
dispatch.mjs logs a notice naming the class and reset while a head waits.

Refs: #9826

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-dead-runs

# Conflicts:
#	.github/scripts/pr-unblock/dispatch.mjs
#	.github/workflows/pr-run-activity-read.yml
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T09:39:34.820163Z 893071a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed 893071a30bdb89bf349134f2575d3455e427ed3b: 0 finding(s), 0 as inline comments.

No findings. I read the full diff (.claude-lane/pr.diff) and the PR context, and checked the changed code against the files it touches. I ran no tests or commands; the PR's test counts are the author's claim and unverified by me.

What I checked

  • Workflow wiring. pr-run-activity-read.yml now exposes record-class and record-resets from the lane-record step (.github/workflows/pr-run-activity-read.yml:1718). The report job passes them to report-check-run as lane-class and lane-resets (:3381-3382). That matches how pr-run-activity-write.yml does it (:491-492, :2230-2231). The step is skipped on a handoff and records nothing, so report-check-run then posts no record, as before.
  • Record shape. .github/actions/report-check-run/lane-record.mjs accepts auth as a class and writes resets only for rate-limit. An auth record therefore has no reset, and limit-stop.mjs falls back to fallback-reset through clampReset, as the PR describes.
  • limit-stop.mjs. limitStop still requires a completed run with conclusion failure. A success or a skipped check carrying a class is never a stop. limitResetsAt keeps its old contract, so the other callers behave consistently: feedback-dispatch.mjs:345 and dependency-dispatch.mjs:142 keep pre-cut auth stops, and anyHandles and permanentlyHandles count them toward limit-stops-per-head.
  • dispatch.mjs. The waiting notice fires only when the head is handled now but would not be handled at now: Infinity. A head held back for another reason (a success, or a skip while held) is not mislabelled, and the second new test covers that. The cap-warning suffix reuses limitStop on the newest completed run.
  • Forged records. The PR body states the risk and bounds it: a forged class can only restart the lane after a clamped reset, at most limits.limit-stops-per-head times per head. That is the same exposure the write lanes already carry. The record is not blanked when steps.head.outputs.forged == 'true', unlike gate-reason. A forged run result there is posted as failure on the trusted head. I judged this acceptable under the PR's own bound, so I raised no finding.

Scope notes

  • No changed file matches a TRUSTED PATHS glob, and there is no .claude-plugin manifest change, so there are no manifest lines.
  • The change fragment is bump: patch, which fits a fix.
  • The PR names the fix-ci cap (fix-ci-dispatch.mjs capCount) as left for a follow-up. I did not count that as a finding.
  • No closing issue is attached; the PR says Refs: #9826, so there are no acceptance criteria to check.
  • This change spends no billed resource, so I set no hold.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 893071a30bdb89bf349134f2575d3455e427ed3b: 0 finding(s), 0 as inline comments.

No security issues found in this pull request.

critical-findings: 0

What I read. All 8 changed files, limit-stop.mjs in full, the lane-record step in pr-run-activity-read.yml, and how report-check-run handles the new inputs.

Trusted paths. None of the changed files match a TRUSTED PATHS glob, and none of the trust-control bullets apply. limit-stop.mjs and dispatch.mjs are not among the pr-unblock/ modules the merge-hold scripts import. Nothing here changes the trusted-trigger check, the lane token scope, the kill switch, the broker's loaded rules, or any merge hold. No manifest or instruction-surface files changed, so there are no manifest <field> lines.

Forged lane record.

  • The new record-class and record-resets outputs come from a step in the job that runs head code, so a PR author could try to forge them.
  • report-check-run limits what they can write into the check summary. lane-record.mjs:37 keeps resets only for rate-limit and only when it matches a strict ISO timestamp (RESETS regex). limit-stop.mjs then re-parses the first summary line against a strict regex and requires the check name to match lane / activity.
  • The worst case is a forged auth or rate-limit stop. clampReset limits its reset to between 1 hour and 7 days, or fallback-reset (5 hours) when no reset is named. It then counts toward limit-stops-per-head (3). The effect is that the lane restarts at most 3 times on a head, or waits for a bounded time. This matches what the write lanes already accept. The restarts spend only prepaid or subscription credit.
  • The new "wait on auth" behaviour only delays a retry, and the cap still ends it. It grants no token scope, bypasses no hold, and passes no untrusted text to a lane.
  • The workflow change does not widen permissions. It adds only two job outputs and two with: inputs on an already-pinned action.

Log injection. The new ::notice:: and ::warning:: lines interpolate only failureClass, which the [a-z-]+ regex restricts, and a reset time formatted with toISOString(). The remaining values are PR numbers and SHAs.

Spend. The change can cause up to 3 extra lane restarts on a head with a forged or genuine stop. It spends prepaid or subscription usage only, so it is not a hold and not counted as a finding.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit b43f53c Oct 26, 2026
38 checks passed
@kyle-sexton
kyle-sexton deleted the fix/resolve-conflicts-dead-runs branch October 26, 2026 10:11
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