# Ошибки брокера секретов требуют доказательств

Брокер секретов должен сообщать то, что подтверждают его записи, а не то, на что намекает исключение. Если брокер не может доказать, что удаленную операцию не пытались выполнить, нельзя называть вызов ошибкой предварительной проверки. Как только байты запроса или запрос SSH `exec` могли попасть на другую сторону, результат может остаться неизвестным, даже если локальная ошибка говорит «timeout» или «connection reset».

От этого различия зависит, создаст ли агент второй платеж, дважды сменит одни учетные данные, повторно развернет тот же выпуск или безопасно повторит работу, которая не покидала компьютер. Поэтому полезный контракт ошибок хранит два независимых факта: на каком этапе остановился брокер и что он знает об удаленном выполнении. Одно поле «transient» не передаст оба факта.

Ниже описана схема для HTTP-вызовов и SSH-команд. Она предполагает, что брокер ведет долговечную запись действия. Состояние только в памяти помогает улучшить текст ошибки, но не дает оснований для повтора после перезапуска брокера или вызывающей стороны.

## Причина и результат отвечают на разные вопросы

Для каждой ошибки нужны этап, результат и указание о повторе. Этап подсказывает оператору, где искать причину. Результат сообщает вызывающей стороне, могло ли измениться состояние удаленной системы. Указание о повторе говорит автоматике, что сейчас разрешено сделать с учетом записанных доказательств и смысла операции.

Опубликуйте пять этапов:

- `validation`: брокер отклонил действие до выбора или использования секрета.
- `credential_injection`: брокер не смог получить, разрешить или добавить учетные данные.
- `connection_setup`: разрешение имени, маршрутизация, TCP, TLS, транспорт SSH, проверка узла или удаленная аутентификация завершились ошибкой до отправки.
- `remote_execution`: брокер отправил операцию и ждет удаленный результат либо уже получил его.
- `result_delivery`: брокер записал удаленный результат, но не смог полностью передать его вызывающей стороне.

Эти этапы нужны для диагностики, а не для политики повторов. Ошибка `connection_setup` может доказать, что прикладной запрос не отправляли. При разрыве повторно используемого соединения брокер может не знать, получила ли другая сторона записанные данные. Ошибка `result_delivery` иногда сопровождает известный успешный удаленный результат. Если назвать оба случая обычной сетевой ошибкой, пропадет факт, нужный вызывающей стороне.

Используйте четыре состояния результата:

- `not_attempted`: долговечные доказательства показывают, что удаленная операция не пересекла границу отправки.
- `rejected`: удаленная система вернула полный достоверный отказ и не заявила об успехе.
- `committed`: у брокера есть полный достоверный результат операции.
- `unknown`: отправка могла состояться, но у брокера нет полного результата, который определяет исход операции.

`committed` не означает успех. Команда, завершившаяся со статусом 23, и HTTP-запрос с полным ответом 500 имеют известный результат. Удаленное приложение выполнилось как минимум до момента ответа. Формулировка «не удалось выполнить» подталкивает к созданию дубликата.

Указание о повторе должно быть таким же точным: `never`, `after_correction`, `backoff`, `same_idempotency_key`, `reconcile` или `fetch_result`. Брокер вычисляет его по доказательствам, смыслу метода, гарантиям удаленной системы и состоянию журнала результатов. Вызывающая сторона не должна угадывать его по тексту ошибки.

Не объединяйте `rejected` и `committed` в одно публичное значение `known`. После отказа можно отправить исправленный запрос, а зафиксированный результат нужно использовать или сверить с удаленным состоянием. В обоих случаях судьба попытки известна, но дальнейшие действия различаются.

Для асинхронного приема нужна еще одна запись доказательства, но не новое состояние результата. Полный ответ HTTP 202 подтверждает прием задания, а не завершение поставленной в очередь работы. Запишите идентификатор задания и ресурс его состояния, которые вернул сервис, а затем отслеживайте задание как отдельную операцию. Если повторить отправку из-за того, что задание еще ожидает выполнения, оно может попасть в очередь дважды.

