@@ -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