Skip to content

fix(review): auto-clear stale manual-review lock-contention hold; sync engine gate-decision twin#9107

Merged
JSONbored merged 2 commits into
mainfrom
fix/9009-lock-contention-label-autoclear
Jul 26, 2026
Merged

fix(review): auto-clear stale manual-review lock-contention hold; sync engine gate-decision twin#9107
JSONbored merged 2 commits into
mainfrom
fix/9009-lock-contention-label-autoclear

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Two fixes bundled together:

#9009 — manual-review has no removal path. Once applied, the label sticks forever even when its cause was purely transient. Narrow scope: auto-clears specifically for AI-review lock contention (aiReviewLockContendedResult's "AI review already in progress for this PR head" finding, #8999's exact trigger) once a later pass shows the contention has resolved and nothing else currently justifies the hold. Every other hold reason (guardrail, migration collision, breaker downgrades, a human's own hold, ...) still requires a maintainer to lift it — see the manualHoldReason === null safety check in planAgentMaintenanceActions.

A (repo, PR)-keyed transient marker (24h TTL) records when contention fires; the next pass consumes it once contention no longer shows up in that pass's gate warnings.

Closes #9009

Engine gate-decision twin drift. #9082/#9103 (merged) added the secret_scan_incomplete neutral-hold branch to src/rules/advisory.ts but never mirrored it into the hand-duplicated engine-package twin (packages/loopover-engine/src/advisory/gate-advisory.ts). This drift is live on main right now, and is exactly what stranded #9103's own CI (the version-bump-or-co-edit guard correctly flagged the host-only edit). Mirrors both the new branch and the updated secret_leak provenance comment into the twin, and bumps @loopover/engine 3.15.0 → 3.15.1 (the guard's own documented escape hatch for a one-sided fix landing after the fact) — package-lock.json and the miner's expected-engine.version pin synced alongside it.

Test plan

  • test/unit/agent-actions.test.ts: 5 new pure-function tests for the label-removal branch (313/313 passing)
  • test/unit/queue-2.test.ts: 2 new full-pipeline integration tests — one proving the label is applied then auto-cleared across two passes, one proving a co-occurring guardrail hit keeps the label despite contention resolving
  • 100% line+branch coverage on every new line in both changed source files (verified via lcov)
  • npx tsc --noEmit clean
  • check-engine-parity.ts, check-migrations.ts, check-schema-drift.ts all green

@superagent-security

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (db5f7c1) and head (f2565a1) 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).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 26, 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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui f2565a1 Commit Preview URL

Branch Preview URL
Jul 26 2026, 07:14 PM

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.35294% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.88%. Comparing base (db5f7c1) to head (f2565a1).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ages/loopover-engine/src/advisory/gate-advisory.ts 25.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9107      +/-   ##
==========================================
- Coverage   93.89%   93.88%   -0.01%     
==========================================
  Files         813      813              
  Lines       80677    80694      +17     
  Branches    24479    24486       +7     