Такое разделение уточняет различие, которое многие SDK размывают. Причина ошибки отвечает на вопрос «что сломалось локально?». Удаленный результат отвечает на вопрос «что уже могло произойти?». Механизм повторов, который читает только причину, небезопасен.

## Проверка и добавление учетных данных завершаются до отправки

Ошибка проверки остается настоящей предварительной ошибкой, только пока брокер не открыл канал, способный перенести действие. Отклоняйте неверно заданные адресаты, неподдерживаемые методы, отсутствующие поля, слишком большие нагрузки, неизвестные ссылки на учетные данные и запрещенные подмены заголовков до разрешения имени узла или доступа к секрету. Запишите `phase=validation`, `outcome=not_attempted` и обычно `retry=after_correction`.

Автоматический повтор не исправит детерминированную ошибку проверки. Повтор одной и той же неверной нагрузки расходует ресурсы и может скрывать цикл агента. Верните стабильный код, например `INVALID_TARGET`, `UNSUPPORTED_ACTION` или `PAYLOAD_LIMIT`, а также путь к полю, которое может исправить вызывающая сторона. Не возвращайте отклоненную ссылку на секрет, если ее имя содержит чувствительные сведения.

Добавление учетных данных все еще относится к предварительному этапу, если брокер прекращает работу до передачи хотя бы одного байта удаленного запроса. Сюда входят заблокированное хранилище, отклоненное подтверждение, отсутствие учетных данных, неподдерживаемый способ добавления и локальная ошибка расшифровки ключа. Результат остается `not_attempted`, но совет о повторе меняется. Заблокированное хранилище может разрешить повтор после действия пользователя; отказ в подтверждении обычно требует `never` для этого вызова; отсутствующий секрет нужно исправить.

Не помещайте содержимое учетных данных ни в ошибку, ни в запись действия. Можно записать идентификатор секрета или его несекретную версию, способ добавления и решение, остановившее вызов. Если ради доказательства добавления записать готовый заголовок `Authorization`, брокер потеряет смысл.

Граница здесь неочевидна. Если брокер собрал полный HTTP-запрос с учетными данными в закрытом буфере и остановился до записи, удаленную операцию по-прежнему не пытались выполнить. Если он передал буфер транспортному API, а API сообщил о частичной или неопределенной записи, добавление завершилось и отправка могла начаться. Классифицируйте ошибку по последней доказанной границе, а не по функции, в стеке которой поймали исключение.

Для предварительного этапа также нужен снимок конфигурации. Если при проверке использовалось одно описание маршрута, а перед отправкой прочитали уже измененное, доказательства не описывают фактически выполненное действие. До подтверждения привяжите к вызову нормализованный адресат, версию учетных данных, разрешенный способ добавления и отпечаток запроса. При изменении любого связанного значения создайте новый вызов, не редактируйте прежнюю запись.

Это особенно важно при подтверждении человеком. Карточка подтверждения может оставаться открытой, пока агент или перезагрузка конфигурации меняет тело, узел или выбранный секрет. Брокер должен подтвердить отпечаток будущей отправки и повторно проверить его непосредственно перед записью. Несовпадение дает `validation/not_attempted` и не разрешает отправить новый запрос по старому подтверждению.

## Для настройки соединения нужна точная конечная точка

Сбой нового соединения до создания прикладного канала обычно доказывает `not_attempted`. Ошибка DNS или маршрута, отказ TCP-соединения, отклонение сертификата TLS или ключа узла SSH, ошибка аутентификации SSH происходят до возможного выполнения HTTP-запроса или SSH-команды. Запишите точный завершенный этап, чтобы вызывающая сторона отличила неверное имя узла от отклоненных учетных данных, не видя секрет.

Фраза «не удалось подключиться» слишком широка для HTTP-клиента с пулом соединений. Когда брокер берет готовое соединение, настройка уже завершена. Запись может завершиться ошибкой, потому что другая сторона закрыла неактивный сокет. Операционная система может сообщить о разрыве канала после доставки части байтов или после получения всего запроса удаленной стороной, но до того, как клиент заметил закрытие. Такой случай относится к `remote_execution` с `outcome=unknown`, если транспорт не дает более сильного доказательства.

