Skip to content

fix(github-webhook): debounced reviewer re-review on PR synchronize - #529

Merged
kkroo merged 1 commit into
masterfrom
fix/reviewer-rereview-on-synchronize
Jun 28, 2026
Merged

fix(github-webhook): debounced reviewer re-review on PR synchronize#529
kkroo merged 1 commit into
masterfrom
fix/reviewer-rereview-on-synchronize

Conversation

@kkroo

@kkroo kkroo commented Jun 28, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work.
  • GitHub webhooks are one of the main ways Paperclip reacts to PR lifecycle events and wakes the right agent.
  • Ally/reviewer wakeups already happen for reviews, mentions, and terminal CI events, but pull_request.synchronize was intentionally ignored to avoid push-thrash.
  • That skip left a real review gap: when an author pushed fixes after an earlier review, the reviewer was not automatically woken to re-review the new head.
  • The fix needs to wake the reviewer on synchronize without creating one reviewer job per rapid push.
  • This PR reuses the existing PR-scoped idempotency key to coalesce synchronize wakeups while still reviewing the latest head when Ally actually runs.
  • The benefit is reviewer re-review after author fixes, without reintroducing capacity thrash or redundant author wakeups.

Linked Issues or Issue Description

No tracked GitHub issue exists for this exact webhook gap, so the bug is described inline using the issue-template fields.

What happened?

pull_request.synchronize did not wake the PR reviewer agent after an author pushed follow-up commits, so a PR could remain waiting even though new code was ready for re-review.

Expected behavior

Synchronize should wake the reviewer once for the PR, coalescing rapid pushes into one pending reviewer wake.

Steps to reproduce

  1. Create a PR that has already been reviewed.
  2. Push a new commit to the PR branch.
  3. Observe that the previous webhook path skipped reviewer wakeup for synchronize.

Paperclip version or commit

Branch fix/reviewer-rereview-on-synchronize, commit 4ba5ad8c3f9a3bc1bb116234aa27fc09a03b7c63.

Deployment mode

Paperclip GitHub webhook handling in the server.

Related PR search: searched GitHub PRs for synchronize reviewer re-review webhook; only this PR matched.

What Changed

  • Maps pull_request.synchronize to a github_pr_synchronized reviewer wake reason.
  • Wakes the reviewer on synchronize while suppressing author-assignee fan-out for that reason.
  • Reuses the existing PR-scoped reviewer idempotency key, intentionally excluding head SHA and delivery ID so rapid fixup pushes coalesce.
  • Updates the webhook unit test to assert the new wake reason, reviewer wake gate, and head-sha-independent debounce key.

Verification

  • Paperclip PR CI is green for policy, typecheck, build, e2e, server tests, workspace tests, serialized server suites, canary dry run, security review, and verify.
  • Relevant PR check: General tests (server) passed.
  • Targeted coverage: server/src/__tests__/github-webhook.test.ts covers the synchronize reviewer wake behavior.

Risks

  • Low-to-moderate behavioral risk: synchronize events now wake reviewers where they were previously ignored.
  • The wake is reviewer-only and idempotency-key coalesced to avoid redundant author wakeups and push burst fan-out.
  • Existing terminal CI and review-submitted wake paths are unchanged.

For core feature work, check ROADMAP.md first and discuss it in #dev before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See CONTRIBUTING.md.

Model Used

Claude Opus 4.8 with 1M context assisted with the code change. Codex GPT-5 was used to inspect CI state and update this PR metadata.

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
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

Wake the PR reviewer agent on pull_request.synchronize (author pushed a
fixup after an earlier review) instead of skipping it. The wake is
debounced by reusing the existing PR-scoped idempotency key
(repo+prNumber+reason, deliberately head-sha- and delivery-id-independent):
a burst of rapid fixup pushes collapses onto one still-pending reviewer
wake rather than fanning out one review per push. The coalesced wake
reviews whatever the latest head is when Ally actually runs.

synchronize is reviewer-only: the author-assignee fan-out is suppressed for
this reason since the assignee is the one who just pushed (waking them per
push would be redundant and the exact capacity thrash this path guards
against). Authors are still woken by check_run/workflow_run on terminal CI
and by review-submitted / @ally feedback.

Updates the prior "ignores synchronize to avoid push-thrash" unit test to
assert the new wake reason and the head-sha-independent debounce key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Jun 28, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • No linked issue or inline issue description found — either tag an existing issue with Fixes #NNN / Closes #NNN / Refs #NNN, or describe the underlying issue inline in the PR body following one of our issue templates (https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast

allyblockcast Bot commented Jun 28, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors) + native-codex.

