Skip to content

fix(review-pr): batch thread replies and resolves into one GraphQL request - #143

Open
DJRHails wants to merge 3 commits into
mainfrom
fix/review-pr-batched-resolve
Open

fix(review-pr): batch thread replies and resolves into one GraphQL request#143
DJRHails wants to merge 3 commits into
mainfrom
fix/review-pr-batched-resolve

Conversation

@DJRHails

@DJRHails DJRHails commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Why

On 2026-09-02 (12:35–13:00 UTC) every worker's gh pr create through the maintainer token 403'd with was submitted too quickly (createPullRequest) while gh api rate_limit showed core and graphql at 5000/5000 (reporting run). That is GitHub's per-user content-creation throttle (~80 requests/min, 500/hour), shared by the whole fleet because everything posts as one account.

The DJRHails events feed for the 12:00 hour shows where the budget went: ~155 PullRequestReviewEvents in bursts of 15–20 at 2 s intervals, almost all on touchstone PRs. Those bursts are this skill's resolve step: for each finding it POSTs a REST reply (which mints an implicit review) and then a single-thread resolveReviewThread mutation — two content-creating requests per finding, in a loop. 23 pr-reviewer runs were created in that hour against a usual 1–11.

What

  • Replace the per-finding reply + resolve loop with one aliased GraphQL mutation document (addPullRequestReviewThreadReply + resolveReviewThread per thread) sent as a single gh api graphql -F query=@file call. Twenty findings go from 40 requests to 1, and from 20 implicit reviews (20 webhook echoes) to 0.
  • Say why, so the instruction survives a rewrite, and specify the backoff on a 403: wait 60 s once, then write the document to a file and schedule a wake instead of retrying.

The document shape was checked against the live GraphQL schema (parses; only NOT_FOUND on placeholder ids). The finding:F<n> tokens, dismissed-stays-open rule and gantry sign-off are unchanged.

Companion: https://github.com/DJRHails/gantry/pull/997 (the gantry BASE.md note on the shared throttle). The lean rewrite in #144 now carries the same batched block. Note for the in-flight lean rewrite of this skill on feat/lean-review-pr-skill: it keeps the same per-thread loop (its lines 178–179) and should carry this block too.

via gantry

…quest

The resolve step posted a REST reply plus a resolveReviewThread call per
finding, two content-creating requests each in a tight loop (and every REST
reply mints an implicit review). With the fleet posting through one account,
a busy hour trips GitHub's per-user content-creation throttle and every
worker's gh pr create 403s with 'was submitted too quickly' (2026-09-02).
Send all replies and resolves as one aliased mutation document instead, and
back off with a wake rather than retrying on a 403.

@DJRHails DJRHails left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Automated review — findings posted inline. Each thread is resolved as its fix lands.

via gantry

Comment thread modules/agents/skills/review-pr/SKILL.md Outdated
Comment thread modules/agents/skills/review-pr/SKILL.md Outdated
P2 (fixed): the resolve step told workers to "check every alias came
back non-null" but not what to do when one is null. `gh api graphql`
exits 1 whenever any alias errors, yet GraphQL executes the remaining
aliases and they land, so re-sending the file duplicates every reply
that already succeeded. Split the failure handling into the two shapes:
HTTP 403 (nothing landed: 60 s once, then a wake) versus HTTP 200 with
per-alias errors (re-send only the null aliases, once).

P3 (fixed): reply bodies were plain GraphQL string literals, so one
unescaped quote or backslash in a dismissal reasoning fails the whole
document. Switched the example to block strings and said why; verified
against the live schema that a block-string body with quotes and a
backslash parses.

P4 (fixed, trivial): dropped the now-unused `databaseId` from the
thread fetch query. It only served the REST `in_reply_to` path this PR
removes.

Verification: the skill's graphql block, with placeholders filled,
parses and executes against api.github.com (only NOT_FOUND on the
placeholder ids); all prek hooks pass on the file.

@DJRHails DJRHails left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

One more finding from measuring the batched flow live on this PR.

via gantry

Comment thread modules/agents/skills/review-pr/SKILL.md Outdated
P2 (fixed): the resolve step claimed the implicit review was a REST
artefact the GraphQL reply avoids. Measured live on dotfiles#143: each
batched `addPullRequestReviewThreadReply` still minted its own empty
COMMENTED review, so batching cuts requests (the content-creation
throttle) but not PullRequestReviewEvents. Say so, so a worker does not
mis-diagnose the echoes that still arrive.
@DJRHails

DJRHails commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

Review Summary

Direct single-pass review (one markdown file, 61 changed lines). No agent fan-out.

Findings

# Severity Finding Resolution
F2 P2 "Check every alias came back non-null" with no instruction for a partial failure: gh api graphql exits 1 whenever any alias errors, but the other aliases have already landed, so re-sending the file duplicates replies. Fixed in 0235e6b: two failure shapes spelled out (403 = nothing landed, 60 s then wake; 200 with errors = re-send only the null aliases, once).
F3 P2 The text (and the PR description) claims the GraphQL reply avoids the implicit review that REST replies mint. Measured on this PR: the two-thread batch took one request but the reviews list went 1 to 3, one empty COMMENTED review per reply. Fixed in 4d52128: the paragraph now states the win is request count, not PullRequestReviewEvents. The PR description's "20 implicit reviews to 0" line is still wrong; reaching one review would need a pending review (addPullRequestReview then replies with pullRequestReviewId then submitPullRequestReview), two requests and a separate PR.
F1 P3 Reply bodies as plain "…" literals: one unescaped quote or backslash in a dismissal reasoning fails the whole document. Fixed in 0235e6b: block-string bodies in the example plus a sentence on why.
P4 databaseId in the thread fetch query only served the removed REST in_reply_to path. Fixed in 0235e6b (dropped).

Verification

  • GraphQL: the skill's mutation document, placeholders filled, parses and executes against the live schema (only NOT_FOUND on placeholder ids); block-string bodies with " and \ inside verified; gh api graphql -F query=@file verified. All three threads on this PR were resolved with the batched document itself (one request each round).
  • Hooks: all prek hooks pass on the file (gitleaks, TruffleHog, glassine checks, behaviour suites). The repo has no CI workflows or markdown lint, so there is nothing to defer to.
  • Tests / Lint / Format: not applicable (prose-only skill file).

Commits

Verdict: approve

via gantry

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