Refresh visible reviews automatically and stop settled merged polling (#25788)

Refresh only actual on-screen review cards and the selected visible review panel, with a single metadata owner per execution host. Open reviews refresh every 60 seconds when selected and 120 seconds otherwise. Settled merged reviews stop automatic refresh; pending merged checks continue, hidden rows stop, and work changes or stale re-exposure discover fresh state.

Remove the polling setting. Preserve provider/SSH/runtime ownership, mixed-version cache behavior, failure backoff, bounded foreground admission, request coalescing, and mutation invalidation. Cover older persisted caches and unknown HEADs.

Validated with 1,628 focused tests, typechecking, lint/code-quality gates, hidden Electron viewport checks, and parallel Codex/Claude Opus 5.5 adversarial reviews. Measurements and policy details are in PR #25788.

Fixes #25746

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
This commit is contained in:
Neil
2026-10-06 00:53:49 -07:00
committed by GitHub
co-authored by ggbdpq
parent 9905765e3b
commit a2f197fde7
100 changed files with 5190 additions and 1795 deletions
+30 -1
View File
@@ -1,3 +1,6 @@
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/coalesced-probe'
import { getSshGitProviderGeneration } from '../../../providers/ssh-git-dispatch'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { PRCheckDetail } from '../../../../shared/github/check-types'
import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/work-items'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
@@ -120,7 +123,7 @@ export async function getPRChecksViaRestFallback(
* Uses GitHub's combined GraphQL rollup so check runs and legacy commit statuses
* arrive in one cached request; suite-only approval blockers are included too.
*/
export async function getPRChecks(
async function readPRChecks(
repoPath: string,
prNumber: number,
headSha?: string,
@@ -231,3 +234,29 @@ export async function getPRChecks(
throw err
}
}
const checksReads: CoalescedProbes<PRCheckDetail[]> = new Map()
export function getPRChecks(
repoPath: string,
prNumber: number,
headSha?: string,
prRepo?: GitHubApiRepository | null,
options?: { noCache?: boolean },
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<PRCheckDetail[]> {
const key = JSON.stringify([
repoPath,
prNumber,
headSha ?? null,
prRepo ?? null,
Boolean(options?.noCache),
connectionId ?? null,
connectionId ? getSshGitProviderGeneration(connectionId) : null,
githubReadExecutionScope(localGitOptions)
])
return runCoalescedProbe(checksReads, key, () =>
readPRChecks(repoPath, prNumber, headSha, prRepo, options, connectionId, localGitOptions)
)
}
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { createHash } from 'node:crypto'
import { githubReadExecutionScope as workItemSearchScope } from '../../github-read-execution-scope'
export { githubReadExecutionScope as workItemSearchScope } from '../../github-read-execution-scope'
import { BoundedMap } from '../../../../shared/bounded-map'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/coalesced-probe'
import { createGhRateLimitBlockedError } from '../../../git/gh-rate-limit-breaker'
@@ -48,22 +49,6 @@ const responses = new BoundedMap<string, { at: number; value: unknown }>({
sizeOf: (value, key) => Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(value))
})
export function workItemSearchScope(
options: GitHubRepoExecOptions,
environment: NodeJS.ProcessEnv = options.env ?? process.env
): string {
// gh wrappers and credential selection can depend on cwd and the inherited environment.
return createHash('sha256')
.update(
JSON.stringify([
options,
process.cwd(),
Object.entries(environment).sort(([a], [b]) => a.localeCompare(b))
])
)
.digest('hex')
}
export function requestWorkItemSearch<T>(request: SearchRequest): Promise<SearchResponse<T>> {
const environment = { ...(request.environment ?? request.options.env ?? process.env) }
const scope = workItemSearchScope(request.options, environment)
@@ -1,9 +1,19 @@
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import {
hostedReviewRepoScope,
scopeGeneration
} from '../../../source-control/hosted-review-scope-generations'
import { runCoalescedProbe, type CoalescedProbes } from '../../../git/coalesced-probe'
import { getSshGitProviderGeneration } from '../../../providers/ssh-git-dispatch'
import { githubReadExecutionScope } from '../../github-read-execution-scope'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-request-refresh-types'
import { acquire, release, ghRepoExecOptions, githubRepoContext } from '../../gh-utils'
import { hostedReviewLocalGitOptionArgs, githubPRStackExecutionScope } from './../github-exec-scope'
import type { GitHubPRBranchLookupOptions } from './pull-request-lookup-data'
import { prRefreshUpstreamError } from './../gh-error-predicates'
import { resolvePRForBranchOutcome } from './branch-lookup-resolution'
const reads: CoalescedProbes<PRRefreshOutcome> = new Map()
export async function getPRForBranchOutcome(
repoPath: string,
branch: string,
@@ -23,22 +33,41 @@ export async function getPRForBranchOutcome(
const ghOptions = ghRepoExecOptions(context)
const executionScope = githubPRStackExecutionScope(connectionId, localGitOptions)
await acquire()
try {
return await resolvePRForBranchOutcome({
repoPath,
branchName,
linkedPRNumber,
connectionId,
fallbackPRNumber,
options,
localGitOptions,
ghOptions,
executionScope
})
} catch (err) {
return prRefreshUpstreamError(err)
} finally {
release()
}
const key = JSON.stringify([
executionScope,
connectionId ? getSshGitProviderGeneration(connectionId) : null,
repoPath,
scopeGeneration(hostedReviewRepoScope(repoPath, getRepoExecutionHostId({ connectionId }))),
branchName,
linkedPRNumber ?? null,
fallbackPRNumber ?? null,
options.acceptMergedFallbackPR ?? false,
options.currentHeadOid ?? null,
githubReadExecutionScope(ghOptions)
])
return runCoalescedProbe(
reads,
key,
async () => {
await acquire()
try {
return await resolvePRForBranchOutcome({
repoPath,
branchName,
linkedPRNumber,
connectionId,
fallbackPRNumber,
options,
localGitOptions,
ghOptions,
executionScope
})
} catch (err) {
return prRefreshUpstreamError(err)
} finally {
release()
}
},
2 * 60_000
)
}
+7 -1
View File
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import type { PRConflictSummary } from '../../../../shared/github/pull-request-types'
import { getPRConflictSummary } from '../../conflict-summary'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
@@ -59,7 +60,7 @@ export async function mergePR(
)
release()
concurrencySlotHeld = false
return await mergeGitHubPRStack({
const result = await mergeGitHubPRStack({
repository: ownerRepo,
prNumber,
method,
@@ -67,6 +68,10 @@ export async function mergePR(
headSha: restData.headRefOid,
ghOptions
})
if (result.ok) {
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
}
return result
}
const mergeBlocker = await getPRMergeBlocker(
repoPath,
@@ -89,6 +94,7 @@ export async function mergePR(
...ghOptions,
env: { ...process.env, GH_PROMPT_DISABLED: '1' }
})
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import type { GitHubPRMergeMethod } from '../../../../shared/github/pull-request-types'
import {
ghExecFileAsync,
@@ -165,13 +166,17 @@ export async function setPRAutoMerge(
await acquire()
try {
if (enabled) {
return await enablePRAutoMerge(
const result = await enablePRAutoMerge(
prNumber,
method,
ownerRepo,
ghOptions,
githubPRStackExecutionScope(connectionId, localGitOptions)
)
if (result.ok) {
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
}
return result
}
const args = ['pr', 'merge', String(prNumber), '--disable-auto']
if (ownerRepo) {
@@ -181,6 +186,7 @@ export async function setPRAutoMerge(
...ghOptions,
env: { ...process.env, GH_PROMPT_DISABLED: '1' }
})
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import {
ghExecFileAsync,
acquire,
@@ -35,6 +36,7 @@ export async function updatePRTitle(
await ghExecFileAsync(args, {
...ghOptions
})
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return true
} catch (err) {
console.warn('updatePRTitle failed:', err)
@@ -89,6 +91,7 @@ export async function updatePRDetails(
],
ghOptions
)
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import {
acquire,
classifyPullRequestUpdateError,
@@ -30,6 +31,7 @@ export async function markPRReadyForReview(
['pr', 'ready', String(prNumber), '--repo', `${ownerRepo.owner}/${ownerRepo.repo}`],
ghOptions
)
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import { ghExecFileAsync, acquire, release, type LocalGitExecOptions } from '../../gh-utils'
import { resolveGitHubRepoExecution, type GitHubApiRepository } from '../../github-api-repository'
export async function requestPRReviewers(
@@ -31,6 +32,7 @@ export async function requestPRReviewers(
...ghOptions,
env: { ...process.env, GH_PROMPT_DISABLED: '1' }
})
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -72,6 +74,7 @@ export async function removePRReviewers(
...ghOptions,
env: { ...process.env, GH_PROMPT_DISABLED: '1' }
})
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =
@@ -1,3 +1,4 @@
import { invalidateReviewLookupsAfterPRMutation } from '../../pr-mutation-review-invalidation'
import type { GitHubPullRequestStateUpdate } from '../../../../shared/issue-mutation-types'
import {
ghExecFileAsync,
@@ -35,6 +36,7 @@ export async function updatePRState(
...ghOptions
}
)
invalidateReviewLookupsAfterPRMutation(repoPath, connectionId)
return { ok: true }
} catch (err) {
const message =