[2026-06-21] taiga-vault: family/how-to/htpc-emulators-setup.md personal/agent/Phase 2 — Approval Reactions.md personal/creative/songs/Рыба.md personal/projects/balda/balda-valera.md personal/projects/balda/fast-rlm-integration.md personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md personal/projects/hermes/no-edit-segment-send-bug.md personal/projects/zulip-router-approval-reactions.md personal/projects/zulip-router.md
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
---
|
||||
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`.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
tags:
|
||||
- hermes
|
||||
- bug
|
||||
- gateway
|
||||
- stream_consumer
|
||||
- fixed
|
||||
---
|
||||
# No-edit (webhook) segment send bug
|
||||
|
||||
**Дата:** 20.06.2026
|
||||
**Файл:** `gateway/stream_consumer.py` (lines 574, 581, 591)
|
||||
**Статус:** ✅ Исправлено, закоммичено
|
||||
|
||||
## Симптом
|
||||
|
||||
При webhook-доставке (Zulip, Telegram через no-edit) текст, который модель генерирует между tool calls (перед tool call, после возврата результата), **не отправлялся промежуточно**. Весь текст накапливался в `_accumulated` и улетал единым куском только на `got_done`. Пользователь видел только tool calls и финальный ответ.
|
||||
|
||||
## Root Cause
|
||||
|
||||
В `stream_consumer.py` в `adapter_supports_edit=False` ветке:
|
||||
|
||||
```python
|
||||
# Строка 574 — ДО (баг):
|
||||
_has_nl = "\\\\n" in self._accumulated # ищет буквальный \n (2 символа: \ + n)
|
||||
|
||||
# Строка 581 — ДО (баг):
|
||||
_should_send = _has_nl and _can_send # segment_break не отправляет без \n
|
||||
|
||||
# Строка 591 — ДО (баг):
|
||||
_nl_pos = self._accumulated.rfind("\\\\n") # ищет 2 символа вместо 1
|
||||
```
|
||||
|
||||
`"\\\\n"` в Python-файле — это **4 слеша**: Python видит `\\n` (строка из 2 символов: `\` + `n`). А `_accumulated` содержит реальные символы newline (ASCII 10, один символ). Проверка всегда возвращала `False`, поэтому:
|
||||
- `_has_nl = False` → `_should_send = False` (даже с `got_segment_break=True`)
|
||||
- Текст копился до `got_done`
|
||||
|
||||
## Фикс
|
||||
|
||||
```python
|
||||
# Строка 574 — ПОСЛЕ:
|
||||
_has_nl = "\\n" in self._accumulated # Python escape для newline
|
||||
|
||||
# Строка 581 — ПОСЛЕ:
|
||||
_should_send = _can_send # segment break/got_done всегда отправляют
|
||||
|
||||
# Строка 591 — ПОСЛЕ:
|
||||
_nl_pos = self._accumulated.rfind("\\n") # Python escape для newline
|
||||
```
|
||||
|
||||
## Тесты
|
||||
|
||||
Новый файл: `tests/gateway/test_stream_consumer_no_edit.py`
|
||||
|
||||
Три теста:
|
||||
1. `test_no_edit_sends_text_across_segment_breaks` — текст с \n, 3 сегмента → 3 sends
|
||||
2. `test_no_edit_sends_text_without_newline_on_segment_break` — текст без \n, segment_break → отправляет
|
||||
3. `test_no_edit_multiple_segments_are_separate_messages` — 4 сегмента → 4 sends
|
||||
|
||||
Все 92 существующих теста stream_consumer проходят зелеными.
|
||||
|
||||
## Верификация
|
||||
|
||||
После фикса тест с 3x вызова терминала + промежуточные фразы — все сообщения дошли до пользователя.
|
||||
Reference in New Issue
Block a user