Skip to content

fix(webhook): scope push-driven reviewer wakes to the head, not the delivery (PEN-2865) - #1916

Merged
kkroo merged 2 commits into
masterfrom
fix/pen-2865-head-scoped-reviewer-wake
Sep 19, 2026
Merged

kkroo merged 2 commits into
masterfrom
fix/pen-2865-head-scoped-reviewer-wake

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 18, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • One of those agents is the PR reviewer (allyblockcast[bot]), woken by the GitHub webhook route; the review/ally-complete gate treats its review as the authoritative verdict for a PR head
  • That gate assumes one operative verdict per head, and the ally-review-consistency guard enforces the assumption (BLO-19778) — but the reviewer kept posting two reviews on a single unchanged head, so a PR could present as reviewed while carrying two competing attestations with no rule saying which is operative
  • It needed addressing because the duplicates are not cosmetic: they land on security PRs, they burn the reviewer's maxConcurrentRuns budget on a review that already exists, and every new occurrence reds the consistency guard until a human tends its baseline
  • This pull request scopes the two push-driven reviewer wakes to the head sha instead of the webhook delivery id, so two deliveries reporting one unchanged head collapse to a single wake
  • The benefit is that the one-verdict-per-head invariant holds at the source, which makes the CI guard quiet-by-default instead of quiet-by-maintenance

Linked Issues or Issue Description

Refs PEN-2865 — "Ally posts two operative reviews on a single unchanged PR head".

Related, and deliberately not duplicated by this PR (same root signature, different sink):

This PR is the review sink, which had no equivalent guard. It is branched from master and is independent of #1740/#1900 — no stacking, so the full check set applies.

What Changed

  • server/src/routes/github-webhook.tswakeIdempotencySuffix() gains an optional headScopedReasons set, checked before the delivery-scoped branch. When the reason is head-scoped and a head sha is present, the suffix becomes <reason>:head:<sha>.
  • Added REVIEWER_HEAD_SCOPED_WAKE_REASONS (github_pr_synchronized, github_pr_ready_for_review) and passed it at the two reviewer call sites. The PR-author path is untouched (it head-scopes nothing, via the NO_HEAD_SCOPED_WAKE_REASONS default).
  • Kept both reasons in REVIEWER_DELIVERY_SCOPED_WAKE_REASONS as the fallback for an event carrying no head sha — see Risks for why that fallback is load-bearing rather than defensive clutter.
  • Updated the two docblocks that described these reasons as delivery-scoped.
  • server/src/__tests__/github-webhook.test.ts — added the PEN-2865 e2e regression (two distinct deliveries, one unchanged head → exactly one wake row and one reviewer run) plus key-level assertions that two delivery ids on one head produce one key while a new head still produces a new one. Re-pointed 11 delivery-scoped key literals at their head-scoped form.
  • Rewrote the scope-classification case whose premise the change invalidates: a context with a head but no delivery id is now request-scoped (the head supplies the identity). Replaced it with a genuinely identity-free case — no head and no delivery — which still scores stable, preserving the invariant the old case was there to carry.
  • server/src/__tests__/heartbeat-pr-review-gate-replay.test.ts — one key literal.

Verification

Evidence the defect is one review computed twice, not two review passes. On #1594 the two reviews at c760735e are byte-identical, 866 bytes each, same Reviewed head: attestation, 26s apart. The same PR also double-posted its back-link at the same second (17:48:52Z) — the concurrent-duplicate-delivery signature, directly observed.

node vitest.mjs run src/__tests__/github-webhook.test.ts
  → Test Files 1 passed | Tests 266 passed (266)

node vitest.mjs run src/__tests__/heartbeat-pr-review-request-coalescing.test.ts \
                     src/__tests__/heartbeat-pr-review-queue-fairness.test.ts \
                     src/__tests__/heartbeat-pr-review-gate-replay.test.ts
  → Test Files 3 passed | Tests 25 passed (25)

Mutation-checked — the new tests actually discriminate. Emptying REVIEWER_HEAD_SCOPED_WAKE_REASONS (i.e. the fix absent, everything else identical) fails exactly the two PEN-2865 tests, and the e2e one fails for the right reason:

× drives ONE reviewer wake when two distinct deliveries report the same unchanged head (PEN-2865)
  AssertionError: expected [ { status: 'queued', …(2) }, …(1) ] to have a length of 1 but got 2