==========================================
+ Hits        75748    75762      +14     
- Misses       3562     3564       +2     
- Partials     1367     1368       +1     
Flag Coverage Δ
backend 95.17% <82.35%> (-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 95.63% <100.00%> (+0.01%) ⬆️
src/settings/agent-actions.ts 98.54% <100.00%> (+0.02%) ⬆️
...ages/loopover-engine/src/advisory/gate-advisory.ts 95.83% <25.00%> (-1.73%) ⬇️

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

loopover-orb Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-26 19:11:12 UTC

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

🛑 Suggested Action - Manual Review

Review summary
This PR bundles two logically-distinct fixes: (1) an opt-in, provenance-tracked auto-clear of the manual-review label specifically for AI-review lock contention (closing #9009), gated by a `manualHoldReason === null` safety check so no other hold reason is ever silently overridden, and (2) mirroring the `secret_scan_incomplete` neutral-hold branch and updated `secret_leak` comment into the engine-package twin (`gate-advisory.ts`), with a coordinated version bump (3.15.0→3.15.1) across `package.json`, `package-lock.json`, and the miner's pin. Both changes are traced end-to-end with dedicated pure-function and full-pipeline tests, and the diff correctly gates removal on `hasLabel`/no-duplicate-pending-action checks. The main gap is coverage: codecov/patch is at 82.35% against a 99% target, meaning some added branches (likely including edge cases in the new transient-marker TTL/consumption logic in `processors.ts`) are untested despite the two new integration tests.

Nits — 6 non-blocking
  • src/queue/processors.ts:3223 — the 24h TTL (`24 * 60 * 60`) is a bare magic number; consider extracting a named constant (e.g. `LOCK_CONTENTION_MARKER_TTL_SECONDS`) for readability, especially since the comment already explains the reasoning in prose.
  • The engine-twin mirror in gate-advisory.ts duplicates a large comment block verbatim from the host copy — worth double-checking `scripts/check-engine-parity.ts` actually asserts this new branch too, not just the marker functions, so future drift is caught automatically rather than relying on manual PR review again.
  • The failing `codecov/patch` check (82.35% vs 99% target) should be investigated before merge — some added branch in the bundled changes lacks a real test, even though the two new integration tests look reasonably thorough.
  • Confirm which lines/branches codecov/patch flagged as uncovered — likely candidates are the `hadPriorLockContentionHold`/`aiReviewLockContentionThisPass` combination branches in processors.ts that aren't hit by the two new queue-2 tests.
  • Consider naming the 24h TTL constant in src/queue/processors.ts for consistency with other TTL constants likely already defined elsewhere in the codebase (e.g. OFFICIAL_MINER_DETECTION_TTL_MS visible in the file's tail).
  • 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

  • validate
  • validate-code
  • codecov/patch — 82.35% 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 #9009
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High 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: 13 registered-repo PR(s), 13 merged, 315 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 315 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Partially addressed
The PR implements a real provenance-tracked auto-clear path for exactly one of the six transient entries the issue enumerates (AI-review lock contention/#8999), with a transient marker, planner-side safety check, and dedicated tests, but explicitly leaves the other five transient triggers (merge/close precision circuit breakers, unstable mergeable_state, migration collision, ciUnverified/checks-AP

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: 13 PR(s), 315 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
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 &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; 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: 03a7f8b529a9 · pack: oss-anti-slop
  • record: 1e52794a1667 (schema v3, head 9d5f01e)

🟩 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

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 26, 2026
…ntion resolves

manual-review has no removal path today, so once applied it sticks forever even
when its underlying cause was a purely transient infra artifact. Narrow scope:
only auto-clears for the one case with live evidence of being transient --
aiReviewLockContendedResult's "AI review already in progress for this PR head"
finding (a pass losing the AI-review lock race, #8999). Every other hold
reason (guardrail, migration collision, breaker downgrades, a human's own
hold, ...) still requires a maintainer to lift it.

A (repo, PR)-keyed transient marker (24h TTL) records when contention fires;
the next pass clears the marker once contention no longer shows up in that
pass's gate warnings, and planAgentMaintenanceActions removes the label only
when nothing else currently justifies the hold.

Closes #9009
…et_scan_incomplete change

#9082/#9103 (merged) added the secret_scan_incomplete neutral-hold branch to
src/rules/advisory.ts's evaluateGateCheckCore, plus an updated secret_leak
provenance comment, but never touched the hand-duplicated engine-package twin
(packages/loopover-engine/src/advisory/gate-advisory.ts) -- the two are
supposed to stay logically identical (see check-engine-parity.ts's
GATE_DECISION_TWIN_PAIR). Confirmed the drift is live on main right now (the
engine copy is missing the whole branch), which is exactly what stranded PR
#9103's own CI: the version-bump-or-co-edit guard (checkGateDecisionVersionBump)
correctly flagged the host-only edit at merge time.

Mirrors both pieces into the engine twin and bumps @loopover/engine 3.15.0 ->
3.15.1 (the guard's own documented escape hatch for a one-sided fix landing
after the fact), syncing package-lock.json and the miner's
expected-engine.version pin alongside it -- same shape as the precedent manual
bump in bc9e81a.
@JSONbored
JSONbored force-pushed the fix/9009-lock-contention-label-autoclear branch from 9d5f01e to f2565a1 Compare July 26, 2026 19:11
JSONbored added a commit that referenced this pull request Jul 26, 2026
…check

The miner-UI typecheck reaches into packages/loopover-miner/lib/** (declared
as an input), and those files import @loopover/engine -- whose types resolve
to packages/loopover-engine/dist/index.d.ts. But unlike @loopover/ui#typecheck
(which declares the explicit @loopover/engine#build dependsOn edge for exactly
this reason), the miner-UI task only had ^build, and miner-ui has no
package.json dependency on engine to create that edge implicitly. Turbo
therefore ran engine build CONCURRENTLY with this typecheck; whenever the
engine cache missed AND the scheduler interleaved the two the wrong way, the
typecheck raced ahead of the dist emit and failed with dozens of phantom
"Cannot find module '@loopover/engine'" errors -- an intermittent, whole-job
validate-code failure with no real defect behind it (observed live on #9107's
first run and on main run 30214733191, while sibling runs of the identical
commit passed).
@JSONbored
JSONbored merged commit 6aacf08 into main Jul 26, 2026
6 of 7 checks passed
@JSONbored
JSONbored deleted the fix/9009-lock-contention-label-autoclear branch July 26, 2026 19:14
JSONbored added a commit that referenced this pull request Jul 26, 2026
…ibutor-cap-lock TTL (#9109)

* fix(orb): flush orphaned locks at boot, restore sweep candidacy on resume, and widen the contributor-cap-lock TTL

Three related lock/liveness fixes:

#9021 -- every Redis-backed lock (pr-actuation-lock, ai-review-lock,
contributor-cap-wake/-lock) survives a container restart with its TTL intact.
On this single-instance deployment, any lock present at boot is provably
orphaned -- the process that claimed it is gone. Left alone, each class
strands real work for its own TTL (30 min for ai-review-lock, #8998). Adds
flushOrphanedLocksAtBoot (selfhost/redis-cache.ts), a best-effort SCAN-delete
over the four exclusivity-lock prefixes, wired in at server boot before the
queue starts. Deliberately leaves delivery:*, pr-panel-retrigger-pending:*,
ci-pending-first-seen:*, and fresh-rebase-forced:* untouched -- none are
exclusivity locks, and each has its own restart-survival contract.

#9018 -- a paused repo's PRs can go green DURING the pause window (CI-
completion passes plan-and-suppress the whole time), and resuming performed no
catch-up: if a PR was ALSO regated once before the pause, agent-sweep.ts's
#never-endless-reregate rule permanently excludes it from future sweep
candidacy, stranding it silently. Adds clearPullRequestsRegatedAtForOpenPrs and
calls it on the paused->live transition, both from the single-repo MCP
pause/resume tool and the installation-wide bulk-settings route -- restoring
one-shot sweep candidacy for the repo's open PRs exactly once.

#9024 -- claimContributorCapLock's 30s TTL was sized only for the executor's
brief pre-merge recheck, but maybeCloseForContributorCapOnOpen holds the SAME
lock across a much longer body (token mint, live GitHub calls, label writes,
the nested pr-actuation-lock, a full executeAgentMaintenanceActions pass) that
can exceed 30s under GitHub rate-limit backoff -- reopening the exact #7284
TOCTOU this lock exists to close. Widened to 600s, matching claimPrActuationLock's
own TTL for a comparably long mutating body.

Closes #9018, #9021, #9024

Tests: 3 new flushOrphanedLocksAtBoot tests (deletes matching keys, returns 0
with nothing to do, fails open per-pattern on a scan error), a paused->live
MCP-tool test (clears open PRs' markers, never closed ones, never on pause or
a repeat resume), two bulk-settings-route tests (clears across all repos in an
installation; a non-agentPaused bulk change never touches the marker), and a
TTL-parity regression pinning claimContributorCapLock's TTL to
claimPrActuationLock's. 100% line+branch coverage on every changed line
(src/server.ts is Codecov's own documented ignore-listed entrypoint file).

* fix(ci): add the missing engine-build edge to @loopover/ui-miner#typecheck

The miner-UI typecheck reaches into packages/loopover-miner/lib/** (declared
as an input), and those files import @loopover/engine -- whose types resolve
to packages/loopover-engine/dist/index.d.ts. But unlike @loopover/ui#typecheck
(which declares the explicit @loopover/engine#build dependsOn edge for exactly
this reason), the miner-UI task only had ^build, and miner-ui has no
package.json dependency on engine to create that edge implicitly. Turbo
therefore ran engine build CONCURRENTLY with this typecheck; whenever the
engine cache missed AND the scheduler interleaved the two the wrong way, the
typecheck raced ahead of the dist emit and failed with dozens of phantom
"Cannot find module '@loopover/engine'" errors -- an intermittent, whole-job
validate-code failure with no real defect behind it (observed live on #9107's
first run and on main run 30214733191, while sibling runs of the identical
commit passed).
JSONbored added a commit that referenced this pull request Jul 26, 2026
…s enforce

Both of these are broken on main right now and fail every PR branched from it,
including this one:

1. .release-please-manifest.json still pinned packages/loopover-engine at
   3.15.0 while its package.json says 3.15.1 -- the manual bump in #9107 (which
   the engine twin-parity guard required) never synced the release manifest, so
   release-manifest:sync:check has failed on every commit since. Regenerated via
   `npm run release-manifest:sync`.

2. apps/loopover-ui's prettier gate failed on two files last touched by #8848
   (proof-of-power-stats-model.ts and proof-of-power-stats.test.tsx) -- two
   over-long object literals prettier wants wrapped. Applied `prettier --write`;
   pure formatting, no behavior change.
JSONbored added a commit that referenced this pull request Jul 26, 2026
…erdicts, fix holdout misattribution (#9110)

* fix(orb): retry the disposition on lock contention, retry inconclusive verdicts, and stop misattributing holdout holds

Three disposition-integrity fixes:

#9025 -- maybeRunAgentMaintenance returned silently when it lost the per-PR
actuation lock: the job completed "successfully", nothing re-queued the
disposition, and no audit row recorded that a planned action was abandoned.
That silently amplified every restart incident -- the recovered job re-ran,
hit its own dead predecessor's orphaned lock, and lost the disposition a
SECOND time with no trace at all. Now throws PrActuationLockContendedError
(the same contract review-evasion.ts's withPrActuationLock already used for
this exact condition; the queue honors its 5s retryAfterMs via
consumingRetryDelayMs) and records a named audit event. Both maintenance call
sites' catch handlers now re-throw retryable/rate-limit errors instead of
logging-and-dropping them, matching the review pipeline's own propagation
contract; a plain non-retryable failure is still swallowed and logged.

#9019 -- `cacheable=0` conflates two unrelated things: a DYNAMIC review
context (grounding/RAG), where the verdict is conclusive but not durable
across time, and a genuinely INCONCLUSIVE verdict (a provider outage, a
consensus-disputed roll). Because published rows were exempt from the
non-cacheable cooldown, a transient outage verdict became FINAL for that head
the moment it surfaced -- the bot never retried, directly contradicting the
finding's own "re-evaluates on the next update" text, while a green PR gave
the contributor no reason to push the commit that would force one. Worse, the
head-AGNOSTIC one-shot lookup pinned that same outage verdict across ALL
future heads, so a contributor pushing new code could not escape it either.
Records the review's own verdict as metadata.inconclusive and keys both
behaviors on it: the publish exemption still applies to dynamic-context rows
(#2119 unchanged) but not to inconclusive ones, and the one-shot cadence skips
inconclusive rows entirely -- that PR never got its one real shot. The cooldown
still bounds retries to at most one attempt per window.

#9040 -- every "auto-action held by precision circuit breaker" audit row was
wrong. agentHoldAuditDetail inferred the breaker purely from "a terminal action
was planned but is not in the final plan", but the call site passes the
POST-HOLDOUT plan, so every ε-holdout adjudication hold (#8831) was attributed
to a breaker that had never engaged -- 6 of 6 live rows paired 1:1 (within
20ms) with decision_audit_holdout events while system_flags contained no
engaged breaker at all. Each transform now REPORTS its own engagement, derived
from its own before/after pair, and the holdout gets its own reason string; the
residual set-difference case returns an honest generic reason instead of a
false specific attribution.

Closes #9019
Closes #9025
Closes #9040

* fix(ci): repair two pre-existing main breakages the drift/format gates enforce

Both of these are broken on main right now and fail every PR branched from it,
including this one:

1. .release-please-manifest.json still pinned packages/loopover-engine at
   3.15.0 while its package.json says 3.15.1 -- the manual bump in #9107 (which
   the engine twin-parity guard required) never synced the release manifest, so
   release-manifest:sync:check has failed on every commit since. Regenerated via
   `npm run release-manifest:sync`.

2. apps/loopover-ui's prettier gate failed on two files last touched by #8848
   (proof-of-power-stats-model.ts and proof-of-power-stats.test.tsx) -- two
   over-long object literals prettier wants wrapped. Applied `prettier --write`;
   pure formatting, no behavior change.
JSONbored added a commit that referenced this pull request Jul 26, 2026
…ses (#9111)

* fix(orb): retry the disposition on lock contention, retry inconclusive verdicts, and stop misattributing holdout holds

Three disposition-integrity fixes:

#9025 -- maybeRunAgentMaintenance returned silently when it lost the per-PR
actuation lock: the job completed "successfully", nothing re-queued the
disposition, and no audit row recorded that a planned action was abandoned.
That silently amplified every restart incident -- the recovered job re-ran,
hit its own dead predecessor's orphaned lock, and lost the disposition a
SECOND time with no trace at all. Now throws PrActuationLockContendedError
(the same contract review-evasion.ts's withPrActuationLock already used for
this exact condition; the queue honors its 5s retryAfterMs via
consumingRetryDelayMs) and records a named audit event. Both maintenance call
sites' catch handlers now re-throw retryable/rate-limit errors instead of
logging-and-dropping them, matching the review pipeline's own propagation
contract; a plain non-retryable failure is still swallowed and logged.

#9019 -- `cacheable=0` conflates two unrelated things: a DYNAMIC review
context (grounding/RAG), where the verdict is conclusive but not durable
across time, and a genuinely INCONCLUSIVE verdict (a provider outage, a
consensus-disputed roll). Because published rows were exempt from the
non-cacheable cooldown, a transient outage verdict became FINAL for that head
the moment it surfaced -- the bot never retried, directly contradicting the
finding's own "re-evaluates on the next update" text, while a green PR gave
the contributor no reason to push the commit that would force one. Worse, the
head-AGNOSTIC one-shot lookup pinned that same outage verdict across ALL
future heads, so a contributor pushing new code could not escape it either.
Records the review's own verdict as metadata.inconclusive and keys both
behaviors on it: the publish exemption still applies to dynamic-context rows
(#2119 unchanged) but not to inconclusive ones, and the one-shot cadence skips
inconclusive rows entirely -- that PR never got its one real shot. The cooldown
still bounds retries to at most one attempt per window.

#9040 -- every "auto-action held by precision circuit breaker" audit row was
wrong. agentHoldAuditDetail inferred the breaker purely from "a terminal action
was planned but is not in the final plan", but the call site passes the
POST-HOLDOUT plan, so every ε-holdout adjudication hold (#8831) was attributed
to a breaker that had never engaged -- 6 of 6 live rows paired 1:1 (within
20ms) with decision_audit_holdout events while system_flags contained no
engaged breaker at all. Each transform now REPORTS its own engagement, derived
from its own before/after pair, and the holdout gets its own reason string; the
residual set-difference case returns an honest generic reason instead of a
false specific attribution.

Closes #9019
Closes #9025
Closes #9040

* fix(ci): repair two pre-existing main breakages the drift/format gates enforce

Both of these are broken on main right now and fail every PR branched from it,
including this one:

1. .release-please-manifest.json still pinned packages/loopover-engine at
   3.15.0 while its package.json says 3.15.1 -- the manual bump in #9107 (which
   the engine twin-parity guard required) never synced the release manifest, so
   release-manifest:sync:check has failed on every commit since. Regenerated via
   `npm run release-manifest:sync`.

2. apps/loopover-ui's prettier gate failed on two files last touched by #8848
   (proof-of-power-stats-model.ts and proof-of-power-stats.test.tsx) -- two
   over-long object literals prettier wants wrapped. Applied `prettier --write`;
   pure formatting, no behavior change.

* fix(ci): fail closed on truncated CI reads and partial GraphQL responses

Two silent-failure classes on the wrong-merge path, both in the live CI/review
readers that gate every merge decision.

#9051 -- a FAILED check-runs page fetch already set checkRunsIncomplete, but
EXHAUSTING the 10-page cap with `rel="next"` still present did not: the loop
just exited and reduceLiveCiAggregate treated a truncated set as complete. A red
check on page 11+ was therefore invisible -> ciState "passed" -> planner
reviewGood -> MERGE. The executor's act-boundary recheck calls the same function
so it reproduced the wrong verdict rather than catching it, and the false
"passed" was persisted into the durable cross-job CI cache. Fixed for both the
check-runs and classic-status loops. Separately, the check-suites backstop --
the LAST gate before a commit is certified settled -- read page 1 only with no
Link follow, so a first-party suite still running on page 2 never set anyPending;
it now paginates and returns null (which the reducer already fails closed on)
when its own cap is exhausted. The GraphQL twin's `checkSuites` selection had no
hasNextPage guard either, unlike its `contexts` sibling one line above; added.

#9052 -- fetchLiveReviewThreadBlockers read `connection?.nodes` and returned []
without ever checking the GraphQL top-level `errors` array. GitHub's standard
partial-failure shape under load is HTTP 200 with `reviewThreads: null` plus
`errors`, which yielded [] -- indistinguishable from a genuinely thread-free PR
-- so a maintainer's unresolved blocking thread was dropped from the findings and
the gate could conclude success and merge over the open objection. Unlike a
transport error (nothing read at all -> fail open, unchanged), a partial result
means the answer is known-unreliable, so it now fails CLOSED with a synthetic
blocker. The sibling readers in this file already guarded this; this one was the
outlier. Same class, second instance: fetchLivePullRequestReviewDecision also had
no errors check and returned undefined, which let the caller's
`liveReviewDecision ?? pr.reviewDecision` substitute a STALE stored APPROVED for
a read that failed -- so a PR later flipped to CHANGES_REQUESTED still merged. It
now returns an explicit REVIEW_DECISION_UNREADABLE sentinel that survives the ??
fallback, matches no real enum value (so every === comparison is correctly
false), and is checked by name at the one approval-queue site that would
otherwise have read it as "confirmed no changes requested".

Closes #9051
Closes #9052

Tests: 3 cap-exhaustion regressions (check-runs, statuses, check-suites), a
GraphQL checkSuites-truncation guard, a 200-with-errors review-threads
fail-closed test, two review-decision sentinel tests, and an approval-queue test
proving an unreadable decision no longer clears a conflict-justified close. 100%
coverage on all 88 added lines; 1368/1368 across the 12 affected suites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orb(disposition): manual-review label has no autonomous removal path — every transient hold becomes a permanent human-only stuck state

1 participant