Не считайте сообщение «локальная запись подтвердила ноль байтов» доказательством того, что удаленная сторона ничего не получила. API с буферизацией может локально принять байты и только позже сообщить об ошибке. Неудачная запись почти ничего не говорит о том, что уже прочитала другая сторона. Полезная граница отправки проходит при первой передаче транспорту, способному доставить прикладные данные. После ее пересечения результат по умолчанию меняется на `unknown`.

Настройка соединения для HTTP и SSH заканчивается в разных местах. Для HTTPS до отправки завершите DNS, TCP, TLS, проверку сертификата и создание туннеля через прокси, если он нужен. Для SSH завершите согласование транспорта, проверку узла, аутентификацию пользователя, создание канала сеанса и необходимую подготовку окружения. Эти этапы не доказывают, запускалась ли последующая команда, но ошибка на них может доказать, что команду никогда не запрашивали.

Перенаправление создает вторую границу отправки. Полный ответ 307 или 308 определяет результат первого HTTP-обмена, но переход создает новый запрос к другому адресату. До этого запроса снова проверьте адресат, область действия учетных данных и метод. Не передавайте учетные данные авторизации между источниками только потому, что библиотека автоматически следует перенаправлениям.

HTTP-прокси добавляет еще одного наблюдателя, но не устраняет неопределенность. Успешный туннель доказывает лишь то, что прокси открыл путь. Пересылающий прокси может вернуть полную ошибку о своей попытке, а RFC 9209 может указать место сбоя пересылки, однако результат в исходной системе все равно может остаться неизвестным. Запишите, какой переход создал доказательство. Не выдавайте уверенность посредника в собственном ответе за уверенность в последствиях для исходной системы.

Брокер может повторить настройку соединения внутри системы, когда у каждой попытки есть отдельная запись и ни одна не пересекла границу отправки. Ограничьте число попыток и покажите их в одной записи действия:

```json
{"attempts":[{"n":1,"stage":"tcp_connect","outcome":"not_attempted","code":"ECONNREFUSED"},{"n":2,"stage":"tls_handshake","outcome":"not_attempted","code":"CERT_EXPIRED"}]}
```

Итоговая ошибка не должна стирать прежние доказательства. Она должна сообщить, что обе попытки не дошли до удаленной операции и требуют исправления.

## Отправка меняет бремя доказательства

Момент отправки должен быть явным долговечным переходом состояния. До первой передачи транспорту добавьте `dispatch_started` в запись действия и сохраните его в соответствии с гарантией долговечности системы. Если процесс завершится после записи в транспорт, но до фиксации перехода, после перезапуска операция может ошибочно выглядеть как не предпринимавшаяся.

Строгая предварительная запись увеличивает задержку, поэтому команды часто хотят журналировать после отправки. Такой совет популярен: обычный путь становится быстрее, а программа выглядит проще. Для неидемпотентных действий он неверен. Редкий сбой попадает именно в промежуток, где брокер вынужден выбирать между потерей работы и ее дублированием.

Предварительная запись сама по себе не доказывает получение запроса удаленной стороной. Она намеренно переносит неопределенность в безопасную сторону. После `dispatch_started` результат начинается с `unknown`. Поздний достоверный ответ может изменить его на `rejected` или `committed`. Брокер никогда не возвращает его к `not_attempted`.

Для HTTP отправка начинается до передачи первого байта запроса в соединение. Отслеживайте отправку заголовков, полного тела, получение заголовков ответа и полного тела ответа. Эти отметки помогают диагностике, но `request_body_sent=true` не доказывает обработку запроса приложением. Аналогично, `request_body_sent=false` не доказывает бездействие приложения: сервер может отклонить запрос или совершить действие по заголовкам, не читая все тело.

Для SSH отправка начинается до передачи запроса канала `exec` в аутентифицированный транспорт. Используйте `want reply=true`. RFC 4254 говорит, что сервер ответит успехом или ошибкой канала, но успех означает только прием запроса на запуск команды. Он не означает завершение команды и не подтверждает безопасность повтора ее последствий.

