{"record":{"id":"f56ef7597366849e","repo":"eyaltoledano/claude-task-master","slug":"failed-to-delete-workflow-state-error-message","errorCode":null,"errorMessage":"Failed to delete workflow state: ${error.message}","messagePattern":"Failed to delete workflow state: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts","lineNumber":197,"sourceCode":"\n\t\t\tawait fs.writeFile(backupPath, JSON.stringify(backup, null, 2), 'utf-8');\n\n\t\t\t// Clean up old backups\n\t\t\tawait this.pruneBackups();\n\t\t} catch (error: any) {\n\t\t\tthrow new Error(`Failed to create backup: ${error.message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Delete workflow state file\n\t */\n\tasync delete(): Promise<void> {\n\t\ttry {\n\t\t\tawait fs.unlink(this.statePath);\n\t\t} catch (error: any) {\n\t\t\tif (error.code !== 'ENOENT') {\n\t\t\t\tthrow new Error(`Failed to delete workflow state: ${error.message}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * List available backups\n\t */\n\tasync listBackups(): Promise<string[]> {\n\t\ttry {\n\t\t\tconst files = await fs.readdir(this.backupDir);\n\t\t\treturn files\n\t\t\t\t.filter((f) => f.startsWith('workflow-state-') && f.endsWith('.json'))\n\t\t\t\t.sort()\n\t\t\t\t.reverse();\n\t\t} catch (error: any) {\n\t\t\tif (error.code === 'ENOENT') {\n\t\t\t\treturn [];\n\t\t\t}","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/eyaltoledano/claude-task-master/blob/c0c98d367c55296bfe69e65680625b6db437af02/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts#L179-L215","documentation":"delete() removes the workflow state file with fs.unlink. A missing file (ENOENT) is treated as success, but any other unlink failure — permissions, directory-is-file mismatch, EBUSY — is wrapped as 'Failed to delete workflow state: <cause>'.","triggerScenarios":"Calling delete() when statePath points to a file the process lacks permission to remove, the path is a non-empty directory, or the file is locked/busy (EPERM, EACCES, EBUSY, EISDIR).","commonSituations":"State file owned by root or another user after sudo runs; read-only mounted volume; Windows file lock from an editor or AV scanner; statePath misconfigured to a directory.","solutions":["Read the cause after the colon (EACCES/EPERM/EBUSY etc.).","Check and correct permissions/ownership on the state file and its parent directory.","Close processes holding the file open (editors, sync clients, antivirus on Windows).","If EISDIR, fix the statePath configuration — it must point to a file.","Manually remove the file if the process context cannot (e.g. container user mismatch)."],"exampleFix":"// before\nawait manager.delete(); // EACCES under container user\n// after\ntry {\n  await manager.delete();\n} catch (e) {\n  if (/EACCES|EPERM/.test(e.message)) {\n    await exec(`sudo rm -f ${statePath}`); // or fix ownership: chown\n  }\n}","handlingStrategy":"try-catch","validationCode":"import { stat, access, constants } from 'fs/promises';\ntry {\n  const s = await stat(statePath);\n  if (!s.isFile()) throw new Error('statePath must be a file');\n  await access(path.dirname(statePath), constants.W_OK);\n} catch { /* missing file = delete is a no-op, fine */ }","typeGuard":"null","tryCatchPattern":"try {\n  await manager.delete();\n} catch (e) {\n  if (/EACCES|EPERM|EBUSY/.test(e.message)) {\n    console.error('State file locked or not removable:', e.message);\n  }\n  throw e;\n}","preventionTips":["Run all workflow processes under the same user to avoid ownership mismatches","Exclude state files from AV/backup locks on Windows","Configure statePath to a file, never a directory","Treat ENOENT as success (the manager already does)"],"tags":["filesystem","delete","workflow"],"backgroundTag":"file-delete-failed","analyzedSha":"c0c98d367c55296bfe69e65680625b6db437af02","analyzedAt":"2026-08-29T02:56:26.071Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}