stablyai/orca · warning

Worktree deletion already in progress

Error message

Worktree deletion already in progress

What it means

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.

Source

Thrown at src/main/ipc/watcher-removal-gate.ts:113

}

export function acquireWatcherRemovalGate(
  rootPath: string,
  connectionId?: string
): WatcherRemovalGate {
  const normalizedRoot = normalizeRuntimePathForComparison(rootPath)
  const hostStates = matchingHostStates(connectionId)
  if (
    hostStates.some(
      (state) =>
        state.removalCount > 0 &&
        (isPathInsideOrEqual(state.rootPath, normalizedRoot) ||
          isPathInsideOrEqual(normalizedRoot, state.rootPath))
    )
  ) {
    // Why: desktop and runtime removal entry points have separate request
    // dedupe; the shared root fence must still prevent two destructive runs.
    throw new Error('Worktree deletion already in progress')
  }
  const key = watcherRemovalGateKey(normalizedRoot, connectionId)
  const state = states.get(key) ?? createState(key, normalizedRoot, connectionId)
  state.removalCount++
  // Why: deleting a parent root must wait for native installs already admitted
  // under that root, not only installs keyed to the exact same spelling.
  const fenced = matchingHostStates(connectionId)
    .filter(
      (candidate) =>
        candidate.installs.size > 0 && isPathInsideOrEqual(normalizedRoot, candidate.rootPath)
    )
    .map((candidate) => ({ state: candidate, tokens: new Set(candidate.installs) }))
  const drains = fenced.map(
    ({ state: candidate }) =>
      new Promise<void>((resolve) => candidate.installDrainWaiters.add(resolve))
  )
  const ready = drains.length === 0 ? Promise.resolve() : Promise.all(drains).then(() => undefined)
  let released = false

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Wait for the in-flight deletion to complete (the first gate's ready/release) before issuing the second.
  2. Serialize deletions in the caller via a per-root queue keyed on normalized path.
  3. In the UI, disable the delete action while a deletion for an overlapping root is pending.

Example fix

// before
const gate1 = acquireWatcherRemovalGate(rootPath)
const gate2 = acquireWatcherRemovalGate(rootPath) // throws
// after
const gate1 = acquireWatcherRemovalGate(rootPath)
await gate1.ready
try { /* do removal */ } finally { gate1.release() }
const gate2 = acquireWatcherRemovalGate(rootPath)
Defensive patterns

Strategy: try-catch

Validate before calling

function isRemovalInProgress(rootPath: string, connectionId?: string): boolean {
  // expose a peek helper from the gate module that mirrors acquireWatcherRemovalGate's overlap check
  return peekRemovalInProgress(normalizeRuntimePathForComparison(rootPath), connectionId)
}

Try / catch

try {
  const gate = acquireWatcherRemovalGate(rootPath, connectionId)
  await gate.ready
  try { /* perform removal */ } finally { gate.release() }
} catch (e) {
  if (/Worktree deletion already in progress/.test((e as Error).message)) {
    showToast('A deletion for this root is already running; please wait.')
  } else throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/2595a444c9b1eb0c. Report an issue: GitHub.