feat: finalize matrix platform audit and docs

This commit is contained in:
Mikhail Putilovskij 2026-04-21 15:35:03 +03:00
parent 6422c7db58
commit 4524a6abc8
30 changed files with 3093 additions and 176 deletions

View file

@ -0,0 +1,245 @@
# Баг-репорт: регрессия стриминга платформы после 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-соединения