Стабилизация, итерация 4: устойчивость тулов — валидация входа, MCP-кэш, ретраи по write-классу (один PR, 2 коммита) #489
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. Зависит от #487 (коммит 1 — abortSignal у in-app тулов). Координаты — develop@363f20ab.
Коммит 1 — fix(ai-chat): валидация клиентских parts + insert после конвертации
ai-chat.service.ts:815-839:partsюзер-сообщения персистятся в metadata БЕЗ валидации (body читается без DTO) и реплеятся вconvertToModelMessagesна каждом ходе; insert user-строки идёт ДО конвертации.Сценарий: битая структура parts (например, tool-part в
input-available) → конвертация кидает → 500 на КАЖДОМ ходе навсегда («окирпиченный» чат); каждый ретрай добавляет дубль user-строки в уже битую историю.Правка:
convertToModelMessagesДО insert'а user-строки (ретрай не плодит дубли).textPart(content)) с маркером «[tool context omitted]» (молчаливая потеря tool-контекста недопустима — модель должна видеть усечение).Тесты: unit трёх веток; регресс «чат с битым сообщением в истории продолжает работать; маркер присутствует».
Коммит 2 — fix(ai-chat): MCP-кэш — per-server evict, in-run восстановление, ретраи только для readOnly
Сейчас: кэш внешних MCP-клиентов ключуется по workspaceId и держит клиентов ВСЕХ серверов в одной записи (
mcp-clients.service.ts:121, TTL 60 c :72-73); health-check/reconnect-on-error нет — после разрыва SSE-транспорта (bodyTimeout 60 c режет при >60 c тишины МЕЖДУ вызовами,ai-streaming-fetch.ts:126) кэш отдаёт труп до конца TTL. Тулы рана захватывают клиент приtoolsForв начале хода — evict сам по себе лечит только СЛЕДУЮЩИЙ ход, а мотивационный сценарий («модель ретраит и жжёт шаги») происходит ВНУТРИ текущего рана.Правка:
readOnly/idempotent: новое поле в SHARED_TOOL_SPECS (packages/mcp/src/tool-specs.ts) + registration-time assert «каждый спек объявляет write-класс». Транспортная ошибка НЕ означает «не применилось» (reset после обработки сервером) — слепой ретрай write-тула = double-apply (класс инцидента #435). Write-тул: без авторетрая, модели — indeterminate-ошибка «могло примениться, проверь перед повтором».isRetryableConnectError-подход), НЕ по мок-ошибкам (иначе evict в проде молча не сработает при зелёном CI).bodyTimeoutдля SSE-транспорта MCP — отдельно от HTTP-вызовов (тишина между вызовами легальна), поднять.Тесты: property на реальных error-шейпах «повторный вызов после обрыва ВНУТРИ того же рана получает живой клиент» (in-run путь, не только кросс-turn); «write-тул не авторетраится»; «ретрай после Stop не происходит».