## Purpose Конвейер расшифровки: как задача движется по состояниям, что делает воркер, когда работы нет, что считается отказом шага и что бывает с ответом отправителю, когда доставить его некуда. Описаны пустой прогон воркера, неделимость захвата и срок его протухания, число попыток и состояние «мертва», нарастающая пауза перед повтором, условие записи результата держателем захвата и недоставка ответа при неподнятом входе. Сознательно не описаны: цепочка переходов `created → converted → transcribe → done | failed`, отмена контекста посреди шага и освобождение ресурсов внешних клиентов. Это не значит, что такого поведения нет: оно живёт в коде, а требования на него не написаны, потому что требование без проверки — предположение, а не норма. Первая задача, которая трогает любое из перечисленного, дописывает его сюда. ## ADDED Requirements ### Requirement: Недоставленный ответ не роняет шаг Шаг конвейера SHALL доводить задачу до достигнутого состояния, когда ответ отправителю доставить не удалось, и MUST не считать недоставку отказом шага. Недоставка MUST быть записана в журнал владельца, MUST нести идентификатор задачи, MUST называть причину и MUST считаться отдельной метрикой с причиной меткой. Причин у недоставки две, и исход у них общий: **вход отправителя не поднят** — задача заведена прошлым запуском, а сервис поднялся без этого входа; и **адресат у задачи не назван** — источником значится Telegram, а чата в задаче нет. Уровень записи MUST различать эти причины. Неподнятый вход — объявленный режим, и его уровень «может стать проблемой». Неназванный адресат — симптом порчи записи: у задачи из Telegram чат есть всегда, и пропасть он может только от дефекта, самый коварный источник которого назван инвариантом проекта про колонки очереди. Один уровень на обе причины утопил бы этот сигнал в потоке штатных записей о ненастроенном боте. Общий исход — не упрощение, а следствие момента: ответ уходит **после** того, как достигнутое состояние сохранено. Работа к этой минуте сделана, и объявленный отказ засчитался бы воркеру сбоем и лёг бы владельцу записью отказа — то есть соврал бы про исход дважды. Повтор делу не помогает: ни бот, ни адресат от ожидания не появятся. Поэтому задача остаётся в достигнутом состоянии, в повтор не уходит и в `failed` не переводится, а причина недоставки живёт в записи журнала, а не в состоянии задачи. Идентификатор задачи в записи обязателен: без него владелец видит, что ответ не ушёл, но не может найти, чей. Текст расшифровки и сообщение отправителя в эту запись MUST не попадать — приватность содержимого записи требование не ослабляет. Отложенной доставки это требование не заводит: ответ, не ушедший сегодня, не уходит и потом. Забрать расшифровку можно там же, где лежат остальные. #### Scenario: Вход отправителя не поднят - **GIVEN** задача принята входом Telegram прошлым запуском сервиса - **AND** сервис поднялся без этого входа - **WHEN** шаг конвейера доходит до ответа отправителю - **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем - **AND** задача остаётся в достигнутом состоянии, в повтор не уходит и в `failed` не переводится - **AND** в журнале есть запись уровня `WARN` о недоставке с идентификатором задачи и причиной - **AND** счётчик недоставленных ответов вырос с этой причиной меткой - **AND** ни текста расшифровки, ни сообщения отправителя в этой записи нет #### Scenario: Адресат у задачи не назван - **GIVEN** у задачи источником значится Telegram, а чат не назван - **WHEN** шаг конвейера доходит до ответа отправителю - **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем - **AND** задача остаётся в достигнутом состоянии - **AND** в журнале есть запись уровня `ERROR` о недоставке с идентификатором задачи и причиной: неназванный адресат — симптом порчи записи #### Scenario: Отвечать некуда, потому что запись пришла не из Telegram - **GIVEN** задача принята по HTTP - **WHEN** шаг конвейера доходит до ответа отправителю - **THEN** шаг завершается без отказа и без записи о недоставке