@@ -11,6 +11,7 @@ import { findPackageRoot } from '../../../package-paths.js';
1111import { findExactCloakProfileProcesses } from './process-matcher.js' ;
1212import { log } from '../../../logger.js' ;
1313import { CliError , EXIT_CODES } from '../../../errors.js' ;
14+ import { isClosedContextError } from '../../run/types.js' ;
1415
1516const UNRESOLVED = Symbol ( 'unresolved' ) ;
1617const TARGET_PAGE_MATCH_TIMEOUT_MS = 1_000 ;
@@ -180,11 +181,6 @@ function pageIsClosed(page: PlaywrightPage): boolean {
180181 return page . isClosed ?.( ) === true ;
181182}
182183
183- function isClosedContextError ( error : unknown ) : boolean {
184- const message = error instanceof Error ? error . message : String ( error ) ;
185- return / T a r g e t p a g e , c o n t e x t o r b r o w s e r h a s b e e n c l o s e d / i. test ( message ) ;
186- }
187-
188184function errorMessage ( error : unknown ) : string {
189185 return error instanceof Error ? error . message : String ( error ) ;
190186}
@@ -282,11 +278,21 @@ export class CloakSessionManager {
282278 const sessionRuntime = this . getSessionRuntime ( runtime , sessionId ) ;
283279 const existing = sessionRuntime . pages . get ( leaseKey ) ;
284280 if ( existing && ! pageIsClosed ( existing . page ) && ! freshPage ) {
285- await this . assertOwnedWindow ( runtime , sessionId , existing ) ;
286- runtime . lastSeenAt = Date . now ( ) ;
287- existing . idleTimeout = input . idleTimeout ;
288- this . refreshIdleTimer ( runtime , sessionRuntime , leaseKey , existing ) ;
289- return { profileId, leaseKey, context : runtime . context , page : existing . page , pageId : existing . pageId } ;
281+ try {
282+ await this . assertOwnedWindow ( runtime , sessionId , existing ) ;
283+ runtime . lastSeenAt = Date . now ( ) ;
284+ existing . idleTimeout = input . idleTimeout ;
285+ this . refreshIdleTimer ( runtime , sessionRuntime , leaseKey , existing ) ;
286+ return { profileId, leaseKey, context : runtime . context , page : existing . page , pageId : existing . pageId } ;
287+ } catch ( error ) {
288+ if ( ! isClosedContextError ( error ) ) throw error ;
289+ // isClosed() reported false, but the liveness probe above shows the
290+ // underlying CDP connection is actually dead. Invalidate the Profile
291+ // runtime and fall through to acquire a fresh page instead of handing
292+ // the same broken lease back out (webcmd#314).
293+ this . invalidateProfileRuntime ( profileId , runtime ) ;
294+ if ( ! pageIsClosed ( existing . page ) ) await existing . page . close ( ) . catch ( ( ) => { } ) ;
295+ }
290296 }
291297 const acquired = await this . acquireSessionPage ( profileId , sessionId , input . windowMode ) ;
292298 const entry = await this . registerOwnedPage ( acquired . runtime , acquired . session , acquired . page , {
@@ -667,6 +673,19 @@ export class CloakSessionManager {
667673 return entries . length ;
668674 }
669675
676+ /**
677+ * Invalidates the Profile runtime backing `context`, if it's still the active
678+ * one, without evicting or retrying the command that observed it. Used by the
679+ * `run` action (webcmd#314) when a post-run snapshot capture surfaces a
680+ * closed-context signature: the run itself may have genuinely succeeded, but
681+ * the connection is dying, so the next command on this Session shouldn't be
682+ * handed the same lease.
683+ */
684+ invalidateIfClosedContext ( profileId : string , context : BrowserContext ) : void {
685+ const runtime = this . profiles . get ( profileId ) ;
686+ if ( runtime ?. context === context ) this . invalidateProfileRuntime ( profileId , runtime ) ;
687+ }
688+
670689 async shutdown ( ) : Promise < void > {
671690 this . shuttingDown = true ;
672691 while ( this . profileLaunches . size > 0 ) {
0 commit comments