Ограничения бюджета рабочего пространства MCP
Обзор
WorkspaceMcpBudget (packages/core/src/tools/mcp-workspace-budget.ts) — это контроллер бюджета клиента MCP на уровне рабочего пространства из F2 (#4175, коммит 6). Он содержит ту же машину состояний, что и McpClientManager (резервирование слотов, гистерезис 75%, объединение отказов в пакеты в течение прохода discoverAllMcpTools*), но живёт один раз на рабочее пространство внутри McpTransportPool, а не один раз на сессию в менеджере каждого дочернего ACP. Пул делегирует вызовы acquire и release сюда, чтобы ограничение применялось к рабочему пространству, а не к каждой сессии.
Устаревшая механика бюджета McpClientManager остаётся для автономных серверов Qwen и SDK MCP (которые обходят пул, согласно исправлению в коммите 4). Режим пула → применяется WorkspaceMcpBudget; автономный режим / SDK MCP → применяется встроенная механика менеджера. Двойного учёта нет, так как обнаружение в режиме пула никогда не вызывает tryReserveSlot менеджера.
Обязанности
- Отслеживать
reservedSlots: Set<string>— имена серверов, удерживающих слоты в данный момент (ключ слота — по имени, соответствует PR 14 v1). tryReserve(name) → 'reserved' | 'already_held' | 'refused'— атомарно и синхронно, чтобы одновременныеPromise.allзахваты не могли превысить лимит на границе await.release(name) → boolean— идемпотентно (семантикаSet.delete).- Генерировать событие
mcp_budget_warningодин раз при превышении 75% порога вверх (reservedSlots.size / clientBudget); повторная готовность только после снижения до 37.5%. - Объединять отказы для каждого сервера в рамках массового прохода обнаружения —
beginBulkPass()/endBulkPass()накапливают отказы в единое событиеmcp_child_refused_batch. - Поддерживать
lastRefusedServerNamesдля потребителей снимков (GET /workspace/mcp) — очищается в НАЧАЛЕ следующего массового прохода, а не при отправке, чтобы снимок между проходами всё ещё видел последний набор отказов.
Архитектура
Конфигурация
new WorkspaceMcpBudget({
clientBudget?: number, // undefined = без ограничений
mode: 'off' | 'warn' | 'enforce',
onEvent?: (event: McpBudgetEvent) => void,
});Семантика mode:
off— все методы не выполняют никаких действий;tryReserveбезусловно возвращает'reserved'; события не генерируются.warn— слоты отслеживаются, иmcp_budget_warningгенерируется при 75%, ноtryReserveНИКОГДА не отказывает.enforce—tryReserveотказывает, если превышенclientBudget;recordRefusalставит отказы в очередь;endBulkPassгенерируетmcp_child_refused_batch.
Константы из mcp-client-manager.ts
MCP_BUDGET_WARN_FRACTION = 0.75— порог срабатывания вверх.MCP_BUDGET_REARM_FRACTION = 0.375— порог гистерезиса для повторной готовности.McpBudgetMode = 'off' | 'warn' | 'enforce'.
Внутреннее состояние
| Состояние | Назначение |
|---|---|
reservedSlots: Set<string> | Авторитетный набор резервирований; гистерезис вычисляется как size / clientBudget. |
pendingRefusalNames: Set<string> | Имена отказов, накопленные в текущем окне beginBulkPass/endBulkPass; очищается при endBulkPass. |
pendingRefusalTransports: Map<string, transport> | Дополнительное хранилище, чтобы сгенерированный пакет содержал транспорт каждого отказавшего сервера. |
lastRefusedServerNames: readonly string[] | Список отказов из последнего завершённого прохода, видимый в снимках. Очищается при начале следующего прохода. |
warnArmed: boolean | Состояние гистерезиса — true = готово к срабатыванию, false = уже сработало с момента последнего снижения до 37.5%. |
bulkPassDepth: number | Счётчик реентерабельности для вложенных массовых проходов (вложенные проходы не должны генерировать события дважды). |
Workflow
tryReserve
tryReserve выполняется синхронно. acquire пула асинхронен, но резервирование происходит до любого await, поэтому два одновременных вызова Promise.all acquire для разных имён не могут оба пройти лимит.
Гистерезис
Гистерезис предотвращает повторные предупреждения, когда нагрузка колеблется около 75%. Первое пересечение генерирует событие; последующие пересечения без снижения до 37.5% не вызывают повторного срабатывания.
Объединение отказов в пакет
Отказы вне прохода (например, ленивый spawn readResource, который полностью обходит массовый проход) генерируют пакеты длиной 1 встроенно для единообразия формы. Вложенные проходы (bulkPassDepth > 0) не генерируют события; только внешний проход при завершении отправляет объединённый пакет.
Состояние и жизненный цикл
- Контроллер бюджета создаётся один раз на рабочее пространство при инициализации пула.
clientBudgetнеизменяем после создания; изменения во время выполнения требуют пересборки пула.modeтакже неизменяем (onEventсохраняется какundefined, когдаmode === 'off', для защиты в глубину).warnArmedизначально true; сбрасывается в true при пересечении порога 37.5% вниз.lastRefusedServerNamesНЕ очищается при отправкеendBulkPass— только в НАЧАЛЕ следующего массового прохода. Это позволяет маршруту снимка, вызванному между проходами, всё ещё сообщать последний набор отказов (иначе панели мониторинга показывали бы пустые отказы сразу после доставки события пакета отказов).
Зависимости
packages/core/src/tools/mcp-client-manager.ts— повторно используетMcpBudgetEvent,McpBudgetMode,McpRefusedServer,MCP_BUDGET_WARN_FRACTION,MCP_BUDGET_REARM_FRACTION,BudgetExhaustedError(выбрасываетсяacquireпула при отказе).packages/core/src/tools/mcp-transport-pool.ts— потребляет бюджет; передаёт события в шину событий демона через механизмonEventпула.- Маршрут снимка демона
GET /workspace/mcp— читаетgetReservedSlots(),getRefusedServerNames(),getReservedCount(),getBudget(),getMode().
Конфигурация
| Источник | Параметр | Эффект |
|---|---|---|
| Флаг | --mcp-client-budget=N | Устанавливает clientBudget для контроллера рабочего пространства. |
| Флаг | --mcp-budget-mode={off,warn,enforce} | Устанавливает mode. enforce требует положительного clientBudget; иначе запуск явно завершается ошибкой. |
| Переменная среды | QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE | Передаётся дочернему ACP через childEnvOverrides; дочерний процесс читает их через readBudgetFromEnv(). |
| Теги возможностей | mcp_guardrails (всегда; modes: ['warn', 'enforce']), mcp_guardrail_events (всегда) | См. 11-capabilities-versioning.md. |
Оговорки и известные ограничения
- Ключ резервирования — по имени. Две записи пула с одним именем сервера, но разными отпечатками (например, сессии, передающие различные заголовки OAuth), занимают ОДИН слот вместе. Учёт подпроцессов раскрывается отдельно через поле
subprocessCountв снимке пула. Операторам следует рассматривать бюджет как «настроенные слоты серверов», а не «количество подпроцессов». - Гистерезис срабатывает по количеству резервирований, а не по количеству живых (CONNECTED) подключений. Резервирования включают текущие подключения и переживают временные разрывы, поэтому гистерезис остаётся стабильным во время циклов переподключения. Количество живых подключений раскрывается в полезных нагрузках событий как
liveCountдля тех потребителей SDK, кому нужна такая перспектива. - Режим
warnникогда не отказывает. Он по-прежнему отслеживает резервирования и генерируетmcp_budget_warning, ноtryReserveвсегда возвращает'reserved'. Семантика отказа доступна только в режимеenforce. - События бюджета на уровне рабочего пространства содержат
scope: 'workspace', что позволяет им распространяться на все подключённые сессии одновременно. СчётчикиmcpBudgetWarningCount/mcpChildRefusedBatchCountв редьюсерах SDK увеличиваются синхронно во всех сессиях на одном соединении. Устаревшие события на уровне сессии отMcpClientManagerне содержатscope(по умолчанию семантически равно'session'). - Аварийный переключатель
QWEN_SERVE_NO_MCP_POOL=1полностью отключает пул; бюджет рабочего пространства также отключается, и бюджет сессии (McpClientManager) вступает в силу. Конверт возможностей удаляетmcp_workspace_poolиmcp_pool_restart, чтобы сообщить об этом. ServeMcpBudgetStatusCell.scope— это форма списка, совместимая с будущими расширениями. Ячейки снимка раскрываютbudgets[], а не одно полеbudget?. PR 14 v1 генерирует одну ячейку сscope: 'session'для каждой ACP-сессии, так какacpAgent.newSessionConfig()создаётConfig/McpClientManagerэтой сессии. Область'pool'зарезервирована для ячейки на уровне пула в Wave 5, PR 23, которая будет располагаться рядом с ячейками на уровне сессий. Потребители должны допускать дополнительные неизвестные значенияscope, игнорируя их, а не завершаясь с ошибкой.
Ссылки
packages/core/src/tools/mcp-workspace-budget.ts(весь класс)packages/core/src/tools/mcp-client-manager.ts(BudgetExhaustedError,McpBudgetEvent, константы гистерезиса)packages/core/src/tools/mcp-transport-pool.ts(место вызоваacquireпула, которое вызываетtryReserve)- Дизайн-документ F2 (v2.2):
../../design/f2-mcp-transport-pool.md§11 для бюджета на уровне рабочего пространства и записей в changelog v2.2 о доработках бюджета и отпечатков. - Заметки к дизайну F2: задача #4175 , коммит 6.