Стабилизация, итерация 1: прод-пожары и безопасность (один PR, каждая фича отдельным коммитом) #486

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

Один 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, env METRICS_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) prop showErrors в ToolCallCard, share-виджет передаёт false; (2) санитизация на уровне share-тулсета — обернуть execute тулов PublicShareChatToolsService.forShare catch'ем с классифицированной безопасной ошибкой («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 — пакет мелочи (по коммиту на пункт)

  1. fix(mcp): REGISTRY_STAMP — glob по src/ packages/mcp/scripts/gen-registry-stamp.mjs хэширует ТОЛЬКО tool-specs.ts — skew build/src в client.ts/comment-signal/drawio-* не детектится. Glob src/**/*.ts С ИСКЛЮЧЕНИЕМ *.generated.ts (иначе цикл через собственный выход) + СИММЕТРИЧНОЕ обновление рантайм-пересчёта в docmost-client.loader.ts. Тест: негативный — «правка client.ts без пересборки → лоадер отказывает».
  2. fix(mcp): сброс collab-токена на WS-auth-failure. packages/mcp/src/client.ts — кэш collabTokenCache (:424) инвалидируется только в HTTP-401-интерцепторе (:498) и login() (:565); падение Hocuspocus-handshake кэш не сбрасывает → до 5 минут падающих мутаций без self-heal. В onAuthenticationFailed — инвалидация + один ретрай с forceRefresh (симметрично HTTP-пути).
  3. fix(ai-chat): сброс lastDegenerationCheckLen в onStepFinish. ai-chat.service.ts:1255-1287: onStepFinish сбрасывает inProgressText, но НЕ lastDegenerationCheckLen → после первого длинного шага детектор токен-лупов молчит, пока новый текст не превысит старую отметку. Однострочник + unit.
  4. fix(client): flushNext в onFinish гейтится mountedRef. chat-thread.tsx:662: финальный flushNext() не гейтится → «призрачные» отправки и чаты-призраки после переключения чата мид-стрим.
  5. fix(auth): провенанс для API-key пути. apps/server/src/core/auth/strategies/jwt.strategy.ts:46-48: early-return до резолва провенанса; перенести «выше» нельзя (user ещё не загружен) — резолвить от user, возвращённого ApiKeyService.validateApiKey (isAgent → 'agent'), определить клейм-семантику API-ключей.
  6. fix(mcp): пробросить truncated в tree-mode listPages. client.ts:843 — флаг теряется; по образцу check_new_comments.
  7. chore(mcp): McpService.onModuleDestroy → destroyAllSessions (сейчас только sweep-таймер; loopback-сессии пинят доки при shutdown) + удалить мёртвый код (SEARCH_REMOVE_*, импорты parseNodeArg).

Зависимости: нет. Блокирует: #487 (коммит 4 — его предусловие), #495 (коммит 6 — предусловие выравнивания версий).

Один 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, env `METRICS_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) prop `showErrors` в ToolCallCard, share-виджет передаёт false; (2) санитизация на уровне share-тулсета — обернуть execute тулов `PublicShareChatToolsService.forShare` catch'ем с классифицированной безопасной ошибкой («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 — пакет мелочи (по коммиту на пункт) 7. **fix(mcp): REGISTRY_STAMP — glob по src/** `packages/mcp/scripts/gen-registry-stamp.mjs` хэширует ТОЛЬКО tool-specs.ts — skew build/src в client.ts/comment-signal/drawio-* не детектится. Glob `src/**/*.ts` С ИСКЛЮЧЕНИЕМ `*.generated.ts` (иначе цикл через собственный выход) + СИММЕТРИЧНОЕ обновление рантайм-пересчёта в docmost-client.loader.ts. Тест: негативный — «правка client.ts без пересборки → лоадер отказывает». 8. **fix(mcp): сброс collab-токена на WS-auth-failure.** `packages/mcp/src/client.ts` — кэш `collabTokenCache` (:424) инвалидируется только в HTTP-401-интерцепторе (:498) и login() (:565); падение Hocuspocus-handshake кэш не сбрасывает → до 5 минут падающих мутаций без self-heal. В `onAuthenticationFailed` — инвалидация + один ретрай с forceRefresh (симметрично HTTP-пути). 9. **fix(ai-chat): сброс lastDegenerationCheckLen в onStepFinish.** `ai-chat.service.ts:1255-1287`: onStepFinish сбрасывает inProgressText, но НЕ lastDegenerationCheckLen → после первого длинного шага детектор токен-лупов молчит, пока новый текст не превысит старую отметку. Однострочник + unit. 10. **fix(client): flushNext в onFinish гейтится mountedRef.** `chat-thread.tsx:662`: финальный flushNext() не гейтится → «призрачные» отправки и чаты-призраки после переключения чата мид-стрим. 11. **fix(auth): провенанс для API-key пути.** `apps/server/src/core/auth/strategies/jwt.strategy.ts:46-48`: early-return до резолва провенанса; перенести «выше» нельзя (user ещё не загружен) — резолвить от user, возвращённого `ApiKeyService.validateApiKey` (isAgent → 'agent'), определить клейм-семантику API-ключей. 12. **fix(mcp): пробросить truncated в tree-mode listPages.** `client.ts:843` — флаг теряется; по образцу check_new_comments. 13. **chore(mcp): McpService.onModuleDestroy → destroyAllSessions** (сейчас только sweep-таймер; loopback-сессии пинят доки при shutdown) + удалить мёртвый код (`SEARCH_REMOVE_*`, импорты parseNodeArg). **Зависимости:** нет. **Блокирует:** #487 (коммит 4 — его предусловие), #495 (коммит 6 — предусловие выравнивания версий).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#486