agent-ecosystem/docs/team-management/research-messaging.md

10 KiB
Raw Blame History

Research: Подходы к отправке сообщений тиммейтам

Сравнение 3 подходов

Критерий Inbox-файлы Agent SDK CLI subprocess
Скорость ~5ms ~12с 10-15с
Стоимость $0 $0.01-0.08/msg токены
Работает с запущенными YES NO NO
Прерывает mid-turn NO NO NO
Требует API ключ NO YES NO
Расход памяти 0 0 100-320MB

1. Inbox-файлы (ВЫБРАНО)

Как работает

Прямая запись JSON в файл ~/.claude/teams/{team}/inboxes/{member}.json. Claude Code мониторит эти файлы через fs.watch и доставляет сообщения агентам между turns.

Плюсы

  • Мгновенная запись (~5ms)
  • $0 — никаких API вызовов
  • Единственный способ общаться с запущенными тиммейтами
  • Работает с idle и active агентами (но доставка между turns)

Минусы

  • Race condition при одновременной записи (см. research-inbox.md)
  • Формат недокументирован (internal API)
  • Доставка между turns, не real-time

Формат сообщения

{
  "from": "user",
  "text": "Не трогай файл auth.ts, я его сам изменю",
  "timestamp": "2026-02-17T15:30:00.000Z",
  "read": false,
  "summary": "Do not modify auth.ts",
  "messageId": "uuid-for-retry-check"
}

2. Agent SDK (ОТВЕРГНУТ)

Как работает

import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
  model: 'claude-opus-4-7',
  messages: [{ role: 'user', content: 'Send message to teammate...' }],
  tools: [/* SendMessage, TaskUpdate, etc. */]
});

Почему отвергнут

  1. Создаёт НОВУЮ сессию — не подключается к работающему тиммейту. SendMessage и TaskCreate — это инструменты модели, не программные вызовы
  2. ~12 секунд на каждый вызов (полный API round-trip)
  3. Стоит токены — $0.01-0.08 за сообщение
  4. Нужен API ключ — отдельная оплата, а не подписка Claude

Когда может пригодиться

  • Создание новых команд программно
  • Автоматизация workflow (вне real-time UI)

3. CLI subprocess (ОТВЕРГНУТ)

Как работает

claude --message "Send message to teammate-1: stop working on X"

Почему отвергнут

  1. Новый процесс — не инжектится в работающего тиммейта
  2. 10-15 секунд холодный старт
  3. 100-320MB памяти на процесс
  4. Каждый вызов стоит токены

Архитектура доставки (обновлено 2026-03-23)

Два разных механизма: лид vs тиммейты

Лид читает ТОЛЬКО stdin (stream-json). Для доставки сообщений лиду используется relayLeadInboxMessages() — конвертирует inbox-записи в stream-json на stdin. Без relay лид не видит inbox.

Тиммейты — полноценные независимые Claude Code процессы. Каждый мониторит свой inbox файл через fs.watch и читает сообщения напрямую. Relay через лида НЕ нужен.

Поток сообщений: Юзер → Тиммейт

User → [UI] → TeamInboxWriter → inboxes/{member}.json (read: false)
                                        ↓
                              Teammate CLI (fs.watch) → читает → обрабатывает
                                        ↓
                              Teammate → inboxes/user.json (ответ)
                                        ↓
                              [UI] ← TeamInboxReader ← читает user.json

Лид в этой цепочке НЕ участвует. Сообщение доставляется напрямую.

Поток сообщений: Юзер → Лид

User → [UI] → stdin (stream-json) → Lead CLI
                                        ↓
Lead → sentMessages.json / liveLeadProcessMessages
                                        ↓
                              [UI] ← читает и отображает

Для лида дополнительно работает relayLeadInboxMessages() при изменении inboxes/{lead}.json.

Ответы тиммейтов

Тиммейт отвечает юзеру через SendMessage(to="user"), что записывается в inboxes/user.json. UI читает этот файл через TeamInboxReader.getMessages() (читает ВСЕ inbox файлы в директории).

Сообщения в user.json могут не содержать messageIdTeamInboxReader генерирует детерминированный ID из sha256(from + timestamp + text).

from: "user" — подтверждено работает

from: "user" работает корректно (подтверждено эмпирически 2026-03-23):

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

Почему relay через лида был ОТКЛЮЧЁН (2026-03-23)

Ранее при отправке DM тиммейту, помимо записи в inbox, вызывался relayMemberInboxMessages() — инструкция лиду переслать сообщение через SendMessage(to=member). Это вызывало 3 бага:

  1. Лид отвечал вместо тиммейта — LLM интерпретировал relay-инструкцию как обращение к себе и отвечал юзеру напрямую
  2. Дубликаты сообщенийmarkInboxMessagesRead() записывал в файл → FileWatcher срабатывал → relay запускался повторно → цикл
  3. Тиммейт не отвечал юзеру — relay-промпт содержал "Do NOT send to user", что тиммейт тоже видел через лида

Relay отключён в teams.ts (handleSendMessage) и index.ts (FileWatcher). Код закомментирован, не удалён. Relay для лида (relayLeadInboxMessages) не затронут.


Доставка: Timing и ограничения

Цикл тиммейта

Turn N:
  1. Читает inbox → видит новые (read: false)
  2. Обрабатывает сообщения/задачи
  3. Вызывает инструменты
  4. Reasoning
  5. Output
  → idle_notification → IDLE

... ожидание ...

Turn N+1:
  1. Пробуждение (новое сообщение в inbox / назначение задачи)
  2. Читает inbox → видит новые
  ...

Задержка

  • Idle agent: получит при следующем пробуждении (доли секунды если inbox-change triggers)
  • Active agent (mid-turn): получит только после завершения текущего turn (1-30 секунд)

Нельзя прервать

Если агент уже вызвал Edit/Bash — инструмент выполнится. Наше сообщение придёт ПОСЛЕ.

Пример:

17:12:30 — Agent начинает Edit на auth.ts
17:12:31 — Мы шлём "Не трогай auth.ts"
17:12:32 — Agent завершает Edit (auth.ts изменён)
17:12:33 — Agent читает inbox, видит наше сообщение
→ Поздно, файл уже изменён

Hard Interrupt (будущее)

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

  1. kill -SIGINT процесса тиммейта (жёсткое прерывание, потеря контекста)
  2. Файловый flag .interrupt-{member} (нужна поддержка в Claude Code)
  3. API от Anthropic (если появится)

Текущее решение: задержка приемлема, hard interrupt — в будущем.


Финальное решение

messageId — обязателен в каждом исходящем сообщении

Каждое исходящее сообщение включает messageId: crypto.randomUUID():

{
  "from": "user",
  "text": "Please review task #12",
  "timestamp": "2026-02-17T15:30:00.000Z",
  "read": false,
  "summary": "Review request for task #12",
  "messageId": "550e8400-e29b-41d4-a716-446655440000"
}

Verify: проверка сразу после записи

  • После atomic write читаем inbox и ищем наш messageId
  • Если не найден — потеря обнаружена → warning в UI (не silent fail)
  • Не автоматический retry на MVP

3 состояния offline-участника

Состояние Условие Отображение
ACTIVE idle < 5 минут Зелёный dot
IDLE idle > 5 минут Жёлтый dot
TERMINATED Получен shutdown_response с approve: true Серый dot, "Завершён"

Определение состояния по timestamp последнего события в inbox (idle_notification, любое сообщение). TERMINATED — исключительно по явному shutdown_response.