## Why Загрузка magnet'ом ушла в терминальный `failed`/`magnet_timeout` по wall-clock таймауту (возраст от `created_at`, ~1ч), хотя qBittorrent просто долго тянул метаданные (медленные трекеры / мало пиров). Метаданные в итоге пришли, торрент жив и качается, но задача застряла в терминальном состоянии без выхода — восстановить её нельзя. Вдобавок падение происходит молча (нет уведомления автору), а единственный путь возврата `Worker.Retry` баговый (не сбрасывает базис времени → задача мгновенно снова падает) и доступен только через REST, но не из веб-UI и Telegram. ## What Changes - **Терпеливость к `metaDL`.** Перестаём убивать долгий magnet агрессивным таймаутом. Дефолт `[worker].magnet_timeout` поднимается до `24h` — это редкий страховочный предохранитель, а не рабочий механизм. Настоящие провалы определяются по статусам ошибок qBittorrent (`error`/`missingFiles` → `qbit_error`), а не по wall-clock. *(У qBittorrent нет статуса «magnet мёртв» — зависший magnet вечно висит в `metaDL`, поэтому единственный сигнал на этот кейс — большой страховочный таймаут.)* - **Базис таймаута — от факта, а не от `created_at`.** Возраст для `magnet_timeout`/`stalled` считаем от времени добавления торрента в qBittorrent (`added_on`), а не от создания записи. Это чинит неверный отсчёт для усыновлённых раздач и устраняет мгновенное повторное падение после возврата в `downloading`. - **Уведомление о любом падении.** Переход в `failed` (любой `error_code`) и `stuck` уведомляет автора загрузки через `notifier` (раньше уведомления слались только для `review`/`done`/`orphaned`/`target_missing`). - **Авто-восстановление из `failed`/`stuck`.** Фоновая сверка (state-reconciliation) замечает, что у задачи в восстановимом `failed`/`stuck` (наша нетерпеливость: `magnet_timeout`, `stalled`) источник в qBittorrent жив и продвинулся, и возвращает задачу в поток (`downloading` либо `completed` по статусу торрента). Пользовательские и реальные провалы (`qbit_error`, `reverted`, `cancelled`) сверка не воскрешает. - **Ручной retry из UI и Telegram.** Кнопка повторной попытки добавляется в веб-UI и Telegram-бот (раньше — только Cancel; retry был только в REST). `Worker.Retry` чинится: перецепляется к уже живому торренту вместо слепого повторного `Add`, базис таймаута сбрасывается. ## Capabilities ### New Capabilities Нет. Уведомления о падении и семантика таймаута относятся к жизненному циклу загрузки, который пока живёт в `docs/specs/workflow.md` (ещё не мигрирован в OpenSpec); заводить отдельную capability `notifications` сейчас — преждевременное дробление. ### Modified Capabilities - `state-reconciliation`: восстановимые `failed`/`stuck` (`magnet_timeout`, `stalled`) перестают быть «неприкосновенными» для сверки и подлежат авто-восстановлению при живом продвинувшемся источнике; добавляется требование о восстановлении и о доступности ручного retry. `qbit_error`, `reverted`, `cancelled`, `deleted` остаются вне восстановления. ## Impact - **Спеки:** дельта `state-reconciliation`; обновление графа переходов и семантики таймаута/уведомлений в `docs/specs/workflow.md` (источник истины по жизненному циклу до миграции). - **Конфиг:** дефолт `[worker].magnet_timeout` → `24h` (`internal/config/config.go`), `config.example.toml`. - **Код:** `internal/worker/worker.go` — `transition` (уведомление о failed/stuck), `checkTimeouts` (базис от `added_on`), `reconcile`/ `reconcileDesync` (воскрешение из failed/stuck), `Retry` (перецепление + сброс базиса); новый `Event` падения и его обработка в `notifier`/ `tgbot`/`httpapi`; кнопка retry в `internal/httpapi` и `internal/tgbot`; вынос строки `"magnet_timeout"` (и смежных `error_code`) в именованные константы рядом с состояниями. - **qBittorrent-клиент:** возможно потребуется поле `added_on` в `qbt.Torrent` (если ещё не читается). - **Миграции БД:** не ожидаются (восстановление опирается на состояние qBit и существующие поля задачи).