# Research: Inbox-файлы Claude Code

## Формат

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

**Структура**: 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):
```typescript
const tmpPath = targetPath + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, JSON.stringify(messages, null, 2));
fs.renameSync(tmpPath, targetPath); // atomic на macOS/Linux
```

2. **Retry с проверкой** (обнаруживает потерю):
```typescript
// После записи — перечитать и проверить
const written = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
const found = written.some(m => m.messageId === ourMessageId);
if (!found) {
  // Наше сообщение потеряно — retry
  await appendToInbox(inboxPath, message);
}
```

3. **Уникальный messageId**: добавлять `messageId: uuid()` в наши сообщения

4. **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" — валидация

### Факты

- В реальных inbox-файлах видны ТОЛЬКО зарегистрированные agent names
- Нет примеров `from: "user"` в 256+ KB данных
- Неизвестно, валидирует ли Claude Code поле `from` по config.json members

### Решение

Пробуем `from: "user"`. Если агент не получает сообщение → fallback на `from: "team-lead"` (всегда есть в config.json).

### Тест

При первой реализации:
1. Запустить команду с 1 тиммейтом
2. Записать сообщение с `from: "user"` в inbox тиммейта
3. Проверить — получит ли тиммейт сообщение
4. Если нет — повторить с `from: "team-lead"`

---

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

| Метрика | Значение |
|---------|----------|
| Размер сообщения | ~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`:

```typescript
// 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 с блокировкой (избыточно для данной частоты записей)
