# How Motrix's TaskRecoveryService Recovers Tasks After a Crash: 8-Step Source Code Deep Dive

> Discover how Motrix's TaskRecoveryService recovers tasks after a crash. Explore the 8-step source code deep dive and understand the decision logic behind task recovery.

- Repository: [Dr_rOot/Motrix](https://github.com/agalwood/Motrix)
- Tags: internals
- Published: 2026-08-19

---

**Motrix's `TaskRecoveryService` recovers crashed downloads by scanning in-flight tasks, mapping live engine state, inspecting the filesystem, and running a pure decision function that issues one of six `RecoveryAction`s such as `ResumeFromRename`, `AdoptExistingGid`, or `MarkError`.**

When Motrix restarts after an unexpected termination, the `TaskRecoveryServiceImpl` in the agalwood/Motrix repository orchestrates an 8-step pipeline to restore downloads without data loss. This service combines persisted task metadata from the `TaskManager`, real-time engine state from the active adapter, and filesystem presence checks to decide exactly how each interrupted task should resume. Understanding how Motrix's TaskRecoveryService recovers tasks after a crash reveals a defensive design that prioritizes atomic moves, live GID adoption, and detailed diagnostic reporting.

## Step 1: Collect In-Flight Tasks from the TaskManager

The entry point is `recoverOnStartup` inside [`src/core/task/task-recovery-service.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/task/task-recovery-service.ts). It fetches every persisted task from the `TaskManager` and filters for any item whose `transitionPhase` is not `Idle` or whose status is `Finalizing`【/src/core/task/task-recovery-service.ts#L174-L190】. Each qualifying task is cloned if a mutable `set` method exists, ensuring the recovery logic works on copies rather than live references. The total count is stored in `report.totalScanned`.

## Step 2: Gather Live Engine Information by Info-Hash

Next, the service queries the active download engine via `adapter.listActiveAndWaiting()` to retrieve the current set of active and waiting rows【/src/core/task/task-recovery-service.ts#L202-L218】. It builds a map called `byInfoHash` that associates each **info-hash** to the set of engine-provided GIDs currently in flight. This map is later used to adopt a live seeding identity when a task's content is still active in the engine but the local GID was lost during the crash.

## Step 3: Index Persisted Task Metadata

Two auxiliary indexes are derived from the complete published task list to accelerate lookups during decision-making【/src/core/task/task-recovery-service.ts#L220-L232】:

- **`ownerByGid`** – Maps each persisted GID to the task ID that currently owns it.
- **`taskIdsByInfoHash`** – Maps each info-hash to the list of task IDs sharing that content.

These indexes prevent collisions when multiple tasks reference the same torrent or magnet link.

## Step 4: Inspect the Filesystem for Output Artifacts

For every in-flight task, `inspectFs` checks whether the temporary output (`diskPath`) and the final output (`finalPath`) exist on disk【/src/core/task/task-recovery-service.ts#L27-L41】. It returns one of four `FsState` values:

- `temp_only`
- `final_only`
- `both`
- `neither`

This filesystem state is a critical input because it tells the service whether a download finished writing, is partially complete, or was already moved before the crash.

## Step 5: Select a Matching Live Engine GID

The `selectMatchingGid` function uses the `byInfoHash` map to locate a live engine GID that belongs to the same content【/src/core/task/task-recovery-service.ts#L93-L125】. The algorithm prefers a persisted GID that the task already owns. If that is unavailable, it may adopt a unique GID only when the info-hash maps to exactly one active task and no other Motrix task currently owns that GID. This heuristic prevents one task from stealing another's live engine session.

## Step 6: Determine the Recovery Action

All prior inputs feed into the pure decision function `determineAction`, which returns a `RecoveryAction` based on the task's `TransitionPhase`, `FsState`, whether a matching GID was found, and the task type【/src/core/task/task-recovery-service.ts#L53-L84】. The possible actions include:

- **`ResumeFromRename`** – Move temporary data to its final destination.
- **`ResumeFromReseed`** – Finalize a torrent that is still seeding elsewhere.
- **`AdoptExistingGid`** – Take over a live engine session.
- **`MarkCompleted`** – Treat the task as already finished.
- **`MarkError`** – Flag a filesystem inconsistency.
- **`NoOp`** – Skip tasks that are already idle.

Because `determineAction` is pure, it is easily unit-tested under every combination of phase and filesystem state.

## Step 7: Apply the Chosen Action

`applyAction` executes the concrete side effects for each `RecoveryAction` returned by the decision engine【/src/core/task/task-recovery-service.ts#L150-L164】. The implementation uses a TypeScript `switch` over the action enum:

```ts
private async applyAction(
  task: DownloadTask,
  action: RecoveryAction,
  fsState: FsState,
  matchingGid: string | undefined,
  report: RecoveryReport
): Promise<void> {
  switch (action) {
    case RecoveryAction.ResumeFromRename:
      await this.deps.fs.renameAtomic(task.diskPath, task.finalPath)
      // …persist and report
      break
    case RecoveryAction.AdoptExistingGid:
      task.engineTaskId = matchingGid!
      // …set status to Seeding, persist, and add warning
      break
    // other cases omitted for brevity
  }
}

```

### ResumeFromRename

When a task was in the `Renaming` phase and only the temporary file exists, the service moves the file atomically from `task.diskPath` to `task.finalPath`. For media tasks this performs the rename; for other types it may finalize the task directly【/src/core/task/task-recovery-service.ts#L51-L58】.

### ResumeFromReseed

If the final output already exists but the engine still has a matching info-hash, the service records completion, resets the transition phase to `Reseeding`, persists the task, and then finalizes it【/src/core/task/task-recovery-service.ts#L59-L66】.

### AdoptExistingGid

When the filesystem shows `final_only` and a live engine GID matches, the task adopts that GID via `task.engineTaskId = matchingGid`, transitions to `Idle` and `Seeding`, persists the updated state, and records a warning in `report.warnings`【/src/core/task/task-recovery-service.ts#L67-L85】.

### MarkCompleted

If the final file exists and no live engine match is found, the task is treated as already finished. The service applies a completed status, persists the transition, and fires the `afterComplete` hook【/src/core/task/task-recovery-service.ts#L86-L95】.

### MarkError

A filesystem state of `both`—where temporary and final outputs coexist—indicates an inconsistency. The service moves the task to the `Error` state with the diagnostic code `TaskRecoveryFsMismatch`. If available, it also triggers `applyDiagnosisUpgrade` via the database to record extended diagnostics【/src/core/task/task-recovery-service.ts#L96-L123】.

### NoOp

Tasks in the `Idle` phase with no filesystem anomalies receive `NoOp` and are left untouched.

## Step 8: Persist and Log the Recovery Report

After all tasks are processed, the service aggregates `report.recovered`, `report.warnings`, and `report.errors`, calculates the total duration, and emits a structured log【/src/core/task/task-recovery-service.ts#L254-L265】. The `RecoveryReport` is then returned to the caller, which in Motrix is triggered from the main process entry point in [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts).

## How TaskRecoveryService Is Instantiated

In [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts), the application instantiates `TaskRecoveryServiceImpl` during startup and passes it the `TaskManager`, active engine `adapter`, filesystem inspector (`defaultRecoveryFs`), and an `activityRecorder`. When the renderer process requests recovery, the IPC command in [`src/server/ipc/commands.ts`](https://github.com/agalwood/Motrix/blob/main/src/server/ipc/commands.ts) forwards the call to the supervisor, keeping the heavy logic in the main process.

```ts
// Simplified startup pattern from src/main/index.ts
import { TaskRecoveryServiceImpl, defaultRecoveryFs } from '@core/task/task-recovery-service'

const recoveryService = new TaskRecoveryServiceImpl({
  taskManager,
  adapter,
  fs: defaultRecoveryFs,
  activityRecorder,
  finalizeTask,
  log: console
})

const report = await recoveryService.recoverOnStartup()
console.info('Recovery finished', report)

```

## Decision Logic Example

The core of the recovery algorithm is deterministic. Here is the `determineAction` excerpt that maps phase and filesystem state to an action:

```ts
export function determineAction(input: RecoveryInput): RecoveryAction {
  const { phase, fsState, aria2HasMatchingInfoHash, taskType } = input

  if (phase === TransitionPhase.Idle) return RecoveryAction.NoOp

  if (phase === TransitionPhase.Renaming) {
    if (fsState === 'temp_only') return RecoveryAction.ResumeFromRename
    if (fsState === 'final_only')
      return isTorrentLikeType(taskType)
        ? RecoveryAction.ResumeFromReseed
        : RecoveryAction.MarkCompleted
    return RecoveryAction.MarkError
  }

  if (phase === TransitionPhase.Reseeding) {
    if (fsState === 'final_only')
      return aria2HasMatchingInfoHash
        ? RecoveryAction.AdoptExistingGid
        : RecoveryAction.ResumeFromReseed
    if (fsState === 'temp_only') return RecoveryAction.ResumeFromRename
    return RecoveryAction.MarkError
  }

  return RecoveryAction.NoOp
}

```

## Summary

- **`recoverOnStartup`** filters all non-idle or `Finalizing` tasks from the `TaskManager` and clones them for safe inspection.
- The engine's active and waiting rows are mapped by info-hash so Motrix can adopt live GIDs when appropriate.
- **`inspectFs`** returns `temp_only`, `final_only`, `both`, or `neither`, which drives the recovery decision.
- **`determineAction`** is a pure function that selects one of six `RecoveryAction`s based on phase, filesystem state, and engine presence.
- **`applyAction`** handles atomic renames, live GID adoption, completion hooks, and error diagnostics including `TaskRecoveryFsMismatch`.
- Results are aggregated into a `RecoveryReport` with counts for recovered tasks, warnings, and errors.

## Frequently Asked Questions

### What happens if a task was in the Renaming phase during a crash?

If the filesystem shows `temp_only`, `determineAction` returns `ResumeFromRename` and `applyAction` moves the temporary file to `task.finalPath`. If the final file already exists and the task is torrent-like, it may instead receive `ResumeFromReseed`. Any inconsistency results in `MarkError`.

### How does Motrix prevent duplicate downloads after recovery?

The service indexes owned GIDs in `ownerByGid` and only allows `AdoptExistingGid` when the info-hash maps to exactly one active engine task and no other Motrix task owns that GID【/src/core/task/task-recovery-service.ts#L93-L125】. This ensures a one-to-one mapping between a recovered task and a live engine session.

### What does the `MarkError` recovery action indicate?

`MarkError` is triggered when the filesystem check returns `both`, meaning temporary and final output paths exist simultaneously. The task is moved to the `Error` state with the code `TaskRecoveryFsMismatch`, and the database may store extended diagnostics via `applyDiagnosisUpgrade`【/src/core/task/task-recovery-service.ts#L96-L123】.

### Where is `TaskRecoveryServiceImpl` initialized in the codebase?

`TaskRecoveryServiceImpl` is constructed in [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts) during application startup. The renderer process can trigger recovery through an IPC command defined in [`src/server/ipc/commands.ts`](https://github.com/agalwood/Motrix/blob/main/src/server/ipc/commands.ts), which delegates to the supervisor running in the main process.