Skip to content

Commit ac3e558

Browse files
committed
feat(hot-reload): enhance client recovery during extension hot-reload to prevent multiple restarts
1 parent 550a096 commit ac3e558

1 file changed

Lines changed: 30 additions & 4 deletions

File tree

src/main.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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).
307310
registerClientRecoveryHandler(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.
360368
const 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. */
363378
let 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

Comments
 (0)