@@ -34,6 +34,10 @@ type SharedCodexAppServerClientState = {
3434 leasedReleases : WeakMap < CodexAppServerClient , Array < ( ) => void > > ;
3535} ;
3636
37+ // Clients we already force-closed for suspect retirement; a repeat retire must
38+ // report closed:false instead of pretending to close the corpse again.
39+ const suspectClosedClients = new WeakSet < CodexAppServerClient > ( ) ;
40+
3741// Symbol.for shares one client table across duplicate module copies (dist +
3842// src bundles in one process). Plugin updates restart the gateway, so every
3943// copy writing this state runs the same code and the shape never migrates.
@@ -436,6 +440,9 @@ export function retireSharedCodexAppServerClientIfCurrent(
436440 if ( opts ?. failActiveLeases ) {
437441 entry . closeError = new Error ( "codex app-server client is closed" ) ;
438442 const closed = closeRetiredSharedClientEntry ( entry ) ;
443+ if ( closed ) {
444+ suspectClosedClients . add ( client ) ;
445+ }
439446 return { activeLeases : entry . activeLeases , closed } ;
440447 }
441448 const closed = closeRetiredSharedClientEntryIfIdle ( entry ) ;
@@ -444,6 +451,14 @@ export function retireSharedCodexAppServerClientIfCurrent(
444451 }
445452 const activeLeases = state . leasedReleases . get ( client ) ?. length ?? 0 ;
446453 if ( activeLeases > 0 ) {
454+ // A gracefully detached client (e.g. one-shot cleanup) can still be leased
455+ // when a later terminal-idle kill declares it suspect; the map miss must
456+ // not let the poisoned process keep serving those co-leases.
457+ if ( opts ?. failActiveLeases && ! suspectClosedClients . has ( client ) ) {
458+ suspectClosedClients . add ( client ) ;
459+ client . close ( ) ;
460+ return { activeLeases, closed : true } ;
461+ }
447462 return { activeLeases, closed : false } ;
448463 }
449464 return undefined ;
0 commit comments