# How TaskManager and SessionManager Manage Download Tasks and State Persistence in Motrix

> Discover how Motrix's TaskManager and SessionManager handle download tasks and state persistence. Learn about their crash recovery and atomic update system for reliable downloads.

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

---

**Motrix separates in-memory task coordination from durable state persistence by delegating real-time download tracking to TaskManager and periodic snapshotting to SessionManager, ensuring atomic updates and crash recovery through a reservation system that maps aria2 engine IDs to Motrix task IDs.**

Motrix, the open-source download manager built on aria2, implements a robust two-layer architecture to handle download lifecycle management and data durability. The separation between **TaskManager** and **SessionManager** ensures that transient engine states never corrupt persisted data while maintaining real-time synchronization with the aria2 backend. Understanding how these components coordinate is essential for developers extending Motrix or troubleshooting task recovery issues.

## Architectural Separation of Concerns

### TaskManager: The In-Memory Authority

Located in [`src/core/task/task-manager.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/task/task-manager.ts), the **TaskManager** serves as the single source of truth for all active downloads during runtime. It maintains a primary map keyed by Motrix-specific task IDs, storing `DownloadTask` objects that encapsulate metadata, progress, and status.

The manager implements several critical indexing mechanisms to handle the asynchronous nature of the aria2 engine:

- **Engine Index**: A secondary mapping that translates aria2 engine task IDs (gids) to Motrix task IDs, enabling reverse lookups when the engine reports status updates.
- **Reservation System**: Before aria2 creates a task row, `reserveEngineTaskId(gid)` claims the future engine ID to prevent orphan detection during polling intervals.
- **Retired Shield**: The `retiredEngineGids` set protects against transient engine rows that persist briefly after task removal or ID swaps, preventing duplicate entries.

Cleanup operations like `remove()` and `clear()` handle de-indexing logic, ensuring the engine index and primary task map remain consistent when tasks complete or get deleted.

### SessionManager: Durable State Persistence

The **SessionManager** in [`src/core/session/session-manager.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/session/session-manager.ts) owns the durability layer, bridging the volatile TaskManager with the SQLite backend. It orchestrates periodic snapshots of the entire task collection to the `task_instances` table while managing the RPC client connection to aria2.

Key responsibilities include:

- **Atomic Persistence**: The `runExclusivePersistence()` method acquires a write-lock on the database, ensuring that `save()` operations (default 15-second intervals) never interleave with concurrent mutations.
- **State Restoration**: On application startup, `restore()` reconstructs `DownloadTask` objects from persisted rows and re-injects them into TaskManager via `taskManager.set()`, rebuilding the engine index from stored gids.
- **Immediate Flush**: APIs like `persistTask()`, `persistTaskWithOccurrence()`, and `saveNow()` allow UI components to force synchronous writes when critical state changes occur.

## Coordination Flow Between Managers

The interaction between TaskManager and SessionManager follows a strict protocol to maintain consistency:

1. **Task Creation**: UI components create a `DownloadTask` instance and register it via `taskManager.add(task)`, making it available for immediate engine operations.

2. **Engine ID Reservation**: Before submitting the download request to aria2, the system calls `taskManager.reserveEngineTaskId(gid)` to claim the engine task ID. This reservation prevents SessionManager's polling logic from treating the upcoming aria2 row as an orphaned entry.

3. **GID Binding**: When aria2 confirms the download with a concrete gid, `taskManager.setReservedEngineTaskOwner(id, task, gid)` swaps the reservation for the actual task binding, activating the engine index entry.

4. **Periodic Checkpointing**: Every 15 seconds (configurable), `sessionManager.save()` iterates over `taskManager.getAll()`, serializing each task to the `task_instances` table through the atomic persistence wrapper.

5. **Graceful Recovery**: During application startup, `sessionManager.restore()` hydrates the TaskManager from disk, re-establishing the engine index and merging live aria2 data with the recovered state.

## Implementation Examples

The following TypeScript examples demonstrate typical usage patterns in Motrix's main process:

