processCatched перед Add проверяет присутствие торрента в qBittorrent (один листинг на тик): если раздача уже есть — усыновляем (promote catched→downloading без повторного Add и без LLM-namer, имя из раздачи), иначе добавляем как раньше. Это убирает бесконечный цикл дубль-Add → 409 → ретрай и лишние вызовы LLM. Инвариант приёма «одна активная на infohash» делает различие «наш/чужой» ненужным. source_type перечитывается под замком (сужение гонки апгрейда F6); при недоступности qBittorrent тик пропускается без вызова LLM. Дедуп на приёме (дубль на уже активную задачу) теперь отражается явным ответом бота «дубль уже активной #id — добавление отменено». Спека download-tracking обновлена (OpenSpec change заархивирован); закрыта задача беклога review-f2-promote-without-add. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.0 KiB
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
Нет.