{"record":{"id":"b20aee4e69b2c914","repo":"agalwood/Motrix","slug":"taskremovenotavailableduringfinalize","errorCode":"TaskRemoveNotAvailableDuringFinalize","errorMessage":"Cannot remove task while it is being finalized","messagePattern":"Cannot remove task while it is being finalized","errorType":"exception","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/task/actions/remove-task.ts","lineNumber":89,"sourceCode":"  await deps.runTaskMutation([taskId], remove)\n}\n\n/**\n * The task snapshot and every external side effect must share one admission\n * lock. A late lock around only the durable delete lets reAddTask create and\n * publish a replacement GID while removal still holds the stale original GID,\n * leaving the replacement live in aria2 after the parent row is deleted.\n */\nasync function removeTaskUnderMutation(\n  taskId: string,\n  options: RemoveTaskOptions,\n  deps: RemoveTaskDeps\n): Promise<void> {\n  const task = getTaskOrWarn(deps, taskId, 'removeTask')\n  if (!task) return\n\n  if (task.status === TaskStatus.Finalizing) {\n    throw new AppError(\n      ErrorCode.TaskRemoveNotAvailableDuringFinalize,\n      'Cannot remove task while it is being finalized'\n    )\n  }\n\n  // Both removal paths delete the durable parent row under one\n  // exclusive-persistence window, with the Inspector Activity deletion\n  // barrier (when wired) installed before the row disappears. The barrier\n  // ordering is the delicate part — keep it in exactly one place.\n  const deleteParentBarrier = (publish: () => void): Promise<void> =>\n    deps.taskPersistence.runExclusivePersistence(async () => {\n      if (deps.deleteParentTasks) {\n        await deps.deleteParentTasks([taskId], publish)\n      } else {\n        publish()\n      }\n    })\n","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/task/actions/remove-task.ts#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the remove shortly after the Finalizing window closes (finalize is bounded and short).","Have the UI hide/disable the remove action while task.status === Finalizing.","If removal must be forced, wait for the finalize completion event then issue remove.","In batch clear logic, skip Finalizing tasks and re-sweep them on the next pass."],"exampleFix":"// before\nif (task.status === TaskStatus.Finalizing) {\n  throw new AppError(ErrorCode.TaskRemoveNotAvailableDuringFinalize, 'Cannot remove task while it is being finalized')\n}\n\n// after — caller retries with backoff\ntry {\n  await removeTask(taskId, {}, deps)\n} catch (e) {\n  if (e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize) {\n    await once(statusBus, `task:${taskId}:status`)\n    return removeTask(taskId, {}, deps)\n  }\n  throw e\n}","handlingStrategy":"retry","validationCode":"function canRemoveNow(task) { return task.status !== TaskStatus.Finalizing }","typeGuard":"function isRemoveDuringFinalize(e) { return e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize }","tryCatchPattern":"try {\n  await removeTask(taskId, {}, deps)\n} catch (e) {\n  if (e instanceof AppError && e.code === ErrorCode.TaskRemoveNotAvailableDuringFinalize) {\n    await once(statusBus, `task:${taskId}:status`) // wait out the finalize window\n    return removeTask(taskId, {}, deps)\n  }\n  throw e\n}","preventionTips":["Hide/disable the remove action in the UI while task.status === Finalizing.","In batch-clear logic, skip Finalizing tasks and re-sweep on the next pass.","Wait for the finalize completion event before issuing a forced remove.","Avoid automation that removes tasks without accounting for the finalize window."],"tags":["remove","finalize","concurrency","task-lifecycle","race"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}