Files
obsidian-vault/personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md
T

196 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- hermes
- gateway
- stream-consumer
- bug
---
# Hermes Gateway Stream Consumer — потеря текста ответов
> **Ключевой вывод (2026-06-19): DeepSeek не возвращает interim commentary между tool calls**
> Причина отсутствия комментариев в Zulip между тулами — не в gateway, а в том что DeepSeek (с cache hit 95-100%) при каждом tool call возвращает только `tool_calls`, без `content`. `interim_assistant_callback` вызывается только когда есть текст между тулами. DeepSeek этого текста не выдаёт.
**UPDATED FINDINGS:** Ответы ДОХОДЯТ в Zulip. Проблема не в полной потере, а в:
1. **Дублирование текста**`_accumulated` не очищается после `_send_or_edit` с `got_done=True`, затем `got_done` блок (строки 607-619) снова отправляет то же самое
2. **Подавление финального send**`content_delivered=True` устанавливается после first-send, gateway подавляет нормальную отправку (но first-send уже отправил текст, так что ответ доходит)
3. **Partial на `\n` отправляет пустые чанки** — первый `\n` в начале текста триггерит send пустой строки
**Что не так:**
- В no-edit ветке, `got_done=True`:
1. `_should_send=True``_send_or_edit(accumulated)` — отправляет весь текст
2. Строка 596: `if _sent_len and not got_done`**False** (got_done=True) → **аккумулятор НЕ очищен**
3. Строка 607: `if self._accumulated:` → True (старый текст всё ещё там)
4. Строка 618-619: `_final_content_delivered = True`
5. Gateway видит content_delivered и подавляет fallback send — но текст уже отправлен
**Почему пользователь видит "только tool calls":**
- В webhook режиме user может видеть tool calls мгновенно, а текстовый ответ приходит **пачкой позже** — потому что весь текст накапливается до `got_done`
- Между partial sends и tool calls нет синхронизации — текст может прийти после следующего tool call
## Симптом
При ответе на webhook-сообщение (Zulip → zulip-router → webhook POST на whale gateway → обработка → ответ) часть или весь текстовый ответ агента не доставляется в чат. Пользователь видит только tool calls, approval запросы, и иногда первые несколько символов текста. Полноценный текстовый ответ пропадает.
## Детектированная проблема
### Механизм
Gateway настроен со streaming для webhook:
- `display.platforms.webhook.streaming: true` (из конфига)
- `adapter_supports_edit=False` для webhook (задано кодом)
Stream consumer (no-edit branch) должен накапливать текст и отправлять частями по `\n` с rate-limit, затем на `got_done` — отправить остаток целиком.
### Что происходит на самом деле
**Паттерн из лога (все ответы):**
1. `first-send: text_len=N text=[...полный текст ответа...]` — stream consumer получает весь текст
2. `Suppressing normal final send ... streamed=True previewed=False content_delivered=True` — gateway подавляет финальную отправку
3. В Zulip приходит только обрезанная версия или ничего
**Ключевая строка лога:**
```
2026-06-19 17:17:20,668 INFO gateway.run: Suppressing normal final send for session ...: streamed=True previewed=False content_delivered=True
```
**`content_delivered=True` устанавливается СЛИШКОМ РАНО** — до того как весь текст отправлен.
### Root Cause
В `stream_consumer.py` в no-edit branch:
1. Первый `\n` в начале ответа (пустая строка перед началом текста) триггерит `send` через `_send_or_edit`
2. Этот send отправляет `\n\n` (пустоту или пару символов) в webhook
3. После отправки через webhook, stream consumer помечает `content_delivered=True`
4. Когда приходит `got_done` — остальной огромный текст тоже отправляется, **НО**
5. Gateway в `run.py` видит `streamed=True AND content_delivered=True` и **ПОДАВЛЯЕТ** свою нормальную финальную отправку, думая что контент уже доставлен стримером
**В итоге:** первый пустой/короткий `\n` чанк — единственное что доставляется. Весь реальный контент теряется в подавлении.
### Почему раньше не было видно
Ранее гейтвей по умолчанию не имел streaming для webhook. Когда streaming включили (для фикса tool progress), стали видны эти проблемы:
- streaming активируется
- первый \n отправляет пустоту
- content_delivered=True
- got_done подавлен
- текст пропадает
## Подтверждение из лога
Все ответы после последнего рестарта (17:13:06, 17:16:55):
- `17:13:44` → first-send text_len=44 → `Suppressing ... content_delivered=True`**Ты не видел моего сообщения**
- `17:14:09` → first-send text_len=71 → `Suppressing ... content_delivered=True` → Видел обрывок
- `17:14:22` → first-send text_len=223 → `Suppressing ... content_delivered=True` → Видел
- `17:15:13` → first-send text_len=335 → `Suppressing ... content_delivered=True` → Видел
- `17:16:30` → first-send text_len=439 → `Suppressing ... content_delivered=True` → Видел
- `17:17:20` → first-send text_len=356 → `Suppressing ... content_delivered=True`**Обрезано**
- `17:18:16` → first-send text_len=785 → `Suppressing ... content_delivered=True`**Не дошло**
- `17:19:02` → first-send text_len=780 → `Suppressing ... content_delivered=True`**Не дошло**
- `17:21:10` → first-send text_len=1314 → `Suppressing ... content_delivered=True`**Не дошло**
Из лога: все большие ответы (>300 символов) подавляются. Маленькие (<200 символов) проходят.
## Сравнение Zulip DB с логом
Последние сообщения Кит в треде (из Zulip DB):
- msg 70116: "✅ Command approved" — это approval ответ, маленький
- msg 70115-70107: tool calls (терминал, read_file)
- msg 70106: read_file
**Ни одного текстового ответа больше 100 символов!** Все они были подавлены гейтвеем.
## Фикс
Два варианта:
### Вариант A: Не подавлять финальную отправку для webhook (no-edit адаптер)
В `run.py`, где проверяется `content_delivered`:
```python
# Не подавлять, если адаптер не поддерживает редактирование
# (webhook не может отправлять partials — только один финальный ответ)
if streamed and content_delivered and adapter_supports_edit:
suppress = True
```
Либо: не устанавливать `content_delivered=True` для no-edit адаптера вообще — пусть всегда проходит через нормальный send.
### Вариант B: Не слать первый \n чанк если текст пустой/минимальный
```python
# Не отправлять partial если это просто \n без реального контента
if _has_nl and len(self._accumulated.strip()) < 3:
# Пропустить — это просто форматирование, не настоящий контент
continue
```
Но вариант A надёжнее — он не полагается на эвристику.
## DeepSeek commentary — руководство по диагностике (2026-06-19)
### Когда текст агента не доходит между tool calls
**ПЕРВОЕ что проверять:** какой провайдер используется.
DeepSeek (особенно с cache hit 95-100%) не генерирует `.content` между tool calls — он возвращает только `tool_calls`. Gateway вызывает `interim_assistant_callback` ТОЛЬКО когда в ответе есть `.content`. Если content пустой — commentary не шлётся.
**Это особенность модели, не баг gateway.**
### Где смотреть в логе
Лог: `~/.hermes/hermes-whale/logs/gateway.log`
Искать размер ответа у каждого API call:
```bash
grep -n 'Received' ~/.hermes/hermes-whale/logs/gateway.log | tail -20
```
Пример — commentary отправлен (out > 0):
```
Received 31 additional tokens (total 31, out=31) in session ...
```
Пример — только tool call (out < 200, cache 100%):
```
Received 153 tokens (total 3787, out=153, 100% cache) ...
Received 4 tokens (total 7811, out=4, 100% cache) ...
```
Эвристика:
- `out > 300` — commentary + tool calls (есть текст)
- `out < 200` + `100% cache` — чистый tool call, commentary не было
### Как проверить в Zulip DB
```sql
SELECT id, content, sender_id FROM zulip_message
WHERE subject = 'Название треда'
ORDER BY id DESC LIMIT 20;
```
Сопоставить: сколько API calls → сколько сообщений в Zulip.
### Где смотреть в коде
| Файл | Что искать |
|------|-----------|
| `gateway/run.py:~16960` | `interim_assistant_callback` — вызывается только если `.content is not None` |
| `gateway/run.py:~16997` | `source.platform != Platform.WEBHOOK` — был гард для webhook (исправлен) |
| `gateway/stream_consumer.py` | `_send_or_edit` и `_has_nl` — отправка commentary (исправлено подавление) |
| `gateway/adapters/webhook.py` | `_delivery_info` инстанс-переменная — терялась при рестарте (исправлена) |
### Сводка исправлений (2026-06-19)
| Файл | Исправление |
|------|------------|
| `gateway/run.py:16997` | Убран `source.platform != Platform.WEBHOOK` — interim messages разрешены для webhook |
| `gateway/stream_consumer.py` | Commentary отправляется независимо от `\n` в no-edit ветке |
| `gateway/adapters/webhook.py` | `delivery_info` восстанавливается из route config при рестарте |
**Важно:** все эти фиксы не влияют на DeepSeek commentary — DeepSeek не шлёт `.content` между tool calls независимо от gateway.
Если проблема появится с другим провайдером (Claude, GPT) — надо смотреть `interim_assistant_callback` в `run.py`.