Critical Issues (1)

  • [silent-failure-hunter] server/src/routes/github-webhook.ts (buildPrReviewerWakeIdempotencyKey) — Missing guard on prNumber parameter in debounce key function
    • If context.prNumber is undefined, the key silently includes the literal string undefined, causing all synchronize events without a PR number to dedup onto the same malformed key.
    • Recommendation: Add an explicit null/undefined guard before using context.prNumber in key construction, with error logging if the synchronize wake arrives without a PR number.

Important Issues (3)

  • [code-reviewer & comment-analyzer] server/src/routes/github-webhook.ts:64-68, 97-106, 114-117 and github-webhook.test.ts:40-45 — Comments attribute debounce to wrong mechanism and cite non-existent symbol

    • Every comment claims debouncing works via head-sha-independent idempotency key deduplication against IDEMPOTENT_REVIEWER_WAKE_STATUSES (which does not exist in the codebase).
    • The actual debounce works via the head-sha-independent taskKey in buildPrReviewerTaskKey and the isSameTaskScope check in enqueueWakeup, not the idempotency key (which is never read for dedup purposes).
    • A future maintainer reading "change the idempotency key" to enable per-push reviews would believe they're changing the debounce lever, when the taskKey is what actually controls coalescing.
    • Recommendation: Rewrite comments to attribute coalescing to taskKey / isSameTaskScope / enqueueWakeup instead of idempotency key. Remove the IDEMPOTENT_REVIEWER_WAKE_STATUSES reference (or replace with actual symbol if it exists elsewhere).
  • [silent-failure-hunter] server/src/routes/github-webhook.ts:1142 (synchronizeReviewerOnly condition) — Implicit condition with no validation

    • The context?.wakeReason === "github_pr_synchronized" check has no guard ensuring the context is well-formed or that this code path is only reached for synchronize events.
    • If context is unexpectedly null, the expression throws. If wakeReason is undefined or malformed, the condition silently falls through to the default behavior (waking the author), defeating the debounce safety.
    • Recommendation: Add explicit assertion and error logging before the loop: if (synchronizeReviewerOnly && context?.wakeReason !== "github_pr_synchronized") { logError(...) }.
  • [pr-test-analyzer] server/src/__tests__/github-webhook.test.ts — Missing coverage of core debounce behavior

    • The test asserts the idempotency key format and single-scenario wake firing, but does not verify:
      1. Two synchronize events with different head SHAs produce the same key (rapid-push dedup simulation)
      2. Author-assignee wakes are suppressed for synchronize but not for other wake reasons
      3. Edge cases (missing prNumber, null identifiers)
    • Recommendation: Add integration tests for rapid-push coalescing (two pushes → same key) and author-assignee suppression verification. Add edge-case tests for missing prNumber and null identifiers in pull_request.body.

Suggestions (2)

  • [comment-analyzer] github-webhook.test.ts:40-42 — Misleading comment on function parameters

    • Comment states "the key omits head sha + delivery id" but the test shows deliveryId being passed to the function. Clarify that the function accepts delivery ID but does not use it in the final key for synchronize events.
  • [comment-analyzer] github-webhook.ts:62-68, 114-117 — Terminology clarity

    • Define "coalesced" and "coalesced PR-scoped" more explicitly for future readers. Consider: "the wake is coalesced PR-scoped (multiple pushes map to the same idempotency key, preventing per-push reviewer runs)."

Strengths

  • Author-assignee suppression logic is sound and well-named (synchronizeReviewerOnly variable reinforces the intent)
  • Test fixture is realistic and includes valid edge case (null PR body)
  • The core feature is correct: synchronize events do coalesce, just with misleading explanation in comments
  • Comprehensive inline documentation at route handler shows clear thinking about capacity safety

Recommended Action

  1. Fix Critical: Add prNumber null guard with error logging in buildPrReviewerWakeIdempotencyKey.
  2. Fix Important: Rewrite comments to attribute debounce to taskKey (not idempotency key); remove the non-existent IDEMPOTENT_REVIEWER_WAKE_STATUSES reference.
  3. Add Important: Add integration test for rapid-push dedup and author-assignee suppression verification.
  4. Address Suggestions: Clarify "coalesced" terminology and function-parameter semantics in comments.

reviewed head: 4ba5ad8

@kkroo
kkroo merged commit 737159e into master Jun 28, 2026
15 of 17 checks passed

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + native-codex.

Reviewed head: 4ba5ad8


Critical Issues (1)

[code-reviewer] server/src/routes/github-webhook.ts — Debounce permanence defeats re-review across multiple push cycles (confidence: 88)

The synchronize idempotency key is constant per PR (pr_review:<repo>:<prNumber>:github_pr_synchronized), deliberately omitting head SHA and delivery ID to coalesce rapid pushes. However, the dedup pre-check in the reviewer wake path treats both pending statuses (queued/claimed/running) AND terminal statuses (completed/coalesced) as "wake exists, skip." The wakeup request rows are never pruned or aged.