Отмена после отправки не относится к предварительным ошибкам. Если у вызывающей стороны истек срок ожидания и она закрыла канал, удаленный процесс может продолжить работу. Укажите локальную причину `CALLER_CANCELLED` и сохраняйте `outcome=unknown`, пока записанный удаленный результат не определит исход. Отмена описывает интерес вызывающей стороны, а не удаленное состояние.

Для пакетного запроса нужен результат каждого элемента. Если брокер отправляет пять изменений одним HTTP-запросом и получает полный ответ только для четырех, нельзя безопасно назначить одно указание о повторе всему пакету. Запишите родительский транспортный результат и пять дочерних результатов. Повторяйте только тот элемент, для которого это разрешают записи и удаленный контракт, либо сверяйте пакет целиком, если удаленный API применяет изменения атомарно.

То же правило действует для shell-скрипта, отправленного по SSH. Один статус завершения относится к процессу скрипта, но не обязательно к каждому его последствию. Если вызывающей стороне нужны решения о повторе отдельных действий, назначьте каждой операции удаленный идентификатор и запись результата, не пытайтесь определить ход выполнения по stdout.

## Полный удаленный результат определяет исход выполнения

Достоверный ответ с полностью завершенным кадрированием переводит неопределенность в известный результат. Для HTTP запишите итоговый статус, выбранные несекретные заголовки, полное тело или его хеш и завершенность кадрирования. Для SSH запишите прием команды, завершенность stdout и stderr, статус выхода или сигнал при наличии, а также закрытие канала.

RFC 9112 требует, чтобы клиент помечал HTTP-ответ как неполный при преждевременном закрытии соединения или ошибке декодирования частей. Брокеру секретов нужно более строгое правило: никогда не показывать частичное тело как полный удаленный результат, даже если первые байты похожи на корректный JSON. Возвращайте частичное содержимое только в явно помеченном диагностическом поле либо удаляйте его, если оно может содержать чувствительные данные.

Один HTTP-статус не определяет безопасность повтора прикладного действия. Полный 401 доказывает, что сервер отклонил эти учетные данные для запроса, поэтому брокер может записать `rejected`; повтор с теми же данными бесполезен. Полный 429 или 503 может разрешить `backoff`, если метод безопасно повторять и ответ задает подходящее время. Полный 500 известен, но приложение могло изменить состояние до его создания. Не превращайте каждый 5xx в разрешение повторить POST.

RFC 9110 определяет идемпотентность по ожидаемому эффекту нескольких одинаковых запросов и разрешает автоматически повторять идемпотентные методы после ошибки связи. Важно уточнение «точно известна как идемпотентная». Название метода служит доказательством, но не гарантией. Неверно спроектированный GET, который запускает развертывание, небезопасен, несмотря на имя метода. Правильно реализованный PUT можно повторять, хотя он меняет состояние.

Информационные HTTP-ответы не определяют исход действия. `100 Continue` разрешает клиенту отправить тело запроса, но ничего не говорит об итоговом результате приложения. Другие ответы 1xx также оставляют вызов в работе. Только полный итоговый ответ или более сильная прикладная квитанция с понятным брокеру контрактом могут вывести результат из `unknown`.

Полнота и подлинность ответа должны выполняться одновременно. Идеально кадрированный ответ от неверного TLS-узла, недоверенного SSH-сервера или неожиданного прокси не дает достоверных сведений о нужном адресате. Проверка личности обычно заканчивается при настройке соединения, однако для возобновленных сеансов и пулов соединений нужно связать подтвержденную личность другой стороны с записью действия.

RFC 9209 определяет `http_response_incomplete` для посредника, который получил только часть ответа от следующего перехода. Рекомендованный статус 502 полезен для совместимости с HTTP, но один `502` теряет доказательства результата. Храните структурированное `outcome=unknown` брокера рядом с любым сопоставленным статусом.

В SSH есть похожая ловушка. RFC 4254 рекомендует серверу возвращать `exit-status`, но не требует этого. Если канал закрылся после stdout без статуса выхода или сигнала, брокер знает о завершении потока, но не знает об успехе команды. Верните `REMOTE_RESULT_INCOMPLETE` и выберите `unknown`, если контракт действия не задает другой достоверный признак завершения.

## Ошибка доставки результата не должна повторять выполнение

