# 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`.