Skip to content

fix(gate,queue,ai): defer the gate on a renderer outage, re-drive merge-train waiters, debounce force-push storms, and book the real retry budget (#9464, #9483, #9479) - #9529

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
fix/capture-blip-and-stranding
Jul 28, 2026

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Four defects that each let the pipeline act on an answer it did not actually have, or spend on work it did not need.

Closes #9464
Closes #9483
Closes #9479

#9464 — a browserless outage could auto-close legitimate visual PRs

#9030 and #9207 taught the screenshot-table gate to defer its one-shot close when the bot's own capture pipeline blips — but only for failures that report previewPending or throw. A browserless failure does neither: captureShot swallows its own renderer errors per shot and returns a null PNG, so buildCapture came back normally, previewPending: false, nothing thrown, no real pair.

That is byte-identical to a legitimately evidence-free PR right up until the gate closes it. The gate is action: close on all three repos, and closes are one-shot — the contributor's only remedy is opening a fresh PR. Last time this gate misfired it closed five of them.

The signal is deliberately "a render threw", not "zero pairs were produced". The latter is also true when the author genuinely supplied no evidence, which is the case the gate exists for — inferring the blip from an empty result neuters the gate entirely. captureShot now reports renderFailed from its catch only; an SSRF refusal, an auth wall, a redirect block, and an unconfigured binding all stay definite answers.

#9483 — a merge-train waiter had no path back to evaluation

The merge-train denial does audit("denied"); continue with no re-enqueue. The sibling wake covers the blocker merging — and nothing else. A blocker that is closed unmerged (that wake returns early on !mergedAt), ages past MERGE_TRAIN_MAX_WAIT_MS, or gains the manual-review label leaves the waiter open indefinitely with no signal, because each of those fires a webhook about the blocker and the age/label checks are consulted only at the waiter's own evaluation time — which nothing schedules. Under one-shot review that reads to the contributor as a silent rejection.

Nothing else picked it up either: the sweep is permanently ineligible (#never-endless-reregate), surface repair requires mergeableState !== "clean" and a merge-ready waiter is clean, and the wedge alert needs 5 denials/hour against one blocker while a stranded waiter produces exactly one, ever.

Fixed by option (b) from the issue: the merge-train wait joins STALE_RECHECK_DENIAL_DETAIL_PATTERN. It was excluded there as "durable, externally-actioned" — a classification that only holds for the merge case. It is in fact the purest instance of the gap that mechanism exists for. Bounded by construction: it inherits the existing 5-attempts-per-PR repair budget, whose lookback window is rolling and the same 24h as the train's own cap, so a waiter regains looks exactly when the age-out makes them useful.

#9479 (3) — force-push storms were undebounced

Every dedup layer was keyed on the head SHA — this coalesce key, and the AI-review lock's ...@${headSha}:${mode} — which is exactly the wrong key for a force-push storm, because each push mints a new SHA. Five amend-and-repush cycles in a minute looked like five unrelated events and bought five full prologues (file list, up to 96k chars of grounding fetch, RAG + impact-map embeddings, an enrichment POST) plus five LLM calls, for four heads that no longer exist by the time their reviews land. skipStaleReviewOutput suppresses the stale comment, but only after the spend.

A push now keys on the PR alone and carries a 45s trailing quiet window; the queue's coalesce keeps the newest payload while extending run_after, so a burst converges to one review of the surviving head.

Deliberately a separate key from pr-refresh rather than dropping the SHA there. A shared PR-scoped key would let a push overwrite a still-pending opened/ready_for_review payload, trading a spend bug for a lost-lifecycle-event bug.

#9479 (4) — the neuron budget under-booked by up to 6×

The reservation booked one call per opinion slot, but runWorkersOpinion retries each model 3× and then falls through to that slot's fallback with its own full budget — so a dual-model block review can make 12 calls where 2 were booked. The daily budget is a runaway-loop backstop; booking the best case made it 6× looser than it reads, the one direction a backstop must never be wrong in. The tie-break judge and ai-slop.ts already reserve worst case; the main review path was the outlier.

Consequence worth stating plainly: the recorded per-review estimate rises with the reservation, and sumAiEstimatedNeuronsSince debits the same number, so a deployment with an explicitly configured AI_DAILY_NEURON_BUDGET will see effective daily review throughput fall correspondingly. That is the intended meaning of the cap. The fail-safe default is the clamp maximum, so an unconfigured deployment is unaffected.

Also fixed in passing

test/unit/github-webhook-coalesce.test.ts's burst assertion covered reopened + synchronize + ready_for_review collapsing to one key. With the push split that is no longer true by design, so it now asserts the non-push burst still collapses and that a push does not join it — the property that actually protects lifecycle events.

Validation

  • npx tsc --noEmit, db:migrations:check (contiguous 0001..0197), selfhost:env-reference:check, git diff --check — all clean
  • 1198 passed across the visual, queue, webhook, db-parser and AI-review suites
  • Patch coverage against this diff: 0 uncovered changed lines

Regressions, each verified to fail against the unfixed code (by reverting the fix and re-running): a resolved capture whose renderer failed defers the close and schedules the bounded retry; a merge-train wait now surfaces in the repair-priority set; five rapid pushes collapse to one job carrying the final head; a budget covering only the best case is refused.

Invariants: a healthy renderer that found no evidence still closes — the #4110 guard, and the test that an earlier "infer the blip from zero pairs" attempt broke; an auth wall never sets renderFailed; one failed shot among healthy ones still defers, since a partial outage can still hide evidence; a merge-train detail without the executor's exact suffix, or with a non-numeric blocker, does not match; pushes to different PRs never coalesce; a push never shares a key with a lifecycle event; the quiet window extends rather than pulls in, so the debounce is not limited to the tail; every non-push action keeps zero delay; and the reservation tracks the real fallback structure, so a no-distinct-fallback pair books strictly less rather than being flatly inflated.

…ge-train waiters, debounce force-push storms, and book the real retry budget (#9464, #9483, #9479)
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-28 06:18:24 UTC

16 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR bundles four distinct, well-reasoned fixes: (1) captureShot/buildCapture now distinguish a swallowed renderer failure (renderFailed) from a healthy capture with no evidence, threading a new boolean through capturePage/resolveFallbackAfterShot/buildCapture/the maintenance-pass gate check so a browserless outage defers rather than triggers the one-shot screenshot-table close; (2) the merge-train wait detail string joins STALE_RECHECK_DENIAL_DETAIL_PATTERN so a waiter whose blocker closes-unmerged/ages-out/gets labeled is re-evaluated instead of sitting silently forever; (3) push (`synchronize`) webhooks get a dedicated PR-scoped coalesce key plus a 45s delaySeconds so a force-push storm collapses into one review of the surviving head instead of N reviews of abandoned SHAs, deliberately kept separate from the lifecycle-event key so a push can't clobber a pending opened/ready_for_review; (4) the AI-review neuron pre-booking now reserves the real worst case (retries-per-model × fallback-model count) instead of one call per slot. Each fix is traceable to its stated root cause and is backed by both regression and invariant tests that exercise the real production functions (buildCapture, githubWebhookCoalesceKey/DelaySeconds, recentStaleRecheckDeniedPullNumbers, runLoopOverAiReview), including a producer-source-text parity test guarding the new merge-train regex literal against drift. CI is fully green.

Nits — 6 non-blocking

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9464, #9483, #9479
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (3 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 328 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 328 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Addressed
The diff implements the issue's core deliverables: a `renderFailed` marker threaded from `captureShot` through `capturePage`/`buildCapture`, wired in processors.ts to trigger the same recapture-preview retry as `previewPending`, and a `loopover_visual_capture_total` metric on both success and error paths, with regression tests proving genuine no-evidence PRs still close while renderer outages defe

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 14 PR(s), 328 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (fe6fe93) and head (e8e4bbd) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.66%. Comparing base (fe6fe93) to head (e8e4bbd).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9529      +/-   ##
==========================================
- Coverage   89.56%   88.66%   -0.90%     
==========================================
  Files         843      843              
  Lines      110190   110201      +11     
  Branches    26227    26230       +3     
==========================================
- Hits        98690    97714     -976     
- Misses      10236    11515    +1279     
+ Partials     1264      972     -292     
Flag Coverage Δ
backend 93.64% <100.00%> (-1.63%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/db/repositories.ts 96.80% <100.00%> (ø)
src/github/webhook-coalesce.ts 100.00% <100.00%> (ø)
src/github/webhook.ts 100.00% <100.00%> (ø)
src/queue/processors.ts 94.83% <100.00%> (ø)
src/review/visual/capture.ts 96.29% <100.00%> (+0.43%) ⬆️
src/review/visual/shot.ts 96.72% <100.00%> (+0.98%) ⬆️
src/services/ai-review.ts 96.79% <100.00%> (+0.01%) ⬆️

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 28, 2026

@loopover-orb loopover-orb 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.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 6f3b613 into main Jul 28, 2026
9 checks passed
@loopover-orb
loopover-orb Bot deleted the fix/capture-blip-and-stranding branch July 28, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment