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