Consequence: the first burst of rapid pushes correctly coalesces onto one pending wake. But once that reviewer run completes and the row moves to completed, that constant key will suppress every subsequent push to the same PR. The second and later rounds of author fixes never wake the reviewer — defeating the core purpose of this PR.

The code comments describe the debounce as collapsing pushes "against the still-pending wake," implying dedup should end when the wake completes. The implementation extends suppression to terminal states.

For other wake reasons (opened, reopened, ready_for_review), a permanent terminal-status dedup is harmless — they're once-per-PR events. But synchronize is repeating per-push; the permanent dedup breaks the feature.

Recommended fix: For github_pr_synchronized, either (a) exclude terminal statuses from the idempotency pre-check so a new push after completed re-fires, or (b) vary the key per re-review cycle (e.g., include a coarse time bucket), or (c) expire/clear the synchronize wake row on completion so the key frees up.


Important Issues (3)

[silent-failure-hunter] server/src/routes/github-webhook.ts (line ~1140) — No validation that context exists before accessing wakeReason (confidence: 78)

The synchronizeReviewerOnly gate checks context.wakeReason === "github_pr_synchronized" but doesn't validate that context itself is non-null first. If somehow a null/malformed context slips through upstream with the wrong type signature, the code silently returns an empty matched array rather than logging the error, masking a programming bug.

Recommended fix: Add explicit validation before the wakeReason check, or add an explicit type guard that narrows null.


[silent-failure-hunter] server/src/routes/github-webhook.ts (lines 582-591 in buildPrReviewerWakeIdempotencyKey) — Missing error log when debounce key building falls back to "unknown" (confidence: 72)

When context.repoFullName is null/undefined, the key builder silently uses "unknown". If a subsequent event provides the correct repo name, a different idempotency key is generated, causing the dedup to fail and each push to fire a separate reviewer wake — exactly the capacity thrash this debounce was designed to prevent. No log entry indicates why deduplication broke.

Recommended fix: Log a warning when repoFullName or other critical key fields are missing, so operators can diagnose why debounce failed.


[pr-test-analyzer] server/src/__tests__/github-webhook.test.ts — Missing integration test for malformed PR payloads (confidence: 68)

The test provides a complete, well-formed PR context. It does NOT verify what happens when payloads are malformed (missing pull_request.number, missing repository.full_name, missing head.sha). These should all resolve to null and skip the reviewer wake, but the test doesn't validate this. If a regression breaks null-checking in shouldFirePrReviewerWake, malformed webhooks could attempt a reviewer wake anyway.

Recommended fix: Add test cases for missing required fields to verify the null-check guards actually prevent invalid wakes.


Important Observations (Type Safety)

[type-design-analyzer] — Wake reason type system relies on string equality, not compile-time narrowing (confidence: 82)

The wakeReason field is a bare string union with no discriminated union or exhaustiveness checking. The type guard in shouldFirePrReviewerWake hardcodes the set of reasons it recognizes. If a new wake reason is added to resolveEventContext, the guard silently excludes it without compile-time error. The synchronizeReviewerOnly boolean gate (context.wakeReason === "github_pr_synchronized") is fragile — a typo or constant rename breaks it silently.

The test code includes a @ts-expect-error comment to bypass the type check, with a misleading explanation ("test fixture omits the prNumber field") when the fixture actually includes number: 318.

Recommended fix (medium-term): Adopt a discriminated union on wake reason so TypeScript can enforce exhaustiveness and catch missing cases at compile time. Replace the hardcoded Set in shouldFirePrReviewerWake with a switch statement that TypeScript will flag as incomplete if a new reason is added.


Strengths

  • Debounce logic is well-reasoned: The decision to omit head SHA and delivery ID from the synchronize key, so rapid pushes coalesce into one pending wake, is sound and well-documented.
  • Author-assignee suppression is correct: The synchronizeReviewerOnly gate appropriately skips author wakes for synchronize (the author is the pusher; they don't need to be reminded). Other paths (CI completion, review feedback) still wake the author as intended.
  • Test coverage is strong for the happy path: Idempotency key stability across different head SHAs, verification that synchronize produces the right wake reason, confirmation that author wakes are suppressed — all tested.
  • Comments explain the design intent clearly: The multi-line comments in resolveEventContext and around synchronizeReviewerOnly articulate why synchronize is reviewer-only and how the debounce is meant to work.

Recommended Action

  1. Before next deploy: Fix Critical Issue C1 — the debounce will stop working after the first completed reviewer pass. This is a correctness regression that will be reported as a bug in production.
  2. This cycle: Add error logging to buildPrReviewerWakeIdempotencyKey when critical fields are missing, and add integration tests for malformed payloads (Important Issues 2–3).
  3. Future refactor: Adopt a discriminated union on wake reason to catch new-reason-handling gaps at compile time (type-safety improvement).

Review completed 2026-06-28T06:10:06Z

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