Доставка результата начинается только после долговечного сохранения определенного удаленного результата. Если не удалось сериализовать ответ для вызывающей стороны, закрылся канал MCP или завершился вызывающий процесс, удаленная операция не становится снова неизвестной. Сообщите `phase=result_delivery`, сохраните `outcome=committed` или `rejected` и задайте `retry=fetch_result`.

На этом этапе нужен идентификатор вызова, по которому вызывающая сторона получит сохраненный результат. Повторная отправка действия не равна получению результата. Разделите эти операции, чтобы обычная клиентская библиотека не превратила разрыв канала ответа во второй удаленный вызов.

Порядок записей имеет значение:

1. Завершить и проверить удаленный ответ.
2. Добавить определенный результат и его хеш в долговечную запись.
3. Зафиксировать доступный для получения результат под идентификатором вызова.
4. Передать результат вызывающей стороне.

Если шаг 4 завершится ошибкой, шаги 2 и 3 докажут произошедшее. Если брокер сначала доставляет результат, а потом ведет журнал, сбой может оставить вызывающую сторону с успешным ответом, тогда как аудит будет показывать неизвестный исход. Это дефект аудита, даже если немедленного повтора не произойдет.

Для больших или потоковых результатов действует то же правило. Храните фрагменты с порядковыми номерами и итоговой отметкой полноты. Вызывающая сторона может продолжить доставку с последнего проверенного фрагмента, но брокер не должен объявлять результат полным до получения терминатора, заявленной длины или характерного для протокола закрытия, которое доказывает завершение.

Подтверждение вызывающей стороны полезно для хранения, но не для определения удаленного результата. Отмечайте `RESULT_DELIVERED` только после того, как протокол подтвердил полную передачу вызывающей стороне. Если подтверждения нет, сохраняйте доступность результата до конца срока хранения и считайте повторные запросы чтением. Не запускайте действие повторно ради восстановления результата, который брокер решил не хранить.

Хранение результата может завершиться ошибкой после удаленного выполнения. Если полный ответ есть в памяти брокера, но его не удается зафиксировать, брокер знает больше, чем выражает простое `unknown`, однако доказательства не переживут сбой. Верните `phase=result_delivery`, добавьте `outcome=committed` только тогда, когда контракт долговечности позволяет подтвердить это текущей записью, и потребуйте немедленного вмешательства оператора. Исправлять нужно резервирование места и проверки отказов хранилища, а не повтор удаленного действия.

## Идемпотентность задается удаленным контрактом

Ключ идемпотентности делает неизвестный результат пригодным для повтора, только когда удаленный сервис обещает связать ключ с одной логической операцией. Если просто создать UUID в брокере и записать его, ничего не изменится. Удаленный адресат должен принимать ключ, сравнивать отпечаток запроса, достаточно долго хранить первый определенный результат и возвращать его при повторе.

До отправки брокер должен сохранить четыре факта: ключ идемпотентности, отпечаток запроса, удаленную область действия, а также сведения об истечении или сроке хранения, если сервис их публикует. При повторе нужно использовать тот же ключ и идентичный отпечаток. Повторное использование ключа с измененным телом должно локально завершаться `IDEMPOTENCY_MISMATCH`.

Не добавляйте заголовок идемпотентности без уведомления к адресатам, которые не описывают его смысл. Некоторые сервисы игнорируют неизвестные заголовки. Другие ограничивают ключи учетной записью или маршрутом. Политике повторов нужно настроенное и проверенное знание удаленного контракта, а не надежда на имя заголовка.

Сроки хранения входят в контракт. Если сервис забывает ключи через день, более поздний повтор может создать новый эффект, хотя брокер увидит тот же запрос. Сохраните самый ранний безопасный срок, остановите автоматические повторы до его наступления, а затем проводите сверку. Если сервис не публикует гарантию хранения, считайте ключ полезным только в пределах осторожно настроенного окна.

Параллельность может нарушить правильную последовательную схему. Два обработчика могут прочитать одну неизвестную запись и решить повторить запрос с тем же ключом. Правильный удаленный контракт дедупликации должен объединить их, но брокеру все равно нужно взять аренду на вызов, записать поколение повтора и разрешить только одну активную попытку. Так нагрузка снизится, а журнал останется понятным.

