Skip to content

Commit e61216a

Browse files
committed
🤖 fix: retry workspace creation with hash suffix on name collision
When creating a new workspace fails because a workspace with the same name already exists, automatically retry with a random 4-char suffix (e.g., 'my-workspace-ab12'). Retries up to 3 times before failing. This applies to both: - Auto-generated workspace names (from first message) - User-specified workspace names (via WORKSPACE_CREATE IPC) _Generated with mux_
1 parent 580111d commit e61216a

File tree

1 file changed

+83
-21
lines changed

1 file changed

+83
-21
lines changed

src/node/services/ipcMain.ts

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ import { PTYService } from "@/node/services/ptyService";
4444
import type { TerminalWindowManager } from "@/desktop/terminalWindowManager";
4545
import type { TerminalCreateParams, TerminalResizeParams } from "@/common/types/terminal";
4646
import { ExtensionMetadataService } from "@/node/services/ExtensionMetadataService";
47+
48+
49+
/** Maximum number of retry attempts when workspace name collides */
50+
const MAX_WORKSPACE_NAME_COLLISION_RETRIES = 3;
51+
52+
/**
53+
* Checks if an error indicates a workspace name collision
54+
*/
55+
function isWorkspaceNameCollision(error: string | undefined): boolean {
56+
return error?.includes("Workspace already exists") ?? false;
57+
}
58+
59+
/**
60+
* Generates a unique workspace name by appending a random suffix
61+
*/
62+
function appendCollisionSuffix(baseName: string): string {
63+
const suffix = Math.random().toString(36).substring(2, 6);
64+
return `${baseName}-${suffix}`;
65+
}
4766
import { generateWorkspaceName } from "./workspaceTitleGenerator";
4867
/**
4968
* IpcMain - Manages all IPC handlers and service coordination
@@ -307,18 +326,40 @@ export class IpcMain {
307326

308327
const initLogger = this.createInitLogger(workspaceId);
309328

310-
const createResult = await runtime.createWorkspace({
311-
projectPath,
312-
branchName,
313-
trunkBranch: recommendedTrunk,
314-
directoryName: branchName,
315-
initLogger,
316-
});
329+
// Retry workspace creation with hash suffix on name collision
330+
let currentBranchName = branchName;
331+
let createResult;
332+
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
333+
createResult = await runtime.createWorkspace({
334+
projectPath,
335+
branchName: currentBranchName,
336+
trunkBranch: recommendedTrunk,
337+
directoryName: currentBranchName,
338+
initLogger,
339+
});
340+
341+
if (createResult.success) {
342+
break;
343+
}
344+
345+
// If collision and not last attempt, retry with suffix
346+
if (isWorkspaceNameCollision(createResult.error) && attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES) {
347+
log.debug(`Workspace name collision for "${currentBranchName}", retrying with suffix`);
348+
currentBranchName = appendCollisionSuffix(branchName);
349+
continue;
350+
}
317351

318-
if (!createResult.success || !createResult.workspacePath) {
352+
// Non-collision error or exhausted retries
319353
return Err({ type: "unknown", raw: createResult.error ?? "Failed to create workspace" });
320354
}
321355

356+
if (!createResult!.success || !createResult!.workspacePath) {
357+
return Err({ type: "unknown", raw: createResult!.error ?? "Failed to create workspace" });
358+
}
359+
360+
// Use the final branch name (may have suffix if collision occurred)
361+
branchName = currentBranchName;
362+
322363
const projectName =
323364
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
324365

@@ -337,7 +378,7 @@ export class IpcMain {
337378
config.projects.set(projectPath, projectConfig);
338379
}
339380
projectConfig.workspaces.push({
340-
path: createResult.workspacePath!,
381+
path: createResult!.workspacePath!,
341382
id: workspaceId,
342383
name: branchName,
343384
createdAt: metadata.createdAt,
@@ -359,7 +400,7 @@ export class IpcMain {
359400
projectPath,
360401
branchName,
361402
trunkBranch: recommendedTrunk,
362-
workspacePath: createResult.workspacePath,
403+
workspacePath: createResult!.workspacePath!,
363404
initLogger,
364405
})
365406
.catch((error: unknown) => {
@@ -618,19 +659,40 @@ export class IpcMain {
618659

619660
const initLogger = this.createInitLogger(workspaceId);
620661

621-
// Phase 1: Create workspace structure (FAST - returns immediately)
622-
const createResult = await runtime.createWorkspace({
623-
projectPath,
624-
branchName,
625-
trunkBranch: normalizedTrunkBranch,
626-
directoryName: branchName, // Use branch name as directory name
627-
initLogger,
628-
});
662+
// Phase 1: Create workspace structure with retry on name collision
663+
let currentBranchName = branchName;
664+
let createResult;
665+
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
666+
createResult = await runtime.createWorkspace({
667+
projectPath,
668+
branchName: currentBranchName,
669+
trunkBranch: normalizedTrunkBranch,
670+
directoryName: currentBranchName,
671+
initLogger,
672+
});
629673

630-
if (!createResult.success || !createResult.workspacePath) {
674+
if (createResult.success) {
675+
break;
676+
}
677+
678+
// If collision and not last attempt, retry with suffix
679+
if (isWorkspaceNameCollision(createResult.error) && attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES) {
680+
log.debug(`Workspace name collision for "${currentBranchName}", retrying with suffix`);
681+
currentBranchName = appendCollisionSuffix(branchName);
682+
continue;
683+
}
684+
685+
// Non-collision error or exhausted retries
631686
return { success: false, error: createResult.error ?? "Failed to create workspace" };
632687
}
633688

689+
if (!createResult!.success || !createResult!.workspacePath) {
690+
return { success: false, error: createResult!.error ?? "Failed to create workspace" };
691+
}
692+
693+
// Use the final branch name (may have suffix if collision occurred)
694+
branchName = currentBranchName;
695+
634696
const projectName =
635697
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
636698

@@ -656,7 +718,7 @@ export class IpcMain {
656718
}
657719
// Add workspace to project config with full metadata
658720
projectConfig.workspaces.push({
659-
path: createResult.workspacePath!,
721+
path: createResult!.workspacePath!,
660722
id: workspaceId,
661723
name: branchName,
662724
createdAt: metadata.createdAt,
@@ -684,7 +746,7 @@ export class IpcMain {
684746
projectPath,
685747
branchName,
686748
trunkBranch: normalizedTrunkBranch,
687-
workspacePath: createResult.workspacePath,
749+
workspacePath: createResult!.workspacePath!,
688750
initLogger,
689751
})
690752
.catch((error: unknown) => {

0 commit comments

Comments
 (0)