@@ -81,6 +81,8 @@ export class WorkspaceService extends EventEmitter {
8181 string ,
8282 { chat : ( ) => void ; metadata : ( ) => void }
8383 > ( ) ;
84+ // Tracks workspaces currently being renamed to prevent streaming during rename
85+ private readonly renamingWorkspaces = new Set < string > ( ) ;
8486
8587 constructor (
8688 private readonly config : Config ,
@@ -688,37 +690,44 @@ export class WorkspaceService extends EventEmitter {
688690 // Wait for the stream to complete before renaming (rename is blocked during streaming)
689691 await this . waitForStreamComplete ( workspaceId ) ;
690692
691- // Attempt to rename with collision retry (same logic as workspace creation)
692- let finalName = aiGeneratedName ;
693- for ( let attempt = 0 ; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES ; attempt ++ ) {
694- const renameResult = await this . rename ( workspaceId , finalName ) ;
693+ // Mark workspace as renaming to block new streams during the rename operation
694+ this . renamingWorkspaces . add ( workspaceId ) ;
695+ try {
696+ // Attempt to rename with collision retry (same logic as workspace creation)
697+ let finalName = aiGeneratedName ;
698+ for ( let attempt = 0 ; attempt <= MAX_WORKSPACE_NAME_COLLISION_RETRIES ; attempt ++ ) {
699+ const renameResult = await this . rename ( workspaceId , finalName ) ;
700+
701+ if ( renameResult . success ) {
702+ log . info ( "Successfully renamed workspace to AI-generated name" , {
703+ workspaceId,
704+ oldName : currentName ,
705+ newName : finalName ,
706+ } ) ;
707+ return ;
708+ }
695709
696- if ( renameResult . success ) {
697- log . info ( "Successfully renamed workspace to AI-generated name" , {
710+ // If collision and not last attempt, retry with suffix
711+ if (
712+ renameResult . error ?. includes ( "already exists" ) &&
713+ attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
714+ ) {
715+ log . debug ( `Workspace name collision for "${ finalName } ", retrying with suffix` ) ;
716+ finalName = appendCollisionSuffix ( aiGeneratedName ) ;
717+ continue ;
718+ }
719+
720+ // Non-collision error or out of retries - keep placeholder name
721+ log . info ( "Failed to rename workspace to AI-generated name" , {
698722 workspaceId,
699- oldName : currentName ,
700- newName : finalName ,
723+ aiGeneratedName : finalName ,
724+ error : renameResult . error ,
701725 } ) ;
702726 return ;
703727 }
704-
705- // If collision and not last attempt, retry with suffix
706- if (
707- renameResult . error ?. includes ( "already exists" ) &&
708- attempt < MAX_WORKSPACE_NAME_COLLISION_RETRIES
709- ) {
710- log . debug ( `Workspace name collision for "${ finalName } ", retrying with suffix` ) ;
711- finalName = appendCollisionSuffix ( aiGeneratedName ) ;
712- continue ;
713- }
714-
715- // Non-collision error or out of retries - keep placeholder name
716- log . info ( "Failed to rename workspace to AI-generated name" , {
717- workspaceId,
718- aiGeneratedName : finalName ,
719- error : renameResult . error ,
720- } ) ;
721- return ;
728+ } finally {
729+ // Always clear renaming flag, even on error
730+ this . renamingWorkspaces . delete ( workspaceId ) ;
722731 }
723732 } catch ( error ) {
724733 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
@@ -891,6 +900,9 @@ export class WorkspaceService extends EventEmitter {
891900 return Err ( validation . error ?? "Invalid workspace name" ) ;
892901 }
893902
903+ // Mark workspace as renaming to block new streams during the rename operation
904+ this . renamingWorkspaces . add ( workspaceId ) ;
905+
894906 const metadataResult = await this . aiService . getWorkspaceMetadata ( workspaceId ) ;
895907 if ( ! metadataResult . success ) {
896908 return Err ( `Failed to get workspace metadata: ${ metadataResult . error } ` ) ;
@@ -958,6 +970,9 @@ export class WorkspaceService extends EventEmitter {
958970 } catch ( error ) {
959971 const message = error instanceof Error ? error . message : String ( error ) ;
960972 return Err ( `Failed to rename workspace: ${ message } ` ) ;
973+ } finally {
974+ // Always clear renaming flag, even on error
975+ this . renamingWorkspaces . delete ( workspaceId ) ;
961976 }
962977 }
963978
@@ -1102,6 +1117,15 @@ export class WorkspaceService extends EventEmitter {
11021117 } ) ;
11031118
11041119 try {
1120+ // Block streaming while workspace is being renamed to prevent path conflicts
1121+ if ( this . renamingWorkspaces . has ( workspaceId ) ) {
1122+ log . debug ( "sendMessage blocked: workspace is being renamed" , { workspaceId } ) ;
1123+ return Err ( {
1124+ type : "unknown" ,
1125+ raw : "Workspace is being renamed. Please wait and try again." ,
1126+ } ) ;
1127+ }
1128+
11051129 const session = this . getOrCreateSession ( workspaceId ) ;
11061130 void this . updateRecencyTimestamp ( workspaceId ) ;
11071131
@@ -1144,6 +1168,15 @@ export class WorkspaceService extends EventEmitter {
11441168 options : SendMessageOptions | undefined = { model : "claude-3-5-sonnet-latest" }
11451169 ) : Promise < Result < void , SendMessageError > > {
11461170 try {
1171+ // Block streaming while workspace is being renamed to prevent path conflicts
1172+ if ( this . renamingWorkspaces . has ( workspaceId ) ) {
1173+ log . debug ( "resumeStream blocked: workspace is being renamed" , { workspaceId } ) ;
1174+ return Err ( {
1175+ type : "unknown" ,
1176+ raw : "Workspace is being renamed. Please wait and try again." ,
1177+ } ) ;
1178+ }
1179+
11471180 const session = this . getOrCreateSession ( workspaceId ) ;
11481181 const result = await session . resumeStream ( options ) ;
11491182 if ( ! result . success ) {
0 commit comments