Skip to content

fix(ci): send every released head on a sweep tick, up to a cap - #13105

Merged
kyle-sexton merged 5 commits into
mainfrom
fix/feedback-sweep-every-released-head
Nov 4, 2026
Merged

kyle-sexton merged 5 commits into
mainfrom
fix/feedback-sweep-every-released-head

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: arm-wait trend (poteto, .work/gh-monitor/trends/arm-wait/result.md, cause 1); a sweep tick re-sends at most one released PR, so every other released head waits a full tick.

Summary

The half-hourly pr-unblock tick's sweep (feedback-dispatch.mjs sweepReleased) returned on the first PR it dispatched (sweepPulls), so one tick re-sent one PR. On 2026-11-04 the 02:18Z tick (pr-feedback-dispatch run 41759929522) sent only #13032; #13031, whose publish the kill switch had stopped at 01:52, waited for the 02:40Z tick (round 41762504146). Each extra released head adds a tick, and ticks start 5 to 25 min late.

Sending more than one needs each dispatched run's own pr-feedback-dispatched-<up>-<run> record within the trust gate's window: check-trusted-trigger.mjs requireRecorded stops a dispatched run whose record is missing after about 2 min of waits, and records upload only after the dispatch step. So the sweep now decides first and sends after, back to back.

Fix

  • feedback-dispatch.mjs: on a sweep tick, dispatchFeedback and sweepStale return the decided request instead of sending it. The sweep collects up to SWEEP_TICK_CAP (5) PRs across the passes in their existing order, with a set of PR numbers so none is sent twice, then sends them back to back. main() writes run-id/upstream-run-id for the first send and run-id-N/upstream-run-id-N for the rest. A decide-time budget (SWEEP_DECIDE_BUDGET_MS, 5 min) keeps the sends inside the gate's 10-min WINDOW_MS (check-trusted-trigger.mjs:33). Single-PR paths and recoverLost (only on a tick that sent nothing, at most one) are unchanged.
  • pr-feedback-dispatch.yml: record, scan and upload steps for sends 2 to 5, each guarded on its own run-id-N. The trust gate is untouched: findRecord already reads every artifact of the dispatcher run.
  • docs/conventions/pr-pipeline/README.md: three "one per tick" statements corrected; a .changes/pr-pipeline/ fragment.

Risk

  • More rounds per tick: at most 5 dispatches, each costing the reads one dispatch costs today, under the GITHUB_TOKEN 1,000/h planning line.
  • The token broker's reading of pr-feedback-dispatched-* records, if it reads them, was not checked (UNVERIFIED); the gate in this repository is the only reader found.

Verification

  • feedback-dispatch.test.mjs: 222/222. Three new or changed tests (two released heads both sent, the cap, fix-request-before-released ordering) fail on origin/main's code.
  • Suites that read the workflow pass: workflow-gates, resolve-config, log-api-budget, check-trusted-trigger (404), reconcile, refire-refused, address-feedback, ci-rerun-ci-status, ci-review-lanes, read-runner-dispatcher-gate and others; actionlint and zizmor (offline) clean.
  • scripts/check-affected.sh --base origin/main --no-fetch: exit 0.
  • Left out: a live tick with two or more released heads; the next such tick shows it.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits November 4, 2026 00:34
The half-hourly sweep returned on the first PR it dispatched, so each
released head beyond the first waited a tick (about 30 min) per PR.

The sweep now decides up to SWEEP_TICK_CAP (5) PRs across its passes,
in pass order, each PR at most once, and stops deciding after
SWEEP_DECIDE_BUDGET_MS (5 min) so the dispatch job's 10-minute timeout
holds. dispatchFeedback and sweepStale return a send step when the
sweep defers them; the sweep sends the decided rounds back to back, so
each dispatched-run record uploads seconds after its POST, inside the
trusted-trigger gate's 2-minute wait. main() writes the first send as
run-id/upstream-run-id and later ones as run-id-<n>/upstream-run-id-<n>
as each is sent, and pr-feedback-dispatch.yml records each through its
own numbered record, scan and upload steps. Lost-run recovery still
runs only on a tick that sent nothing, and still sends at most one.

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

