Коротко
Автор показывает, как конкурентная работа нескольких процессов выявила проблемы в простой очереди вопросов на SQLite. Решение строится на конечном автомате, атомарных операциях и раздельном учёте ответа человека и его обработки агентом — это помогает не терять и не перезаписывать данные.
Контекст
Автор разработал Agent Inbox — систему, где агент передаёт человеку вопросы, а затем забирает ответы. Начальная версия была таблицей SQLite с вопросом, ответом и статусом; проблемы обнаружились при запуске двух CLI-процессов и взаимодействии через несколько каналов.
Главное
- Строка доски проходит состояния tracked, partial, missing, na, done и blocked. blocked означает, что требуется действие человека.
- handled_at фиксирует, что человек завершил свою часть, но строка остаётся blocked, пока агент не сохранит непустой outcome. Незабранные ответы можно найти условием WHERE handled_at IS NOT NULL AND outcome = ”.
- annotation_seen_at и handled_seen_at отмечают доставку ответа экземпляру агента, но не подтверждают завершение обработки. Забор ответа имеет семантику «как минимум один раз».
- Ответ можно дать в Agent Inbox или в чате. Inbox имеет приоритет, пока агент не забрал его ответ: запись из Inbox сбрасывает reply_seen_at, а запись из чата выполняется условным атомарным UPDATE.
- Для защиты от дублирующих и противоречивых вопросов действует правило «один вопрос — одно место взаимодействия». При изменении контекста агент обновляет существующую строку и увеличивает revision.
- board_advance использует expected_revision и board_version как CAS-маркеры. В одной транзакции операция архивирует предыдущий шаг и устанавливает следующий; устаревшие версии отклоняются.
- Тесты test/store.test.ts и test/mcp.integration.test.ts используют реальные временные базы SQLite. Проверки гонок между ответом и обработкой выполняются с двумя соединениями; интеграционный тест прогоняет запрос-ответ через MCP-сервер stdio.
- MCP-сервер stdio живёт столько же, сколько CLI-процесс, а субагенты внутри процесса могут использовать то же соединение. При передаче управления, где важна актуальность, автор рекомендует читать контекст с full:true, загружая его из базы, а не из кэша.
- SQLite настроена в режиме WAL с busy_timeout=5000. WAL помогает читателям работать параллельно с писателем, но не устраняет SQLITE_BUSY и логические гонки между отдельной проверкой и записью.
- Удалённый режим с общей базой пока не реализован: для него отсутствуют сетевой слой и аутентификация (issue #8). Также остаётся нерешённым пробуждение локальной сессии агента после удалённого ответа (issue #51).
Как сделано
Автор моделирует передачу управления как конечный автомат с явным владельцем строки и разрешёнными переходами. Состояние ответа человека, его доставка агенту и подтверждение обработки представлены отдельно: это позволяет восстановиться после сбоя и повторно забрать необработанный ответ.
Конкурирующие каналы записи защищены асимметричным правилом приоритета. Запись из Inbox может заменить прежний ответ; запись из чата допускается одним условным UPDATE, только если ответа ещё нет или он уже был забран. Проверку и изменение не разделяют на SELECT и UPDATE, чтобы избежать окна гонки.
Для перехода доски используется board_advance с expected_revision и board_version; несоответствие требует перечитать состояние. Контекст можно кэшировать на уровне процесса, но full:true загружает актуальные данные из базы. SQLite работает с WAL и busy_timeout=5000, а тесты проверяют как операции хранилища, так и реальное взаимодействие через MCP stdio.
Результаты
Модель и тесты позволяют выявлять устаревшие переходы, не перезаписывать непрочитанный ответ из Inbox ответом из чата и находить ответы, которые человек отправил, но агент не обработал. Автор сообщает, что WAL с тайм-аутом сглаживает большинство кратковременных всплесков конкуренции, однако количественных показателей производительности или надёжности не приводит. Исправлены также пропуск аннотаций в pending() (issue #37) и невозможность корректировать ответы из чата — через тип clarify (issues #34 и #69).
Ограничения
Удалённый режим с общей базой, сетевым слоем и аутентификацией не реализован; не решено и возобновление локальной сессии агента после удалённого ответа. WAL не устраняет логические гонки и не гарантирует отсутствия SQLITE_BUSY или SQLITE_BUSY_SNAPSHOT: вызывающий код должен уметь обрабатывать такие ошибки. Автор не приводит количественных тестов нагрузки. Сценарий с удалённым режимом на демонстрационном скриншоте синтетический и пока не поддерживается.
Что взять себе
- Считайте взаимодействие человека и агента управлением состоянием, а не просто обменом сообщениями: задайте владельца и допустимые переходы.
- Разделяйте факт ответа, доставку ответа процессу и подтверждение его обработки — это упрощает восстановление после сбоев.
- Защищайте конкурентные записи атомарными условными UPDATE и проверяйте версии внутри транзакций; одного WAL недостаточно.
- Используйте кэш для скорости, но предусмотрите чтение из источника истины при передаче управления или риске рассинхронизации.
- Проверяйте гонки реальными соединениями и интеграционными тестами, а не только моками.
Читать оригинал, если…
Откройте репозиторий github.com/shariqh/agent-inbox, если нужны точные контракты операций, реализация в src/store.ts и подробности тестов в test/store.test.ts и test/mcp.integration.test.ts.