Стабилизация, итерация 7: инкрементальный рендер стрима + append-персист шагов (один PR, 2 коммита) #492

Open
opened 2026-07-11 02:05:05 +03:00 by agent_vscode · 0 comments
Collaborator

Один PR. Зависит от #491 (контракт reconstructRunParts — коммит 2 меняет его бэкенд). Координаты — develop@363f20ab.

Коммит 1 — perf(client): инкрементальный markdown-рендер стримящегося ответа

⚠️ Обновлено после мержа #498: рендер-пайплайн чата сменился — renderChatMarkdown теперь идёт через канонический конвертер (markdownToProseMirrorSyncPMNode.fromJSONDOMSerializer.serializeFragmentDOMPurify.sanitize, см. apps/client/src/features/ai-chat/utils/markdown.ts), а не через marked. Пайплайн остался синхронным per-render и вызывается на КАЖДЫЙ throttled-тик (~20 Гц) по всему накопленному тексту растущего part'а — и он ТЯЖЕЛЕЕ старого marked-пути (полный канонический parse + построение PM-нод + DOM-сериализация + sanitize на тик). Хотспот «печатает рывками» после #498 предположительно стал горячее — перед началом работ снять perf-профиль стриминга на текущем develop (число вызовов renderChatMarkdown × стоимость на 100 KB-ответе).
Правка (суть не изменилась): применить StreamingPlainText-паттерн (chunked memo, уже используется для reasoning) к answer-пути — стабилизированная часть (до последнего безопасного разреза: конец markdown-блока) рендерится мемоизированным канониче­ским пайплайном; живой хвост — плоским текстом; финальный полный рендер один раз на finish. Плюс: сигнатура memo MessageItem → exhaustive-switch по union part-kinds (compile-time защита вместо WARNING-комментария).
Тесты: perf-смок (вызовы renderChatMarkdown на синтетическом 100 KB стриме — O(число блоков), не O(число тиков)); визуальный регресс финального рендера (учесть <li><p>-обёртки схемы и scoped CSS из #498).

Коммит 2 — perf(ai-chat): append-персист — per-step INSERT вместо полной перезаписи

Сейчас каждый onStepFinish переписывает ВСЮ строку (O(n²) объёма за ран, autovacuum-чарн). Правка: per-step INSERT в отдельную лёгкую таблицу шагов; полная строка собирается один раз на finalize. Вариант «jsonb-append ||» — НЕ вариант: UPDATE jsonb в Postgres из-за MVCC переписывает TOAST-версию строки целиком — WAL/чарн (весь заявленный вред) остаётся, || экономит только сетевой payload; тест на мок-репо этого не увидит.
Обязательные стыковки:

  • reconstructRunParts(runId) → {parts, stepsPersisted} (введён в #491) переключает бэкенд: parts строки ДО этого коммита, таблица шагов ПОСЛЕ — потребители (attach, дельта, экспорт) не меняются;
  • источник сигнала «персист шага подтверждён» для ротации кольца (#491) меняется с flushAssistant-флаша на INSERT — за тем же контрактом;
  • era-маркер toolTraceVersion (#490) перестаёт быть консистентным через полную перезапись (её больше нет) — проверить путь простановки.
    Тест: объём записи через дельту pg_current_wal_lsn() на живой БД — O(Σ шагов), не O(квадрата); регресс attach/дельты/экспорта на живом ране через контракт.
Один PR. **Зависит от #491** (контракт `reconstructRunParts` — коммит 2 меняет его бэкенд). Координаты — develop@363f20ab. ### Коммит 1 — perf(client): инкрементальный markdown-рендер стримящегося ответа **⚠️ Обновлено после мержа #498:** рендер-пайплайн чата сменился — `renderChatMarkdown` теперь идёт через канонический конвертер (`markdownToProseMirrorSync` → `PMNode.fromJSON` → `DOMSerializer.serializeFragment` → `DOMPurify.sanitize`, см. `apps/client/src/features/ai-chat/utils/markdown.ts`), а не через marked. Пайплайн остался синхронным per-render и вызывается на КАЖДЫЙ throttled-тик (~20 Гц) по всему накопленному тексту растущего part'а — и он ТЯЖЕЛЕЕ старого marked-пути (полный канонический parse + построение PM-нод + DOM-сериализация + sanitize на тик). Хотспот «печатает рывками» после #498 предположительно стал горячее — **перед началом работ снять perf-профиль стриминга на текущем develop** (число вызовов renderChatMarkdown × стоимость на 100 KB-ответе). Правка (суть не изменилась): применить `StreamingPlainText`-паттерн (chunked memo, уже используется для reasoning) к answer-пути — стабилизированная часть (до последнего безопасного разреза: конец markdown-блока) рендерится мемоизированным канониче­ским пайплайном; живой хвост — плоским текстом; финальный полный рендер один раз на finish. Плюс: сигнатура memo `MessageItem` → exhaustive-switch по union part-kinds (compile-time защита вместо WARNING-комментария). Тесты: perf-смок (вызовы renderChatMarkdown на синтетическом 100 KB стриме — O(число блоков), не O(число тиков)); визуальный регресс финального рендера (учесть `<li><p>`-обёртки схемы и scoped CSS из #498). ### Коммит 2 — perf(ai-chat): append-персист — per-step INSERT вместо полной перезаписи Сейчас каждый `onStepFinish` переписывает ВСЮ строку (O(n²) объёма за ран, autovacuum-чарн). Правка: per-step INSERT в отдельную лёгкую таблицу шагов; полная строка собирается один раз на finalize. **Вариант «jsonb-append `||`» — НЕ вариант:** UPDATE jsonb в Postgres из-за MVCC переписывает TOAST-версию строки целиком — WAL/чарн (весь заявленный вред) остаётся, `||` экономит только сетевой payload; тест на мок-репо этого не увидит. Обязательные стыковки: - `reconstructRunParts(runId) → {parts, stepsPersisted}` (введён в #491) переключает бэкенд: parts строки ДО этого коммита, таблица шагов ПОСЛЕ — потребители (attach, дельта, экспорт) не меняются; - источник сигнала «персист шага подтверждён» для ротации кольца (#491) меняется с flushAssistant-флаша на INSERT — за тем же контрактом; - era-маркер `toolTraceVersion` (#490) перестаёт быть консистентным через полную перезапись (её больше нет) — проверить путь простановки. Тест: объём записи через дельту `pg_current_wal_lsn()` на живой БД — O(Σ шагов), не O(квадрата); регресс attach/дельты/экспорта на живом ране через контракт.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#492