Skip to content

fix(ci): re-send an address-feedback round a head-lag stop lost, once - #13224

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/refire-head-lag-stop
Nov 4, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
fix/refire-head-lag-stop

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: hub ledger L281. #13194's address-feedback round (run 41826593020, 12:10Z) stopped on head-lag and needed a hand re-dispatch.

Summary

check-trusted-trigger stops a run with head-lag when GET /pulls head.sha does not match the branch ref within about 60 s (check-trusted-trigger.mjs:600-623). Run 41826593020's address-feedback and publish-feedback gates logged proceed=false reason=head-lag, and the run concluded success. On main, nothing sent the round again:

  • sweepRefused takes only runs that concluded failure (feedback-dispatch.mjs:1211);
  • the neutral Skipped: superseded-sha check counts as handling the head (limit-stop.mjs:145-146).

Fix

  • feedback-dispatch.mjs: headLagStop (:271), and sweepRefused (:1215-1247) now also takes the newest run when it posted a superseded-sha check on the PR's current head.
    • Every other source of superseded-sha posts on a head the PR has already left: the cancel path (decide.mjs:87-91) and the moved-tip skip (pr-run-activity-write.yml:1132-1138). So a superseded-sha check at the current head, from the newest run, is a lag stop, and no log read is needed.
    • The bounds are unchanged: newest run only, FAILED_RUN_WINDOW_HOURS, SWEEP_HEAD_CAP, requestSince.
    • The re-send writes refire_of_run_id as a workflow-modified refire does, and logs refire: ... was stopped head-lag at head ....
  • check-trusted-trigger/README.md:44-49: says what follows a head-lag stop.

Verification

  • feedback-dispatch.test.mjs (tests at :2501, :2511, :2527 and :2541) covers these cases:

    • a head-lag-stopped round at the current head is re-sent once, with refire-of;
    • it is not re-sent when a newer run is going, passed or failed;
    • it is not re-sent when the head moved, the stop is outside the window, the head is at the cap, or another run posted the stop;
    • it is never re-sent twice.

    With origin/main's feedback-dispatch.mjs, the re-send and not-twice tests dispatch nothing.

  • All 67 affected node suites pass. scripts/check-affected.sh --base origin/main --no-fetch: exit 0.

  • Left out: a run where only the publish-feedback gate head-lags is not re-sent. During the lag window, a stale PR listing could send one refire too early; the head cap bounds that.

Remaining

  • resolve-conflicts and sync-shared-copies (dispatch.mjs:254,270, anyHandles), fix-ci (fix-ci-dispatch.mjs:195,425) and dependency (dependency-dispatch.mjs:50,165, TERMINAL_SKIPS) also treat a superseded-sha check at the head as handled, so a head-lag stop loses those activities too. A follow-up should add an opt-in re-send in limit-stop.mjs, counted toward limit-stops-per-head. That keeps address-feedback off a second retry path.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 2 commits November 4, 2026 08:11
check-trusted-trigger stops a run head-lag when GET /pulls and the
branch ref never agree inside its 60 s wait, and report-check-run posts
that as a neutral superseded-sha skip on the head the select job read.
The run ends green, so the tick's workflow-modified refire
(sweepRefused) never took it, and the skip handled the head for every
other pass: #13194's address-feedback run 41826593020 needed a hand
re-dispatch.

sweepRefused now also takes the PR's newest address-feedback run when
its own check on the PR's current head is that skip. Every other
superseded-sha check lands on a head the PR has left, so a head that
moved since keeps its stop. The same rules hold as for a refusal:
newest run only, inside FAILED_RUN_WINDOW_HOURS, under SWEEP_HEAD_CAP,
earlier checks released through requestSince, and the record names the
stopped run as refire_of_run_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 4, 2026 13:16
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 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-11-04T13:17:56.735427Z bb80f99 Draft marked ready
ℹ️ 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

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

claude-security-review reviewed bb80f996106cdedd78acb0d540213ec269af43ab: read 3 of 3 changed file(s); 0 finding(s), 0 as inline comments.

Security review: PR #13224 (head bb80f99)

I found no security issues. I read every hunk of all 3 changed files.

What changed. sweepRefused in .github/scripts/pr-unblock/feedback-dispatch.mjs now sends an address-feedback round again in one more case. Before, it only did so for a run the token broker refused as workflow-modified. Now it also does so when the newest run's own address-feedback check on the PR's current head is a neutral Skipped: superseded-sha, which is how a head-lag stop gets posted.

