## Context `processCatched` (`internal/worker/worker.go`) вызывается из `pollOnce` сразу после `Poll`. Сейчас он безусловно зовёт namer (LLM) и `qbt.Add`, а `409`/`Fails.` на дубле трактует как транзиентный сбой → вечный повтор с тратой LLM. Приём (`ingest`) уже дедуплицирует по infohash на **активную** задачу (`FindActiveByInfohash`/`CreateDownloadIfNoActive`). Значит до `processCatched` доходит только загрузка, для которой в jellybit нет другой активной задачи. Отсюда упрощение: если торрент такой загрузки уже присутствует в qBittorrent, это не «конфликт с чужой задачей», а «раздачу уже кто-то (пользователь вручную, прошлый тик) добавил» — надо просто **усыновить** её и разложить. ## Goals / Non-Goals **Goals:** - Пойманная загрузка, чей торрент уже в qBittorrent, доводится до `downloading` без повторного `add` (без 409) и без LLM; дальше — обычная раскладка. - LLM-namer не вызывается ни при усыновлении, ни при недоступности qBittorrent. - Гонка апгрейда F6 сужена перечитыванием `source_type` под блокировкой. - Повторное добавление уже активной в jellybit загрузки транспорт отражает как дубль (сообщение + лог), без новой записи. **Non-Goals:** - Не заводим новых состояний загрузки. Дедуп на приёме записи не создаёт; усыновление — это `downloading`, а не отдельный статус. - Не различаем «наш/чужой» торрент по категории/тегу: инвариант приёма делает различие ненужным (до воркера доходит лишь загрузка без другой активной). - Не добавляем счётчик попыток `add` (предел — время `catch_timeout`). - Не проверяем присутствие на приёме (`ingest` остаётся быстрым, без qBittorrent). ## Decisions **1. Источник снимка присутствия: один листинг `qbt.Torrents("")` на входе в `processCatched`.** Строим `byHash` (по `Hash`/`InfohashV1`/`InfohashV2`, lowercase), переиспользуем для всех catched-задач тика (как это делает Poll для своего снимка). Провал листинга → qBittorrent недоступен → в этот тик пойманные не трогаем (namer не зовём), повтор на следующем; отсечка — `catch_timeout`. Поиск торрента задачи — существующий `torrentFor(d, byHash)`. **2. Присутствует → усыновляем; ветвление до namer.** Если `torrentFor` нашёл раздачу — `PromoteCatched(id, t.Name)` (перевод `catched → downloading` + имя из `qbt.Torrent.Name`, без LLM), под коротким замком с ре-валидацией `state='catched'`. Иначе — обычный путь: re-read под замком → namer → `sourceAddParts` → `Add` → `PromoteCatched`. namer (LLM) на ветке усыновления и при недоступности qBit не зовётся. **3. Никакого различия «наш/чужой» и никакого `duplicated`.** Инвариант приёма («одна активная на infohash») гарантирует, что усыновляемая раздача не отберётся у другой активной задачи. `PromoteCatched` (гард `state='catched'`) корректен; `ActivateIfNoOtherActive` (как в `Retry` из терминального `failed`) здесь не нужен — `catched` нетерминален и уже единственный активный владелец infohash. Имя раздачи в `display_name` полезно и уведомлениям, и заголовку в UI (у `catched` оно пусто). **4. Re-read `source_type` под замком перед добавлением (сужение гонки F6).** На absent-ветке перед сбором `addReq` берём короткий замок, перечитываем запись (`GetDownload`): ре-валидация `state='catched'` и актуальный `source_type` (апгрейд magnet→torrent мог случиться после снятия списка). Тяжёлые вызовы (`GetTorrentData`, namer, `Add`) — вне замка. **5. Дедуп на приёме (case 1) — только сообщение.** `ingest` при попадании на активную задачу уже возвращает `Deduplicated=true` (запись не создаётся). Меняем лишь текст ответа транспорта: вместо «Уже в работе #id» — «♻️ дубль уже активной #id, добавление отменено». Лог дедупа (`download attached to active`) уже есть. Новых состояний/записей не заводим. ## Risks / Trade-offs - **[Остаточное окно F6]** → re-read `source_type` под замком + `Add` вне замка окно резко **сужают**, но не закрывают полностью. Полное закрытие требует держать замок через `Add`, что нарушает инвариант «тяжёлые вызовы вне замка». Оверлап крайне редок, цена промаха — один неудачный magnet-add, повтор на следующем тике уже увидит `torrent`. Принимаем суженное окно осознанно. - **[Снимок присутствия на тик «отстаёт»]** → каждая catched-задача обрабатывается раз за тик; если наш `add` прошёл, а запись перехода сорвалась, усыновление случится на следующем тике, где листинг уже видит раздачу. - **[Усыновление раздачи, добавленной вручную с иными savepath/категорией]** → инвариант источника не нарушается: файлы не наши, раскладка хардлинчит отдельно; infohash совпадает — контент тот же. Осознанное поведение (как `discover`). ## Open Questions Нет.