agent-ecosystem/docs/team-management/research-inbox.md
iliya 2eac440fe2 fix(team): disable teammate DM relay through lead, read user.json directly
Teammates are independent Claude Code processes that read their own inbox
files via fs.watch. Relaying DMs through the lead caused three bugs:
lead responding instead of the teammate, duplicate messages from relay
loops, and teammates not responding to user due to conflicting prompts.

- Disable relayMemberInboxMessages for teammate DMs (teams.ts, index.ts)
- Add SendMessage(to="user") filter in captureSendMessages as safety net
- Generate deterministic messageId for inbox entries lacking one (sha256)
- Wrap notification instructions in agent block, italic task subject
- Style system comments in task view with blue background, hide avatar
- Update CLAUDE.md, research docs, and code comments with architecture
2026-03-23 12:57:16 +02:00

8.8 KiB
Raw Blame History

Research: Inbox-файлы Claude Code

Формат

Путь: ~/.claude/teams/{teamName}/inboxes/{memberName}.json

Структура: JSON-массив объектов (весь файл = массив)

[
  {
    "from": "contracts-cleaner",
    "text": "Готово. Вот что было удалено из packages/core/...",
    "timestamp": "2026-02-09T17:12:32.316Z",
    "color": "blue",
    "read": true,
    "summary": "Cleanup complete"
  },
  {
    "from": "team-lead",
    "text": "{\"type\":\"shutdown_request\",\"from\":\"team-lead\",\"timestamp\":\"...\"}",
    "timestamp": "2026-02-09T17:25:43.886Z",
    "read": true
  }
]

Поля

Поле Обязательно Тип Описание
from YES string Имя отправителя (зарегистрированный agent)
text YES string Текст сообщения или JSON-строка
timestamp YES string ISO 8601
read YES boolean Прочитано ли Claude Code
summary NO string Краткое описание для UI
color NO string Цвет агента (blue, green, red, yellow, purple, cyan, orange, pink)

text: plain vs structured

text может быть:

  • Plain text: обычное сообщение
  • JSON-строка: структурированное сообщение (парсится из text)

Типы структурированных:

idle_notification  — агент ушёл в idle (idleReason: "available")
shutdown_request   — запрос на завершение работы
shutdown_approved  — подтверждение завершения
message           — обычное DM (content, summary)
task_completed    — задача завершена (taskId)

read поведение

  • Claude Code ставит read: true после обработки
  • Сообщения НЕ УДАЛЯЮТСЯ — остаются навечно
  • Нет автоматической чистки inbox
  • 106 сообщений = ~256 KB

Race Condition (КРИТИЧНЫЙ РИСК)

Сценарий

T=0ms: App читает inbox [msg1, msg2]
T=1ms: Claude Code читает inbox [msg1, msg2]
T=5ms: App пишет [msg1, msg2, msg3_app]
T=6ms: Claude Code пишет [msg1, msg2, msg3_agent]
→ msg3_app ПОТЕРЯНО

Почему происходит

Inbox — JSON-массив. Append = read whole array → add element → write whole file. Два процесса читают одну версию, каждый добавляет своё, последний перезаписывает первого.

Вероятность

НИЗКАЯ — записи в inbox происходят нечасто:

  • Юзер шлёт 1-2 сообщения в минуту
  • Агенты шлют idle_notification раз в 5-30 секунд
  • Коллизия = оба пишут в ОДИН файл в пределах ~10ms

Митигация

  1. Atomic write (предотвращает partial writes):
const tmpPath = targetPath + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, JSON.stringify(messages, null, 2));
fs.renameSync(tmpPath, targetPath); // atomic на macOS/Linux
  1. Retry с проверкой (обнаруживает потерю):
// После записи — перечитать и проверить
const written = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
const found = written.some(m => m.messageId === ourMessageId);
if (!found) {
  // Наше сообщение потеряно — retry
  await appendToInbox(inboxPath, message);
}
  1. Уникальный messageId: добавлять messageId: uuid() в наши сообщения

  2. Debounce: не писать чаще раз в 500ms

Что НЕ решает

Atomic write предотвращает corrupted JSON, но НЕ предотвращает overwrite race. Retry с проверкой — best effort, но не 100%.


Доставка сообщений

Между turns, НЕ real-time

Цикл тиммейта:
1. Читает inbox (видит новые сообщения с read: false)
2. Обрабатывает
3. Вызывает инструменты (Bash, Edit, Read...)
4. Turn заканчивается → шлёт idle_notification → IDLE
5. Ждёт...
6. Новый turn → читает inbox

Задержка: 1-30 секунд (зависит от длительности turn)

Нельзя прервать mid-turn

Если агент уже вызвал Edit/Bash — инструмент будет выполнен. Сообщение "стоп, не трогай файл X" придёт ПОСЛЕ.

Idle → Active

Сообщение idle-агенту пробуждает его при следующем цикле проверки inbox.

Hard Interrupt (будущее)

Возможные подходы:

  • kill -SIGINT процесса (жёсткое)
  • Файловый flag .interrupt-{member}
  • Ждать API от Anthropic

from: "user" — подтверждено работает (2026-03-23)

Эмпирически подтверждено: from: "user" корректно доставляется тиммейтам. Тиммейт получает сообщение, определяет что оно от юзера, и отвечает в inboxes/user.json. Fallback на from: "team-lead" не нужен.

Ранее были опасения что Claude Code валидирует from по config.json members — это не так.


Размер и масштабирование

Метрика Значение
Размер сообщения ~2.4 KB
100 сообщений ~240 KB
1000 сообщений ~2.4 MB
Парсинг 1000 сообщений <10ms
Реальный inbox (106 msgs) 256 KB

Проблема начнётся при 10000+ сообщений — JSON.parse будет заметно медленнее. Для долгоживущих команд нужна архивация.


Финальное решение (после 3 раундов ревью)

Подход: Atomic write + messageId verify

Выбрана комбинация atomic write с постфактум-верификацией через messageId:

// 1. Генерируем уникальный ID для нашего сообщения
const messageId = crypto.randomUUID();
const message: InboxMessage = {
  from: 'user',
  text,
  timestamp: new Date().toISOString(),
  read: false,
  summary,
  messageId,
};

// 2. Читаем текущий inbox
const existing = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));

// 3. Добавляем сообщение
const updated = [...existing, message];

// 4. Atomic write (tmp + rename)
const tmpPath = inboxPath + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2));
fs.renameSync(tmpPath, inboxPath);

// 5. Verify: перечитываем и проверяем messageId
const written = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
const found = written.some((m: InboxMessage) => m.messageId === messageId);
if (!found) {
  // Потеря обнаружена — показать warning в UI, не silent fail
  throw new Error(`Message ${messageId} lost during write`);
}

Решения по итогам ревью

  • Полный CAS не нужен на MVP: verify при следующем read достаточен для обнаружения потерь
  • messageId проверяется сразу после записи (а не только при следующем read)
  • Не silent fail: если сообщение потеряно — UI показывает предупреждение пользователю
  • Retry не автоматический: потеря крайне редка, ручная отправка достаточна на MVP

Риски

Риск Вероятность Митигация
Race condition: агент пишет одновременно Низкая Atomic write + verify
Потеря при race Очень низкая messageId verify → warning в UI
Corrupted JSON Практически 0 Atomic write (tmp + rename)

Что не входит в MVP

  • Автоматический retry при потере (добавить в Phase 2 при необходимости)
  • Debounce записи (не нужен при редкой записи)
  • Полный CAS с блокировкой (избыточно для данной частоты записей)