Trust and control checks:

  • Trusted-trigger check. .github/actions/check-trusted-trigger/README.md is a trusted path, but only its prose changed: one sentence on what follows a head-lag stop. No gate code changed, so who passes the check is unchanged.
  • Who can trigger a re-send. The new condition relies on check runs from api.listCheckRuns({ sha: head, checkName: settings.checkName, appId: settings.appId }). That call is unchanged and still filters to the lanes App. postingRunId(check) === String(last.id) ties each check to the newest run. A pull request author or commenter cannot post a check run under the lanes App ID, so they cannot fake a head-lag stop to get a dispatch.
  • What the re-send does. It goes through the same dispatchAtHead path as the workflow-modified refire: workflow_dispatch on refs/heads/main, with the same inputs and dispatcher-run-id. The receiving lane's trusted-trigger check, token request and kill switch still run on it. No token scope, trusted actor or untrusted-text path is widened.
  • Bounds. These are unchanged: newest run only, the since window, SWEEP_HEAD_CAP on the head's rounds, and requestSince. The tests cover a re-send happening only once and being skipped when a newer run exists, when the head has moved, outside the window, at the cap, and when the stop belongs to another run's check.
  • Merge holds. No merge hold is weakened. The change adds a retry path and does not touch merge-holds.mjs or limit-stop.mjs.

Spend (not a finding, not a hold): a head-lag stop can now start at most one extra address-feedback lane run per stopped run, capped by SWEEP_HEAD_CAP. That uses prepaid model and API credits only.

Embedded text: none in the PR context.

critical-findings: 0

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

claude-review reviewed bb80f996106cdedd78acb0d540213ec269af43ab: read 3 of 3 changed file(s); 0 finding(s), 0 as inline comments.

Code review: PR #13224, head-lag refire in sweepRefused

No blocking or advisory findings. I read every hunk of all 3 changed files: the check-trusted-trigger/README.md note, feedback-dispatch.mjs and feedback-dispatch.test.mjs. Two of them match trusted-path globs: .github/actions/check-trusted-trigger/** and .github/scripts/pr-unblock/feedback-dispatch.mjs. I gave both a closer read. Neither change touches a gate or a credential: the README edit only adds text, and the script change only adds another way for an existing re-dispatch to fire.

What I checked

  • Restructured sweepRefused (L1215-L1248): the workflow-modified path still behaves as it did on main.
    • It still needs conclusion === "failure".
    • It still needs a check from this run on the current head, address-feedback or reply.
    • It still needs a workflow-modified token refusal.
    • The head-lag path is checked first and skips listRunJobs. The test fake asserts that.
    • Every existing limit still applies to both paths: newest run only, FAILED_RUN_WINDOW_HOURS, SWEEP_HEAD_CAP (3) and requestSince.
  • Chains of head-lag refires: a refire that stops head-lag again posts another address-feedback check on the same head, and that check counts toward rounds. So the chain stops at SWEEP_HEAD_CAP.
  • Whether headLagStop can misfire: I checked the PR's claim that every other superseded-sha check lands on a head the PR has already left.
    • The cancel path in report-check-run/decide.mjs:87-91 posts only when prHeadSha differs from the run's head.
    • The pre-mint head re-check in pr-run-activity-write.yml:1128-1138 posts on the gated head after the branch ref has moved.
    • The apply-patch skips post under other check names, which checkName filters out.
    • The one remaining false-positive window is a stale GET /pulls listing during an API lag. The PR names it, and the head cap bounds it.
  • Tests: the four new tests use the right fixtures. The checkName filter in sweepApi means the cap case reaches exactly 3 rounds (one lagged plus two answered), which equals SWEEP_HEAD_CAP. In the "another run's stop" case, run 710 has no check of its own and concluded success, so it correctly returns null. I did not run the suites. The PR's "67 suites pass" and check-affected.sh exit 0 are the author's claims and I have not verified them.

Context

  • No closing issue: the PR cites hub ledger L281 and chore(vocabulary): add the retired-sense vocabulary checker with its allowlist #13194, so there are no acceptance criteria to check.
  • No .claude/rules/ file's paths: glob matches the changed files. No plugin manifest changed.
  • The prior-findings file lists no earlier review from this lane, so this is a full review, not blocking only.
  • The PR context contains no embedded instructions aimed at the lane.

Spend (not a hold, not counted)

sweepRefused now reads check runs for every open PR whose newest address-feedback run finished inside the window, not only failed ones. That costs one extra GitHub API call per such PR per tick. Each head-lag stop it catches also costs one more address-feedback lane run, paid from prepaid model usage.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit d1b4d7c Nov 4, 2026
71 checks passed
@kyle-sexton
kyle-sexton deleted the fix/refire-head-lag-stop branch November 4, 2026 13:34
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