@@ -304,7 +304,15 @@ lifecycleService.registerShutdownHandlers();
304304
305305// Wire up auto-recovery: when any tool detects the Client pipe is dead,
306306// the recovery handler asks the Host to restart the Client window.
307+ // If an extension hot-reload is already in progress, wait for it
308+ // rather than kicking off a separate recovery (which would restart
309+ // the Client window a second time).
307310registerClientRecoveryHandler ( async ( ) => {
311+ if ( extensionHotReloadInProgress ) {
312+ logger ( '[client-recovery] Extension hot-reload in progress — waiting instead of independent recovery…' ) ;
313+ try { await extensionHotReloadInProgress ; } catch { /* hot-reload error handled elsewhere */ }
314+ return ;
315+ }
308316 await lifecycleService . recoverClientConnection ( ) ;
309317} ) ;
310318
@@ -359,6 +367,13 @@ const hotReloadMutex = new Mutex();
359367// subsequent callers wait then see the updated fingerprint/timestamp.
360368const extHotReloadMutex = new Mutex ( ) ;
361369
370+ // Set while an extension hot-reload is in progress so the client
371+ // recovery handler (ensureClientAvailable) can wait for it instead of
372+ // triggering an independent window restart. Without this, parallel
373+ // tool calls that detect a dying Client pipe during a hot-reload would
374+ // each kick off separate recovery attempts → multiple window restarts.
375+ let extensionHotReloadInProgress : Promise < void > | null = null ;
376+
362377/** Shared result from the hot-reload check — set by the winner, consumed by waiters. */
363378let hotReloadResult : CallToolResult | null = null ;
364379
@@ -504,10 +519,21 @@ function registerTool(tool: ToolDefinition): void {
504519 if ( stale || buildNewerThanWindow ) {
505520 const reason = stale ? 'source stale' : 'manual build detected' ;
506521 logger ( `[tool:${ tool . name } ] Extension needs hot-reload (${ reason } ) — reloading…` ) ;
507- await lifecycleService . handleHotReload ( ) ;
508- writeExtSourceFingerprint ( config . extensionBridgePath ) ;
509- extensionHotReloadInfo = { builtAt : Date . now ( ) } ;
510- logger ( `[tool:${ tool . name } ] Hot-reload complete — reconnected` ) ;
522+
523+ // Expose the in-flight hot-reload as a promise so the client
524+ // recovery handler can wait for it instead of triggering a
525+ // separate (duplicate) window restart.
526+ let resolveHotReload ! : ( ) => void ;
527+ extensionHotReloadInProgress = new Promise < void > ( r => { resolveHotReload = r ; } ) ;
528+ try {
529+ await lifecycleService . handleHotReload ( ) ;
530+ writeExtSourceFingerprint ( config . extensionBridgePath ) ;
531+ extensionHotReloadInfo = { builtAt : Date . now ( ) } ;
532+ logger ( `[tool:${ tool . name } ] Hot-reload complete — reconnected` ) ;
533+ } finally {
534+ resolveHotReload ( ) ;
535+ extensionHotReloadInProgress = null ;
536+ }
511537 }
512538 } finally {
513539 extHrGuard . dispose ( ) ;
0 commit comments