```typescript
// Initialize core managers
import { TaskManager } from '@core/task/task-manager';
import { SessionManager } from '@core/session/session-manager';
import { createRpcClient } from '@core/rpc/client';
import { createDbAdapter } from '@core/db/adapter';

const taskManager = new TaskManager();
const sessionManager = new SessionManager(
  taskManager, 
  createRpcClient(), 
  createDbAdapter()
);

// Start automatic persistence (15s default interval)
sessionManager.startAutoSave();

```

Adding a new download and triggering immediate persistence:

```typescript
import { createDownloadTask } from '@shared/helpers/task';

const task = createDownloadTask({
  url: 'https://example.com/file.zip',
  fileName: 'file.zip'
});

taskManager.add(task);
sessionManager.saveNow(); // Force immediate flush to database

```

Handling engine ID reservations before aria2 submission:

```typescript
const futureGid = '1234567890abcdef';
taskManager.reserveEngineTaskId(futureGid);

// After aria2 confirmation:
taskManager.setReservedEngineTaskOwner(task.id, task, futureGid);

```

Restoring state after application restart:

```typescript
await sessionManager.restore(); 
// TaskManager now contains all previously active downloads

```

## Key Source Files

| File | Responsibility |
|------|----------------|
| [`src/core/task/task-manager.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/task/task-manager.ts) | In-memory task collection, engine-to-task ID mapping, reservation and retired-gid shields |
| [`src/core/session/session-manager.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/session/session-manager.ts) | Periodic persistence, atomic database writes, startup restoration, RPC coordination |
| [`src/server/task-persistence.ts`](https://github.com/agalwood/Motrix/blob/main/src/server/task-persistence.ts) | Thin wrapper delegating persistence operations to SessionManager |
| [`src/main/index.ts`](https://github.com/agalwood/Motrix/blob/main/src/main/index.ts) | Entry point instantiating both managers and launching the autosave loop |

## Summary

- **TaskManager** maintains the runtime authority over download metadata, using bidirectional indexing (Motrix ID to task, gid to Motrix ID) to reconcile engine state with application state.
- **SessionManager** guarantees durability through atomic database transactions, persisting the complete task collection every 15 seconds and reconstructing it on startup.
- The **reservation system** bridges the gap between task creation and engine acknowledgment, preventing race conditions during aria2 polling.
- **Retired shields** protect against transient engine artifacts that could otherwise spawn duplicate task entries.
- Together, these components ensure that Motrix survives crashes and restarts without losing download progress or creating inconsistent state between the UI and aria2 backend.

## Frequently Asked Questions

### What happens if Motrix crashes between 15-second save intervals?

The SessionManager persists tasks atomically every 15 seconds by default. If a crash occurs between intervals, only the state changes since the last checkpoint are lost. When Motrix restarts, `SessionManager.restore()` loads the last complete snapshot from the `task_instances` table and merges it with live aria2 data, reconstructing the task queue to its last persisted state.

### How does TaskManager prevent duplicate tasks when aria2 reports the same gid twice?

TaskManager implements a **reservation system** via `reserveEngineTaskId()` and `setReservedEngineTaskOwner()`. Before submitting to aria2, the gid is reserved; when the engine confirms, the reservation converts to an active mapping in the engine index. Additionally, the `retiredEngineGids` set shields against recently removed gids that might appear in final polling cycles, preventing orphan rows from re-entering the task collection.

### Can I adjust the autosave interval in Motrix?

Yes. The SessionManager accepts configuration for the autosave interval when instantiated, though the default is 15 seconds. You can force immediate persistence at any time by calling `sessionManager.saveNow()`, which is useful when handling critical UI actions like pause/resume or configuration changes that must survive an unexpected termination.

### Why does SessionManager use exclusive locking for database writes?

The `runExclusivePersistence()` method ensures that database writes occur atomically relative to in-memory mutations. Without this lock, a task update could modify the TaskManager state during a `save()` operation, resulting in a corrupted snapshot where some tasks reflect old states and others reflect new states. The write-lock guarantees consistency between the in-memory TaskManager and the persisted `task_instances` rows.