Skip to content

perf(webhook): four of five coalesce families are keyed but windowless, so they never coalesce #10127

Description

@JSONbored

Four of the five webhook coalesce families never coalesce

src/github/webhook-coalesce.ts computes a job coalesce key for five event families. githubWebhookCoalesceDelaySeconds returns a non-zero window for exactly one of them — the push family (#9479) — and 0 for the rest.

A zero-delay job is claimed off the queue before a sibling can arrive, so there is no pending row to merge into. The file says so itself, in the push section:

On a queue that coalesces (self-host pg/sqlite, which is where the ORB runs) this is what CREATES the window: without a delay the first push is claimed immediately and later pushes find no pending row to merge into.

That reasoning applies to every family, but the window was only ever wired for pushes. The other four keys are derived, attached to the job, and structurally unable to do anything.

Measured cost on the Orb

 event_name    | action     | deliveries   (since 2026-07-30)
 check_suite   | completed  |       1727
 issue_comment | edited     |        998
 issue_comment | created    |        996
 pull_request  | labeled    |        177
 total_records | distinct_heads | records_per_head
          1519 |            510 |             2.98

Nearly three full gate passes per distinct head SHA. Each repeat buys a prologue (PR + file list, CI aggregate, config resolve), GitHub REST/GraphQL quota, a decision_records write, and label/comment reconciliation, over state that has not changed.

The LLM review itself is not duplicated — ai_review_pr shows 415 calls against 510 distinct heads, so the per-head review cache is holding. This is API, queue, and DB waste rather than model spend, which is why it went unnoticed: the cost line looked fine.

A worked example, #10098 — eleven hold records at one head SHA in 33 minutes, each upstream_state_change, all reason_code: success.

Contributing factor: the bot's own writes come back as deliveries

shouldProcessPullRequestPublicSurface treats a disposition label change as a re-sync trigger (#9059/#9171 — correct in itself; those labels are read as inputs elsewhere). But the gate writes those labels. So a pass that adds manual-review and clears a stale sibling generates two pull_request.labeled deliveries, each of which re-enters the gate. With pr-label keyed but windowless, both become full re-evaluations.

Fix

Return the key and its window from one classifier, so a family cannot have one without the other:

type WebhookCoalescePlan = { key: string; quietWindowSeconds: number };
function planGithubWebhookCoalesce(eventName, payload): WebhookCoalescePlan | null;

Both exported functions become projections of it. Windows:

family window why
pr-push 45s unchanged (#9479)
ci-completed 45s interchangeable — whichever wins re-fetches the same settled CI state. Kept under CI_COALESCE_WINDOW_SECONDS (60s) so the downstream per-PR throttle stays the outer bound
pr-label 15s shorter: a maintainer removing a hold label expects prompt notice
review-surface 15s payload-interchangeable within a family
pr-refresh 0 lifecycle — opened/reopened/ready_for_review each happen once and start the clock a contributor waits on. Keyed, never delayed

The coupling is the actual fix; the numbers are tuning. The invariant is asserted as key implies window over the real families rather than as expected constants, so a family added later cannot reintroduce a keyed-but-inert delivery.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions