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