# 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](./research-inbox.md))
- Формат недокументирован (internal API)
- Доставка между turns, не real-time

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

```json
{
  "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 (ОТВЕРГНУТ)

### Как работает

```typescript
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 (ОТВЕРГНУТ)

### Как работает

```bash
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` могут не содержать `messageId` — `TeamInboxReader` генерирует детерминированный ID из sha256(from + timestamp + text).

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

`from: "user"` работает корректно (подтверждено эмпирически 2026-03-23):
- Тиммейт получает сообщение
- Тиммейт корректно определяет что это от юзера
- Тиммейт отвечает в `inboxes/user.json`
- Fallback на `from: "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()`:

```json
{
  "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`.
