Files
avandClaude Opus 4.8 b8657120fe Приём: усыновление присутствующего в qBittorrent торрента вместо дубль-Add (409)
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>
2026-07-10 18:36:47 +03:00

88 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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
Нет.