Proteções de Orçamento do Workspace MCP
Visão Geral
WorkspaceMcpBudget (packages/core/src/tools/mcp-workspace-budget.ts) é o controlador de orçamento do cliente MCP com escopo de workspace do F2 (#4175 commit 6). Ele possui a mesma máquina de estados que o McpClientManager carrega inline (reserva de slots, aviso de histerese de 75%, coalescência de recusas em lote durante uma passagem discoverAllMcpTools*), mas vive uma vez por workspace dentro do McpTransportPool em vez de uma vez por sessão dentro do gerenciador de cada filho ACP. O pool delega chamadas acquire e release aqui, de modo que o limite se aplica ao workspace, não a cada sessão.
A mecânica de orçamento legada do McpClientManager permanece para servidores MCP qwen standalone e SDK (que ignoram o pool conforme o fix do commit 4). Modo pool → WorkspaceMcpBudget aplica; standalone / SDK MCP → mecânica inline do gerenciador aplica. Sem dupla contagem porque a descoberta em modo pool nunca chama o tryReserveSlot do gerenciador.
Responsabilidades
- Rastrear
reservedSlots: Set<string>dos NOMES de servidores atualmente mantidos (a chave do slot é por NOME, correspondendo ao PR 14 v1). tryReserve(name) → 'reserved' | 'already_held' | 'refused'— atômico e síncrono, de modo que as aquisições concorrentes comPromise.allnão podem ultrapassar o limite em um ponto deawait.release(name) → boolean— idempotente (semântica deSet.delete).- Disparar
mcp_budget_warninguma vez ao ultrapassar 75% para cima dereservedSlots.size / clientBudget; rearmar apenas após uma ultrapassagem de 37,5% para baixo. - Coalescer recusas por servidor em uma passagem de descoberta em massa — os métodos
beginBulkPass()/endBulkPass()delimitam a acumulação de recusas em um único eventomcp_child_refused_batch. - Manter
lastRefusedServerNamespara consumidores de snapshot (GET /workspace/mcp) — limpo no INÍCIO da próxima passagem em massa, NÃO na emissão, para que um snapshot entre passagens ainda veja o último conjunto de recusas.
Arquitetura
Configuração
new WorkspaceMcpBudget({
clientBudget?: number, // undefined = unlimited
mode: 'off' | 'warn' | 'enforce',
onEvent?: (event: McpBudgetEvent) => void,
});Semântica do mode:
off— todos os métodos são no-ops;tryReserveretorna'reserved'incondicionalmente; nenhum evento é disparado.warn— os slots são rastreados emcp_budget_warningé disparado em 75%, mastryReserveNUNCA recusa.enforce—tryReserverecusa além declientBudget;recordRefusalenfileira recusas por servidor;endBulkPassemitemcp_child_refused_batch.
Constantes de mcp-client-manager.ts
MCP_BUDGET_WARN_FRACTION = 0.75— limiar superior.MCP_BUDGET_REARM_FRACTION = 0.375— rearme da histerese descendente.McpBudgetMode = 'off' | 'warn' | 'enforce'.
Estado interno
| State | Purpose |
|---|---|
reservedSlots: Set<string> | Conjunto de reserva autoritativo; histerese avalia size / clientBudget. |
pendingRefusalNames: Set<string> | Nomes de recusa acumulados durante a janela atual beginBulkPass/endBulkPass; drenados em endBulkPass. |
pendingRefusalTransports: Map<string, transport> | Sidecar para que o lote emitido carregue o transport de cada servidor recusado. |
lastRefusedServerNames: readonly string[] | Lista de recusas visível no snapshot da passagem completa mais recente. Limpa no início da próxima passagem. |
warnArmed: boolean | Estado de histerese — true = pronto para disparar, false = já disparou desde o último dreno de 37,5%. |
bulkPassDepth: number | Contador de reentrância para passagens em massa aninhadas (passagens aninhadas não devem emitir duas vezes). |
Fluxo de Trabalho
tryReserve
tryReserve é síncrono. O acquire do pool é assíncrono, mas a reserva acontece antes de qualquer await, então duas aquisições concorrentes com Promise.all para nomes diferentes não podem ambas ultrapassar o limite.
Histerese
A histerese evita avisos repetidos quando uma carga de trabalho oscila em torno de 75%. A primeira ultrapassagem dispara; ultrapassagens subsequentes sem cair para 37,5% não disparam.
Coalescência de recusas em lote
Recusas fora da passagem (por exemplo, spawn lazy de readResource que ignora completamente a passagem em massa) emitem lotes de tamanho 1 inline para consistência de formato. Passagens aninhadas (bulkPassDepth > 0) não disparam; apenas o fim da passagem mais externa emite o lote coalescido.
Estado e Ciclo de Vida
- O controlador de orçamento é construído uma vez por workspace na inicialização do pool.
clientBudgeté imutável após a construção; alterações em tempo de execução exigem reconstrução do pool.modetambém é imutável (onEventé armazenado comoundefinedquandomode === 'off'como defesa em profundidade).warnArmedcomeça como true; é redefinido para true via a ultrapassagem descendente de 37,5%.lastRefusedServerNamesNÃO é limpo na emissão deendBulkPass— apenas no INÍCIO da próxima passagem em massa. Isso permite que uma rota de snapshot chamada entre passagens ainda reporte o último conjunto de recusas (caso contrário, os dashboards mostrariam recusas vazias imediatamente após um evento de lote recusado ser entregue).
Dependências
packages/core/src/tools/mcp-client-manager.ts— reutilizaMcpBudgetEvent,McpBudgetMode,McpRefusedServer,MCP_BUDGET_WARN_FRACTION,MCP_BUDGET_REARM_FRACTION,BudgetExhaustedError(lançado peloacquiredo pool na recusa).packages/core/src/tools/mcp-transport-pool.ts— consome o orçamento; passa eventos para o EventBus do daemon através da tubulaçãoonEventdo pool.- Rota de snapshot do daemon
GET /workspace/mcp— lêgetReservedSlots(),getRefusedServerNames(),getReservedCount(),getBudget(),getMode().
Configuração
| Source | Knob | Effect |
|---|---|---|
| Flag | --mcp-client-budget=N | Define clientBudget para o controlador do workspace. |
| Flag | --mcp-budget-mode={off,warn,enforce} | Define mode. enforce requer um clientBudget positivo; caso contrário, a inicialização falha explicitamente. |
| Env | QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE | Encaminhado para o filho ACP via childEnvOverrides; o readBudgetFromEnv() do filho os capta. |
| Capability tags | mcp_guardrails (always; modes: ['warn', 'enforce']), mcp_guardrail_events (always) | Veja 11-capabilities-versioning.md. |
Advertências e Limitações Conhecidas
- A chave de reserva é por NOME. Duas entradas de pool com o mesmo nome de servidor, mas fingerprints diferentes (por exemplo, sessões injetando cabeçalhos OAuth divergentes) consomem UM slot juntas. A contabilidade de subprocessos é exposta separadamente via
subprocessCountdo snapshot do pool. Operadores devem pensar no orçamento como “slots de servidor configurados”, não “contagem de subprocessos”. - A histerese dispara com base na contagem de reservas, não na contagem de ativos (CONECTADOS). As reservas incluem conexões em andamento e sobrevivem a desconexões transitórias, então a histerese permanece estável durante ciclos de reconexão. A contagem de ativos é exposta nos payloads de eventos como
liveCountpara consumidores SDK que desejam essa perspectiva. - O modo
warnnunca recusa. Ele ainda rastreia reservas e disparamcp_budget_warning, mastryReservesempre retorna'reserved'. As semânticas de recusa são exclusivas doenforce. - Eventos de orçamento com escopo de workspace carregam
scope: 'workspace'para que se propaguem para todas as sessões anexadas simultaneamente. OsmcpBudgetWarningCount/mcpChildRefusedBatchCountdos redutores SDK incrementam em sincronia entre sessões na mesma conexão. Eventos legados por sessão doMcpClientManagernão carregamscope(padrão semântico é'session'). - A chave de desativação
QWEN_SERVE_NO_MCP_POOL=1desabilita o pool completamente; o orçamento do workspace também é desabilitado, e o orçamento por sessão doMcpClientManagerassume o controle. O envelope de capacidades removemcp_workspace_poolemcp_pool_restartpara reportar isso com precisão. ServeMcpBudgetStatusCell.scopeé uma forma de lista compatível com versões futuras. As células de snapshot expõembudgets[], não um único campobudget?. PR 14 v1 emite uma célulascope: 'session'para cada sessão ACP porqueacpAgent.newSessionConfig()constrói oConfig/McpClientManagerdessa sessão. O escopo'pool'é reservado para a célula com escopo de pool do Wave 5 PR 23 que ficará ao lado das células com escopo de sessão. Os consumidores devem tolerar valores adicionais desconhecidos descopedescartando-os em vez de falhar.
Referências
packages/core/src/tools/mcp-workspace-budget.ts(classe inteira)packages/core/src/tools/mcp-client-manager.ts(BudgetExhaustedError,McpBudgetEvent, hysteresis constants)packages/core/src/tools/mcp-transport-pool.ts(local doacquiredo pool que chamatryReserve)- Documento de design F2 (v2.2):
../../design/f2-mcp-transport-pool.md§11 para orçamento em nível de workspace e as entradas do changelog v2.2 sobre orçamento e acompanhamentos de fingerprint. - Notas de design F2: issue #4175 commit 6.