The pipeline README said the sweep sends one head per tick; it now
states SWEEP_TICK_CAP. SWEEP_DECIDE_BUDGET_MS's comment now cites the
binding limit, check-trusted-trigger.mjs WINDOW_MS (10 minutes from the
dispatcher run's start), with setup measured at 18 s on run
41759929522: 5 min deciding plus one 2-minute verifier read in flight
leaves about 2.5 minutes. Adds the pr-pipeline changelog fragment.

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 06:05
@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-04T06:07:12.147044Z 35f8fbc 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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35f8fbcbe4

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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

Security review (incremental, 35f8fbc..4c9765f): no security issues found

critical-findings: 0

I read every hunk of both files in scope: .github/scripts/pr-unblock/feedback-dispatch.mjs and .github/scripts/pr-unblock/feedback-dispatch.test.mjs. The first is on the trusted-path list, so I also read the whole of sweepReleased (feedback-dispatch.mjs#L850-L909) and the main() caller at #L1669 that consumes its result.

What changed. The send loop now wraps each send() in a try/catch. If one POST throws, the loop logs a ::warning:: and moves on to the next decided round. Before this change, the throw aborted the remaining sends.

What I checked:

  • Records and the trust gate. A rejected POST skips both sent.push and onSent, so the workflow writes no run-id-N/upstream-run-id-N record for it. Now take a POST that errored but still started a workflow run: that run has no pr-feedback-dispatched-* record, so check-trusted-trigger's requireRecorded refuses it. This fails closed. The rounds that do succeed still pair each result with that same round's upstreamRunId (#L898), so no record can name another PR's upstream run.
  • recoverLost fallback. When every send fails, sweepReleased returns null and recoverLost runs. Before, the exception propagated instead. recoverLost still sends at most one round and goes through the same record and gate path. This widens nothing.
  • Log line. error.message comes from the API client's error, and the same pattern already exists at #L887. It adds no new untrusted-text sink, and it does not log a token.

Neither changed file is a merge hold, the kill switch, a token request, a plugin manifest or an instruction surface. The test file only adds a unit test.

The PR description contains no instructions aimed at this lane. No settled threads apply. The prior finding was the summary comment from the last round, which reported no issues, so there is nothing to repeat.

Spend (not a finding): no change beyond the earlier review's note. A failed POST no longer ends the tick early, so the remaining rounds, at most SWEEP_TICK_CAP (5), are still sent. This uses the same GITHUB_TOKEN API calls and lane runs the full sweep already accounted for.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-review: 1 inline finding(s) on 35f8fbcbe4f372eb70bfc21bdbdfbb67111dd2d1; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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

Incremental code review: 35f8fbc → 4c9765f (#13105)

Scope: this push changed 2 files, feedback-dispatch.mjs and feedback-dispatch.test.mjs. I read every hunk of both, and used the full diff only for context.

No new findings.

The earlier finding is fixed

That finding was "a rejected POST now stops the rest of the tick's sends and fails the job". The send loop in sweepReleased now gives each send() its own try/catch (feedback-dispatch.mjs#L895-L907). A failed send logs ::warning::#<n>: … and the loop goes on, which is how the per-PR sweep behaved before. I checked these points:

  • Every decided round has a number. dispatchFeedback returns { number, send } when deferred (L801). sweepStale returns { number, send, upstreamRunId } (L1048). The wrappers at L1108 and L1176 copy those fields, so the warning always names the PR.
  • Output numbering is right after a failure. A failed send never reaches onSent. The write counter in main() (L1662-L1667) counts only successful sends, so the first send that succeeds still gets run-id, and the record steps keyed on run-id/run-id-N stay in line.
  • When every send fails, the fallback matches the old behaviour. sweepReleased returns null and main() falls back to recoverLost, just as it did when sweepPulls dispatched nothing.
  • The comment at L1185-L1186 is accurate again. It says a rejected POST "logs it and ends green".
  • The new test makes dispatchWorkflow throw HTTP 502 for feat: add guardrails plugin (bundled safety hooks) #42, then checks that feat: add bug-report plugin #43 and feat: add diagnose plugin #44 are still POSTed and that the warning is logged. It would fail on the old loop. It does not check the GITHUB_OUTPUT contents after the failure, so the point about run-id going to feat: add bug-report plugin #43 comes from reading the code.

Verification: everything above comes from reading the code. The author reports 223/223 passing in feedback-dispatch.test.mjs. That is their claim; I did not run the tests.

Settled thread: the owner's reply on the L899 thread says it was fixed in this commit, and the code matches. Nothing to raise again.

Spend: no change since the last review. A tick can still start up to 5 pr-unblock.yml address-feedback rounds, which uses lane model usage and GITHUB_TOKEN reads, not a billed resource.

No plugin manifest changed. The PR has no closing issue. The PR context and settled-threads files contain no embedded instructions to this lane.

The deferred send loop had no per-PR catch, so one rejected POST (a 502, a
secondary rate limit) threw out of the sweep, failed the dispatch job and
left the rounds after it unsent. Each send now logs its failure and the loop
goes on, as the per-PR sweep did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 8f87fee Nov 4, 2026
53 checks passed
@kyle-sexton
kyle-sexton deleted the fix/feedback-sweep-every-released-head branch November 4, 2026 06:25
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