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