Skip to content

fix(queue): guard the verdict derivation instead of the pass, so the webhook path is covered (#10227) - #10276

Merged
JSONbored merged 1 commit into
mainfrom
fix/verdict-backoff-chokepoint
Jul 31, 2026
Merged

fix(queue): guard the verdict derivation instead of the pass, so the webhook path is covered (#10227)#10276
JSONbored merged 1 commit into
mainfrom
fix/verdict-backoff-chokepoint

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

#10204 split the verdict-stability backoff (#10184) across two kinds of place and only got one right. The record half sits at the single ledger write every verdict passes through — correct, and its own comment says why. The skip half sat at the entry of the whole publish-and-maintain pass, so it suppressed everything the pass owed, not just the redundant re-derivation.

That turned the escape hatch into an allowlist. Every bounded retry chain had to remember to add itself, and the default for anything unlisted was "suppress me" — so the webhook path could never be guarded at all: adding it truncated #10061's recapture chain from MAX_CAPTURE_RETRY_ATTEMPTS (5) to VERDICT_BACKOFF_MIN_REPEATS (3). That truncation was reproduced first, before anything was changed.

The skip now sits at the one point a pass derives a verdict, inside maybePublishPrPublicSurface — the same kind of place the record half already occupies. Everything the pass owes runs above it and cannot be truncated by construction; the pending gate check-run, the AI review, the gate evaluation, the publish and the maintenance ledger write all sit below it. Both entry points reach a verdict only through that function, so neither has to opt in, and the webhook path is covered without a second call site.

Why the webhook path is the whole point (measured, not asserted)

Decision records on the Orb over 24h:

reevaluation reason records heads records_per_head
upstream_state_change (raw GitHub delivery — the webhook path) 348 99 3.52
sweep / CI completion 317 294 1.08
pipeline_error 58 44 1.32

The entire repeat signal is the one path that was never guarded. github_app.review_skipped_stable_verdict fired 11 times in the same window. 548 of 723 records are hold, which is the only class shouldSkipStableVerdict acts on — so the guard is pointed at the right population.

The structural note worth recording

The issue expected the guard to sit "after the visual-capture scheduling and before the AI review / gate evaluation." That ordering does not exist in the file: the recapture chain is scheduled from inside the comment-publish block, roughly 800 lines below the gate evaluation, not above it. No single line in the current source both runs the chain and skips the derivation.

So the chain is extracted (runVisualCaptureObligation) and run explicitly on the backed-off path. The normal publish path calls it in the same place, in the same order, with the same inputs — behaviour there is unchanged, verified by the two screenshot-table gate (#10061) cases passing untouched both before and after the extraction. Its file resolver is threaded as a thunk rather than a resolved list, so a backed-off pass on a repo with screenshots off pays for no diff fetch.

explicitlyRequested shrank to one entry

webhook.forceAiReview, meaning exactly one thing: a human asked for this pass — an operator's manual re-gate (reReviewStoredPullRequest's options.force) or a consumed "Re-run LoopOver review" click (#7626). previewPollAttempt needed an exemption only because the old guard sat above the recapture chain.

Two #10229 behaviours deliberately reversed

Both are pinned by name in verdict-stability-wire.test.ts rather than left to be rediscovered.

  • A poll tick is now backed off. It is machine-paced, not human-asked, and its chain runs above the guard, so it has no business in a hatch that means "a human asked for this".
  • A backed-off pass charges the regate repair budget again. onReachedReadiness's documented rule is "count executions, not deferrals", and a pass that clears readiness, decides type labels and advances the capture chain is an execution. It is also the anti-starvation direction: a repair on a permanently settled hold would otherwise reselect on every sweep tick forever without ever exhausting.

The drift check

scripts/check-verdict-backoff-chokepoint.ts (wired into test:ci) enforces four rules — one call site, inside the choke point, above the derivation, and every publish-and-maintain entry point routing through it. The fourth is the one the issue asked for: a new entry point that derives a verdict some other way fails the build instead of silently leaving the backoff behind.

Verification of the guards themselves

Every guard added was mutation-tested, and each mutant is killed by a named test:

mutation killed by
explicitlyRequestedfalse force + panel-retrigger regressions
drop the capture obligation from the backed-off path both #10061 cases (5 → 0 recapture jobs) and the new full-budget property test
remove the guard entirely the settled-verdict, poll-tick and webhook cases
add a second call site / move the guard below the derivation / add a bypassing entry point / rename the choke point the drift-check suite

Closes #10227

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (Closes #10227).

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck (run again after the test files were added)
  • npm run test:coverage locally, unsharded
  • Full npx vitest run: 26,703 passed. The single failure was miner-cli --version against a stale dist/; npm run build:miner then 35/35 green.
  • npm run build:mcp
  • npm audit --audit-level=moderate — findings are pre-existing on main; this PR changes no dependency (package.json diff is two script lines, package-lock.json untouched).
  • New or changed behavior has unit tests for new branches, fallback paths, and the reversed behaviours.
  • checkers-wired, dead-exports, dead-source-files, test-wiring, coverage-boltons, import-specifiers

No UI, OpenAPI, wrangler binding, src/selfhost/** env read, or DB change, so no regenerated artifact applies.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized and low-noise. This PR changes no public-facing string.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes: none.
  • API/OpenAPI/MCP behavior: unchanged.
  • UI changes: none.
  • Visible UI changes: none, so no UI Evidence section.
  • Public docs/changelogs: no changelog edit.

Notes

The acceptance criterion this PR cannot close by itself is the last one: records_per_head falling for the upstream_state_change population. The pre-deploy baseline is recorded above (3.52 over 99 heads in 24h) so the post-deploy comparison is against a measured number rather than a remembered one.

@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

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

@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 31, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
loopover-ui 27fad9e Jul 31 2026, 11:43 PM

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@JSONbored
JSONbored force-pushed the fix/verdict-backoff-chokepoint branch from a623142 to 01c50aa Compare July 31, 2026 21:56
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.29%. Comparing base (12f3c6a) to head (27fad9e).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10276   +/-   ##
=======================================
  Coverage   92.28%   92.29%           
=======================================
  Files         939      939           
  Lines      114775   114781    +6     
  Branches    27715    27715           
=======================================
+ Hits       105920   105935   +15     
+ Misses       7553     7546    -7     
+ Partials     1302     1300    -2     
Flag Coverage Δ
backend 95.71% <100.00%> (+0.01%) ⬆️

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

Files with missing lines Coverage Δ
src/queue/processors.ts 94.87% <100.00%> (+0.26%) ⬆️

@JSONbored
JSONbored force-pushed the fix/verdict-backoff-chokepoint branch from 01c50aa to 69f5769 Compare July 31, 2026 22:21
@JSONbored
JSONbored force-pushed the fix/verdict-backoff-chokepoint branch 2 times, most recently from 36b9e1c to 0e71f7c Compare July 31, 2026 22:55
@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 31, 2026
…webhook path is covered (#10227)

right. The record half sits at the single ledger write every verdict passes through -- correct, and
its own comment says why. The skip half sat at the ENTRY of the whole publish-and-maintain pass, so
it suppressed everything the pass OWED, not just the redundant re-derivation.

That made the escape hatch an allowlist. Every bounded retry chain had to remember to add itself,
and the default for anything unlisted was "suppress me" -- so the webhook path could never be
guarded at all: adding it truncated #10061's recapture chain from MAX_CAPTURE_RETRY_ATTEMPTS (5) to
VERDICT_BACKOFF_MIN_REPEATS (3), reproduced here before the fix. The webhook path is where the churn
actually is. Measured on the Orb over 24h: upstream_state_change carries 348 decision records across
99 heads (3.52 records_per_head), while every other reason sits at 1.08-1.32. The sweep-only guard
fired 11 times in the same window.

The skip now sits at the one point a pass derives a verdict, inside maybePublishPrPublicSurface --
the same KIND of place the record half already occupies. Everything the pass owes runs above it and
cannot be truncated by construction; the pending gate check-run, the AI review, the gate evaluation,
the publish and the maintenance ledger write all sit below it. Both entry points reach a verdict only
through that function, so neither has to opt in and the webhook path is covered without a second
call site.

The recapture chain was the one obligation living below that line, inside the comment publish, so it
is extracted (runVisualCaptureObligation) and run explicitly on the backed-off path. Its files
resolver is threaded as a thunk rather than a resolved list so a backed-off pass on a repo with
screenshots off pays for no diff fetch. The normal publish path is unchanged: same call, same place,
same order.

explicitlyRequested shrinks to one value, webhook.forceAiReview, and it means exactly one thing --
a human asked for this pass (an operator re-gate, or a consumed "Re-run LoopOver review" click).
previewPollAttempt needed an exemption only because the old guard sat above the recapture chain.

Two behaviours #10229 established are deliberately reversed, both pinned by name in
verdict-stability-wire.test.ts rather than left to be rediscovered:

  * A poll tick is now backed off. It is machine-paced, not human-asked, and its chain runs above
    the guard, so it has no business in a hatch whose meaning is "a human asked for this".
  * A backed-off pass charges the regate repair budget again. onReachedReadiness's documented rule is
    "count executions, not deferrals", and a pass that clears readiness, decides type labels and
    advances the capture chain is an execution. It is also the anti-starvation direction: a repair on
    a permanently settled hold would otherwise reselect every sweep tick forever without exhausting.

scripts/check-verdict-backoff-chokepoint.ts keeps the structure honest -- one call site, inside the
choke point, above the derivation, and every publish-and-maintain entry point routing through it. The
fourth rule is the one the issue asked for: a NEW entry point that derives a verdict some other way
fails the build instead of silently leaving the backoff behind.

Closes #10227
@JSONbored
JSONbored force-pushed the fix/verdict-backoff-chokepoint branch from 0e71f7c to 27fad9e Compare July 31, 2026 23:43
@JSONbored
JSONbored merged commit cb49431 into main Jul 31, 2026
6 of 7 checks passed
@JSONbored
JSONbored deleted the fix/verdict-backoff-chokepoint branch July 31, 2026 23:51
JSONbored pushed a commit that referenced this pull request Aug 1, 2026
…t block

#10276 added it to the local gate after this branch's last rebase; the
branch's own checker (correctly) fails validate-code until it is wired.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

design: verdict-stability backoff guards the whole pass instead of the verdict derivation, so every bounded retry chain needs an exemption

1 participant