## 1. Усыновление в processCatched - [x] 1.1 В начале `processCatched` один раз получить листинг `qbt.Torrents("")` и построить `byHash` (по `Hash`/`InfohashV1`/`InfohashV2`, lowercase); провал листинга → WARN и ранний выход (пойманные не трогаем этот тик, namer не зовём). - [x] 1.2 Для каждой catched-задачи: `torrentFor(d, byHash)`. Присутствует → усыновление БЕЗ namer/Add: под замком с ре-валидацией `state='catched'` `PromoteCatched(id, t.Name)` (`catched → downloading`, имя из раздачи снимка). - [x] 1.3 Отсутствует в `byHash` — прежний путь, но с re-read записи под `w.mu` перед сбором `addReq` (свежий `source_type`, ре-валидация `state='catched'`); тяжёлые вызовы (`GetTorrentData`, namer, `qbt.Add`) — вне замка. - [x] 1.4 Убедиться, что namer и `qbt.Add` не вызываются на ветке присутствия и при провале листинга. ## 2. Сообщение о дубле на приёме - [x] 2.1 В `internal/tgbot/bot.go` (`ingestAndReply`) на `res.Deduplicated` отвечать явным «♻️ Дубль уже активной загрузки #id — добавление отменено» (вместо «Уже в работе #id»). Лог дедупа в `ingest` уже есть. ## 3. Тесты - [x] 3.1 `catched_test.go`: торрент присутствует в снимке qBittorrent → `downloading` без `Add` и без namer; `display_name` = имя раздачи. (Фейк qBit отдаёт торрент в снимке ДО обработки задачи.) - [x] 3.2 `catched_test.go`: листинг qBittorrent провалился → задача осталась `catched`, namer/Add не вызывались. - [x] 3.3 `catched_test.go`: торрента нет в снимке → обычный путь (namer + Add + promote) остаётся зелёным; re-read `source_type` под замком берёт актуальный тип. - [x] 3.4 Регресс: catch_timeout-предохранитель, отмена во время добавления. ## 4. Проверки и ревью - [x] 4.1 `task test` и `task lint` зелёные. - [x] 4.2 Ревью кода (чекпоинт перед archive): jellybit-review-code + jellybit-review-specs (сверка со спекой download-tracking). - [x] 4.3 `openspec validate --strict catched-promote-without-readd`.