@@ -69,6 +69,9 @@ const CONTROLLER_RECOVERY_INTERVAL_MS = 1_000;
6969const HIDDEN_CONTROLLER_RECOVERY_INTERVAL_MS = 5_000 ;
7070const TAKEOVER_POLL_INTERVAL_MS = 2_000 ;
7171const HIDDEN_TAKEOVER_POLL_INTERVAL_MS = 5_000 ;
72+ // Reloads can briefly land on a stale bootstrap snapshot before runtime replay settles,
73+ // especially on slower CI runners. Reattaching ready tabs a few times closes that gap.
74+ const READY_TAB_RUNTIME_RECOVERY_DELAYS_MS = [ 0 , 1_000 , 3_000 , 7_000 ] as const ;
7275
7376type WorkbenchRuntimeCoordinatorProps = {
7477 appSettings : AppSettings ;
@@ -465,18 +468,35 @@ export const WorkbenchRuntimeCoordinator = ({
465468 ) ;
466469
467470 useEffect ( ( ) => {
468- if ( ! attachedWorkspaceFingerprint ) {
471+ if ( ! attachedWorkspaceFingerprint || ! isOnline ) {
469472 return ;
470473 }
471474
472- const workspaceIds = state . tabs
473- . filter ( ( tab ) => tab . status === "ready" )
474- . map ( ( tab ) => tab . id ) ;
475+ let cancelled = false ;
476+ const recoverReadyTabs = ( ) => {
477+ if ( cancelled ) {
478+ return ;
479+ }
480+ const workspaceIds = stateRef . current . tabs
481+ . filter ( ( tab ) => tab . status === "ready" )
482+ . map ( ( tab ) => tab . id ) ;
483+ void Promise . all ( workspaceIds . map ( async ( workspaceId ) => {
484+ await reattachWorkspaceRuntime ( workspaceId ) ;
485+ } ) ) ;
486+ } ;
475487
476- void Promise . all ( workspaceIds . map ( async ( workspaceId ) => {
477- await reattachWorkspaceRuntime ( workspaceId ) ;
478- } ) ) ;
479- } , [ attachedWorkspaceFingerprint , reattachWorkspaceRuntime ] ) ;
488+ recoverReadyTabs ( ) ;
489+ const timers = READY_TAB_RUNTIME_RECOVERY_DELAYS_MS
490+ . filter ( ( delayMs ) => delayMs > 0 )
491+ . map ( ( delayMs ) => window . setTimeout ( recoverReadyTabs , delayMs ) ) ;
492+
493+ return ( ) => {
494+ cancelled = true ;
495+ timers . forEach ( ( timer ) => {
496+ window . clearTimeout ( timer ) ;
497+ } ) ;
498+ } ;
499+ } , [ attachedWorkspaceFingerprint , isOnline , reattachWorkspaceRuntime ] ) ;
480500
481501 const controllerHeartbeatFingerprint = useMemo (
482502 ( ) => state . tabs
0 commit comments