@@ -393,9 +393,23 @@ export async function executeCreateWorkflow(
393393 }
394394 const workspaceId =
395395 params ?. workspaceId || context . workspaceId || ( await getDefaultWorkspaceId ( context . userId ) )
396- const folderId = params ?. folderId || null
397396
398397 await ensureWorkspaceAccess ( workspaceId , context . userId , 'write' )
398+
399+ const folderPath = typeof params ?. folderPath === 'string' ? params . folderPath . trim ( ) : ''
400+ let folderId =
401+ typeof params ?. folderId === 'string' && params . folderId . trim ( ) ? params . folderId . trim ( ) : null
402+ if ( folderPath ) {
403+ const relativePath = workflowFolderRelativePath ( folderPath )
404+ if ( ! relativePath ) {
405+ folderId = null
406+ } else {
407+ const target = resolveFolderIdByPath ( folderPath , await loadFolderPathIndex ( workspaceId ) )
408+ if ( 'error' in target ) return { success : false , error : target . error }
409+ folderId = target . folderId
410+ }
411+ }
412+
399413 await assertFolderMutable ( folderId )
400414 assertWorkflowMutationNotAborted ( context )
401415
@@ -1230,40 +1244,57 @@ function workflowFolderRelativePath(rawPath: string): string {
12301244 return trimmed . startsWith ( 'workflows/' ) ? trimmed . slice ( 'workflows/' . length ) : trimmed
12311245}
12321246
1247+ type FolderPathIndex = Map < string , string | null >
1248+
12331249/**
1234- * Load a lookup from each folder's canonical encoded VFS path to its id by
1235- * inverting the same {@link buildVfsFolderPathMap} the VFS uses to serve folder
1236- * paths, so a path the agent sees via glob round-trips to the right id. Fetched
1237- * once per manage_folder call and reused across target + parent resolution.
1250+ * Load an index from each canonical encoded VFS path to its folder id. A null
1251+ * value records that multiple folder ids collapse to the same canonical path,
1252+ * so callers can reject the ambiguous path instead of silently choosing one.
12381253 */
1239- async function loadFolderPathToIdMap ( workspaceId : string ) : Promise < Map < string , string > > {
1240- const byPath = new Map < string , string > ( )
1254+ async function loadFolderPathIndex ( workspaceId : string ) : Promise < FolderPathIndex > {
1255+ const byPath : FolderPathIndex = new Map ( )
12411256 for ( const [ folderId , encodedPath ] of buildVfsFolderPathMap (
12421257 await listFolders ( workspaceId )
12431258 ) . entries ( ) ) {
1244- byPath . set ( encodedPath , folderId )
1259+ if ( ! byPath . has ( encodedPath ) ) {
1260+ byPath . set ( encodedPath , folderId )
1261+ } else if ( byPath . get ( encodedPath ) !== folderId ) {
1262+ byPath . set ( encodedPath , null )
1263+ }
12451264 }
12461265 return byPath
12471266}
12481267
1249- function lookupFolderIdByPath ( rawPath : string , byPath : Map < string , string > ) : string | null {
1268+ function resolveFolderIdByPath (
1269+ rawPath : string ,
1270+ byPath : FolderPathIndex ,
1271+ label = 'Folder'
1272+ ) : { folderId : string } | { error : string } {
12501273 const relative = workflowFolderRelativePath ( rawPath )
1251- if ( ! relative ) return null
1252- return byPath . get ( encodeVfsPathSegments ( decodeVfsPathSegments ( relative ) ) ) ?? null
1274+ if ( ! relative ) return { error : `${ label } not found at ${ rawPath } ` }
1275+
1276+ const canonicalPath = encodeVfsPathSegments ( decodeVfsPathSegments ( relative ) )
1277+ if ( ! byPath . has ( canonicalPath ) ) return { error : `${ label } not found at ${ rawPath } ` }
1278+
1279+ const folderId = byPath . get ( canonicalPath )
1280+ if ( ! folderId ) {
1281+ return {
1282+ error : `${ label } path is ambiguous after canonicalization: ${ rawPath } . Rename one of the conflicting folders and retry.` ,
1283+ }
1284+ }
1285+ return { folderId }
12531286}
12541287
12551288/** Resolve the folder a manage_folder op targets, preferring folderId over path. */
12561289async function resolveManageFolderTarget (
12571290 params : ManageFolderParams ,
1258- getFolderPaths : ( ) => Promise < Map < string , string > >
1291+ getFolderPaths : ( ) => Promise < FolderPathIndex >
12591292) : Promise < { folderId : string } | { error : string } > {
12601293 const directId = typeof params . folderId === 'string' ? params . folderId . trim ( ) : ''
12611294 if ( directId ) return { folderId : directId }
12621295 const path = typeof params . path === 'string' ? params . path . trim ( ) : ''
12631296 if ( ! path ) return { error : 'Provide the folder path (e.g. "workflows/Marketing") or folderId.' }
1264- const folderId = lookupFolderIdByPath ( path , await getFolderPaths ( ) )
1265- if ( ! folderId ) return { error : `Folder not found at ${ path } ` }
1266- return { folderId }
1297+ return resolveFolderIdByPath ( path , await getFolderPaths ( ) )
12671298}
12681299
12691300/**
@@ -1273,16 +1304,16 @@ async function resolveManageFolderTarget(
12731304 */
12741305async function resolveManageFolderParent (
12751306 params : ManageFolderParams ,
1276- getFolderPaths : ( ) => Promise < Map < string , string > >
1307+ getFolderPaths : ( ) => Promise < FolderPathIndex >
12771308) : Promise < { parentId : string | null } | { error : string } > {
12781309 const directId = typeof params . parentId === 'string' ? params . parentId . trim ( ) : ''
12791310 if ( directId ) return { parentId : directId }
12801311 if ( params . parentId === null ) return { parentId : null }
12811312 const dest = typeof params . destinationPath === 'string' ? params . destinationPath . trim ( ) : ''
12821313 if ( ! dest || ! workflowFolderRelativePath ( dest ) ) return { parentId : null }
1283- const parentId = lookupFolderIdByPath ( dest , await getFolderPaths ( ) )
1284- if ( ! parentId ) return { error : `Destination folder not found at ${ dest } ` }
1285- return { parentId }
1314+ const parent = resolveFolderIdByPath ( dest , await getFolderPaths ( ) , 'Destination folder' )
1315+ if ( ' error' in parent ) return parent
1316+ return { parentId : parent . folderId }
12861317}
12871318
12881319/**
@@ -1302,8 +1333,8 @@ export async function executeManageFolder(
13021333 // Fetch the workspace folder list at most once, lazily — only when a path
13031334 // (vs an explicit id) actually needs resolving, and shared across the
13041335 // target + parent lookups a single move/create performs.
1305- let folderPathsPromise : Promise < Map < string , string > > | undefined
1306- const getFolderPaths = ( ) => ( folderPathsPromise ??= loadFolderPathToIdMap ( workspaceId ) )
1336+ let folderPathsPromise : Promise < FolderPathIndex > | undefined
1337+ const getFolderPaths = ( ) => ( folderPathsPromise ??= loadFolderPathIndex ( workspaceId ) )
13071338
13081339 switch ( operation ) {
13091340 case 'create' : {
@@ -1318,14 +1349,13 @@ export async function executeManageFolder(
13181349 name = segments [ segments . length - 1 ]
13191350 const parentSegments = segments . slice ( 0 , - 1 )
13201351 if ( parentSegments . length > 0 ) {
1321- const resolved = lookupFolderIdByPath (
1352+ const parent = resolveFolderIdByPath (
13221353 encodeVfsPathSegments ( parentSegments ) ,
1323- await getFolderPaths ( )
1354+ await getFolderPaths ( ) ,
1355+ 'Parent folder'
13241356 )
1325- if ( ! resolved ) {
1326- return { success : false , error : `Parent folder not found for ${ path } ` }
1327- }
1328- parentId = resolved
1357+ if ( 'error' in parent ) return { success : false , error : parent . error }
1358+ parentId = parent . folderId
13291359 }
13301360 } else {
13311361 const parent = await resolveManageFolderParent ( params , getFolderPaths )
0 commit comments