Skip to content

Protocol ru

Anton Sovetov edited this page Nov 8, 2026 · 2 revisions

English · Русский

Протокол

Как agents-room хранит и передаёт состояние координации.

Модель

комната    пространство имён. Сессия сама входит в repo:<имя> и может
           войти в любое число общих комнат — так агенты из разных
           репозиториев достают друг друга.
сессия     один процесс агента: харнесс, cwd, pid, метка, теги, автономия
сообщение  одна отправка: отправитель, селектор, тема, тело, срок
доставка   одна строка на разрешённого получателя, хранит состояние
лиза       заявка на путь: владелец, режим, когда взята, когда истекает
чтение     хеш содержимого, который сессия последней видела по пути
событие    неизменяемая строка аудита

Хранилище — одна база SQLite в режиме WAL по адресу ~/.agents-room/room.db. Переопределяется через AGENTS_ROOM_DB. Демона нет.

Сессии

room_join возвращает id сессии. Все остальные инструменты принимают его обратно как явную ручку.

Неизвестный id сессии — ошибка, а не пустой ответ. Чтение по id, за которым ничего нет, возвращает ноль строк — ровно то же, что возвращает пустой ящик; вызывающий, передавший не тот id, не может их различить. Один агент неделями звал msg_recv с id сессии харнесса, каждый раз получал «no unread messages» и решил, что ему ничего не присылали, пока в комнате не накопилось девятнадцать сообщений.

Ошибка называет id, который скорее всего имелся в виду:

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

Два id

Id Форма Куда идёт
ключ харнесса UUID, например 2b50133e-... в room_join как harness_key; харнесс кладёт его в окружение
id сессии 16 строчных hex везде остальное, как session_id

Для модели, которая держит оба, они похожи, и передача не того раньше была невидима.

last_seen — это активность, а не живость

Чат, думающий над долгой правкой, и чат, вышедший час назад, по нему неотличимы, поэтому room_who сообщает отдельно liveness: alive, gone или unknown:

  • alive и gone определяются по ключу харнесса — сканирование запущенных процессов отвечает, какие ключи есть в чьём окружении. Чат сохраняет ключ через перезапуск MCP-сервера, а pid — нет.
  • unknown значит, что в строке вообще нет pid, и это не то же самое, что gone.

prune удаляет сессию только когда она молчала дольше порога и она gone. Gone он определяет той же функцией, что room_who, из того же сканирования.

Повторный вход поднимает автономию, но не опускает

Только wake может начать ход в простаивающем чате. Ниже него сигнал едет на ходе, который агент и так делает, поэтому простаивающая сессия на notify или hold недостижима и выглядит точно как сломанный хук.

Уровень сохраняется, если повторный вход его явно не поднимает:

  • не указано — сохранённый уровень остаётся;
  • выше — применяется, с упором в кап комнаты;
  • ниже — отказ с предупреждением, в котором назван оставшийся уровень.

Понижение — решение оператора, и у него один путь: кап комнаты.

Селекторы

Селектор Во что разрешается
* все сессии в комнате, кроме отправителя
<session-id> одна сессия; неизвестный id — ошибка, а не тишина
file:<glob> сессии, держащие живую лизу по шаблону
tag:<tag> сессии, вошедшие с этим тегом

Пути нормализуются в абсолютные до записи, поэтому src/wire.go и /abs/src/wire.go из одного каталога спорят за одну лизу. Селектор file: сопоставляется с путём лизы в трёх формах: полный путь, путь относительно рабочего каталога держателя и голое имя файла.

Якорь — каталог держателя, а не комнаты: в общей комнате могут быть сессии из нескольких репозиториев, и src/wire.go означает в каждом свой файл.

Селектор, не нашедший никого, — не ошибка: широковещание в комнате из одного — законная отправка с нулём получателей. Число сообщается обратно, чтобы отправитель не принял сохранённое сообщение за доставленное.

Сообщение принадлежит каждой комнате, куда доходит

Доставки отвечают «что меня ждёт». Они не могут ответить «что здесь решено», потому что создаются в момент отправки: у сессии, вошедшей позже, их нет.

На второй вопрос отвечает room_history. Он читает message_rooms, где на каждую пару (сообщение, комната) лежит строка для каждой комнаты, в которой отправитель был в момент отправки. Именно это делает историю общей комнаты полной.

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

Пустой ящик объясняет, почему он пуст

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

Состояния доставки

У каждого получателя своя строка. Состояние только движется вперёд.

Состояние Значение
stored записано, сигнал не предпринимался
offered сигнал был послан, но принят ли он — неизвестно
accepted харнесс принял сигнал
read получатель забрал тело
acked получатель подтвердил
expired срок истёк до того, как тело забрали
undeliverable у этого харнесса нет рычага пробуждения

Сигнал пробуждения посылается один раз на сообщение, а потом повторяется на событиях инструментов, когда доставка старше окна повтора. Повторяется только offered: accepted значит, что ход уже идёт, и повтор запустил бы второй.

Лизы

lease_claim — единственная операция, которая обязана быть атомарной между процессами. Она идёт внутри BEGIN IMMEDIATE, убирает истёкшие лизы, проверяет держателей и вставляет.

Режим Правило
exclusive один держатель; блокирует любую другую заявку
shared несколько держателей; блокирует исключительные заявки, разрешает другие общие

Расхождения

note_read хранит SHA-256 файла в момент, когда сессия его читала. drift_check сравнивает файл на диске с этим хешем.

Степень Условие
SAFE не изменился, или сессия не записывала базовый хеш
MINOR изменился, и никто другой не держит лизу на нём
CRITICAL изменился, пока лизу держала другая сессия

Справочник инструментов

Инструмент Аргументы Возвращает
room_join room?, extra_rooms?, label?, tags?, autonomy?, harness?, harness_key? session_id, rooms, autonomy, warning?
room_leave session_id ничего, выходит из всех комнат
room_leave_room session_id, room ничего, остаётся в остальных
room_who session_id?, room? сессии, у каждой liveness
room_set_autonomy_cap session_id?, room?, autonomy room, autonomy
msg_send session_id, to, subject?, body?, reply_to?, ttl_s? message_id, recipients, warning?
msg_recv session_id, limit? сообщения, отмеченные прочитанными; inbox когда пусто
msg_ack session_id, message_id ничего
msg_unread session_id count
msg_sent session_id, limit? messages: у каждого states, pending, left, sent
room_history session_id?, room?, limit? room, messages (только чтение)
lease_claim session_id, path, ttl_s?, mode? ok, holder, reason
lease_renew session_id, path, ttl_s? продлённая лиза
lease_release session_id, path ничего
lease_check session_id, path free, lease
lease_list session_id лизы
note_read session_id, path hash
drift_check session_id, path path, severity, changed, holder, since, message

Clone this wiki locally