Потерянное обновление: когда AI-агенты правят один файл, а вклад исчезает без ошибки

Соло-разработчик, который гоняет два и более субагента на один артефакт — файл плана, scratchpad, общую заметку в репозитории — рискует потерять вклад даже при «успешных» ответах: оба агента получают ACK, а в финальном файле остаётся работа только одного. Это не глюк IDE и не «плохой промпт», а классический lost update в multi-agent workflow: каждый съеденный write уже стоит потраченных токенов, а система не подсвечивает потерю красной строкой. В разборе @alex_spinov показывает механизм на воспроизводимом симуляторе и предлагает pre-write compare-and-set gate с измерениями.
Почему ACK не гарантирует целостность при нескольких AI-агентах
В сценарии, который описывает автор, N агентов дописывают свои секции в один общий файл. Корректный исход — все N вкладов на месте. Цикл каждого агента простой: прочитать текущее состояние, поработать локально, записать результат.
Проблема в режиме last-writer-wins (write_lww): запись безусловно перезаписывает значение, версия инкрементируется, событие попадает в append-only WAL, а вызывающий получает "ACK" — даже если агент читал устаревшую версию. Потерянный вклад — это агент, чей write был подтверждён, но чья секция отсутствует в финальном value.
Для vibe-coding это болезненнее, чем в классической БД: исчезнувший write — не абстрактная транзакция, а уже оплаченная работа модели, которую оркестратор посчитал успешной.
Симулятор вместо продакшена — и почему выводы всё равно переносимы
Эксперимент с пятью агентами — не отчёт о коммерческих coding-агентах в бою. Автор собрал офлайн-симулятор lostwrite_sim.py на Python 3.13.5: только стандартная библиотека, без сети, ключей, потоков и реальных денег. «Конкурентность» задаётся явными расписаниями чередования (interleaving schedules), а не настоящим параллелизмом — результат воспроизводим. Код писался с помощью AI-ассистента; три локальных запуска дали байт-идентичный STDOUT с sha256 1df08c5e38894622314a2b052684303853b9b8fa9508eaabdfd877970c88983b.
Хранилище VersionedStore держит номер версии, frozenset вкладов и WAL. Модель намеренно упрощена, но бьёт в реальную боль соло-workflow: несколько субагентов на общий state — plan file, scratchpad, строка в БД, GitHub issue — без координации записи.
Compare-and-set перед write: как gate ловит гонку
Альтернатива LWW — write_cas(expected_version, agent_id, new_value, enforce=True). Коммит происходит только если self.version == expected_version; иначе возвращается "CAS_FAIL". При ACK обновляется value, растёт версия, событие пишется в WAL.
Логика retry в симуляторе:
res = store.write_cas(a.base, a.aid, a.newval, enforce=not disable_version_check)
if res == "ACK":
a.acked = True
else:
cas_failures += 1
if a.retries < max_retries:
a.retries += 1
a.pc = "READ" # re-read, re-work, re-write
else:
a.refused = True # fail-closed
Автор называет это optimistic concurrency control; пессимистичная альтернатива — lease/lock, когда агент ждёт вместо повторной попытки. Фальсификатор F1 — тот же код с enforce=False на проверке версии — при worst-case N=5 снова даёт 4 lost updates, как без gate. Роль именно compare, а не обвязки, доказана.
Цифры: худший случай, честная выборка и цена retries
Автор разделяет детерминированный worst-case (все читают до любой записи) и Monte Carlo fair sample (равномерный случайный выбор готового агента на каждом тике, seed 20260728).
Worst-case при N=5:
| Метрика | Без gate (LWW) | С gate (CAS) |
|---|---|---|
| writes acknowledged | 5 | 5 |
| contributions в финале | 1 | 5 |
| lost_updates (ACK, но вклад пропал) | 4 | 0 |
| integrity_ok | False | True |
| cas_failures | 0 | 4 |
| retries | 0 | 4 |
| total_runs (выполненная работа) | 5 | 9 |
В fair sweep (trials = 40000 // N на каждое N) картина жёстче для LWW и ровная для gate:
| N | trials | NO_GATE: runs с потерями | mean lost | GATE: потери |
|---|---|---|---|---|
| 2 | 20000 | 75,0% | 0,75 | 0 |
| 3 | 13333 | 97,3% | 1,59 | 0 |
| 5 | 8000 | 100,0% | 3,35 | 0 |
| 8 | 5000 | 100,0% | 6,14 | 0 |
Суммарно 46 333 fair interleavings по всем N — gate даёт 0 потерь во всех выборках. При worst-case без gate потеря равна N−1; с gate — 0 lost, delta cost = N−1 retry.
Отдельный сценарий fail-closed: gate с max_retries=0, worst-case N=8 — 7 cas_failures, 7 refusals с видимыми id агентов, 0 silent lost_updates, integrity_ok=False (семь вкладов реально не попали). Это честнее, чем тихий overwrite. Константа COST_UNIT = 1 — учётная единица работы, не доллары и не измеренная цена токенов: gate увеличивает суммарную работу (retries), но не возвращает уже потраченные токены.
Что заложить в agent workflow и rules
Практический вывод для оркестрации субагентов на общем state:
- Pre-write concurrency gate — проверка версии перед commit — уместен для sub-agent dispatch, swarm и retrying planner с workers на одном артефакте.
- Append-only WAL фиксирует факт записи
(version, agent_id)без значения: видно, что писали и сколько раз, но не восстанавливает потерянные вклады при чтении текущего value. Учёт — не контроль. - Один «зелёный» запуск не доказывает безопасность: при N=2 четверть interleavings сериализуется без потерь и даёт ложное спокойствие.
- При исчерпании retry лучше fail-closed с явными refusals, чем silent overwrite.
- Автор отделяет задачу от revert-guard (когда один агент возвращает откатанный код) и от double-charge одного агента на retry.
Для соло-разработчика, который собирает цепочку «планировщик + несколько workers на один файл», паттерн compare-and-set перед write и retry при CAS_FAIL — переносимый минимум. Конкретную интеграцию в IDE или оркестратор автор не описывает; зато показывает, почему без gate даже пять ACK не спасают четыре вклада.
Источники
- @alex_spinov — Lost Update: When Two AI Agents Edit One File, One Silently Wins — Dev.to, опубликовано 30 июля 2026.