From f47828d3cb593cf2b841cb84e3410a325f3d63cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:59:00 -0700 Subject: [PATCH] fix(ssh): bound the background provider-miss throttle map The git and filesystem dispatchers pass every SSH connection id in the app, and scheduleSshProviderMissRecovery recorded each one before asking the owner whether it claimed the connection. Ids the recovery declined were therefore retained forever, and the stale entry also swallowed the next miss for that id until the interval elapsed. Consult the recovery first and record only once it accepts, and prune entries past the throttle interval (an expired entry no longer throttles anything). --- .../ssh-provider-miss-recovery.test.ts | 31 ++++++++++++++++++- .../providers/ssh-provider-miss-recovery.ts | 16 +++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/providers/ssh-provider-miss-recovery.test.ts b/src/main/providers/ssh-provider-miss-recovery.test.ts index d43d85e32be..3d54fa1b911 100644 --- a/src/main/providers/ssh-provider-miss-recovery.test.ts +++ b/src/main/providers/ssh-provider-miss-recovery.test.ts @@ -7,7 +7,8 @@ import { SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE, requireSshGitProvider } from './s import { recoverSshProviderMiss, scheduleSshProviderMissRecovery, - setSshProviderMissRecovery + setSshProviderMissRecovery, + sshProviderMissRecoveryThrottleEntryCount } from './ssh-provider-miss-recovery' const TARGET = 'runtime-ssh-orca-1' @@ -85,4 +86,32 @@ describe('scheduleSshProviderMissRecovery', () => { expect(recovery).toHaveBeenCalledWith('ssh-user-target') expect(recoverSshProviderMiss('ssh-user-target')).toBeUndefined() }) + + it('keeps re-consulting the owner for declined connections and retains none of them', () => { + // Why: these dispatchers are called with every SSH connection id in the app, most of + // which no owner claims. A declined id was never dialed, so there is nothing to back + // off from — throttling it would both retain it forever and swallow the next miss. + const recovery = vi.fn(() => undefined) + setSshProviderMissRecovery(recovery) + + scheduleSshProviderMissRecovery('ssh-user-a') + scheduleSshProviderMissRecovery('ssh-user-a') + scheduleSshProviderMissRecovery('ssh-user-b') + + expect(recovery).toHaveBeenCalledTimes(3) + expect(sshProviderMissRecoveryThrottleEntryCount()).toBe(0) + }) + + it('prunes claimed connections once their throttle interval has passed', () => { + setSshProviderMissRecovery(() => Promise.resolve()) + + scheduleSshProviderMissRecovery('runtime-ssh-orca-1') + scheduleSshProviderMissRecovery('runtime-ssh-orca-2') + expect(sshProviderMissRecoveryThrottleEntryCount()).toBe(2) + + vi.advanceTimersByTime(5_000) + scheduleSshProviderMissRecovery('runtime-ssh-orca-3') + + expect(sshProviderMissRecoveryThrottleEntryCount()).toBe(1) + }) }) diff --git a/src/main/providers/ssh-provider-miss-recovery.ts b/src/main/providers/ssh-provider-miss-recovery.ts index 9453c6e71c2..2cf7c9ae8a2 100644 --- a/src/main/providers/ssh-provider-miss-recovery.ts +++ b/src/main/providers/ssh-provider-miss-recovery.ts @@ -40,14 +40,28 @@ export function scheduleSshProviderMissRecovery(connectionId: string): void { if (startedAt !== undefined && now - startedAt < BACKGROUND_RECOVERY_THROTTLE_MS) { return } - backgroundRecoveryStartedAt.set(connectionId, now) + // Why prune before recording: an expired entry no longer throttles anything, and these + // dispatchers are called with every SSH connection id in the app. + for (const [id, at] of backgroundRecoveryStartedAt) { + if (now - at >= BACKGROUND_RECOVERY_THROTTLE_MS) { + backgroundRecoveryStartedAt.delete(id) + } + } const pending = recovery(connectionId) if (!pending) { + // Declined: the owner does not claim this connection, so there is nothing to throttle + // and recording it would retain an id this map will never act on. return } + backgroundRecoveryStartedAt.set(connectionId, now) pending.catch((error: unknown) => { console.warn( `[ssh] Background provider re-attach failed for ${connectionId}: ${error instanceof Error ? error.message : String(error)}` ) }) } + +/** Test-only: the throttle map is a memory bound, which is not observable from behaviour. */ +export function sshProviderMissRecoveryThrottleEntryCount(): number { + return backgroundRecoveryStartedAt.size +}