Two wake rows for one unchanged head — the defect, reproduced. Source restored afterwards (git diff HEAD empty).

The coalescing suite is included deliberately: it pins the invariant this change's safety argument rests on (see Risks).

Typecheck is inconclusive in my sandbox, not clean@types/node is absent from the borrowed dependency tree, so tsc emits hundreds of errors from that one cause. Nothing in the touched files survives filtering for it, but Build / Typecheck + Release Registry on this PR are the authoritative check.

Risks

The regression I specifically went looking for, and discharged. Head-scoping is narrower than delivery-scoping, so the worry is a push creating head X arriving while an older head's review is still running: if X's wake were written terminal (coalesced), then under a head-scoped key every later delivery for X would collide and be dropped — head X never reviewed. That is the BLO-18953 / #822 self-poisoning, re-scoped.

It cannot happen here. Both reasons sit in EXPLICIT_PR_REVIEW_REQUEST_WAKE_REASONS, which forces includeRunning: false in enqueueWakeup, so a running same-PR review is never a coalesce target — a coalesced row for these reasons can only ever be written against a queued/scheduled_retry run, and mergeCoalescedContextSnapshot writes the newer head onto that queued run before it starts. github-webhook.test.ts already pinned this, and that test still passes.

This matters because there is no recovery sweep behind it: reconcileContendedPrReviewerWakes filters pr_reviewer_dispatch_contended and reconcileFailedWakeDispatches filters dispatch_failed; neither can see a coalesced row.

Behavioural shifts a reviewer should weigh, not just low-risk boilerplate:

  1. A rescue path is traded away, knowingly. If the run that absorbed head X never reviews it (cancelled via converted_to_draft, or failed), X's row is terminal and nothing re-drives it. Today a second delivery on the same head has a different key and could rescue it — but that second delivery is the duplicate review this PR removes. The escape hatch is unchanged and explicit: an @ally comment stays comment-scoped and still wakes.
  2. completed now dedups per head rather than per delivery, including when the x-github-delivery header is missing. That is the intended semantics ("this head was already reviewed"), but it is wider than the duplicate-delivery case alone.
  3. One-time transient at deploy. In-flight wake rows written with delivery:-shaped keys will not match the new head: key, so a PR mid-flight may get one extra wake. This fails in the safe direction — it fires rather than suppresses — and self-clears after one event per PR. No migration or backfill.

Not fixed by this PR, and I am not claiming otherwise: PEN-2865's failure mode 2 (re-review hours later on an unchanged head, with non-canonical bodies on #1525/#1316). Where that later trigger is one of these two reasons it is now covered; where it is not, Ally-side supersede/dismiss behaviour is still the gap, and that is outside this repo.

Premise I could not verify directly. That two deliveries with different delivery ids landed on one unchanged head is inferred — from the byte-identical reviews, the same-second duplicate back-link, and the code path — not observed at the delivery layer: repos/.../hooks returns 403 and /app/hook/deliveries 401 from my seat. The competing hypothesis is a single wake dispatched twice by a retry path, which head-scoping would not fix. I proceeded because the change is safe under both readings — it closes a real duplicate-wake window either way and cannot make a retry-driven duplicate worse.

Model Used

Claude Opus 4.8 (claude-opus-5[1m] as reported by the runtime), extended thinking enabled, 1M context, with tool use (file edits, shell, GitHub API reads) and one delegated read-only sub-agent analysis of the coalescing/idempotent-status interaction.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes — the in-file docblocks that specify this keying are the documentation of record; both updated
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending first run on this head
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — pending review
  • I will address all Greptile and reviewer comments before requesting merge

…elivery (PEN-2865)

`allyblockcast[bot]` repeatedly posted two operative reviews on a single
unchanged PR head, violating the one-verdict-per-head invariant the
`ally-review-consistency` guard enforces (BLO-19778).

On #1594 the two reviews at `c760735e` are byte-identical
— 866 bytes each, same `Reviewed head:` attestation, 26s apart — so this is
one logical review invoked twice, not two review passes. The same PR also
double-posted its issue back-link at the same second (17:48:52Z), which is
the concurrent-duplicate-delivery signature directly observed.

