{"record":{"id":"57bb744544b5891e","repo":"stablyai/orca","slug":"worktree-deletion-already-in-progress-args-work","errorCode":null,"errorMessage":"Worktree deletion already in progress: ${args.worktreeId}","messagePattern":"Worktree deletion already in progress: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/main/ipc/worktrees.ts","lineNumber":2376,"sourceCode":"\n  ipcMain.handle(\n    'worktrees:remove',\n    async (_event, args: RemoveWorktreeArgs): Promise<RemoveWorktreeResult> => {\n      const { repoId, worktreePath } = parseWorktreeId(args.worktreeId)\n      const repo = getRepoForWorktreeRemoval(store, repoId, args.hostId)\n      if (!repo) {\n        throw new Error(`Repo not found: ${repoId}`)\n      }\n      // The resolved repo supplies host ownership when legacy callers omit args.hostId.\n      const removalHostId = getRepoExecutionHostId(repo)\n      const inFlightKey = getWorktreeRemovalInFlightKey(args.worktreeId, removalHostId)\n      const optionsKey = getWorktreeRemovalOptionsKey(args)\n      const inFlightRemoval = worktreeRemovalsInFlight.get(inFlightKey)\n      if (inFlightRemoval) {\n        if (inFlightRemoval.optionsKey === optionsKey) {\n          return inFlightRemoval.promise\n        }\n        throw new Error(`Worktree deletion already in progress: ${args.worktreeId}`)\n      }\n\n      // Why: concurrent stale-toast/double-click/sidebar races can hit the same worktree; share the op so only one path touches Git and disk.\n      const removal = (async (): Promise<RemoveWorktreeResult> => {\n        // Why: worktree.create is traced; delete freezes were invisible without a matching worktree.remove parent span.\n        return withWorktreeSpan({ stage: 'remove', path: worktreePath }, async () => {\n          if (isFolderRepo(repo)) {\n            if (args.worktreeId === getFolderWorkspaceRootId(repo)) {\n              throw new Error(\n                'Cannot delete the project root workspace. Remove the folder project instead.'\n              )\n            }\n            // Why: folder workspaces share one root, so there's no Git remove step to close shells; sweep PTYs before dropping metadata.\n            await withWorktreeRemoveStageSpan('pty_sweep', 'folder', async () => {\n              // Folder projects can be SSH-backed, so fence the sweep to the owning host exactly\n              // like the git paths — the local inventory must never reach a remote workspace's id.\n              // The resolved repo is authoritative here: path-derived metadata is shared by\n              // same-id host copies and can describe a different owner's workspace.","sourceCodeStart":2358,"sourceCodeEnd":2394,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/worktrees.ts#L2358-L2394","documentation":"Thrown by worktrees:remove when worktreeRemovalsInFlight already has an entry for the same inFlightKey (worktreeId+hostId) but with a different optionsKey (options encode force/branch-delete/pty-stop flags). Same-options concurrent calls share the promise; differing-options concurrent calls are rejected to prevent two conflicting destructive operations on one worktree.","triggerScenarios":"Two near-simultaneous worktrees:remove calls for the same worktree with different options — e.g. a non-force delete in flight and a force delete arriving, or a sidebar double-click racing a stale toast's force action.","commonSituations":"Double-click in the sidebar, a stale recovery toast firing while the user retries with force, or an automation client issuing overlapping remove requests with changed flags.","solutions":["De-dupe remove requests in the renderer (track an in-flight flag per worktreeId) and avoid changing options between retries.","If a different option set is genuinely needed, wait for the first removal to settle, then re-evaluate from the new worktree state.","Reuse the same options when retrying so the in-flight promise is shared instead of rejected."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// De-dupe removes per worktreeId in the renderer.\nconst removing = new Set<string>()\nasync function removeOnce(worktreeId, opts) {\n  if (removing.has(worktreeId)) return\n  removing.add(worktreeId)\n  try {\n    await invoke('worktrees:remove', { worktreeId, ...opts })\n  } finally {\n    removing.delete(worktreeId)\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await invoke('worktrees:remove', args)\n} catch (e) {\n  if (e.message.startsWith('Worktree deletion already in progress')) {\n    // Another option set is running; await worktree refresh instead of retrying with different options.\n    return\n  }\n  throw e\n}","preventionTips":["Track an in-flight flag per worktreeId in the renderer.","Keep options stable across automatic retries.","Suppress double-click-triggered duplicate removes."],"tags":["concurrency","ipc","worktree-remove","in-flight"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}