Внешний вызов прошёл, а запись — нет: сверяться или ждать оператора

Автор: Алексей Пасечник (Arhellist). Канонический адрес: https://arhellist.ru/blog/vneshniy-vyzov-proshyol-a-zapis-net-sveryatsya-ili-zhdat-operatora/.

Опубликовано: 2026-10-23.

Вопрос звучит просто: «внешний вызов уже прошёл, а результат не успел записаться на диск — сверяться с внешней системой или останавливаться и ждать оператора?» На самом деле это вопрос о трёх разных вещах, которые часто смешивают: что такое «результат», где проходит граница «внешней системы» и почему нельзя дать один ответ на все случаи.

Почему «сверяться или ждать» — ложная дилемма

Люди, которые задают этот вопрос, обычно хотят услышать одно из двух:

- «сверяйся, это же безопасно»; - «останавливайся, это же надёжно».

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

Возьмём три действия:

Действие A. Агент записывает файл в рабочую директорию: report.md. Файл создаётся атомарно (временный → fsync → rename), но процесс убивают между rename и записью события в журнал. Что мы имеем? Файл на диске есть, события нет. Сверка: «файл существует?» — существует. Действие можно безопасно повторить (запись того же содержимого в тот же файл идемпотентна) или считать выполненным (файл на месте, что ещё нужно?). Сверка решает проблему.

Действие B. Агент отправляет письмо на внешний адрес через сайт почты. Письмо ушло. Процесс убивают до записи события. Что мы имеем? Письмо, возможно, ушло — но мы этого не знаем. Сверка: «письмо в Отправленных?» — может быть, ещё не появилось (индексация). Повторить — значит отправить второе письмо. Остановиться и ждать оператора — единственный безопасный выбор. Сверка не решает проблему, потому что внешняя система не даёт мгновенного подтверждения.

Действие C. Агент запускает shell-команду, которая делает git push. Push прошёл. Процесс убивают. Что мы имеем? Коммит на сервере, возможно, есть. Сверка: «git log на remote?» — есть, но запрос к remote — это отдельный вызов, который может упасть. Повторить — либо no-op (если коммит тот же), либо конфликт (если за это время кто-то успел сделать push). Сверка возможна, но требует внешней точки правды, которая сама может врать.

Три действия — три разных ответа. Значит, вопрос не «сверяться или ждать», а «как классифицировать действие, чтобы выбрать политику».

Классификация: четыре класса, четыре политики

В моём опыте работает такая разбивка:

1. Идемпотентное в файловой системе. Запись файла, создание директории, копирование. Сверка: «цель существует?» Если да — считать сделанным (не перезаписывать, но и не паниковать). Если нет — безопасно повторить один раз. Политика: сверка → alreadydone или saferetry.

2. Идемпотентное в shell. git status, ls, curl -I, make (если цель уже собрана). Сверка: «результат совпадает с ожидаемым?» Если да — alreadydone. Если нет — saferetry, но только для read-only. А вот git push, make install, apt install уже не идемпотентны (может создать коммит, переустановить пакет, изменить состояние). Политика: сверка для read-only → saferetry; для mutate → needsoperator.

3. Браузерное действие. Клик «Отправить», заполнение формы, скачивание файла. Сверка: «состояние страницы изменилось?» — но страница может не успеть обновиться, а DOM — не отражать реальное состояние. Политика: needsoperator, потому что внешняя система не даёт мгновенного подтверждения. Оператор смотрит в Live View или в «Отправленные» и говорит: «ушло» / «не ушло». После этого — либо complete, либо retry.

4. Time-sensitive. Платёж, отправка сообщения в мессенджер, вызов внешнего API с биллингом. Сверка: «есть ли в ответе id транзакции?» — если id есть, но мы не записали — это уже сделано. Если id нет — не знаем. Политика: needsoperator, потому что повтор может стоить денег.

Ключевая идея: политика определяется не тем, что мы можем проверить, а тем, что мы можем повторить без последствий. Файл — можем. Письмо — не можем. Платёж — тем более не можем.

