Stale Write Guard

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

Два отказа, а не один

Их часто сводят к одному «агент испортил файл», но чинятся они по-разному и происходят по разным причинам.

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

Затирание чужой правки. Агент прочитал, подумал — а за это время файл поменял человек в редакторе, линтер, форматтер или соседний процесс. Агент пишет свою версию поверх, и чужое изменение исчезает бесследно: ни ошибки, ни конфликта, ни следа в истории до коммита.

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

Почему идемпотентность здесь не помогает

Соседний паттерн (Idempotency For Agents) отвечает на вопрос «что будет, если повторить этот вызов». Ответ «повтор не меняет результат» устаревшую запись не лечит вовсе: записать одно и то же испорченное содержимое дважды ровно так же разрушительно, как один раз.

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

Устройство

Состояние — одна таблица на сессию: путь после разрешения ссылок → время изменения файла на момент чтения.

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

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

Сообщение об отказе несёт исправление

Оба отказа возвращают не «запрещено», а что делать дальше: «файл не читался в этой сессии — прочитай его, чтобы не переписать невиданное» и «файл изменился на диске с момента чтения, его мог поправить человек или инструмент — прочитай заново». Читатель здесь агент, у которого между запусками нет памяти, и текст отказа — единственное место, куда можно положить порядок действий (Agent First Repository).

Развилка: предохранитель падает открытым

Реализация, из которой снят этот разбор, при любой неожиданной ошибке — не прочиталось время изменения, отказали права — разрешает запись, а не блокирует её.

Решение осознанное, и его стоит принимать явно, а не наследовать. Довод за: это предохранитель от собственной оплошности агента, а не рубеж безопасности; сломавшись, он не должен останавливать всю работу с файлами. Довод против: тот же самый отказ — недоступный stat — бывает признаком того, что с файловой системой что-то не так, и продолжать запись там сомнительно.

Ориентир: предохранитель от своей ошибки падает открытым, предохранитель от опасного действия — закрытым. В том же проекте разбор команды для правил разрешений устроен наоборот: неразобранная команда не получает автоматического одобрения (Human in the Loop).

Третий критерий: направление выводится из обещания режима

Ориентир выше разбирает случаи, где предохранители разной природы: свой промах против опасного действия. Бывает и так, что природа одна — оба режима защищают от одного и того же расхода, — а направление отказа у них всё равно противоположное. Тогда решает то, что режим обещает наружу.

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

Правило формулируется так: при неопределённости ошибаться надо в сторону, дешёвую для обещания этого режима, а не в сторону, дешёвую вообще. Проверка занимает минуту: назвать обещание одной фразой, а потом спросить, какая из двух ошибок его нарушает — пропустить лишнее или не пропустить нужное.

Отсюда же следует, почему пустой результат приходится проектировать отдельно. У режима, падающего закрытым, пустота — штатный исход, и обработчик обязан превратить её в явный отказ; иначе она сливается с «ничего не нашлось» и молча приводит к обходному пути, которого режим и избегал (Unknown Not A Value).

Чего он не заменяет

Это защита от несогласованности, а не от вредоносного действия и не от потери данных. Файл, который агенту вообще нельзя трогать, закрывается областью файловой системы (Capability Containment); возможность вернуть испорченное — границей отката (Rollback Boundary); подтверждение необратимого — воротами (Approval Path). Предохранитель устаревшей записи стоит рядом с ними и решает свою узкую задачу, зато почти бесплатно.

Связано с

  • Tool Calling — где стоит гейт и почему он внутри исполнения, а не в промпте
  • Idempotency For Agents — повтор своего вызова; здесь чужое изменение между чтением и записью
  • Agent Failure Modes — каталог тихих отказов, к которым относятся оба описанных
  • Approval Path — ворота подтверждения, на которых удобно разместить проверку
  • Capability Containment — что агенту недоступно вовсе; предохранитель работает внутри доступного
  • Human in the Loop — постоянные правила разрешений и противоположная посадка отказа
  • Agent First Repository — текст ошибки как носитель инструкции для агента