Files
T
avandClaude Opus 4.8 bcc7b2d76b Жизненный цикл: claim-токен распознавания, индексация хешей, retry сломанного торрента
Три мелких фикса из docs/backlog/review-lifecycle-minor.md (ревью Fable
2026-07-08). MINOR-9 (I/O под глобальным w.mu) осознанно waive для
one-user home-сервера — не трогаем.

MINOR-8: claim-токен распознавания. recognizeOne фиксирует updated_at на
момент claim (перечитывая запись после перехода в recognizing), а
finishRecognition коммитит результат, только если токен совпал. Иначе за
время LLM-вызова задачу увели из recognizing и вернули обратно
(cancel → relink revive) — это уже другой эпизод, устаревший результат
отбрасываем, задача остаётся в recognizing для перезапуска поллингом.

NIT-11: lookup-мапы (byHash/live/torrentByInfohash) больше не индексируют
усечённый 40-hex t.Hash v2-only торрентов. Новый хелпер torrentIndexHashes
зеркалит выбор torrentHashes: t.Hash берём только при отсутствии обоих
infohash_v1/v2. Убирает теоретический ложный матч по коллизии длины.

NIT-12: retry живого, но сломанного торрента (error/missingFiles) теперь
отклоняется с подсказкой починить раздачу (recheck) в qBittorrent, вместо
бессмысленной переотдачи источника (сверка тут же вернула бы задачу в
failed). Повторный Add — только когда раздачи в qBittorrent нет. Меняет
спеку state-reconciliation → дельта openspec/changes/2026-07-17-retry-reject-broken-torrent
(не архивировал).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:35:14 +03:00

85 lines
6.2 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.
## MODIFIED Requirements
### Requirement: Ручной повтор зависшей/упавшей загрузки из транспортов
Система SHALL предоставлять пользователю команду повторной попытки (retry)
для задач в `failed`/`stuck` из веб-UI и Telegram (не только через REST API).
Retry SHALL переводить задачу обратно в `downloading`, не вызывая её
немедленного повторного падения по таймауту: базис отсчёта таймаутов SHALL
сбрасываться.
Сброс базиса система SHALL выполнять сохранением времени retry в поле задачи
(`retried_at`, RFC 3339 UTC), которое приподнимает пол ОБОИХ страховочных мер
(`magnet_timeout` по возрасту и `stuck_after` по простою): отсчёт ведётся от
`max(базис, retried_at)`. `retried_at` SHALL храниться в задаче (не в памяти
процесса), чтобы сброс базиса пережил интервал поллинга и рестарт процесса.
Благодаря этому даже живой, но давно добавленный либо давно простаивающий
торрент после retry SHALL получать свежее окно и на ближайшем тике сверки
падать снова SHALL NOT.
Если источник задачи уже жив и ЗДОРОВ в qBittorrent, retry SHALL перецепляться
к существующему торренту, а не добавлять источник повторно вслепую. Если же
живой торрент в состоянии ошибки qBittorrent (`error`/`missingFiles`), retry
SHALL отклоняться с понятным пользователю сообщением — починить раздачу
(`recheck`/восстановить файлы) в qBittorrent и повторить. Повторная отдача
источника такой торрент не чинит (qBittorrent отверг бы дубль), а простой
возврат в `downloading` тут же снова упал бы `qbit_error` по сверке (+ дебаунс
уведомления) — retry выглядел бы сломанным. При отказе состояние задачи
(`failed`/`stuck`) система менять SHALL NOT и повторный `Add` выполнять SHALL NOT.
Повторный `Add` при retry система SHALL выполнять, только когда раздачи в
qBittorrent нет, — **по типу источника** (`source_type`), как и добавление
пойманной загрузки (см. `download-tracking` «Добавление пойманной загрузки в
qBittorrent»): magnet/url — ссылкой; torrent — сохранёнными байтами `.torrent`
файлом. Для torrent-источника retry БЕЗ живой раздачи система SHALL добавлять
раздачу байтами и SHALL NOT активировать задачу в `downloading`, не добавив её
(иначе задача повиснет как «нет в qBittorrent»).
#### Scenario: Retry упавшей magnet-загрузки из веб-UI
- **GIVEN** задача в `failed`, её торрент жив и здоров в qBittorrent
- **WHEN** пользователь нажимает retry в веб-UI
- **THEN** задача возвращается в `downloading` без повторного `Add`
- **AND** не падает снова на ближайшем тике сверки по таймауту
#### Scenario: Retry живого, но давно простаивающего торрента не падает снова
- **GIVEN** задача в `stuck`/`stalled`, её торрент жив в qBittorrent, но
добавлен давно и данные не двигались дольше `stuck_after`
- **WHEN** пользователь нажимает retry
- **THEN** задача возвращается в `downloading` без повторного `Add`
- **AND** на ближайшем тике сверки НЕ падает снова в `stuck` (базис сброшен
через `retried_at`)
#### Scenario: Retry доступен в Telegram
- **WHEN** для задачи в `failed`/`stuck` пользователь вызывает retry в
Telegram-боте
- **THEN** задача возвращается в `downloading`
#### Scenario: Retry без живого источника добавляет источник заново
- **GIVEN** задача в `failed`, раздачи в qBittorrent нет
- **WHEN** пользователь инициирует retry
- **THEN** источник добавляется в qBittorrent заново — magnet/url ссылкой,
torrent сохранёнными байтами файлом
- **AND** задача переходит в `downloading`
#### Scenario: Retry сломанного живого торрента отклоняется
- **GIVEN** задача в `failed`, её торрент присутствует в qBittorrent, но в
состоянии ошибки (`error`/`missingFiles`)
- **WHEN** пользователь инициирует retry
- **THEN** retry отклоняется с сообщением починить раздачу (`recheck`) в
qBittorrent
- **AND** состояние задачи не меняется (остаётся `failed`), повторный `Add` не
выполняется
#### Scenario: Retry torrent-загрузки без живого источника
- **GIVEN** задача с `source_type = torrent` в `failed`, раздачи в qBittorrent
нет, байты `.torrent` сохранены
- **WHEN** пользователь инициирует retry
- **THEN** сохранённые байты добавляются в qBittorrent файлом
- **AND** задача переходит в `downloading` (не остаётся без раздачи)