Из `unknown` есть только три безопасных пути:

- Повторить операцию, смысл которой точно известен как идемпотентный.
- Повторить с тем же ключом в рамках проверенного удаленного контракта дедупликации.
- Запросить удаленное состояние по стабильному идентификатору, сверить его и решить, нужно ли новое действие.

Во всех остальных случаях нужна остановка для проверки. Она может казаться излишне осторожной, пока агент ждет, но дублированные последствия дороже заметной паузы.

Условные HTTP-запросы способны усилить контракт. `If-Match` с известным тегом сущности может отклонить обновление, если ресурс изменился, а `If-None-Match: *` может предотвратить создание второго ресурса по тому же адресу. Они не устраняют все дубликаты, потому что модель ресурсов адресата по-прежнему важна, но дают доказательство, которое обеспечивает сервер, вместо догадки клиента.

SSH-команды редко поддерживают ключ идемпотентности на уровне протокола. Задайте повторяемость в прикладном контракте команды: создавайте развертывание под уникальным идентификатором выпуска, записывайте с атомарным сравнением или выполняйте запрос для подтверждения нужного состояния. Не считайте shell-команду безопасной только потому, что она ничего не вывела.

## Конверт ошибки должен содержать доказательства

Вызывающей стороне нужны стабильный машинный контракт и короткое понятное сообщение. Исключения транспортной библиотеки оставьте во внутреннем диагностическом поле, потому что их имена различаются между платформами и раскрывают детали реализации. Публичный конверт должен выглядеть так:

```json
{"invocation_id":"act_01J...","error":{"code":"REMOTE_OUTCOME_UNKNOWN","phase":"remote_execution","outcome":"unknown","retry":"same_idempotency_key","message":"Connection closed before a complete response was recorded."},"evidence":{"dispatch_started":true,"request_complete":true,"response_headers_received":false,"response_complete":false,"idempotency":{"key":"req_01J...","scope":"payments.create","fingerprint":"sha256:8b1...","remote_contract":"configured"}}}
```

Сделайте `code`, `phase`, `outcome` и `retry` закрытыми перечислениями. Добавляйте новые поля доказательств, не меняя смысл существующих. Вызывающие стороны могут выбирать ветку по перечислениям и показывать `message` человеку. Разбирать текст сообщения программно не следует.

Доказательство должно сообщать, откуда брокер знает результат, а не просто повторять вывод. Полезны номер попытки, идентификатор соединения, номер события отправки, отпечаток запроса, признак завершения протокола, удаленный идентификатор запроса, хеш ответа, статус выхода и идентификатор записи результата. Исключите секреты, полные заголовки авторизации, закрытые ключи и неотфильтрованные удаленные тела.

Сохраняйте переходы как события только для добавления, а затем проецируйте текущее состояние:

```text
ACTION_ACCEPTED
PREFLIGHT_VALIDATED
CREDENTIAL_AUTHORIZED
DISPATCH_STARTED
REQUEST_SENT
REMOTE_RESPONSE_STARTED
REMOTE_RESPONSE_COMPLETE
RESULT_COMMITTED
RESULT_DELIVERED
```

Для действия, завершившегося после `PREFLIGHT_VALIDATED`, доказано отсутствие попытки. Действие, завершившееся после `DISPATCH_STARTED`, но до достоверного признака конца, остается неизвестным. Действие с `RESULT_COMMITTED` переживет неудачную доставку без нового удаленного выполнения.

Проекция должна отклонять невозможные возвраты состояния. Позднее доказательство может изменить `unknown` на `rejected` или `committed`, но `committed` не может стать `not_attempted`. Второй наблюдатель может приложить результат сверки, однако он не должен переписывать исходную попытку так, будто отправки не было.

Записывайте монотонные порядковые номера, не полагайтесь на порядок настенных часов. Часы могут смещаться, а события параллельных компонентов приходить с опозданием. Временные метки помогают операторам сопоставлять системы, но порядок долговечных переходов задает номер в журнале. К импортированному удаленному доказательству добавьте источник и локальный номер, не вставляйте его в середину истории.

