# Tasks: retry-stall-basis

## Схема и хранилище

- [x] Миграция `0010_retried_at.sql` — колонка `download.retried_at` (nullable TEXT).
- [x] `store.Download.RetriedAt` (`db:"retried_at"`) + метод `RetriedTime()`.
- [x] `store.SetRetriedAt(ctx, id, t)` — перезаписывающая запись базиса retry.
- [x] Обновить ER-схему `docs/specs/database.md` (строка `retried_at`).

## qBittorrent-клиент

- [x] `qbt.Torrent.LastActivity` (`json:"last_activity"`).

## Логика воркера

- [x] `checkTimeouts` — две разные меры: `magnet_timeout` по возрасту,
  `stuck_after` по простою (`last_activity`).
- [x] Разбить `torrentAge` на `addedBasis` (возраст, фолбэк `created_at`),
  `stallDuration` (простой от `last_activity`), `retriedFloor` (пол по
  `retried_at`).
- [x] `Retry` — сброс базиса через `SetRetriedAt`; перецепка только к здоровому
  живому торренту, сломанный (`classErrored`) → повторный `Add` (NIT-12).
- [x] `Store` interface воркера — метод `SetRetriedAt`.

## Спеки

- [x] Дельта `state-reconciliation` — MODIFIED «Восстановление зависшей
  загрузки» (простой от `last_activity`) и «Ручной повтор» (сброс базиса,
  NIT-12).
- [x] `docs/specs/workflow.md` — устранить противоречие «возраст vs простой»,
  описать сброс базиса и перецепку только к здоровому торренту.

## Тесты

- [x] `TestRetryResetsTimeoutBasis` — следующий тик после retry (MAJOR-1).
- [x] `TestStallMeasuredFromLastActivity` — простой vs возраст (MAJOR-2).
- [x] `TestSetRetriedAtOverwrites` — store.
- [x] Обновить фейки (`fakeStore`, `memStore`) методом `SetRetriedAt`.

## Проверки

- [x] `openspec validate --strict retry-stall-basis`.
- [x] `task test`.
- [x] `task lint`.
