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

14 KiB
Raw Permalink Blame History

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

Характерный фрагмент:

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 нестабилен.

Приложение: характерный фрагмент логов

[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 уже приходит в клиента обрезанным

Приложение: характерный фрагмент логов для больших изображений

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-соединения