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

79 lines
6.5 KiB
Markdown
Raw 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.
## 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`).
- Совместимость: существующие загрузки не затронуты; переход одноразовый на
уровне логики приёма.