Skip to content

Commit 33fd3c8

Browse files
committed
🤖 fix: prevent race condition between rename and streaming
Added renamingWorkspaces set to track workspaces being renamed. - sendMessage and resumeStream now check this flag and return error - Both AI-generated rename and user-initiated rename set the flag - Flag is cleared in finally blocks to ensure cleanup This prevents a race where: 1. Stream ends, rename starts 2. Auto-resume or user triggers new stream 3. Stream uses old paths while rename is changing them _Generated with mux_
1 parent 19e9dcd commit 33fd3c8

File tree

1 file changed

+59
-26
lines changed

1 file changed

+59
-26
lines changed

src/node/services/workspaceService.ts

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export class WorkspaceService extends EventEmitter {
8282
string,
8383
{ chat: () => void; metadata: () => void }
8484
>();
85+
// Tracks workspaces currently being renamed to prevent streaming during rename
86+
private readonly renamingWorkspaces = new Set<string>();
8587

8688
constructor(
8789
private readonly config: Config,
@@ -689,37 +691,44 @@ export class WorkspaceService extends EventEmitter {
689691
// Wait for the stream to complete before renaming (rename is blocked during streaming)
690692
await this.waitForStreamComplete(workspaceId);
691693

692-
// Attempt to rename with collision retry (same logic as workspace creation)
693-
let finalName = aiGeneratedName;
694-
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
695-
const renameResult = await this.rename(workspaceId, finalName);
694+
// Mark workspace as renaming to block new streams during the rename operation
695+
this.renamingWorkspaces.add(workspaceId);
696+
try {
697+
// Attempt to rename with collision retry (same logic as workspace creation)
698+
let finalName = aiGeneratedName;
699+
for (let attempt = 0; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES; attempt++) {
700+
const renameResult = await this.rename(workspaceId, finalName);
701+
702+
if (renameResult.success) {
703+
log.info("Successfully renamed workspace to AI-generated name", {
704+
workspaceId,
705+
oldName: currentName,
706+
newName: finalName,
707+
});
708+
return;
709+
}
696710

697-
if (renameResult.success) {
698-
log.info("Successfully renamed workspace to AI-generated name", {
711+
// If collision and not last attempt, retry with suffix
712+
if (
713+
renameResult.error?.includes("already exists") &&
714+
attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
715+
) {
716+
log.debug(`Workspace name collision for "${finalName}", retrying with suffix`);
717+
finalName = appendCollisionSuffix(aiGeneratedName);
718+
continue;
719+
}
720+
721+
// Non-collision error or out of retries - keep placeholder name
722+
log.info("Failed to rename workspace to AI-generated name", {
699723
workspaceId,
700-
oldName: currentName,
701-
newName: finalName,
724+
aiGeneratedName: finalName,
725+
error: renameResult.error,
702726
});
703727
return;
704728
}
705-
706-
// If collision and not last attempt, retry with suffix
707-
if (
708-
renameResult.error?.includes("already exists") &&
709-
attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
710-
) {
711-
log.debug(`Workspace name collision for "${finalName}", retrying with suffix`);
712-
finalName = appendCollisionSuffix(aiGeneratedName);
713-
continue;
714-
}
715-
716-
// Non-collision error or out of retries - keep placeholder name
717-
log.info("Failed to rename workspace to AI-generated name", {
718-
workspaceId,
719-
aiGeneratedName: finalName,
720-
error: renameResult.error,
721-
});
722-
return;
729+
} finally {
730+
// Always clear renaming flag, even on error
731+
this.renamingWorkspaces.delete(workspaceId);
723732
}
724733
} catch (error) {
725734
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -892,6 +901,9 @@ export class WorkspaceService extends EventEmitter {
892901
return Err(validation.error ?? "Invalid workspace name");
893902
}
894903

904+
// Mark workspace as renaming to block new streams during the rename operation
905+
this.renamingWorkspaces.add(workspaceId);
906+
895907
const metadataResult = await this.aiService.getWorkspaceMetadata(workspaceId);
896908
if (!metadataResult.success) {
897909
return Err(`Failed to get workspace metadata: ${metadataResult.error}`);
@@ -959,6 +971,9 @@ export class WorkspaceService extends EventEmitter {
959971
} catch (error) {
960972
const message = error instanceof Error ? error.message : String(error);
961973
return Err(`Failed to rename workspace: ${message}`);
974+
} finally {
975+
// Always clear renaming flag, even on error
976+
this.renamingWorkspaces.delete(workspaceId);
962977
}
963978
}
964979

@@ -1103,6 +1118,15 @@ export class WorkspaceService extends EventEmitter {
11031118
});
11041119

11051120
try {
1121+
// Block streaming while workspace is being renamed to prevent path conflicts
1122+
if (this.renamingWorkspaces.has(workspaceId)) {
1123+
log.debug("sendMessage blocked: workspace is being renamed", { workspaceId });
1124+
return Err({
1125+
type: "unknown",
1126+
raw: "Workspace is being renamed. Please wait and try again.",
1127+
});
1128+
}
1129+
11061130
const session = this.getOrCreateSession(workspaceId);
11071131
void this.updateRecencyTimestamp(workspaceId);
11081132

@@ -1145,6 +1169,15 @@ export class WorkspaceService extends EventEmitter {
11451169
options: SendMessageOptions | undefined = { model: "claude-3-5-sonnet-latest" }
11461170
): Promise<Result<void, SendMessageError>> {
11471171
try {
1172+
// Block streaming while workspace is being renamed to prevent path conflicts
1173+
if (this.renamingWorkspaces.has(workspaceId)) {
1174+
log.debug("resumeStream blocked: workspace is being renamed", { workspaceId });
1175+
return Err({
1176+
type: "unknown",
1177+
raw: "Workspace is being renamed. Please wait and try again.",
1178+
});
1179+
}
1180+
11481181
const session = this.getOrCreateSession(workspaceId);
11491182
const result = await session.resumeStream(options);
11501183
if (!result.success) {

0 commit comments

Comments
 (0)