236 lines
10 KiB
Markdown
236 lines
10 KiB
Markdown
# 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: "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()`:
|
||
|
||
```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`.
|