Skip to content

fix(queue): stop the verdict backoff from eating a manual retrigger or an operator force - #10229

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

fix(queue): stop the verdict backoff from eating a manual retrigger or an operator force#10229
JSONbored merged 1 commit into
mainfrom
fix/verdict-backoff-bypasses

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

#10204 placed the verdict-stability skip (#10184) at the entry of the publish-and-maintain pass, past two things it must not run past.

Readiness fires options.onReachedReadiness — which charges regatePullRequest's bounded repair budget — and then consumes the one-shot panel-retrigger marker (#7626). A guard sitting after both meant a backed-off pass had already spent a user's "Re-run LoopOver review" click, with nothing left to re-trigger it, and had charged a repair attempt for work it never did.

Three defects, all live on main:

defect
1 a backed-off pass consumes and discards a manual retrigger marker
2 onReachedReadiness charges the repair budget for a pass that never reviewed
3 options.force — an operator's manual re-gate — is silently suppressed

The guard now sits between the readiness gate and onReachedReadiness, and honours force / previewPollAttempt.

It must not move earlier than readiness either

Readiness legitimately defers a pass, and the screenshot-table recapture chain (#10061) depends on those deferrals to bound its retry budget. Moving the guard before readiness truncated it from 5 attempts to 3 — caught by that test, not by me. Between the two is the only correct place. That constraint is now recorded in the code.

The webhook path is deliberately still unguarded

Adding the guard to handlePullRequestWebhookEvent truncates #10061's recapture budget the same way, and the only way to make it fit here would be another entry in an explicitlyRequested allowlist — or weakening that test, which I won't do.

That allowlist is the design flaw: the default for anything not listed is "suppress me", so every future bounded retry chain is silently truncated the day it ships. The fix is to move the skip to the verdict-derivation choke point that the record half of #10204 already occupies ("the single ledger write every verdict passes through, so no caller can bypass it"). Scoped in #10227.

So this PR fixes the three live defects and explicitly leaves the dominant churn path — 293 of 344 repeat evaluations in a 24h Orb window are upstream_state_change, i.e. raw webhook deliveries — for that redesign rather than forcing it through here.

Testing

#10204 shipped this wiring with no test at allgrep -rn "review_skipped_stable_verdict" test/ returned nothing. test/unit/verdict-stability-wire.test.ts is the first, and pins each defect above as a named regression.

Mutation testing: 5 mutants applied to the guard, 4 killed. The survivor (!headSha removed) is an equivalent mutant at runtime — with no head SHA the lookup misses and shouldSkipStableVerdict returns false anyway. It is enforced by TSC instead (verdictStabilityKey takes a string), and is now documented as an early-out rather than a safety guard, matching verdict-stability.ts's own reasoning about its removed exponent clamp.

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.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Full npx vitest run: 1359 files, 26517 tests, 0 failures. Green: dead-source-files, dead-exports, import-specifiers, engine-parity:drift, coverage-boltons, typecheck-coverage, cf-typegen.

If any required check was skipped, explain why:

  • Not run locally: actionlint, test:workers, mcp, ui:* — this PR touches one file under src/queue/ plus one new test. CI runs them all.
  • npm audit reports pre-existing advisories on main; no dependency changes here.

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, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

This makes the engine do more work, never less: every change here removes a case where a pass was wrongly suppressed.

UI Evidence

Not applicable — no UI, frontend, docs, or extension files are touched.

Closes #10222

…r an operator force

#10204 placed the skip at the entry of the publish-and-maintain pass, past two
things it must not run past.

Readiness fires options.onReachedReadiness -- which charges regatePullRequest's
bounded repair budget -- and then consumes the ONE-SHOT panel-retrigger marker
(#7626). A guard sitting after both meant a backed-off pass had already spent a
user's "Re-run LoopOver review" click with nothing left to re-trigger it, and had
charged a repair attempt for work it never did. The guard now sits between the
readiness gate and onReachedReadiness.

It must not move EARLIER than readiness either: readiness legitimately defers a
pass, and the screenshot-table recapture chain (#10061) depends on those
deferrals to bound its retry budget -- a pre-readiness guard truncated it from 5
attempts to 3, caught by that test.

options.force is now honoured. An operator's manual re-gate passes force: true
and was being silently suppressed; a poll tick (previewPollAttempt) likewise.
Backoff exists to stop the machine re-asking itself a settled question and must
never suppress a pass a human asked for.

The guard is extracted so both publish-and-maintain sites can share it, and its
!headSha half is documented as a TSC-enforced early-out rather than a safety
guard -- mutation testing confirms no runtime test can distinguish its absence,
and an unverifiable guard is a claim, not a safeguard (verdict-stability.ts's own
removed exponent clamp made the same point).

#10204 shipped this wiring with NO test; test/unit/verdict-stability-wire.test.ts
is the first, and pins each defect above as a regression.

The webhook path is deliberately still unguarded. Adding it there truncates
#10061's recapture budget, and the fix is not another exemption but moving the
skip to the verdict-derivation choke point the record half already uses --
scoped in #10227.

Closes #10222
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-31 13:41:04 UTC

2 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
This PR relocates the verdict-stability backoff guard in reReviewStoredPullRequest to sit between the readiness gate and onReachedReadiness/panel-retrigger consumption, and adds an explicitlyRequested escape hatch (force/previewPollAttempt) so a backed-off pass never silently eats a manual retrigger or an operator force. The reasoning is well-documented and matches the historical placement constraint (#10061's recapture budget requiring the guard to stay after readiness). New test file directly pins the three previously-broken properties (retrigger marker survival, force bypass, preview-poll bypass) against the live function rather than a fabricated unit, which is solid regression coverage.

Nits — 6 non-blocking
  • src/queue/processors.ts: the JSDoc cites '293 of 344' as evidence for prioritizing the shared helper — consider extracting these as named constants or at least noting the measurement window/source inline for future auditors.
  • The PR description states the webhook path (handlePullRequestWebhookEvent) is deliberately left unguarded due to the explicitlyRequested allowlist design flaw, but this diff does not address that flaw — worth confirming a follow-up issue is tracked for the choke-point fix mentioned in the truncated description.
  • test/unit/verdict-stability-wire.test.ts: RETRIGGER_KEY is reconstructed by string literal because the real key builder is module-private — a comment already flags this, but a brittle format match is still a nit worth a shared test helper if this pattern recurs.
  • Consider exporting a testable key-builder for pr-panel-retrigger-pending so future tests don't need to hand-reconstruct the cache key string.
  • If the webhook-path gap (defect not fixed here) is tracked, link that follow-up issue number in this PR's description for traceability.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

CI checks failing

  • codecov/patch — 75.00% of diff hit (target 99.00%)

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 #10222
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 (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 9 registered-repo PR(s), 8 merged, 310 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 9 PR(s), 310 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Partially addressed
The PR correctly fixes gaps #2 and #3 (retrigger consumption and force/previewPollAttempt bypass are now placed before onReachedReadiness and honored, with regression tests), but it explicitly and admittedly does not fix gap #1: the webhook path (handlePullRequestWebhookEvent) remains unguarded, with the author choosing to defer that fix to a future changeset rather than the required guard placeme

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: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 9 PR(s), 310 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.

Decision record
  • action: hold · clause: success
  • config: 548f36e0bfef1017b54ff108f540945a997880cd829d558e5fd061d61ed7fe1b · pack: oss-anti-slop · ci: failed
  • record: bba5f0c1dd3a7f86283f3e8347cdbb2a85fac948176b4757ebb36322079e7df0 (schema v6, head aaabefb)

🟩 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.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.36%. Comparing base (07a43da) to head (aaabefb).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 75.00% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10229      +/-   ##
==========================================
- Coverage   92.22%   91.36%   -0.87%     
==========================================
  Files         937      937              
  Lines      114413   114416       +3     
  Branches    27662    27664       +2     
==========================================
- Hits       105520   104533     -987     
- Misses       7588     8772    +1184     
+ Partials     1305     1111     -194     
Flag Coverage Δ
backend 94.13% <75.00%> (-1.54%) ⬇️

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.39% <75.00%> (+0.11%) ⬆️

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 31, 2026
@JSONbored
JSONbored merged commit 853ebe0 into main Jul 31, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the fix/verdict-backoff-bypasses branch July 31, 2026 13:43
JSONbored added a commit that referenced this pull request 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 added a commit that referenced this pull request 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 added a commit that referenced this pull request 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 added a commit that referenced this pull request 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 added a commit that referenced this pull request 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 added a commit that referenced this pull request Jul 31, 2026
…webhook path is covered (#10227) (#10276)

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

fix(queue): the verdict-stability backoff misses the webhook path, eats a manual retrigger, and ignores force

1 participant