agalwood/Motrix · error · AppError

TaskRemoveNotAvailableDuringFinalize

TaskRemoveNotAvailableDuringFinalize

Error message

Cannot remove task while it is being finalized

What it means

Thrown by `removeTaskUnderMutation` (remove-task.ts:89) when the task being removed has `status === TaskStatus.Finalizing`. Removal during the narrow finalize window is forbidden because finalize holds the stale original GID and a concurrent delete could leave a replacement GID live in aria2 after the parent row is gone.

Solutions

  1. Retry the remove shortly after the Finalizing window closes (finalize is bounded and short).
  2. Have the UI hide/disable the remove action while task.status === Finalizing.
  3. If removal must be forced, wait for the finalize completion event then issue remove.
  4. In batch clear logic, skip Finalizing tasks and re-sweep them on the next pass.

Example fix

// before
if (task.status === TaskStatus.Finalizing) {
  throw new AppError(ErrorCode.TaskRemoveNotAvailableDuringFinalize, 'Cannot remove task while it is being finalized')
}

// after — caller retries with backoff
try {
  await removeTask(taskId, {}, deps)
} catch (e) {
  if (e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize) {
    await once(statusBus, `task:${taskId}:status`)
    return removeTask(taskId, {}, deps)
  }
  throw e
}
Defensive patterns

Strategy: retry

Validate before calling

function canRemoveNow(task) { return task.status !== TaskStatus.Finalizing }

Type guard

function isRemoveDuringFinalize(e) { return e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize }

Try / catch

try {
  await removeTask(taskId, {}, deps)
} catch (e) {
  if (e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize) {
    await once(statusBus, `task:${taskId}:status`) // wait out the finalize window
    return removeTask(taskId, {}, deps)
  }
  throw e
}

Prevention

When it happens

Trigger: User or automation issues a remove-task call while finalize is mid-flight (between rename and metadata commit); a bulk 'clear all' races with an active finalize; a UI double-action where the user clicks remove right as a download completes.

Common situations: Race between a completion-triggered finalize and a user remove; scripted cleanup loops that don't account for the Finalizing window; retry-after-failure automation colliding with finalize.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/b20aee4e69b2c914. Report an issue: GitHub.

Appendix: source

Thrown at src/core/task/actions/remove-task.ts:89

  await deps.runTaskMutation([taskId], remove)
}

/**
 * The task snapshot and every external side effect must share one admission
 * lock. A late lock around only the durable delete lets reAddTask create and
 * publish a replacement GID while removal still holds the stale original GID,
 * leaving the replacement live in aria2 after the parent row is deleted.
 */
async function removeTaskUnderMutation(
  taskId: string,
  options: RemoveTaskOptions,
  deps: RemoveTaskDeps
): Promise<void> {
  const task = getTaskOrWarn(deps, taskId, 'removeTask')
  if (!task) return

  if (task.status === TaskStatus.Finalizing) {
    throw new AppError(
      ErrorCode.TaskRemoveNotAvailableDuringFinalize,
      'Cannot remove task while it is being finalized'
    )
  }

  // Both removal paths delete the durable parent row under one
  // exclusive-persistence window, with the Inspector Activity deletion
  // barrier (when wired) installed before the row disappears. The barrier
  // ordering is the delicate part — keep it in exactly one place.
  const deleteParentBarrier = (publish: () => void): Promise<void> =>
    deps.taskPersistence.runExclusivePersistence(async () => {
      if (deps.deleteParentTasks) {
        await deps.deleteParentTasks([taskId], publish)
      } else {
        publish()
      }
    })

View on GitHub (pinned to 1a708ee577)