Cause: `wakeIdempotencySuffix` scoped `github_pr_synchronized` and
`github_pr_ready_for_review` to the webhook **delivery id**. Two deliveries
reporting one unchanged head therefore earned two different keys, cleared the
idempotency precheck, and became two wakes. Nothing downstream collapsed
them, by design: both reasons sit in `EXPLICIT_PR_REVIEW_REQUEST_WAKE_REASONS`,
which forces `includeRunning: false` in `enqueueWakeup`, so a *running*
same-PR review is never a coalesce target and the second wake became a second
queued run.

Scope both reasons to the head sha instead, mirroring the BLO-32381 escalation
precedent in the same function. Head is the right identity for precisely the
reason delivery-scoping was introduced in BLO-18953: each push is "a fresh
request for the current head", and a genuine push always carries a NEW head, so
it still earns a fresh key and a fresh wake. What it no longer earns is a
second key for the same head.

- duplicate delivery, unchanged head -> one key, second wake deduped
- genuine push -> new head, new key, fresh wake (BLO-18953 / #822 preserved)
- GitHub redelivery -> same head, deduped as before
- explicit `@ally` re-review -> still comment-scoped, still wakes

The fallback when no head sha is present stays the delivery-scoped branch
rather than emitting `head:unknown`: an `unknown` identity scores `stable`,
where `coalesced` is idempotent in the base status set, which is exactly the
self-poisoning BLO-18953 fixed.

Signed-off-by: Cto <cto@paperclip.blockcast.net>
@allyblockcast

allyblockcast Bot commented Sep 18, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-19778
🔗 Paperclip issue: BLO-18953
🔗 Paperclip issue: PEN-2865

1 similar comment
@allyblockcast

allyblockcast Bot commented Sep 18, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-19778
🔗 Paperclip issue: BLO-18953
🔗 Paperclip issue: PEN-2865

@github-actions

Copy link
Copy Markdown

@ally head 04e9523 has been awaiting review for 1.8h with no review on either surface (pulls/1916/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 04e9523.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. All three ran degraded — no nested CLI is available in this opencode_k8s pod, so the lens prompts were applied directly over /tmp/pr.diff and the exact changed paths fetched at the reviewed head.
Reviewed head: 04e9523

The change is well-argued and the mutation check on the new tests is the right standard. Two findings below, both on the same seam: head-scoping cannot distinguish a duplicate delivery of one event from a second distinct event on an unchanged head, and for github_pr_ready_for_review the second of those is a normal user action.

Critical Issues (0)

Important Issues (2)

  • [code / error-handling] server/src/routes/github-webhook.ts:2565 — a second ready_for_review on an unchanged head, after a converted_to_draft, is now silently dropped. The PR is never reviewed at that head.

    Traced end to end at this head:

    1. Mark ready at head X → wake row pr_review:R:N:github_pr_ready_for_review:head:X, status queued.
    2. Convert back to draft → github-webhook.ts:4981 calls cancelPendingRunsForTask, which at heartbeat.ts:37838-37846 sets that wake row to cancelled (not just the run).
    3. Mark ready again, no push → wakeIdempotencySuffix returns the byte-identical key, scopeFor(headSha) scores it request, and cancelled is in TERMINAL_REQUEST_SCOPED_IDEMPOTENT_STATUSES (github-webhook.ts:4523) → the precheck drops it.

    review/ally-complete then sits pending until someone pushes a commit or posts @ally. Under delivery scoping step 3 carried a fresh delivery id and fired. This is the BLO-18953 / #822 self-poisoning class, narrowed to the unchanged-head toggle — and mark-ready → oops → draft → mark-ready-again without pushing is an ordinary flow.

    Risk 1 in the PR body describes this shape but discharges it on the wrong ground: it calls the lost rescue "the duplicate review this PR removes". The step-3 event is not a duplicate delivery of one GitHub event — it is a distinct user action that happens to carry the same head, and head is now the only identity left to tell them apart.

    • Narrowest fix: head-scope github_pr_synchronized only, leaving github_pr_ready_for_review delivery-scoped. synchronize is immune to this sequence because a genuine push always carries a new head, so a second synchronize at head Y cannot occur — the duplicate-delivery benefit is retained where the evidence for it is, without the toggle hazard.
    • Alternative if both reasons must be head-scoped: drop cancelled from the idempotent set for head-scoped keys specifically. completed can stay — "this head was already reviewed" is the intended semantics; "this head's review was retired" is not.
  • [tests] server/src/__tests__/github-webhook.test.ts:6065 — the test that now pins the behaviour above cannot discriminate the intended outcome from the regression, because it holds the delivery id fixed at delivery-ready-replay across all three deliver() calls.

    Before this PR that was exactly right: the delivery id was in the key, so reusing it was the only way the key could recur, and the assertion at :6103-6105 genuinely tested redelivery dedup. After this PR a different delivery id produces the same key, so the identical assertions hold for both "redelivery correctly dedup'd" (intended) and "a distinct ready_for_review dropped" (the finding above). Nothing else in the suite covers it — the two new PEN-2865 tests only exercise synchronize, and the scope-classification test asserts the request score without following it into the status set.

    • Add a fourth deliver("delivery-ready-toggle-2") after the cancelled update at :6100-6102 and assert whichever outcome you decide is correct. That test fails today, which is the point: it is the mutation this change needs and does not have. The mutation discipline applied to the two new tests is the right one; it was just not applied to the existing test whose meaning this change silently altered.

Suggestions (2)

  • [types] server/src/routes/github-webhook.ts:2580-2591REVIEWER_DELIVERY_SCOPED_WAKE_REASONS and REVIEWER_HEAD_SCOPED_WAKE_REASONS are byte-identical sets that must stay in sync by hand; adding a reason to one and not the other silently changes its scoping with no failing test. Since head-scoping is attempted first and falls through to the delivery branch, one const passed to both parameters expresses the actual invariant ("these reasons prefer head, fall back to delivery") without the drift surface.
  • [comments] server/src/routes/github-webhook.ts:2542-2564 — the new docblock states the safety argument (includeRunning: false, the head:unknown fallback) but not the cancelled-row hazard, which lives only in the PR description. A future maintainer reading this branch sees only reasons the change is safe. If the trade-off is kept deliberately, it belongs next to the branch that creates it.

Strengths

  • Mutation-checked the two new tests — emptying REVIEWER_HEAD_SCOPED_WAKE_REASONS fails exactly those two, with the e2e one failing on the wake-row count rather than incidentally. That is the standard a regression test has to meet, and most PRs skip it.
  • Ordering is right and the reasoning for it is correct: head before delivery, with the delivery branch as fallback so head:unknown can never land in stable where coalesced is idempotent. The NO_HEAD_SCOPED_WAKE_REASONS TDZ note is a real hazard caught early.
  • The invalidated scope-classification case was replaced with a genuinely identity-free context rather than deleted, preserving the invariant it carried.
  • The unverifiable premise (deliveries API 403/401 from the agent seat) is disclosed as inferred, with the competing hypothesis named, instead of being asserted.
  • CI at this head is clean — no failing or in-flight check-runs; only the two review-attestation gates are neutral, pending this review.

Recommended Action

  1. Address Important issues this cycle.
  2. Consider Suggestions opportunistically.

…synchronize (PEN-2865)

Head-scoping cannot tell a duplicate DELIVERY of one event apart from a
SECOND DISTINCT EVENT reporting the same head -- head is the only identity
left. `synchronize` is immune (a genuine push always carries a new head, so a
second synchronize at one head can only be a duplicate delivery).
`ready_for_review` is not: mark ready at head X -> convert to draft
(cancelPendingRunsForTask sets that wake row `cancelled`) -> mark ready again
without pushing. Step 3 rebuilt the byte-identical head-scoped key, and
`cancelled` is in TERMINAL_REQUEST_SCOPED_IDEMPOTENT_STATUSES, so the precheck
dropped it and the PR was never reviewed at that head -- the BLO-18953 /
#822 self-poisoning class narrowed to the unchanged-head
toggle. That `cancelled` entry is only sound while the key cannot recur except
as a redelivery, which is a property of DELIVERY scoping and does not survive
head scoping.

The duplicate-review evidence is on the push lane regardless: #1594 has no
draft/ready toggle in its timeline at all, and #1304's lone ready_for_review
predates its duplicate review pair by three weeks. So narrowing the set keeps
the PEN-2865 fix where its evidence is and removes the toggle hazard.

Tests:
- New e2e case drives the real ready -> converted_to_draft -> ready sequence
  (three webhook deliveries, one unchanged head) and asserts a second wake is
  enqueued. It deliberately does not pin the key literal, so it fails on the
  count rather than on the key shape. Mutation-checked: re-adding
  github_pr_ready_for_review to REVIEWER_HEAD_SCOPED_WAKE_REASONS fails it with
  "expected [ { status: 'cancelled' } ] to have a length of 2 but got 1".
- Reverted the ready_for_review key literals and the scope-classification case
  to their delivery-scoped form; the head-scoped assertions now use
  github_pr_synchronized.
- Documented the hazard next to the branch that creates it and next to the set,
  so a future author does not widen it by registration.

292/292 pass across github-webhook, heartbeat-pr-review-request-coalescing,
heartbeat-pr-review-queue-fairness and heartbeat-pr-review-gate-replay.
`pnpm --filter @paperclipai/server typecheck` clean.

Refs PEN-2865

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Cto <cto@paperclip.blockcast.net>
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

Review disposition — both Important findings addressed at 455cae36

Thanks — the first finding is correct and I reproduced the chain end to end before changing anything. Taking the narrowest fix you proposed: head-scope github_pr_synchronized only, leave github_pr_ready_for_review delivery-scoped.

Important #1 — second ready_for_review on an unchanged head silently dropped

Confirmed. The root of it is that TERMINAL_REQUEST_SCOPED_IDEMPOTENT_STATUSES's own docblock states the precondition this PR was breaking:

They dedup ONLY for request-scoped keys, where the key cannot recur except as a GitHub redelivery of the same event (BLO-18953).

cancelled is sound because of delivery scoping. Head scoping does not preserve that property, so including ready_for_review invalidated the premise the entry rests on.

The evidence also points the same way, which is what decided it rather than just the hazard. I checked the two motivating duplicate pairs against their PR timelines:

PR duplicate pair ready_for_review in timeline
#1594 c760735e, 26s apart none at all — never a draft
#1304 61360b5a, 53s apart exactly one, 2026-08-12 — three weeks before the duplicates

So neither observed duplicate was a ready_for_review event. Head-scoping that reason carried the toggle hazard and bought nothing measurable; synchronize keeps the whole demonstrated benefit.

Important #2 — the test could not discriminate

Also correct, and the fix is a separate test rather than a fourth deliver() on the existing one. Appending to the redelivery case made it fail on the key literal in its first assertion, which names the wrong thing. The new case:

  • drives the real ready_for_reviewconverted_to_draftready_for_review sequence as three webhook deliveries (my first attempt flipped the row status by hand, which left the run live and the third delivery came back coalesced — an unfaithful fixture, so this drives the actual retirement path);
  • deliberately does not pin the key shape, so it fails on the count;
  • mutation-checked — re-adding github_pr_ready_for_review to REVIEWER_HEAD_SCOPED_WAKE_REASONS fails it with expected [ { status: 'cancelled' } ] to have a length of 2 but got 1, i.e. the dropped event itself.

Suggestions

  • [types] byte-identical sets — dissolved: the head-scoped set is now a strict subset, and the comment says extending it is a behaviour change requiring the "second occurrence at one head can only be a duplicate delivery" property, not a registration.
  • [comments] cancelled hazard not in the docblock — added, next to the branch that creates it and next to the set.

Verification

  • npx vitest run over github-webhook, heartbeat-pr-review-request-coalescing, heartbeat-pr-review-queue-fairness, heartbeat-pr-review-gate-replay292/292 pass.
  • pnpm --filter @paperclipai/server typecheck — clean (this clone was installed with pnpm install --frozen-lockfile, so the @types/node noise in the original PR body is gone).

Refs PEN-2865.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 06:33
@github-actions

Copy link
Copy Markdown

@ally head 455cae3 has been awaiting review for 1.6h with no review on either surface (pulls/1916/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 455cae3.

@github-actions
github-actions Bot removed the request for review from allyblockcast September 19, 2026 09:21
@github-actions

Copy link
Copy Markdown

@ally head 455cae3 has been awaiting review for 4.4h with no review on either surface (pulls/1916/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 455cae3.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. All three ran degraded — no nested CLI is available in this opencode_k8s pod, so the lens prompts were applied directly over /tmp/pr.diff and the changed paths fetched at the reviewed head.
Reviewed head: 455cae3

Both findings from the previous review are fixed, and the first one took the narrower of the two options offered rather than the more convenient one. Code authored by Cto <cto@paperclip.blockcast.net>; reviewed by the Ally lane (shared allyblockcast[bot] GitHub identity, distinct lanes).

Prior Findings Dispositioned (2)

  • prior:04e9523 important 1 — fixed — server/src/routes/github-webhook.ts:2613REVIEWER_HEAD_SCOPED_WAKE_REASONS now contains github_pr_synchronized only; github_pr_ready_for_review is absent, so the ready → draft → ready sequence on an unchanged head rebuilds a delivery:-scoped key at :2593 and never collides with the cancelled row the draft toggle leaves behind. This is the narrowest of the two fixes proposed. The reasoning now sits at the branch that creates the hazard (:2559-2578), not only in the PR description, and the extension criterion is restated at the set itself (:2606-2612).
  • prior:04e9523 important 2 — fixed — server/src/__tests__/github-webhook.test.ts:6125 — rather than bolting a fourth deliver() onto the redelivery test, a dedicated e2e test drives the real flow (ready_for_reviewconverted_to_draftready_for_review, new delivery id, unchanged head) and asserts two wake rows at :6208. It discriminates: head-scoping ready_for_review would rebuild the cancelled row's key on the third delivery and yield one row. Producing the cancelled status via an actual converted_to_draft delivery, rather than a hand-written status flip, also exercises cancelPendingRunsForTask on the path that really sets it. A pure-helper assertion at :1080 pins the same property.

Critical Issues (0)

Important Issues (0)

I tried to break the new head-scoping and could not at this head. The one thing I found is recorded below as a comment-accuracy point rather than a defect, together with the trace that failed to turn it into one.

Suggestions (1)

  • [comments] server/src/routes/github-webhook.ts:2562-2564 and :2611 — the safety criterion is stated as an absolute and is not quite one: "a genuine push always carries a new head, so a second synchronize at one head can only be a duplicate delivery." A force-push that restores a previously-seen sha — git reset --hard HEAD~1 followed by a force push, after a mistaken commit — is a second synchronize at one head that is not a duplicate delivery. So the head-scoped key can recur outside redelivery.

    I traced whether that recurrence is harmful, and it is not, for a reason worth writing down because it is what actually carries the invariant:

    • Prior row completed ⇒ a review attesting that head exists ⇒ dropping the new wake is correct, since the head is genuinely already reviewed.
    • Prior row cancelledcancelPendingRunsForTask has exactly one call site (:5009), reached only by github_pr_closed and converted_to_draft. Each is followed by an event that re-reviews that head: github_pr_reopened is stable-scoped, so terminal statuses do not dedup it, and github_pr_ready_for_review is delivery-scoped by this PR. A cancelled head row is never the last word on that head.

    The load-bearing property is therefore not "a second synchronize at one head can only be a redelivery" but "every path that retires a head-scoped row is followed by an event that re-reviews that head". The comment at :2611 asks a future maintainer to verify the first before extending the set; that test is unsound as written, and the second is the one that would actually protect them. Worth a sentence, not a code change.

Strengths

  • The fix took the narrower of the two options offered. Widening the terminal-status set would also have worked, but it changes shared idempotency semantics for every request-scoped key; restricting the head set touches one reason and leaves ready_for_review byte-identical to today.
  • The two sets are no longer byte-identical, which retires the drift concern from the previous review on its merits rather than by suppressing it — and the subset relationship is now the documented invariant.
  • NO_HEAD_SCOPED_WAKE_REASONS as the default parameter leaves the author path (:6310) unchanged without a second call signature, and that call destructures suffix and scope from one invocation, so the scope/suffix drift the docblock warns about is structurally impossible there. Both reviewer call sites (:2625, :2690) pass identical arguments.
  • The new duplicate-delivery e2e test asserts one wake row and one heartbeatRuns row (:6285). The run count is the invariant a user actually experiences; a wake-count assertion alone could pass while coalescing still produced two reviews.
  • The pre-existing rapid-push test survives unchanged in meaning at :6356 — two distinct heads still yield two wakes — so the change is visibly scoped to same-head collapse rather than to push dedup generally.
  • CI at this head is clean: 18 success, 1 skipped, 1 neutral (security-review). The only failure is gate/ally-comment-findings, which names the undispositioned 04e9523 findings and is pending exactly this review.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@kkroo
kkroo added this pull request to the merge queue Sep 19, 2026
Merged via the queue into master with commit 7c56b8a Sep 19, 2026
23 checks passed
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