surfaces/docs/reports/2026-04-21-platform-streaming-bug-report-ru.md

245 lines
14 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.

# Баг-репорт: регрессия стриминга платформы после file/tool flow
## Кратко
После обновления до текущих upstream-версий платформы стриминг ответов стал нестабильным в сценариях с вложениями и tool/file flow.
Наблюдаемые симптомы:
- первый текстовый chunk ответа может приходить уже обрезанным
- соседние ответы могут "протекать" друг в друга
- после некоторых запросов бот перестаёт присылать финальный ответ
- платформа присылает дублирующий `END`
До обновления платформы этот класс ошибок у нас не воспроизводился.
## Версии платформы
В рантайме используются upstream-репозитории без локальных правок:
- `platform-agent`: `5e7c2df954cc3cd2f5bf8ae688e10a20038dde61`
- `platform-agent_api`: `aa480bbec5bbf8e006284dd03aed1c2754e9bbee`
## Контекст интеграции
- поверхность: Matrix
- транспорт к платформе: websocket через `platform-agent_api`
- `chat_id` на платформу отправляется как стабильный числовой surrogate id
- shared workspace: `/workspace`
Важно: vendored platform repos чистые, совпадают с upstream. Проблема воспроизводится без локальных patch'ей в платформу.
## Пользовательские симптомы
Примеры из живого диалога:
- ожидалось: `Моя ошибка: ...`
- фактически пришло: `оя ошибка: ...`
- ожидалось начало ответа вида `По фото IMG_3183.png ...`
- фактически пришло: `IMG_3183.png**) — это ...`
Также наблюдалось:
- после вопросов по изображениям бот иногда вообще перестаёт отвечать
- в том же чате, до attachment/tool flow, ответы приходят корректно
## Шаги воспроизведения
1. Поднять `platform-agent` и Matrix surface на версиях выше.
2. Отправить несколько обычных текстовых сообщений.
3. Убедиться, что начальные ответы стримятся корректно.
4. Отправить изображения/файлы и задать вопросы вида:
- `что изображено на фото`
- уточняющие follow-up вопросы по тем же вложениям
5. Затем отправить ещё одно обычное текстовое сообщение.
6. Наблюдать один или несколько симптомов:
- первый chunk начинается с середины слова
- ответ начинается с середины фразы
- хвост прошлого ответа загрязняет следующий
- видимого финального ответа нет вообще
## Что удалось доказать
По debug-логам Matrix surface видно, что обрезание присутствует уже в первом chunk, полученном от платформы.
Корректные первые chunk'и до attachment/tool flow:
- `Hey! How`
- `Я`
- `Первый файл не найден — возможно, ...`
Некорректные первые chunk'и после attachment/tool flow:
- `IMG_3183.png**) — это ю...`
- `оя ошибка: в первом запросе...`
Это логируется сразу после десериализации websocket event на клиентской стороне, до Matrix rendering и до финальной сборки текста ответа. Из этого следует, что повреждение происходит на стороне платформенного стриминга, а не в Matrix sender.
## Дополнительное наблюдение по протоколу
Платформа сейчас отправляет дублирующий `END`.
Релевантные места в upstream:
- `external/platform-agent/src/agent/service.py`
- уже `yield MsgEventEnd(...)`
- `external/platform-agent/src/api/external.py`
- после завершения цикла дополнительно отправляет ещё один `MsgEventEnd(...)`
В живых логах это видно как:
- первый `END`
- второй `END`
- клиентская suppression логика, которая гасит дубликат
Это само по себе делает границы ответа неоднозначными и повышает риск "протекания" поздних событий в следующий запрос.
## Предполагаемая первопричина
Похоже, что на стороне платформы одновременно есть две проблемы.
### 1. Двойной сигнал завершения стрима
Для одного ответа генерируется два `END`.
Вероятные последствия:
- нечёткая граница ответа
- поздние события могут относиться не к тому запросу
- соседние ответы могут смешиваться
### 2. Некорректное извлечение текстового chunk'а
В текущем upstream `platform-agent/src/agent/service.py` текст форвардится из `chunk.content` внутри `on_chat_model_stream`.
Однако в документации LangChain для `astream_events()` примеры используют `event["data"]["chunk"].text`, а в Deep Agents stream дополнительно есть `ns`/`source`, которые важны для отделения main-agent output от tool/subagent/model-internal stream.
Потенциальные последствия:
- первый видимый chunk может быть неполным
- во внешний клиент может попадать не только финальный пользовательский текст
- attachment/tool flow сильнее деградирует поведение стрима
## Почему проблема считается платформенной
С нашей стороны были проверены и исключены базовые причины:
- вложения корректно сохраняются в `/workspace`
- контейнер `platform-agent` видит эти файлы
- Matrix surface получает уже обрезанный первый chunk от платформы
- обрезание происходит до сборки финального ответа
- эксперимент с reconnect на каждый запрос не исправил проблему
- платформенные vendored repos сейчас совпадают с upstream
## Ожидаемое поведение
Для каждого пользовательского запроса:
- текстовые chunk'и должны начинаться с реального начала ответа модели
- должен приходить ровно один terminal `END`
- границы ответов должны быть однозначными
- file/tool flow не должен ломать следующий ответ
## Фактическое поведение
После attachment/tool flow:
- первый text chunk может быть уже обрезан
- `END` приходит дважды
- следующий ответ может начаться с середины слова или фразы
- отдельные запросы могут не завершаться видимым ответом
## Дополнительный failure mode: большие изображения
В отдельном воспроизведении текстовые запросы до файлового сценария работали корректно, а сбой возникал только после попытки анализа изображений.
По логам видно уже не только stream corruption, но и конкретный image-path failure:
- `platform-agent` рвёт websocket с `1009 (message too big)`
- провайдер возвращает `400` с причиной:
- `Exceeded limit on max bytes per data-uri item : 10485760`
Характерный фрагмент:
```text
websockets.exceptions.ConnectionClosedError: received 1009 (message too big); then sent 1009 (message too big)
...
Agent error (INTERNAL_ERROR): Error code: 400 - {
'error': {
'message': 'Provider returned error',
'metadata': {
'raw': '{"error":{"message":"Exceeded limit on max bytes per data-uri item : 10485760"...}}'
}
}
}
```
Из этого следует:
- текстовый path сам по себе работоспособен
- image-analysis path в платформе сейчас передаёт изображение как data URI
- для достаточно больших изображений это утыкается в provider limit `10,485,760` байт на один data-uri item
- параллельно websocket path между surface и platform-agent неустойчив к этому failure scenario и закрывается с `1009`
То есть в платформе есть как минимум ещё одна отдельная проблема помимо некорректного стриминга:
- отсутствует безопасная обработка больших изображений до отправки в provider
- отсутствует аккуратная деградация без разрыва websocket-сессии
## Что стоит исправить в платформе
1. Отправлять ровно один `MsgEventEnd` на один ответ.
2. Перепроверить extraction текста из `on_chat_model_stream`:
- вероятно, должен использоваться `chunk.text`, а не `chunk.content`.
3. Учитывать `ns`/`source` и форвардить наружу только main assistant output.
4. Перед отправкой изображений в provider проверять размер payload и не пытаться слать oversized data-uri.
5. Для больших изображений:
- либо делать resize/compression,
- либо возвращать контролируемую user-facing ошибку без разрыва websocket.
6. В идеале добавить более жёсткую request boundary в websocket protocol, чтобы late events не могли прилипать к следующему запросу.
## Наши временные mitigation'ы на стороне surface
Они не исправляют корень, только снижают ущерб:
- suppression duplicate `END`
- короткий post-`END` drain window
- idle timeout для зависшего стрима
- transport failures нормализуются в `PlatformError`, чтобы Matrix bot не падал процессом
Эти меры понадобились только потому, что в текущем сценарии platform stream contract нестабилен.
## Приложение: характерный фрагмент логов
```text
[matrix-bot] text chunk queue=True text='Первый файл не найден — возможно,'
[matrix-bot] ...
[matrix-bot] end event queue=True tokens=0
[matrix-bot] end event queue=True tokens=0
[matrix-bot] dropped duplicate END tokens=0
[matrix-bot] text chunk queue=True text='IMG_3183.png**) — это ю'
[matrix-bot] ...
[matrix-bot] end event queue=True tokens=0
[matrix-bot] end event queue=True tokens=0
[matrix-bot] dropped duplicate END tokens=0
[matrix-bot] text chunk queue=True text='оя ошибка: в первом запросе я случайно постав'
```
Этот фрагмент показывает две вещи:
- duplicate `END` действительно приходит от платформы
- следующий первый chunk уже приходит в клиента обрезанным
## Приложение: характерный фрагмент логов для больших изображений
```text
platform-agent-1 | websockets.exceptions.ConnectionClosedError: received 1009 (message too big); then sent 1009 (message too big)
...
matrix-bot-1 | Agent error (INTERNAL_ERROR): Error code: 400 - {'error': {'message': 'Provider returned error', 'code': 400, 'metadata': {'raw': '{"error":{"message":"Exceeded limit on max bytes per data-uri item : 10485760","type":"invalid_request_error"...}}}}
```
Этот фрагмент показывает ещё две вещи:
- image path в платформе реально упирается в лимит провайдера на размер data URI
- при этом ошибка не изолируется корректно и сопровождается разрывом websocket-соединения