{"record":{"id":"7a731b0f3d385399","repo":"agalwood/Motrix","slug":"tasknotretryable","errorCode":"TaskNotRetryable","errorMessage":"Task ${taskId} cannot be retried: required inputs are unavailable","messagePattern":"Task (.+?) cannot be retried: required inputs are unavailable","errorType":"exception","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/task/actions/re-add-task.ts","lineNumber":301,"sourceCode":" */\nasync function reAddTaskUnderMutation(\n  taskId: string,\n  deps: ReAddTaskDeps\n): Promise<void> {\n  const task = getTaskOrWarn(deps, taskId, 'reAddTask')\n  if (!task) return\n  if (canRetry(task)) {\n    // Retry additionally requires the engine dispatch to be reconstructable\n    // from the task record (canReseed already implies this for its own path,\n    // since it requires torrentMetaPath itself) — reject up front rather than\n    // discovering mid-flow (e.g. a media task with no single re-addable\n    // handle, or a torrent-like task with no persisted sidecar).\n    if (!canRebuildTaskInputs(task)) {\n      deps.log.warn(\n        { taskId, status: task.status, type: task.type, kind: task.kind },\n        'reAddTask: task inputs cannot be rebuilt'\n      )\n      throw new AppError(\n        ErrorCode.TaskNotRetryable,\n        `Task ${taskId} cannot be retried: required inputs are unavailable`\n      )\n    }\n  } else if (!canReseed(task)) {\n    deps.log.warn(\n      { taskId, status: task.status, type: task.type },\n      'reAddTask: task is not in a re-addable state'\n    )\n    return\n  }\n  // Resolve local prerequisites before touching the old engine GID or\n  // installing a durable reservation. From the reservation onward, the only\n  // fallible operation before publication is the engine dispatch itself.\n  const torrentMetadata = await readBtMetadata(task, deps)\n  let opts: EngineTaskOptions | null = null\n  try {\n    opts = await deps.adapter.getEngineTaskOptions(task.engineTaskId)","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/task/actions/re-add-task.ts#L283-L319","documentation":"Thrown by re-add-task.ts:301 when `canRetry(task)` is true but `canRebuildTaskInputs(task)` is false. The task is formally in a retryable status, yet the engine dispatch cannot be reconstructed from the persisted record — e.g. a media task with no single re-addable handle, or a torrent-like task whose sidecar was dropped. The check rejects up front rather than failing mid-flow.","triggerScenarios":"Retry invoked on a media task that has no resumable single handle; a torrent-like task where the persisted sidecar/path is missing; a task type whose kind never has rebuildable inputs.","commonSituations":"UI retry button enabled purely on status without the capability check; partial create left a row that looks retryable; tasks of a kind that the engine cannot re-dispatch (live stream, unsupported protocol).","solutions":["Gate the retry affordance on `canRebuildTaskInputs(task)` in the UI/controller so the button is disabled, not the throw hit.","For torrent-like tasks, restore torrentMetaPath (re-persist the .torrent) before retry.","For media/non-retryable kinds, surface a clear 'cannot be retried' status instead of attempting re-add.","If the task genuinely should be retryable, fix the create path so its inputs are fully persisted."],"exampleFix":"// before\nif (canRetry(task)) {\n  if (!canRebuildTaskInputs(task)) {\n    throw new AppError(ErrorCode.TaskNotRetryable, `Task ${taskId} cannot be retried: required inputs are unavailable`)\n  }\n}\n\n// after — UI layer disables the action upfront\nconst retryable = canRetry(task) && canRebuildTaskInputs(task)\nif (!retryable) {\n  ui.setRetryEnabled(taskId, false, 'Required inputs are no longer available')\n  return\n}","handlingStrategy":"validation","validationCode":"import { canRetry, canRebuildTaskInputs } from '@core/task/shared'\nfunction isRetryable(task) {\n  return canRetry(task) && canRebuildTaskInputs(task)\n}\nif (!isRetryable(task)) {\n  ui.setRetryEnabled(task.id, false, 'Required inputs unavailable')\n}","typeGuard":"function isTaskNotRetryable(e) { return e instanceof AppError && e.code === ErrorCode.TaskNotRetryable }","tryCatchPattern":"if (canRetry(task) && !canRebuildTaskInputs(task)) {\n  deps.log.warn({ taskId }, 'reAddTask: not retryable, inputs missing')\n  return { retried: false, reason: 'inputs_unavailable' }\n}","preventionTips":["Disable the retry affordance unless canRebuildTaskInputs(task) is true.","Ensure all create paths persist the inputs needed to reconstruct the engine dispatch.","For media/non-retryable kinds, mark them non-retryable at create time.","Restore torrentMetaPath for torrent-like tasks before allowing retry."],"tags":["re-add","retry","state","validation","task-lifecycle"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}