{"record":{"id":"2595a444c9b1eb0c","repo":"stablyai/orca","slug":"worktree-deletion-already-in-progress","errorCode":null,"errorMessage":"Worktree deletion already in progress","messagePattern":"Worktree deletion already in progress","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/main/ipc/watcher-removal-gate.ts","lineNumber":113,"sourceCode":"}\n\nexport function acquireWatcherRemovalGate(\n  rootPath: string,\n  connectionId?: string\n): WatcherRemovalGate {\n  const normalizedRoot = normalizeRuntimePathForComparison(rootPath)\n  const hostStates = matchingHostStates(connectionId)\n  if (\n    hostStates.some(\n      (state) =>\n        state.removalCount > 0 &&\n        (isPathInsideOrEqual(state.rootPath, normalizedRoot) ||\n          isPathInsideOrEqual(normalizedRoot, state.rootPath))\n    )\n  ) {\n    // Why: desktop and runtime removal entry points have separate request\n    // dedupe; the shared root fence must still prevent two destructive runs.\n    throw new Error('Worktree deletion already in progress')\n  }\n  const key = watcherRemovalGateKey(normalizedRoot, connectionId)\n  const state = states.get(key) ?? createState(key, normalizedRoot, connectionId)\n  state.removalCount++\n  // Why: deleting a parent root must wait for native installs already admitted\n  // under that root, not only installs keyed to the exact same spelling.\n  const fenced = matchingHostStates(connectionId)\n    .filter(\n      (candidate) =>\n        candidate.installs.size > 0 && isPathInsideOrEqual(normalizedRoot, candidate.rootPath)\n    )\n    .map((candidate) => ({ state: candidate, tokens: new Set(candidate.installs) }))\n  const drains = fenced.map(\n    ({ state: candidate }) =>\n      new Promise<void>((resolve) => candidate.installDrainWaiters.add(resolve))\n  )\n  const ready = drains.length === 0 ? Promise.resolve() : Promise.all(drains).then(() => undefined)\n  let released = false","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/watcher-removal-gate.ts#L95-L131","documentation":"Thrown by acquireWatcherRemovalGate when a removal is already in progress for the same root path (or a parent/child of it). The gate uses isPathInsideOrEqual bidirectionally so that deleting '/a' and '/a/b' cannot run concurrently — the second acquire sees an existing state with removalCount > 0 overlapping its root and refuses. This prevents two destructive runs from racing on shared filesystem roots.","triggerScenarios":"acquireWatcherRemovalGate is called twice for the same rootPath (or nested/overlapping roots) before the first gate is released. Desktop and runtime removal entry points have separate dedupe but converge on this shared fence.","commonSituations":"User rapidly clicks delete on two worktrees sharing a parent root, or on the same worktree from two surfaces (sidebar + command). A parent-root deletion is in flight when a child deletion is requested. Automation triggers a cleanup while the user is manually deleting.","solutions":["Wait for the in-flight deletion to complete (the first gate's ready/release) before issuing the second.","Serialize deletions in the caller via a per-root queue keyed on normalized path.","In the UI, disable the delete action while a deletion for an overlapping root is pending."],"exampleFix":"// before\nconst gate1 = acquireWatcherRemovalGate(rootPath)\nconst gate2 = acquireWatcherRemovalGate(rootPath) // throws\n// after\nconst gate1 = acquireWatcherRemovalGate(rootPath)\nawait gate1.ready\ntry { /* do removal */ } finally { gate1.release() }\nconst gate2 = acquireWatcherRemovalGate(rootPath)","handlingStrategy":"try-catch","validationCode":"function isRemovalInProgress(rootPath: string, connectionId?: string): boolean {\n  // expose a peek helper from the gate module that mirrors acquireWatcherRemovalGate's overlap check\n  return peekRemovalInProgress(normalizeRuntimePathForComparison(rootPath), connectionId)\n}","typeGuard":null,"tryCatchPattern":"try {\n  const gate = acquireWatcherRemovalGate(rootPath, connectionId)\n  await gate.ready\n  try { /* perform removal */ } finally { gate.release() }\n} catch (e) {\n  if (/Worktree deletion already in progress/.test((e as Error).message)) {\n    showToast('A deletion for this root is already running; please wait.')\n  } else throw e\n}","preventionTips":["Serialize deletions per normalized root in the caller using a queue.","Disable the delete affordance in the UI while an overlapping removal is pending.","Always release the gate in a finally block to avoid starving later deletions."],"tags":["worktree","deletion","concurrency","race-condition","filesystem"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}