## Why

Два связанных бага в семантике таймаутов зависания и ручного retry делают
повседневные сценарии сломанными:

- **MAJOR-1 — retry живого торрента мгновенно снова падает.** `Worker.Retry`
  при живой раздаче (`alive=true`) не переиздаёт `Add`, а лишь возвращает
  задачу в `downloading`. Базис отсчёта таймаута (`age = now − added_on`) при
  этом НЕ сбрасывается. Если торрент давно добавлен/давно простаивает,
  ближайший тик снова видит `stalledDL && age > stuck_after` → задача опять
  уходит в `stuck` (~секунды). Спека `state-reconciliation` «Ручной повтор»
  требует сброса базиса, но код его не выполняет (комментарий «базис от
  added_on» верен лишь для ветки повторного `Add`). Существующий тест
  `TestRetryReattachesNoReadd` прячет баг, ставя `added_on` «минуту назад».

- **MAJOR-2 — `stuck_after` мерит ВОЗРАСТ, а не ПРОСТОЙ.** `checkTimeouts`
  считает `stalledDL`-таймаут от `added_on` (возраст торрента). Торрент,
  качавшийся 5 часов и на один тик зашедший в `stalledDL` (нормальный проход
  между пирами), мгновенно получает `stuck` со лживым сообщением «stalled for
  5h» и уведомление `EventFailed`. Результат — флап `stuck`↔`downloading` и
  до-часовые ложные пинги. Спека сама противоречива: «`stalledDL` дольше
  `stuck_after`» (простой) против «возраст от `added_on`».

Дополнительно закрываются два NIT из того же ревью:

- **NIT-10** — фолбэк базиса возраста `added_on → created_at` (когда qBit не
  отдал `added_on`) остаётся, но теперь явно документирован и покрыт.
- **NIT-12** — retry задачи в `qbit_error` мгновенно откатывается: перецепка к
  сломанному (`error`/`missingFiles`) живому торренту бессмысленна — reconcile
  тут же возвращает задачу в `failed`. Retry перестаёт перецепляться к
  сломанному торренту и повторно отдаёт источник.

## What Changes

- **Мера простоя вместо возраста для `stuck_after`.** `stalledDL`-таймаут
  считается от `last_activity` qBittorrent (момент последнего движения данных),
  а не от возраста торрента. Долго качавшийся торрент со свежей активностью в
  `stuck` не уходит (MAJOR-2). `magnet_timeout` по-прежнему мерит **возраст**
  (`metaDL` без метаданных) от `added_on` — это семантически верно.
- **Сброс базиса таймаутов при ручном retry.** Новая колонка `download.retried_at`
  (RFC 3339 UTC) фиксирует момент retry и приподнимает базис ОБОИХ таймаутов
  (`max(базис, retried_at)`). После retry задача получает свежее окно и не
  падает снова на ближайшем тике (MAJOR-1). Хранится в БД (не в памяти), чтобы
  сброс пережил интервал поллинга и рестарт процесса.
- **Retry не перецепляется к сломанному торренту.** Если живой торрент в
  состоянии ошибки qBittorrent (`error`/`missingFiles`), retry повторно отдаёт
  источник вместо перецепки (NIT-12).

## Capabilities

### New Capabilities

Нет. Семантика таймаутов зависания относится к жизненному циклу загрузки,
который пока живёт в `docs/specs/workflow.md` (не мигрирован в OpenSpec).
Нормативная правка `stuck_after`/`magnet_timeout` вносится туда; в OpenSpec
затрагивается только `state-reconciliation` (восстановление и ручной retry).

### Modified Capabilities

- `state-reconciliation`: уточняется, что предотвращение `stuck` для
  прогрессирующего торрента опирается на **простой от `last_activity`**, а не
  на возраст; ручной retry сбрасывает базис таймаутов через `retried_at` и не
  перецепляется к сломанному живому торренту.

## Impact

- **Спеки:** дельта `state-reconciliation` (2 MODIFIED requirements);
  правка семантики таймаутов и retry в `docs/specs/workflow.md` (источник
  истины по жизненному циклу до миграции).
- **Код:** `internal/worker/worker.go` — `checkTimeouts` (две разные меры),
  `torrentAge`/новые `stallDuration`/`addedBasis`/`retriedFloor`, `Retry`
  (сброс базиса + перецепка только к здоровому торренту); `internal/qbt`
  (поле `last_activity`); `internal/store/download.go` (`RetriedAt`,
  `RetriedTime`, `SetRetriedAt`).
- **Миграции БД:** `0010_retried_at.sql` — колонка `download.retried_at`;
  обновление ER-схемы `docs/specs/database.md`.
- **qBittorrent-клиент:** читается новое поле `last_activity` из того же
  ответа `/torrents/info` (без нового вызова API).
