Skip to content

Commit c891f1d

Browse files
committed
feat(hot-reload): implement mutex for extension hot-reload to prevent simultaneous rebuilds
1 parent 26e38ed commit c891f1d

1 file changed

Lines changed: 30 additions & 15 deletions

File tree

src/main.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ const codebaseMutex = new Mutex();
354354
// subsequent callers wait and then re-check the flags.
355355
const hotReloadMutex = new Mutex();
356356

357+
// Guards extension hot-reload so parallel tool calls can't trigger
358+
// simultaneous Client rebuilds — only the first caller reloads,
359+
// subsequent callers wait then see the updated fingerprint/timestamp.
360+
const extHotReloadMutex = new Mutex();
361+
357362
/** Shared result from the hot-reload check — set by the winner, consumed by waiters. */
358363
let hotReloadResult: CallToolResult | null = null;
359364

@@ -480,22 +485,32 @@ function registerTool(tool: ToolDefinition): void {
480485
// Either condition triggers handleHotReload() which tells Host to
481486
// stop Client → build → spawn new Client. If build is already current,
482487
// the rebuild step is a fast no-op.
488+
//
489+
// Serialized via extHotReloadMutex so parallel tool calls can't
490+
// trigger simultaneous Client rebuilds. The first caller does the
491+
// reload; subsequent callers wait, then re-check and see the
492+
// updated fingerprint/timestamp.
483493
if (!isStandalone && config.explicitExtensionDevelopmentPath) {
484-
const stale = isBuildStale(config.extensionBridgePath);
485-
const windowStartedAt = lifecycleService.debugWindowStartedAt;
486-
const buildNewerThanWindow = !stale
487-
&& windowStartedAt !== undefined
488-
&& hasBuildChangedSinceWindowStart(config.extensionBridgePath, windowStartedAt);
489-
490-
logger(`[hot-reload] check: stale=${stale}, buildNewerThanWindow=${buildNewerThanWindow}, extDir=${config.extensionBridgePath}`);
491-
492-
if (stale || buildNewerThanWindow) {
493-
const reason = stale ? 'source stale' : 'manual build detected';
494-
logger(`[tool:${tool.name}] Extension needs hot-reload (${reason}) — reloading…`);
495-
await lifecycleService.handleHotReload();
496-
writeExtSourceFingerprint(config.extensionBridgePath);
497-
extensionHotReloadInfo = {builtAt: Date.now()};
498-
logger(`[tool:${tool.name}] Hot-reload complete — reconnected`);
494+
const extHrGuard = await extHotReloadMutex.acquire();
495+
try {
496+
const stale = isBuildStale(config.extensionBridgePath);
497+
const windowStartedAt = lifecycleService.debugWindowStartedAt;
498+
const buildNewerThanWindow = !stale
499+
&& windowStartedAt !== undefined
500+
&& hasBuildChangedSinceWindowStart(config.extensionBridgePath, windowStartedAt);
501+
502+
logger(`[hot-reload] check: stale=${stale}, buildNewerThanWindow=${buildNewerThanWindow}, extDir=${config.extensionBridgePath}`);
503+
504+
if (stale || buildNewerThanWindow) {
505+
const reason = stale ? 'source stale' : 'manual build detected';
506+
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`);
511+
}
512+
} finally {
513+
extHrGuard.dispose();
499514
}
500515
}
501516

0 commit comments

Comments
 (0)