Тихое усечение перевода при сбое разбора потока JSON (без выброса ошибки)

Резюме
DiscourseAi::Translation::PostLocalizer (локализация контента) иногда
выдает усеченные переводы без регистрации ошибок в любом из журналов (лог Rails,
лог Sidekiq, очереди мертвых/повторных задач Sidekiq). Усечение происходит посередине предложения,
тихо, и частичный результат сохраняется так, будто это полный, успешный
перевод.

Наблюдается на: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash через провайдера Google.

Данные для воспроизведения
Два реальных случая на нашем экземпляре:

  • Немецкий пост длиной ~2090 символов, переведенный на испанский, дал результат всего в 50 символов,
    обрезавшись непосредственно перед цитируемым словом («aufwendiger») в исходном
    тексте.
  • Немецкий пост длиной ~1830 символов, переведенный на английский (83 символа) и словацкий
    (97 символов) — оба были усечены в одной и той же точке предложения,
    непосредственно перед шорткодом эмодзи (:collision:) в исходном тексте.
    Все остальные локали (fr, it, pt, ru, he) для того же поста были переведены
    полностью (1600–1950 символов).

Причина (насколько я могу ее отследить)
В lib/completions/structured_output.rb, StructuredOutput#read_buffered_property
обращается к DiscourseAi::Utils::BestEffortJsonParser.extract_key, когда
@partial_json_tracker.broken? возвращает true. Этот метод возвращает все, что
было распарсено до того, как поток JSON прервался — без исключения, без предупреждения, без
любого указания на то, что вывод неполон. В
lib/translation/base_translator.rb#get_translation это значение используется как есть
в качестве финального перевода и сохраняется через PostLocalization#save!.

Обе точки усечения в наших данных приходятся непосредственно перед «специальным"
символом в исходном тексте (прямая кавычка, шорткод эмодзи),
что указывает на то, что трекер потокового JSON сбивается с определенных
символов/последовательностей в потоковом выводе модели и помечает поток
как сломанный преждевременно.

Влияние
Контент публикуется с явно неполными, иногда бессмысленными
переводами, и нет никаких операционных сигналов (лог, мертвая задача, уведомление админа)
для его обнаружения. Мы обнаружили это только потому, что участник сообщества
заметил мусор посередине предложения в интерфейсе.

Предлагаемые исправления

  • Выводить предупреждение/ошибку (Rails.logger + Discourse-error-log) всякий раз,
    когда StructuredOutput#broken? возвращает true и используется резервный метод best-effort, чтобы
    администраторы могли обнаруживать это, вместо того чтобы тихо отправлять частичный контент.
  • Рассмотреть возможность проверки длины перевода относительно разумного порога по отношению
    к длине исходного текста перед сохранением PostLocalization.

Готов предоставить полные пары исходных/переведенных текстов в частном порядке, если это полезно для
отладки трекера JSON.

1 лайк