Доказательству также нужен указанный источник уверенности. `transport_observed`, `remote_response`, `remote_query` и `operator_attested` объясняют последующей программе причину изменения результата. Оператор вправе определить исход неизвестной попытки после проверки удаленной системы, но этот факт нельзя выдавать за ответ, полученный брокером в исходном соединении.

Целостность аудита и доказательство результата решают разные задачи. Хеш-цепочка может подтвердить, что записанные события не изменили позднее, но не докажет, что брокер записал каждое событие или что удаленное приложение выполнило запрос. Зашифрованный хеш-связанный журнал Sallyport с записью без чтения, а также отдельные представления сеансов и действий дают долговечное место для этих переходов. Результату действия все равно нужен описанный здесь контракт этапа и результата.

## Код повтора должен быть простым и проверяемым

Механизм повтора должен использовать указание, уже выведенное из доказательств. Он может добавить ограничения частоты и числа попыток, но не должен повышать небезопасное указание из-за того, что исключение выглядит временным.

```text
decide(record, operation):
  if record.retry == "fetch_result":
    return FETCH(record.invocation_id)

  if record.outcome == "not_attempted":
    if record.retry == "backoff":
      return RETRY_NEW_ATTEMPT
    return STOP_FOR_CORRECTION

  if record.outcome == "unknown":
    if operation.idempotent:
      return RETRY_NEW_ATTEMPT
    if record.retry == "same_idempotency_key" and
       operation.fingerprint == record.fingerprint:
      return RETRY_SAME_KEY
    return RECONCILE

  if record.outcome == "rejected" and record.retry == "backoff":
    return RETRY_WHEN_ALLOWED

  return RETURN_RECORDED_RESULT
```

Проверяйте переходы, а не классы исключений. Внедрите сбой до доступа к учетным данным, во время TLS, до первой записи запроса, после полной записи запроса, посреди заголовков ответа, посреди кадрированного тела, после фиксации результата и во время доставки. Завершайте брокер между каждой парой долговечных переходов и проверяйте, что восстановление никогда не заявляет `not_attempted` после `DISPATCH_STARTED`.

Добавьте враждебное поведение удаленной стороны. Пусть сервер применит последствие и закроется без ответа. Пусть вернет 500 после фиксации. Пусть обработает ключ идемпотентности, проигнорирует его и отклонит повторный ключ с измененной нагрузкой. Для SSH закройте канал после приема `exec`, пропустите `exit-status` и отправьте статус выхода перед разрывом потока вывода. Ожидаемый результат всегда должен следовать доказательствам.

Метрики должны считать этап и результат отдельно. Рост `connection_setup/not_attempted` указывает на маршрутизацию, сертификаты или аутентификацию. Рост `remote_execution/unknown` требует сверки и может указывать на проблему надежности удаленной системы. Если объединить их под названием «ошибки брокера», скроются и эксплуатационная причина, и риск дублирования действий.

Не позволяйте удобному SDK стирать контракт. Если он обязан выбрасывать исключения, приложите полный конверт и разрешите автоматический повтор только для `backoff` или `same_idempotency_key`. Чтобы повторить действие `unknown/reconcile`, вызывающей стороне должно потребоваться явно небезопасное решение в программе.

В проверки восстановления включите конкурирующие обработчики и устаревшие аренды. Остановите один обработчик после получения аренды на повтор, дождитесь ее истечения и запустите другой. Когда первый продолжит работу, проверка поколения должна остановить его до отправки. Без такой проверки даже безошибочная классификация результата может создать параллельные дубликаты.

Храните бюджет повторов в записи действия, а не в локальных счетчиках процесса. Перезапуск не должен сбрасывать число попыток или срок удаленного ключа. После исчерпания бюджета верните последние доказательства и потребуйте сверку. Общая ошибка «превышено число попыток» уничтожила бы более безопасный диагноз.

Самое сложное состояние должно оставаться неудобным. Если запись говорит о начале отправки, а достоверного завершения нет, брокер не знает удаленный результат. Сохраните этот факт, проведите сверку и не превращайте отсутствие доказательств в разрешение.
