Files
jellybit/openspec/changes/archive/2026-07-07-fast-catch-ingest/proposal.md
T
avandClaude Opus 4.8 0d263270cb Быстрый приём: сохранение в catched, добавление в qBittorrent — шаг worker'а
Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из
полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` —
ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent
вынесены в асинхронный шаг машины состояний, который двигает worker.

- store: состояние `catched` (нетерминальное, активная группа); атомарный
  переход PromoteCatched (catched → downloading + display_name) с гардом
  state='catched' (ре-валидация после сетевых вызовов вне блокировки)
- ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу
- worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий
  переход; сбой add оставляет catched (ретрай тиком); предохранитель
  catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи
- config: worker.catch_timeout (дефолт 10m)
- веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг
  карточки/страницы до перехода в downloading; Telegram-текст без сырого catched
- OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change
  заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:29:28 +03:00

6.5 KiB
Raw Blame History

Why

Сейчас приём (Ingest) синхронно делает всё: парсит источник, выводит отображаемое имя (потенциально медленный вызов LLM в namer.DeriveName) и добавляет источник в qBittorrent — и только потом отвечает клиенту. Долгий LLM и внешний запрос к qBit задерживают ответ HTTP API / веб-UI / Telegram и расширяют окно «строка в БД есть, в qBittorrent ещё нет».

Идея: сделать приём быстрым — синхронно только валидировать и сохранить загрузку (новое состояние catched), сразу вернув ответ; вывод имени и добавление в qBittorrent вынести в отдельный асинхронный шаг машины состояний, который двигает worker.

What Changes

  • Новое состояние catched — загрузка поймана и персистентно сохранена (быстрый путь). Нетерминальное, активное (участвует в инварианте «≤1 активная загрузка на infohash»).
  • Приём (ingest) — быстрый: парс magnet, извлечение инфохэшей, синтез контекста из полей ссылки (дёшево, без сети), атомарный дедуп и запись загрузки в catched. Ответ клиенту сразу. Синхронного вывода имени и добавления в qBittorrent в приёме больше нет.
  • Асинхронный шаг (download-tracking, worker): на каждом тике worker подхватывает catched-загрузки, выводит отображаемое имя из контекста (LLM + фолбек), добавляет источник в qBittorrent (категория/savepath/rename) и переводит catched → downloading (отдельного added-to-qbittorrent нет — downloading и так значит «в qBit, возможно metaDL»).
  • Обработка сбоев вне запроса клиента: неуспешный add оставляет загрузку в catched (worker перетыкивает на следующем тике); предохранитель catch_timeout уводит долго-зависший catched в failed (error_code qbit_add) с уведомлением автора.
  • catched исключён из проверок «раздача не найдена» (у него раздачи нет по дизайну) — ни поллинг, ни сверка не считают его рассинхроном/orphaned.
  • Веб-UI показывает промежуточное состояние catched (бейдж/фаза жизненного цикла), заголовок деградирует, пока имя не выведено (фолбек уже есть). catched попадает в активную группу списка.

Capabilities

New Capabilities

Нет. Изменение переиспользует существующие capabilities.

Modified Capabilities

  • ingest: приём становится быстрым — сохранение в catched и мгновенный ответ; синхронный вывод имени и добавление в qBittorrent из приёма убраны (переезжают в асинхронный шаг). Требования по выводу имени переформулированы: выполняются на шаге добавления, а не в пути ответа клиента.
  • download-tracking: добавляется шаг «добавление пойманной загрузки в qBittorrent» (вывод имени + add + переход catched → downloading), предохранитель catch_timeout, исключение catched из проверок пропажи раздачи.
  • web-ui: человекочитаемый бейдж для catched; активная группа включает catched; заголовок при пустом имени — по фолбеку; карточка catched самообновляется htmx-поллингом до перехода в downloading.

Без спек-правок: state-reconciliation и live-status не меняются. Исключение catched из проверок рассинхрона нормативно закреплено требованием download-tracking «catched не считается пропажей раздачи» (само поведение уже верно — catched не входит в desyncStates и дебаунс пропажи источника его не трогает). Иллюстративное перечисление активных состояний в тексте state-reconciliation остаётся на кросс-ссылке и будет выверено при следующем касании этой спеки. live-status: у catched нет qBit-телеметрии по дизайну — живой прогресс для него корректно отсутствует.

Impact

  • Код: internal/store (состояние StateCatched, группа active), internal/ ingest (быстрый путь: убрать namer/qbt-add, писать catched), internal/ worker (новый шаг обработки catched: namer + qbt-add + переход, таймаут), internal/httpapi+internal/tgbot (без правок API — ответ и так по Result), веб-UI шаблоны (бейдж/фаза catched).
  • Конфиг: новый catch_timeout (предохранитель), дефолт консервативный.
  • Данные: у активной загрузки теперь есть фаза без infohash-раздачи; схема БД не меняется (используем существующие state/error_code/error_msg).
  • Совместимость: существующие загрузки не затронуты; переход одноразовый на уровне логики приёма.