c96fafc4ad
Раньше каждый onStepFinish переписывал ВСЮ строку ассистента (растущий metadata.parts jsonb со всеми выводами инструментов) → O(n²) объёма записи на прогон: под MVCC/TOAST апдейт jsonb переписывает всю версию строки, так что шаг k пишет ~k×вывод. Прогон из 50 шагов по ~100 КБ = сотни МБ WAL и мёртвых кортежей за ход, что молотит autovacuum. (#490 убрал только ВТОРУЮ копию в tool_calls; сам metadata.parts всё ещё рос и переписывался.) Теперь каждый завершённый шаг ДОПИСЫВАЕТСЯ отдельной строкой в лёгкую таблицу ai_chat_run_steps (только парты этого шага), а строка сообщения получает дешёвый маркер (stepsPersisted + toolTraceVersion, без растущего блоба parts). Полный metadata.parts собирается ОДИН раз на финализации. НЕ jsonb-append (||): апдейт всё равно переписывает всю TOAST-версию — экономится только сетевой payload, а WAL/мёртвые кортежи остаются; поэтому именно ОТДЕЛЬНАЯ таблица + INSERT. Три обязательные интеграции: - reconstructRunParts(row, stepRows) → { parts, stepsPersisted }: единый шов переключения бэкенда. Читает парты из СТРОКИ, если она уже несёт inline-parts (старые записи + ЛЮБАЯ финализированная), иначе из ТАБЛИЦЫ ШАГОВ (mid-run запись #492). Дискриминатор — наличие непустого metadata.parts (флаг схемы не нужен). Потребители (attach-seed, delta-poll, export, reconnect) прогоняют строки через hydrateAssistantParts на границе чтения — их контракт/вывод не меняется, старые и новые записи восстанавливаются идентично. - сигнал ротации кольца реестра #491 (confirmPersistedStep) теперь стреляет на подтверждённый INSERT шага, под тем же контрактом (updateStreaming возвращает stepsPersisted / null). - era-marker toolTraceVersion (#490) больше не ставится полной переписью — ставится в маркере шага и на финализации (flushAssistant), остаётся консистентным. Полная обратная совместимость: прогон, записанный по-старому (полная строка, без строк шагов), восстанавливается/attach/export идентично. При отсутствии репозитория шагов (позиционные тест-конструкции) — фолбэк на прежний полнострочный flush (без регрессии, только без выигрыша WAL). Тесты (реальный pg, int-lane): - WAL-дельта (pg_current_wal_lsn) на прогоне 40×100КБ: new=4.3МБ vs old=90.3МБ (20.8x) — O(Σ шагов) против O(n²); старый путь в тесте И есть ревертнутое поведение (мутация-проверка). - reconstruct-контракт: new-style (таблица шагов) и old-style (inline) прогоны восстанавливаются в идентичные parts; hydrate заполняет строку. - миграция up/down roundtrip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>