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

294 lines
16 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.

# Финальный баг-репорт: потеря начала ответа и сбои 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.