# Финальный баг-репорт: потеря начала ответа и сбои streaming/image path в `platform-agent` ## Статус Это финальный отчёт после полного аудита интеграции `surfaces -> platform-agent_api -> platform-agent`. Итог: - текущая реализация `surfaces` рабочая, но проблемная из-за upstream-дефектов платформы - баг с пропадающим началом ответа на текущем состоянии **не локализуется в `surfaces`** - в воспроизведённом сценарии повреждённый первый текстовый chunk рождается уже внутри `platform-agent` - помимо этого подтверждены ещё два независимых platform-side дефекта: - duplicate `END` - некорректная обработка больших изображений (`data-uri > 10 MB`, `WS 1009`) ## Версии и состояние кода Рантайм воспроизводился на vendored upstream-репозиториях без локальных platform patch’ей: - `platform-agent`: `5e7c2df954cc3cd2f5bf8ae688e10a20038dde61` - `platform-agent_api`: `aa480bbec5bbf8e006284dd03aed1c2754e9bbee` Со стороны `surfaces` transport layer был предварительно очищен: - убрана локальная stream-semantics из `sdk/agent_api_wrapper.py` - `sdk/real.py` переведён на pinned upstream `platform-agent_api.AgentApi` - больше нет локального post-END drain, custom listener и wrapper-owned reclassification events Это важно: баг воспроизводился **после** удаления наших транспортных костылей. ## Контекст интеграции - поверхность: Matrix - транспорт к платформе: WebSocket через upstream `platform-agent_api.AgentApi` - `chat_id` на платформу: стабильный числовой surrogate id, выдаваемый со стороны `surfaces` - файловый контракт: shared `/workspace`, вложения передаются как относительные пути в `attachments` ## Пользовательские симптомы Наблюдались несколько классов сбоев: 1. Начало ответа может пропасть - ожидалось: `Моя ошибка: ...` - фактически: `оя ошибка: ...` - ожидалось: `На двух изображениях: ...` - фактически: ` двух изображениях: ...` 2. После tool/file flow ответы могут вести себя нестабильно - следующий ответ стартует с середины фразы - в некоторых сценариях после image/tool path платформа отвечает ошибкой или замолкает 3. На больших изображениях image path падает совсем - provider error `Exceeded limit on max bytes per data-uri item : 10485760` - websocket закрывается с `1009 (message too big)` ## Что было проверено на стороне `surfaces` Ниже перечислено, что именно было перепроверено в нашем коде и почему это не выглядит корнем проблемы. ### 1. Мы больше не режем и не переклассифицируем stream локально В текущем `surfaces`: - `sdk/agent_api_wrapper.py` — thin construction/factory shim над upstream `AgentApi` - `sdk/real.py` — просто итерирует upstream events и склеивает `MsgEventTextChunk.text` - `adapter/matrix/bot.py` — отправляет `OutgoingMessage.text` в Matrix без `strip/lstrip` Наблюдение: - в текущем коде не осталось места, где строка могла бы превратиться из `Моя ошибка` в `оя ошибка` через локальный slicing ### 2. Сборка ответа у нас линейная и тупая `sdk/real.py` делает только следующее: - если пришёл `MsgEventTextChunk` — добавляет `event.text` в `response_parts` - если пришёл `MsgEventSendFile` — превращает его в `Attachment` - не пытается “восстанавливать” поток после `END` Следствие: - если начало уже отсутствует в `event.text`, мы его не можем ни потерять, ни вернуть ### 3. Matrix sender не модифицирует текст `adapter/matrix/bot.py` передаёт текст дальше как есть. Следствие: - Matrix renderer не является объяснением пропажи первого куска ## Что было проверено в `platform-agent_api` Upstream client всё ещё имеет спорную queue-архитектуру: - одна активная `_current_queue` - `MsgEventEnd` съедается внутри `send_message()` - в `finally` очередь отвязывается и дренится orphan messages Это архитектурно хрупко и может быть источником других boundary bugs. Но в конкретном воспроизведении этот слой не был точкой порчи текста. Почему: - в raw logs клиент получил **ровно тот же** первый text chunk, который сервер уже отправил - queue/dequeue не изменили его содержимое ## Что удалось доказать по raw logs Для финальной проверки была временно добавлена точечная диагностика в: - `external/platform-agent/src/agent/service.py` - `external/platform-agent/src/api/external.py` - `external/platform-agent_api/lambda_agent_api/agent_api.py` Эта диагностика **не входила** в рабочую реализацию и использовалась только для локализации бага. ### Ключевое наблюдение На проблемном запросе после tool/file flow сервер сам yield’ил уже обрезанный первый chunk: ```text platform-agent-1 | [raw-stream][server-yield] chat=1 event=TEXT text=' двух изображениях:\n\n**Первое изображение' platform-agent-1 | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение' path=None matrix-bot-1 | [raw-stream][client-listen] agent=matrix-bot chat=1 queue_active=True AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение' matrix-bot-1 | [raw-stream][client-dequeue] agent=matrix-bot chat=1 request=2 AGENT_EVENT_TEXT_CHUNK text=' двух изображениях:\n\n**Первое изображение' ``` Это означает: - порча произошла **до** websocket-клиента - `surfaces` transport layer не является источником именно этого дефекта - `platform-agent_api` не исказил этот конкретный chunk по дороге Дополнительно тот же паттерн виден и вне image-сценария: ```text platform-agent-1 | [raw-stream][server-yield] chat=1 event=TEXT text='сё работает напрямую' ... matrix-bot-1 | [raw-stream][client-dequeue] ... text='сё работает напрямую' ``` То есть сервер уже выдаёт `сё`, а не `Всё`. ## Наиболее вероятный root cause Главный подозреваемый — `external/platform-agent/src/agent/service.py`. Сейчас он делает следующее: - читает `self._agent.astream_events(...)` - обрабатывает только `kind == "on_chat_model_stream"` - берёт `chunk = event["data"]["chunk"]` - если `chunk.content`, форвардит `MsgEventTextChunk(text=chunk.content)` Проблема в том, что это очень грубое преобразование raw event stream в пользовательский текст. ### Почему именно это место выглядит корнем 1. Первый битый chunk уже рождается на server-side - это подтверждено логами выше 2. Код берёт только `chunk.content` - если начало ответа приходит в другой форме, поле или raw event, оно просто теряется 3. Код не учитывает `ns` / `source` - после tool/vision flow у Deep Agents / LangChain может быть более сложная структура потока - текущий adapter flatten’ит её слишком агрессивно 4. Код никак не валидирует, что наружу уходит именно main assistant output Итоговая гипотеза: > После tool/file/vision flow `platform-agent` неправильно адаптирует `astream_events()` в `MsgEventTextChunk`. Начало итогового пользовательского ответа может находиться не в том raw event, который сейчас читается через `chunk.content`, либо теряться из-за упрощённой фильтрации потока. ## Подтверждённый отдельный баг: duplicate `END` Это отдельный platform-side дефект. Сейчас: - `external/platform-agent/src/agent/service.py` уже делает `yield MsgEventEnd(...)` - `external/platform-agent/src/api/external.py` после завершения цикла дополнительно отправляет ещё один `MsgEventEnd(...)` По логам это выглядит так: ```text platform-agent-1 | [raw-stream][server-yield] chat=1 event=END platform-agent-1 | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_END text=None path=None platform-agent-1 | [raw-stream][ws-send] chat=1 event=AGENT_EVENT_END duplicate_end=true matrix-bot-1 | ... AGENT_EVENT_END tokens_used=0 matrix-bot-1 | ... AGENT_EVENT_END tokens_used=0 ``` Независимая оценка: - duplicate `END` — реальный баг платформы - он делает границу ответа менее надёжной - но в текущем воспроизведении он **не объясняет** сам факт потери первого text chunk То есть это важный, но вторичный дефект. ## Подтверждённый отдельный баг: большие изображения ломают image path В отдельном воспроизведении платформа падала на анализе изображений с provider error: ```text Exceeded limit on max bytes per data-uri item : 10485760 ``` И параллельно websocket рвался с: ```text received 1009 (message too big); then sent 1009 (message too big) ``` Это означает: - image path отправляет в provider oversized `data:` URI - безопасной предвалидации / деградации нет - failure scenario сопровождается разрывом websocket-соединения Независимая оценка: - это отдельный platform-side bug - он не объясняет потерю первого чанка в текстовом сценарии напрямую - но подтверждает, что path `tool/file/image -> platform stream` в целом сейчас нестабилен ## Что мы считаем исключённым С достаточной уверенностью можно исключить: 1. Локальный slicing текста в `surfaces` 2. Локальную “умную” реконструкцию потока, потому что она была удалена 3. Matrix sender как источник потери первого чанка 4. `platform-agent_api` queue/dequeue как primary root cause именно в этом воспроизведении ## Финальная независимая оценка Текущая оценка вероятностей: - `75%` — ошибка в `platform-agent`, в адаптере `astream_events() -> MsgEventTextChunk` - `15%` — provider/model stream приносит начало ответа в другой raw event/field, а `platform-agent` его некорректно интерпретирует - `10%` — вторичные platform-side boundary defects (`duplicate END`, queue semantics и т.д.) - `~0-5%` — ошибка в `surfaces` Итоговый вывод: > На текущем состоянии кода баг с пропадающим началом ответа следует считать platform-side дефектом. Воспроизведение после cleanup transport layer показывает, что первый повреждённый text chunk формируется уже внутри `platform-agent` до отправки в websocket. ## Что нужно исправить в платформе ### Обязательно 1. Убрать duplicate `END` - один ответ должен завершаться ровно одним `MsgEventEnd` 2. Перепроверить адаптацию `astream_events()` в `service.py` - логировать и проанализировать raw `event["event"]` - проверить `event.get("name")` - смотреть `event.get("ns")` - сравнить `chunk.content` с тем, что реально лежит в `chunk.text` / raw chunk repr 3. Форвардить наружу только финальный main assistant output - не flatten’ить весь поток без учёта `ns/source` ### Желательно 4. Сделать image path устойчивым к oversized payload - preflight check размера - resize/compress или controlled error без разрыва WS 5. Улучшить client/server protocol boundary - более строгая корреляция запроса и ответа - более однозначная semantics конца ответа ## Что мы сделали со своей стороны Со стороны `surfaces` уже выполнено следующее: - transport layer очищен до thin adapter над upstream `AgentApi` - локальные stream-workaround’ы удалены - рабочая интеграция сохранена - known issue задокументирован То есть текущая интеграция не “идеальна”, но её поведение теперь достаточно чистое, чтобы platform bug было можно локализовать без смешения ответственности. ## Приложение: короткий диагноз Если нужна самая короткая формулировка для issue tracker: > После cleanup transport layer в `surfaces` и воспроизведения на clean upstream platform repos видно, что `platform-agent` иногда сам порождает первый `MsgEventTextChunk` уже обрезанным, особенно после tool/file flow. Дополнительно подтверждены duplicate `END` и отдельный image-path failure на больших `data:` URI.