Skip to content

Commit 5fa9658

Browse files
authored
🤖 fix: retry workspace creation with hash suffix on name collision (#779)
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 15d1de9 commit 5fa9658

File tree

1 file changed

+82
-15
lines changed

1 file changed

+82
-15
lines changed

‎src/node/services/ipcMain.ts‎

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,68 @@ 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+
/** Maximum number of retry attempts when workspace name collides */
49+
const MAX_WORKSPACE_NAME_COLLISION_RETRIES = 3;
50+
51+
/**
52+
* Checks if an error indicates a workspace name collision
53+
*/
54+
function isWorkspaceNameCollision(error: string | undefined): boolean {
55+
return error?.includes("Workspace already exists") ?? false;
56+
}
57+
58+
/**
59+
* Generates a unique workspace name by appending a random suffix
60+
*/
61+
function appendCollisionSuffix(baseName: string): string {
62+
const suffix = Math.random().toString(36).substring(2, 6);
63+
return `${baseName}-${suffix}`;
64+
}
65+
66+
import type {
67+
Runtime,
68+
WorkspaceCreationResult,
69+
WorkspaceCreationParams,
70+
} from "@/node/runtime/Runtime";
71+
72+
/**
73+
* Try to create a workspace, retrying with hash suffix on name collision.
74+
* Returns the final branch name used and the creation result.
75+
*/
76+
async function createWorkspaceWithCollisionRetry(
77+
runtime: Runtime,
78+
params: Omit<WorkspaceCreationParams, "directoryName">,
79+
baseBranchName: string
80+
): Promise<{ branchName: string; result: WorkspaceCreationResult }> {
81+
let currentBranchName = baseBranchName;
82+
83+
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
84+
const result = await runtime.createWorkspace({
85+
...params,
86+
branchName: currentBranchName,
87+
directoryName: currentBranchName,
88+
});
89+
90+
if (result.success) {
91+
return { branchName: currentBranchName, result };
92+
}
93+
94+
// If collision and not last attempt, retry with suffix
95+
if (isWorkspaceNameCollision(result.error) && attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES) {
96+
log.debug(`Workspace name collision for "${currentBranchName}", retrying with suffix`);
97+
currentBranchName = appendCollisionSuffix(baseBranchName);
98+
continue;
99+
}
100+
101+
// Non-collision error or exhausted retries - return failure
102+
return { branchName: currentBranchName, result };
103+
}
104+
105+
// Should never reach here due to return in final iteration
106+
throw new Error("Unexpected: workspace creation loop completed without return");
107+
}
108+
47109
import { generateWorkspaceName } from "./workspaceTitleGenerator";
48110
/**
49111
* IpcMain - Manages all IPC handlers and service coordination
@@ -307,18 +369,21 @@ export class IpcMain {
307369

308370
const initLogger = this.createInitLogger(workspaceId);
309371

310-
const createResult = await runtime.createWorkspace({
311-
projectPath,
312-
branchName,
313-
trunkBranch: recommendedTrunk,
314-
directoryName: branchName,
315-
initLogger,
316-
});
372+
// Create workspace with automatic collision retry
373+
const { branchName: finalBranchName, result: createResult } =
374+
await createWorkspaceWithCollisionRetry(
375+
runtime,
376+
{ projectPath, branchName, trunkBranch: recommendedTrunk, initLogger },
377+
branchName
378+
);
317379

318380
if (!createResult.success || !createResult.workspacePath) {
319381
return Err({ type: "unknown", raw: createResult.error ?? "Failed to create workspace" });
320382
}
321383

384+
// Use the final branch name (may have suffix if collision occurred)
385+
branchName = finalBranchName;
386+
322387
const projectName =
323388
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
324389

@@ -618,19 +683,21 @@ export class IpcMain {
618683

619684
const initLogger = this.createInitLogger(workspaceId);
620685

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-
});
686+
// Phase 1: Create workspace structure with retry on name collision
687+
const { branchName: finalBranchName, result: createResult } =
688+
await createWorkspaceWithCollisionRetry(
689+
runtime,
690+
{ projectPath, branchName, trunkBranch: normalizedTrunkBranch, initLogger },
691+
branchName
692+
);
629693

630694
if (!createResult.success || !createResult.workspacePath) {
631695
return { success: false, error: createResult.error ?? "Failed to create workspace" };
632696
}
633697

698+
// Use the final branch name (may have suffix if collision occurred)
699+
branchName = finalBranchName;
700+
634701
const projectName =
635702
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
636703

0 commit comments

Comments
 (0)