perf(mcp): [follow-up #400/#431] кэш CollabSession в проде почти не хитует — свежий collab-токен на каждую мутацию меняет ключ кэша; нужен кэш токена #435
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?
Проблема
Кэш живого collab-соединения (#400, реализация #431, смержено в develop 10.07 ~04:26) в продовых сценариях почти никогда не переиспользует сессию. Ключ реестра включает сам токен (
packages/mcp/src/lib/collab-session.ts,sessionKey = wsUrl + " " + pageId + " " + token, инвариант 4 «изоляция identity»), а оба пути получения collab-токена выдают свежий токен на каждый content-мутирующий вызов:apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts,buildDocmostClient):getCollabToken = () => tokenService.generateCollabToken(user, workspaceId, {actor:'agent', aiChatId})— каждый вызов подписывает новый JWT.iat/expв секундах ⇒ строка токена меняется каждую секунду.packages/mcp/src/client.ts,getCollabTokenWithReauth()→lib/auth-utils.ts getCollabToken()):POST /auth/collab-tokenна каждую мутацию, сервер подписывает новый JWT — тот же эффект.Дизайн #400 предполагал: «Ротация токена (
getCollabTokenWithReauth) создаёт новую запись, старая уходит по idle» — т.е. что ротация редкая. По факту «ротация» происходит на каждом вызове, и кэш вырождается в легаси-поведение (provider-per-op), ради устранения которого и делался #400. Юнит-тесты #431 это не ловят, потому что гоняют фиксированный токен — «20 правок → 1 connect» в тестах проходит, в проде нет.Последствия (наблюдались вживую 10.07 ~04:30, страница со ~356 тредами комментариев)
Пачка
createCommentот in-app агента (вычитка статьи, ~15–20 инлайн-комментариев):mutatePageContent→acquireCollabSessionс новым ключом → полный connect + auth + onLoadDocument + начальная синхронизация огромного ydoc — на каждый вызов.Tool call did not complete.Тот факт, что same-second токены дают реюз, — прямое подтверждение диагноза: сессия и кэш работают, ломается только стабильность ключа.
Решение
Кэшировать collab-токен в клиенте, в
getCollabTokenWithReauth()(packages/mcp/src/client.ts) — одно место закрывает оба пути (provider-путь in-app агента и REST-путь внешнего MCP):Ключевые требования:
forceRefresh) — иначе ретрай вернёт тот же протухший токен и реаут выродится в no-op.MCP_COLLAB_TOKEN_TTL_MS,0= отключить, легаси): сильно меньше жизни токена (24ч,token.service.ts) и не длиннее session max-age (10 мин) ⇒ окно устаревания прав, осознанно принятое в ревью #431, не расширяется.DocmostClient, клиент строится per-user/per-session (in-app — per chat request), между пользователями ничего не шарится, инвариант 4 не трогаем.login()/сменеthis.token(REST-путь), чтобы токен не пережил смену identity клиента.Альтернатива (рассмотрена, отложена)
Ключевать реестр сессий по декодированному identity (
sub+actor+aiChatId) вместо сырой строки токена. Устойчивее к любой ротации, но трогает security-инвариант 4 и семантику реестра; кэш токена — на порядок меньший и локальный фикс. Если когда-нибудь понадобится реюз поверх TTL — вернуться к этому.Не в скоупе (смежное, наблюдалось в том же инциденте)
createCommentделает полныйgetPageJsonpre-check +fromYdoc+ 2× deep-clone JSON + diff на огромном документе — CPU-цена на комментарий остаётся; батч-инструмент «N комментариев за одну мутацию» — отдельная тема.Tool call did not complete.+ доезжающая после обрыва серверная работа ⇒ дубликаты: идемпотентность/отмена tool-вызовов при аборте хода — отдельная тема (ai-chat).Критерии приёмки
createComment/table_update_cellпо одной странице от in-app агента: ровно 1 connect/auth/initial-sync на серию (DEBUG-логиReusing collab session/ метрики #402), никаких зомби-сессий по одной на вызов.getCollabTokenWithReauthвозвращают ту же строку (provider-функция/REST вызваны 1 раз); после TTL — новый токен; 401/403 ⇒ форс-рефреш мимо кэша;MCP_COLLAB_TOKEN_TTL_MS=0⇒ точное легаси-поведение (фетч на каждый вызов).