Как это устроено технически

Идея простая: перед каждым действием записываем намерение, после него — результат. Намерение — это «что собираемся сделать» (tool, args, idempotency class). Результат — «что получилось» (ok / error / needsreconcile).

Если процесс убивают между намерением и результатом — на следующем старте мы видим «намерение есть, результата нет». Это и есть точка сверки.

Дальше — по классу:

- idempotentfs: проверяем, существует ли цель. Если да — complete (файл на месте). Если нет — pending → saferetry (один раз). - idempotentshell (read-only): проверяем, совпадает ли результат с ожидаемым. Если да — complete. Если нет — saferetry. - browser / timesensitive: не пытаемся сверять автоматически. Помечаем needsreconcile и не даём агенту повторить без явного решения оператора. Оператор смотрит в Live View или во внешнюю систему, говорит «сделано» или «не сделано». После этого — либо complete, либо retry с новым id.

Почему не «всегда сверяться»? Потому что сверка — это тоже внешний вызов, который может упасть, вернуть неверный ответ или занять время. Для файла сверка дешёвая и надёжная. Для письма — нет. Для платежа — тем более нет.

Почему не «всегда ждать оператора»? Потому что оператор — не бесплатный ресурс. Если каждая запись файла требует подтверждения, агент бесполезен. Ждать оператора нужно только там, где повтор опасен, а сверка ненадёжна.

Как это выглядит в жизни

Представим агента, который готовит отчёт и отправляет его на почту. Шаги:

1. Сгенерировать report.pdf (файл). 2. Отправить письмо с вложением (браузер).

Если процесс убивают на шаге 1 — на следующем старте видим «намерение: report.pdf, результата нет». Сверка: файл существует? Если да — complete, идём к шагу 2. Если нет — retry (запись файла идемпотентна).

Если процесс убивают на шаге 2 — видим «намерение: отправить письмо, результата нет». Автоматически не сверяем. Помечаем needsreconcile. Оператор смотрит в «Отправленные»:

- если письмо есть → complete, задача закрыта; - если письма нет → retry с новым id (письмо не ушло, можно отправлять); - если не уверен → оператор решает, что делать (например, отправить повторно с пометкой «дубль»).

Это не «ждать оператора на каждом шаге». Это «ждать оператора только там, где без него нельзя».

Что не работает

Три подхода, которые выглядят разумно, но ломаются на практике.

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

Второй: «всегда останавливаться и ждать оператора». Оператор не бесплатный. Если каждая запись файла требует подтверждения — агент не работает. Ждать нужно только там, где повтор опасен.

Третий: «повторять всё, что не записалось». Этот — самый опасный. Файл — ок (идемпотентно). Письмо — нет (второе письмо). Платёж — нет (двойное списание). Повтор — это не «безопасное действие по умолчанию», это опасное действие, которое нужно обосновать.

Итог

Вопрос «сверяться или ждать» не имеет одного ответа. Правильный ответ: классифицировать действие и выбрать политику по классу.

- Идемпотентное в FS → сверка → alreadydone или saferetry. - Идемпотентное в shell (read-only) → сверка → saferetry. - Браузерное → needsoperator (внешняя система не даёт мгновенного подтверждения). - Time-sensitive → needsoperator (повтор может стоить денег).

Это не бюрократия. Это признание того, что у разных действий разные гарантии, и одна политика на всех делает систему либо опасной, либо бесполезной. Разделение на классы позволяет сделать её одновременно безопасной и полезной.

И ещё одна вещь. Классификация должна быть явной. Не «агент сам решит, к какому классу относится действие», а «в контракте инструмента написано: это idempotentfs, это browser, это timesensitive». Агент не должен угадывать. Контракт — это контракт.

#ЭлисAI #ИИагенты #архитектура

О чём пишем

  • Архитектура и эксплуатация веб-продуктов
  • AI-агенты, RAG и практические контуры
  • Кейсы LMS, маркетплейсов и защищённых коммуникаций
  • Заметки по Next.js, Node.js, PostgreSQL и realtime