Стабилизация, итерация 1: прод-пожары и безопасность (один PR, каждая фича отдельным коммитом) #486
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Один PR с фиче-ветки в develop. Каждый пункт ниже — отдельный коммит в указанном порядке. Все пункты независимы друг от друга. Координаты file:line — develop@363f20ab, перед работой перегенерировать.
Коммит 1 — fix(mcp): ELK-лейаут в worker_thread
packages/mcp/src/lib/drawio-layout.ts:152-173(капы :21-36): elkjs синхронный;Promise.raceс setTimeout(5s) — иллюзия (таймер не сработает при заблокированном event loop, комментарий в коде это сам признаёт). На in-app хостеlayout:"elk"исполняется в процессе основного NestJS по запросу LLM → патологический граф у капа 500 узлов вешает ВСЕ HTTP/SSE/loopback. Правка:elk.layout()в worker_thread по требованию, таймаут черезworker.terminate()(единственный способ прервать синхронный код); убрать лживый комментарий «can never wedge the server». Тест: unit на terminate-по-таймауту; бенчмарк-гард на реальном worst-case графе у капа.Коммит 2 — fix(health): утечка ioredis-клиента в /health
apps/server/src/integrations/health/redis.health.ts(pingCheck):new Redis(...)на каждый вызов,disconnect()только на success-пути → при лежащем Redis каждый тик пробы добавляет вечно реконнектящийся клиент. Правка: один долгоживущий клиент для проб (lazyConnect,maxRetriesPerRequest: 1); желательно единая фабрика Redis-подключений с shutdown-хуком (закрывает класс #254/#256). Тест: интеграционный — N вызовов pingCheck при лежащем Redis не растят число живых хэндлов.Коммит 3 — fix(metrics): bind 127.0.0.1 + METRICS_BIND/METRICS_TOKEN
apps/server/src/integrations/metrics/metrics.server.ts:53:listen(port, '0.0.0.0')без auth. Правка: дефолт 127.0.0.1, envMETRICS_BIND, опциональноMETRICS_TOKEN(Bearer); обновить compose/доки скрейпа. Тест: unit на дефолтный bind + env-переопределение.Коммит 4 — fix(ai-chat): провал beginRun фейлит ход (A_RUN_BEGIN_FAILED)
ai-chat.service.ts:787-806+ai-chat.controller.ts:509-533: ошибка beginRun (кроме unique-violation), напр. blip пула БД → ход продолжается БЕЗ run-строки на socket-signal, который в autonomous никто не абортит:/stopран не видит, дисконнект не абортит, гейт пропускает второй ран — неостанавливаемый невидимый ран до рестарта. Правка: (а) провал beginRun (кроме RunAlreadyActiveError → существующий 409) —ServiceUnavailableExceptionс кодомA_RUN_BEGIN_FAILEDиз stream() до первого байта (catch контроллера :568-579 отдаст честный статус на raw-сокет); (б) клиентский классификатор: ветка «временный сбой — повторите» СТРОГО ДО generic-503-матча (error-message.ts:33-37иначе покажет «provider is not configured»); (в) ветвление по режимам запрещено — #487 наследует эту политику для обоих режимов. Чисто: user-строка вставляется после beginRun, MCP-лизы позже — мусора нет. Тесты: unit fail-fast; unit классификатора из полного реального тела ответа.Коммит 5 — fix(share): errorText тулов и провайдера утекают анониму в публичном шэре (closes #394)
Локализация (проверена по коду и issue): рендер —
apps/client/src/features/ai-chat/components/tool-call-card.tsx:77-81(нет гейта, аналогичного showCitations/showInput), используется в share-ai-widget; байты — живой SSE public-share-chat.service.ts (транскрипт share-чата эфемерен, client-held — «серверного маппера транскрипта» не существует); второй канал — там же :319-326:onError: describeProviderError(...)отдаёт анониму текст провайдерской ошибки (statusCode+тело: может нести внутренний baseUrl, имя модели). Правка, три слоя, все обязательны: (1) propshowErrorsв ToolCallCard, share-виджет передаёт false; (2) санитизация на уровне share-тулсета — обернуть execute туловPublicShareChatToolsService.forSharecatch'ем с классифицированной безопасной ошибкой («page not found in this share» / «tool failed») — одно место закрывает байты, рендер и контекст модели (self-correction сохраняется); ошибка тула в UI-стриме v6 едет одним атомарным фреймомtool-output-error, буферизация не нужна; (3) анонимный onError маппит на фиксированные классифицированные строки (rate-limited/unavailable/provider error), полный текст — только в серверный лог. Тест: интеграционный с реальным падением тула — assert по СЫРЫМ SSE-байтам (не по DOM — рендер-гейт маскирует дыру байтов); аналогично для провайдерской ошибки.Коммит 6 — fix(ai): drain-hang в writeToServerResponse — расширить patches/ai@6.0.134.patch
Серверная
ai@6.0.134(dist/index.mjs:4961):write()===false → await once('drain')без выхода по close/error;finally { response.end() }недостижим;read()fire-and-forget с throw → unhandledRejection. В autonomous ран продолжается после дисконнекта → КАЖДЫЙ дисконнект мид-ран паркует пайп и держит reader/буферы до рестарта. Правка: расширить существующий патч:Promise.race(['drain','close','error'])с гигиеной once-слушателей (events.onceс AbortSignal либо явное снятие проигравших — иначе копятся по одному на stall); при close/error —reader.cancel()и выход (безопасно для detached-ранов: независимый дренаж делает consumeStream, service:1475); поглотить rejection read() с логом. Тесты: трипвайр-спека ОБЯЗАТЕЛЬНО в apps/server (только там резолвится патченная копия; в корне — непатченная клиентская 6.0.207), по образцу ai-sdk-partial-output.patch.spec.ts; интеграционный «дисконнект мид-ран не оставляет висящий reader / не растит heap». Выравнивание версии ai в монорепе — только ПОСЛЕ этого коммита (#495, коммит 2).Коммиты 7–13 — пакет мелочи (по коммиту на пункт)
packages/mcp/scripts/gen-registry-stamp.mjsхэширует ТОЛЬКО tool-specs.ts — skew build/src в client.ts/comment-signal/drawio-* не детектится. Globsrc/**/*.tsС ИСКЛЮЧЕНИЕМ*.generated.ts(иначе цикл через собственный выход) + СИММЕТРИЧНОЕ обновление рантайм-пересчёта в docmost-client.loader.ts. Тест: негативный — «правка client.ts без пересборки → лоадер отказывает».packages/mcp/src/client.ts— кэшcollabTokenCache(:424) инвалидируется только в HTTP-401-интерцепторе (:498) и login() (:565); падение Hocuspocus-handshake кэш не сбрасывает → до 5 минут падающих мутаций без self-heal. ВonAuthenticationFailed— инвалидация + один ретрай с forceRefresh (симметрично HTTP-пути).ai-chat.service.ts:1255-1287: onStepFinish сбрасывает inProgressText, но НЕ lastDegenerationCheckLen → после первого длинного шага детектор токен-лупов молчит, пока новый текст не превысит старую отметку. Однострочник + unit.chat-thread.tsx:662: финальный flushNext() не гейтится → «призрачные» отправки и чаты-призраки после переключения чата мид-стрим.apps/server/src/core/auth/strategies/jwt.strategy.ts:46-48: early-return до резолва провенанса; перенести «выше» нельзя (user ещё не загружен) — резолвить от user, возвращённогоApiKeyService.validateApiKey(isAgent → 'agent'), определить клейм-семантику API-ключей.client.ts:843— флаг теряется; по образцу check_new_comments.SEARCH_REMOVE_*, импорты parseNodeArg).Зависимости: нет. Блокирует: #487 (коммит 4 — его предусловие), #495 (коммит 6 — предусловие выравнивания версий).