Agent Merge: recover from a checks fragment the host refuses - #332003
Merged
Benjamin Christopher Simmonds (benibenj) merged 2 commits intoAug 21, 2026
Merged
Conversation
Benjamin Christopher Simmonds (benibenj)
enabled auto-merge (squash)
August 21, 2026 17:13
Copilot started reviewing on behalf of
Benjamin Christopher Simmonds (benibenj)
August 21, 2026 17:14
View session
Contributor
There was a problem hiding this comment.
Pull request overview
Improves Agent Merge recovery when GitHub refuses protected checks data.
Changes:
- Retries checks without workflow names and degrades unavailable expected suites.
- Adds authorization diagnostics, polling backoff, and indeterminate-state budgeting.
- Adds tests for query fallback and fragment diagnostics.
Show a summary per file
| File | Description |
|---|---|
src/vs/platform/github/test/node/pullRequestQueryService.test.ts |
Tests checks-query fallbacks. |
src/vs/platform/github/common/pullRequestResourceService.ts |
Adjusts failure polling cadence. |
src/vs/platform/github/common/pullRequestQueryService.ts |
Implements authorization fallbacks. |
src/vs/platform/github/common/githubService.ts |
Supplies logging to query service. |
src/vs/platform/agentHost/test/common/agentMerge.test.ts |
Tests fragment-specific reasons. |
src/vs/platform/agentHost/node/agentMergeController.ts |
Adds authorization handling and timeout budget. |
src/vs/platform/agentHost/common/agentMerge.ts |
Adds stable indeterminate causes. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Suppressed comments (3)
src/vs/platform/agentHost/node/agentMergeController.ts:442
- Please add an automated controller test for this new timeout state machine. The committed tests do not cover the 30-minute threshold, observation-gap reset, changed-cause reset, or eventual disable behavior, so the central guard against another unbounded loop can regress despite the one-off compiled-method verification described in the PR.
if (this._isIndeterminateBudgetExhausted(session, runtime, gate.cause)) {
this._disable(session, agentMerge, `the pull request state could not be evaluated for ${Math.round(maximumIndeterminateDuration / 60_000)} minutes: ${gate.reason}`);
return;
src/vs/platform/github/common/pullRequestResourceService.ts:701
- Please cover this cadence branch in
pullRequestResourceService.test.tsfor both a never-loaded checks fragment and a stale pending value. Existing polling tests do not assert thatstatus === 'error'switches either case from the pending interval tochecksBackstop, which is one of the failure-loop fixes introduced here.
const checks = entry.snapshot.get().checks;
return checks.status !== 'error' && checksPending(checks.value)
? visible ? this._policy.checksPendingVisible : this._policy.checksPendingBackground
: this._policy.checksBackstop;
src/vs/platform/agentHost/node/agentMergeController.ts:811
- This JSDoc exceeds the repository's 1–2 sentence limit and duplicates the surrounding control flow. Reduce it to the method's timing contract.
/**
* Reports whether one unchanged indeterminate cause has persisted past its
* budget.
- Files reviewed: 7/7 changed files
- Comments generated: 5
- Review effort level: Balanced
roblourens
previously approved these changes
Aug 21, 2026
roblourens
approved these changes
Aug 21, 2026
Agent Merge could hang indefinitely on a pull request whose checks fragment was refused by GitHub, showing the user nothing at all. When an organization enforces SAML SSO and the signed-in token is not authorized for it, the checks GraphQL query is refused with HTTP 200 and a FORBIDDEN error in the body. `CheckRun.checkSuite` is non-nullable, so the refusal on the GitHub Actions data behind the workflow-name subselection null-propagates and fails the whole fragment rather than that one field. Checks then never load, the gate is permanently indeterminate, and nothing surfaces: - checks never loaded reads as pending, so the fragment held the fast poll cadence and re-requested a permanently refused query roughly once a minute, forever; - indeterminate was the only gate outcome with neither an action nor a budget, so the session stayed resident with nothing to show; - only `authentication` raised an auth requirement, so an `authorization` refusal never prompted the user to re-authorize. Recover the fragment and make the failure legible: - gate the workflow-name subselection and drop it for a repository whose host refuses it, keeping the checks themselves. Only the rollup request is retried, and an expected-check-suites refusal degrades to absent and incomplete, so neither is mistaken for the other; - raise an auth requirement for `authorization` too, naming the organization and calling out SSO when GitHub reports it; - give indeterminate a budget over continuously observed time, so a pull request that can never be read stops instead of idling while a turn or a sleeping host cannot exhaust it; - name the fragment and its error in the indeterminate reason instead of one string shared by five fragments; - back off persistent authorization failures, and stop an errored checks fragment from holding the fast cadence. Partial check data is still never accepted, so a refused fragment continues to fail closed rather than reporting checks it could not read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only the first refresh of a subscription reports a failure by throwing, so raising the auth requirement from the evaluation catch missed the case it was meant to cover: every later refusal is recorded on the snapshot and read as an ordinary indeterminate gate, leaving the session waiting on a credential the user was never asked for. Detect a refused gate fragment on the snapshot and request a credential there, once per distinct failure so a persistent refusal does not nag and a failure after recovery can prompt again. Share one path with the throw site, and keep the fragment list with the gate that defines it. Also shorten the comments added with this change to the limits in the coding guidelines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Benjamin Christopher Simmonds (benibenj)
force-pushed
the
benibenj/agents/agent-merge-failure-debugging
branch
from
August 21, 2026 20:50
0f5ce1a to
e79657d
Compare
Benjamin Christopher Simmonds (benibenj)
merged commit Aug 21, 2026
86d7ed0
into
main
44 of 45 checks passed
Benjamin Christopher Simmonds (benibenj)
deleted the
benibenj/agents/agent-merge-failure-debugging
branch
August 21, 2026 23:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agent Merge could hang indefinitely on a pull request whose
checksfragment GitHub refuses, showing the user nothing at all. I hit this on a real session and traced it from the agent host logs.What happens
When an organization enforces SAML SSO and the signed-in token is not authorized for it, the checks GraphQL query is refused with HTTP 200 and a
FORBIDDENentry in the body (hence the oddauthorization:200in the logs).microsoft/vscodeis public, socore,mergeability, review threads and comments all load fine — onlychecksfails, because it reaches into GitHub Actions data that is org-protected even on a public repo.Verified by introspection:
CheckRun.checkSuiteisCheckSuite!(NON_NULL), so a refusal on the Actions data behindcheckSuite { workflowRun { workflow { name } } }null-propagates and destroys the entire check node — the whole fragment fails, not just that one informational field.From there, five things compounded into a silent, unbounded loop:
checksPending(undefined)returnstrue, so a fragment that never loaded read as "checks still running" and held the fast poll cadence — a measured steady ~66s re-request of a permanently refused query, forever, with no backoff._scheduleAfterFailuretreatedauthorizationas "just reschedule": no failure counting, no backoff.checkstherefore never satisfiedisCompleteHeadFragment, so the gate was permanentlyindeterminate.indeterminatewas the only gate outcome with neither an action nor a budget —prompthas repeat/total caps andterminaldisables, butindeterminatejust rescheduled the backstop unconditionally.kind === 'authentication'raised an auth requirement, so anauthorizationrefusal never prompted the user. Agent Merge looked enabled and did nothing.The generic reason string made it hard to diagnose too: one
"Pull request state is incomplete or stale"covered five different fragments and never said which one, or why.The fix
workflowNameis purely informational (one consumer: repair-agent context inagentMergeTools.ts). The decision is remembered per repository so later polls don't re-pay for a rejected request._fetchExpectedCheckSuitesreads the same protected Actions surface and Agent Merge always requests it (checks: { required: true }), so a refusal there would otherwise be misattributed — re-running the whole paginated query for nothing and disabling workflow names on a repo where they were never the problem. That request now degrades to absent-and-incomplete instead.authorizationas well asauthentication, naming the organization and calling out SSO when GitHub reports it — turning a silent hang into an actionable prompt.indeterminatea budget, measured over continuously observed time. Evaluation is suspended while a turn runs and stops entirely while the host sleeps, so a gap between observations restarts the window rather than counting toward it. Keyed on a stablecause(e.g.checks:authorization) rather than the volatile reason text, so an oscillating reason can't defeat it.FragmentState.erroralready carried this; it was simply never read._setErrorpreserves the previous value, so this covers both the never-loaded and the stale-value cases.Safety
Partial check data is still never accepted.
throwGraphQLErrorscontinues to throw on any non-emptyerrorsarray, so blocked nodes (which arrive asnullbecause of the NON_NULL propagation) can never reachtoCheck. Silently dropping refused checks could let Agent Merge conclude required checks passed and merge unsafely — the existing behaviour fails closed, which is correct; what was missing was visibility, not permissiveness.classifyAgentMergeRequiredChecksignoresexpectedSuitesentirely, so degrading it does not affect any merge decision.Validation
npm run typecheck-clientclean.{ checks: { required: true } }config), the expected-suites refusal, and the fragment-naming reasons.