fix(terminal): retry every dead split renderer

This commit is contained in:
Brennan Benson
2026-07-14 03:42:59 -07:00
parent 82669d2127
commit 467d01acc4
2 changed files with 49 additions and 3 deletions
@@ -301,6 +301,47 @@ describe('requestTerminalPaneRecovery', () => {
secondSplit.unregister()
})
it('does not abandon a certified sibling behind a failed liveness retry', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
await requestTerminalPaneRecovery({
tabId: 'tab-1',
ptyId: 'pty-initial',
reason: 'write-stalled'
})
const recoveryGeneration = captureTerminalPaneRecoveryGeneration('tab-1')
const livenessSplit = registerTerminalPaneRecoveryInstance('tab-1')
const certifiedSplit = registerTerminalPaneRecoveryInstance('tab-1')
mocks.hasPty.mockResolvedValue(false)
await requestTerminalPaneRecovery({
tabId: 'tab-1',
ptyId: 'pty-not-live',
reason: 'input-undeliverable',
terminalRecoveryGeneration: recoveryGeneration,
terminalRecoveryInstanceId: livenessSplit.id,
requireAuthoritativeLiveness: true
})
await requestTerminalPaneRecovery({
tabId: 'tab-1',
ptyId: 'pty-certified-dead',
reason: 'write-stalled',
terminalRecoveryGeneration: recoveryGeneration,
terminalRecoveryInstanceId: certifiedSplit.id
})
await vi.advanceTimersByTimeAsync(15_000)
expect(mocks.hasPty).toHaveBeenCalledWith('pty-not-live')
expect(mocks.remountTerminalTabForRecovery).toHaveBeenCalledTimes(2)
expect(mocks.recordRendererCrashBreadcrumb).toHaveBeenLastCalledWith(
'terminal_pane_recovery_remount',
{ tabId: 'tab-1', reason: 'write-stalled' }
)
livenessSplit.unregister()
certifiedSplit.unregister()
})
it('budgets tabs independently', async () => {
expect(
await requestTerminalPaneRecovery({ tabId: 'tab-1', ptyId: 'pty-1', reason: 'write-stalled' })
@@ -138,13 +138,18 @@ function scheduleRecoveryRetry(request: RecoveryRequest, delayMs: number): void
const timer = setTimeout(
() => {
pendingRetryByTabId.delete(request.tabId)
const currentRequest = [...requestsByInstanceId.values()].find(
const currentRequests = [...requestsByInstanceId.values()].filter(
isCurrentTerminalRecoveryRequest
)
if (!currentRequest) {
if (currentRequests.length === 0) {
return
}
void requestTerminalPaneRecovery(currentRequest)
// Why: one split's liveness probe may fail or never settle while a
// sibling has a probe-certified dead renderer. Start every current
// request so the first valid remount wins and invalidates the rest.
void Promise.all(
currentRequests.map((currentRequest) => requestTerminalPaneRecovery(currentRequest))
)
},
Math.max(delayMs, 1_000)
)