ACP Bridge
Visão Geral
packages/acp-bridge/ gerencia a fronteira entre a camada HTTP do daemon e o processo filho ACP. É consumido por packages/cli/src/serve/ (o daemon qwen serve) e foi extraído no #4175 F1 passo 3 para que futuros consumidores (channels/base/AcpBridge.ts, o companheiro de IDE VS Code) possam usar o mesmo núcleo da ponte sem acessar o pacote CLI.
A ponte fornece uma instância HttpAcpBridge, um AcpChannel para o filho ACP, sessões multiplexadas sobre esse canal, EventBuses por sessão, um MultiClientPermissionMediator, um adaptador BridgeFileSystem e ajudantes orientados ao ACP (spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, respondToPermission, além de RPCs extMethod para status do workspace e reinício do MCP).
Responsabilidades
- Iniciar ou anexar ao filho ACP via uma
ChannelFactoryplugável. Fábrica padrão:defaultSpawnChannelFactory(subprocessoqwen --acp). Testes injetaminMemoryChannel. - Manter
aliveChannels(registro de canais) ebyId(registro de sessões). - Multiplexar N sessões do lado HTTP em um filho ACP via
connection.newSession(). - Serializar prompts por sessão através de
promptQueue(ACP impõe um prompt ativo por sessão). - FIFO por sessão para chamadas
setSessionModelpara que anexos concorrentes com modelos diferentes não disputem o agente. EventBuspor sessão que alimentaGET /session/:id/events(veja10-event-bus.md).- Fluxo de permissão:
BridgeClient.requestPermission→MultiClientPermissionMediator.request→ dispersão → coleta de votos → resposta ACP (veja04-permission-mediation.md). - E/S de arquivo: adaptador
BridgeFileSystempara chamadas ACPreadTextFile/writeTextFile(veja07-workspace-filesystem.md). - RPCs extMethod para status do workspace (
/workspace/mcp,/workspace/skills,/workspace/providers) e reinício do MCP. - Ciclo de vida:
shutdown()graciosa comKILL_HARD_DEADLINE_MS(10s) por canal;killAllSync()síncrona para saída forçada em segundo sinal.
Arquitetura
Entrada pública: createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge em packages/acp-bridge/src/bridge.ts.
Tipos principais:
| Tipo | Arquivo | Função |
|---|---|---|
HttpAcpBridge | bridgeTypes.ts | Interface pública: spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, subscribeEvents, respondToPermission, getWorkspaceMcpStatus, restartMcpServer, shutdown, killAllSync, … |
BridgeSession | bridgeTypes.ts | { sessionId, workspaceCwd, attached, clientId?, createdAt? } retornado aos manipuladores HTTP. |
BridgeOptions | bridgeOptions.ts | Configuração em tempo de construção (veja Configuração). |
AcpChannel | channel.ts | { stream, kill(), killSync(), exited } — um canal ACP NDJSON. |
ChannelFactory | channel.ts | (workspaceCwd, childEnvOverrides?) => Promise<AcpChannel>. |
BridgeClient | bridgeClient.ts | Encapsula uma ClientSideConnection ACP; implementa Client ACP (requestPermission, readTextFile, writeTextFile, sessionUpdate, extNotification). |
EventBus | eventBus.ts | Pub/sub em memória por sessão. Veja 10-event-bus.md. |
MultiClientPermissionMediator | permissionMediator.ts | Mediador de quatro políticas. Veja 04-permission-mediation.md. |
Estado interno (fechado por createHttpAcpBridge): |
| Estado | Forma | Propósito |
|---|---|---|
aliveChannels | Map<string, ChannelInfo> | Registro de canais, indexado pelo id do canal. Cada ChannelInfo contém channel, connection, client (um BridgeClient por canal), sessionIds: Set<string>, pendingRestoreIds, statusClosedReject?, isDying: boolean. |
byId | Map<string, SessionEntry> | Registro de sessões, indexado por sessionId. Cada SessionEntry contém channel, connection, events: EventBus, promptQueue: Promise<void>, modelChangeQueue: Promise<void>, pendingPermissionIds: Set<string>, clientIds: Map<string, count>, activePromptOriginatorClientId?, attachCount, spawnOwnerWantedKill, restoreState?, sessionLastSeenAt?, clientLastSeenAt: Map<string, ms>. |
defaultEntry | SessionEntry | null | A sessão “única” usada quando sessionScope: 'single'. |
defaultPolicy | PermissionPolicy | Configurada via BridgeOptions.permissionPolicy. |
mediator | MultiClientPermissionMediator | Um por instância do bridge. |
| Constantes | — | DEFAULT_INIT_TIMEOUT_MS = 10_000, MCP_RESTART_TIMEOUT_MS = 300_000, DEFAULT_MAX_SESSIONS = 20, MAX_EVENT_RING_SIZE = 1_000_000, DEFAULT_PERMISSION_TIMEOUT_MS = 5min, DEFAULT_MAX_PENDING_PER_SESSION = 64. |
Invariante isDying: qualquer caminho de desmontagem deve definir ChannelInfo.isDying = true de forma síncrona antes de aguardar channel.kill(). ensureChannel trata um canal moribundo como ausente e cria um novo. Sem essa flag, um spawnOrAttach concorrente que chegue durante a janela de grace do SIGTERM (até 10s) se anexaria a um transporte prestes a fechar e o sessionId do chamador retornaria 404 em toda requisição subsequente. Locais de definição (devem permanecer em sincronia): ensureChannel (falha na inicialização + rechecagem de desligamento tardio), doSpawn (falha de nova sessão em canal vazio), killSession (última sessão saindo), shutdown (em lote).
Invariante de retenção de channelInfo: não limpe channelInfo ao definir isDying = true. killAllSync ainda deve encontrar o canal durante a janela de grace do SIGTERM para disparar SIGKILL em process.exit(1). aliveChannels mantém a entrada moribunda até que channel.exited seja disparado.
Buffer limitado do BridgeClient: Frames extNotification do ACP que chegam no BridgeClient para um sessionId ainda não presente em byId (porque a resposta de connection.newSession ainda não retornou, mas a descoberta MCP dentro de newSession já disparou eventos de orçamento) são armazenados em buffer em uma fila de eventos antecipados limitada por MAX_EARLY_EVENT_SESSIONS = 64 × MAX_EARLY_EVENTS_PER_SESSION = 32 × EARLY_EVENT_TTL_MS = 60_000. O pior caso é aproximadamente 400 KB de heap. Sem o buffer, o primeiro slot do ring de replay SSE para uma nova sessão perderia eventos que ocorreram durante sua criação.
Fluxo de Trabalho
spawnOrAttach (ponto de entrada principal)
Pontos principais:
sessionScope='single'com umdefaultEntryexistente apenas incrementaattachCount, registraclientIde retornaattached: true.- O caminho frio executa a ChannelFactory, realiza a inicialização ACP (
DEFAULT_INIT_TIMEOUT_MS=10s), chamaconnection.newSession({cwd})e então registra a novaSessionEntry. SessionLimitExceededErroré lançado quandobyId.size >= maxSessions.InvalidClientIdErroré lançado seX-Qwen-Client-Idestiver fora de[A-Za-z0-9._:-]{1,128}.- O coletor de desconexão (disconnect-reaper) em
server.tsrastreia o dono da criação viaattachCount/spawnOwnerWantedKillpara evitar derrubar uma sessão cujo dono da criação desconectou, mas outros clientes já estão anexados (revisão #3889 BQ9tV).
Serialização de prompt
Falhas na cauda da fila são engolidas para que a rejeição de um prompt anterior não contamine prompts subsequentes; o chamador original ainda recebe a rejeição em sua própria promessa retornada. A transportClosedReject armazenada em cache na sessão faz a promessa do prompt competir com channel.exited para que um filho que falhou apareça imediatamente, em vez de travar.
Fluxo de permissão (visão geral)
InvalidPermissionOptionError é lançado antes do mediador quando um voto da rede tenta injetar CANCEL_VOTE_SENTINEL através do campo normal optionId — o sentinela é a única saída de emergência da bridge para encurtar uma requisição como cancelled / agent_cancelled e não deve ser acessível pela rede acidentalmente. Veja 04-permission-mediation.md.
Desligamento
Fábrica de canais
AcpChannel (channel.ts) é a abstração de transporte da bridge. A produção usa defaultSpawnChannelFactory em spawnChannel.ts, que executa qwen --acp como um subprocesso com um par de pipes stdio. Testes injetam inMemoryChannel para executar o agente in-process. A bridge não sabe nada sobre o mecanismo subjacente — ela só precisa de { stream, kill, killSync, exited }.
ChannelFactory aceita childEnvOverrides para que cada handle de daemon possa passar suas próprias variáveis de ambiente de orçamento MCP (QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE) sem modificar process.env (o que causaria concorrência quando dois daemons incorporados rodam no mesmo processo Node).
Estado e Ciclo de Vida
- A construção da bridge é síncrona; o primeiro
spawnOrAttachfaz cold-start do filho do ACP. defaultEntryvive pelo tempo de vida da bridge sobsessionScope: 'single'; o canal é liberado quandosessionIds.size === 0(apóskillSession) EisDyingse torna verdadeiro.MAX_EVENT_RING_SIZE = 1_000_000é um limite superior suave paraBridgeOptions.eventRingSizepara capturar erros de digitação do operador antes de OOMs de ~500 MB por sessão.DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 60 * 1000evita que uma solicitação de permissão travada bloqueie opromptQueuepor sessão para sempre.DEFAULT_MAX_PENDING_PER_SESSION = 64espelhaDEFAULT_MAX_SUBSCRIBERS; chamadasrequestPermissionexcedentes são resolvidas como canceladas com um aviso no stderr.
Dependências
| Upstream | Downstream |
|---|---|
@agentclientprotocol/sdk — ClientSideConnection, PROTOCOL_VERSION, tipos ACP | packages/cli/src/serve/ (o daemon) |
@qwen-code/qwen-code-core — ApprovalMode, TrustGateError, getCurrentGeminiMdFilename | packages/channels/base/ (planejado, F4) |
node:crypto, node:fs, node:path | packages/vscode-ide-companion/ (planejado, F4) |
Configuração
BridgeOptions (bridgeOptions.ts):
| Chave | Padrão | Propósito |
|---|---|---|
boundWorkspace | (obrigatório) | Caminho canônico do workspace que a bridge impõe. |
sessionScope | 'single' | 'single' compartilha uma sessão entre todos os clientes; 'thread' cria uma sessão separada para cada thread de conversa. |
channelFactory | defaultSpawnChannelFactory | Fábrica plugável do filho do ACP. |
initializeTimeoutMs | DEFAULT_INIT_TIMEOUT_MS = 10_000 | Timeout do handshake ACP initialize. |
maxSessions | DEFAULT_MAX_SESSIONS = 20 | Limite de byId.size. 0 / Infinity = ilimitado; NaN/negativo lança erro. |
eventRingSize | DEFAULT_RING_SIZE (de eventBus.ts) | Anel de eventos por sessão; limitado suavemente por MAX_EVENT_RING_SIZE. |
permissionResponseTimeoutMs | DEFAULT_PERMISSION_TIMEOUT_MS = 5 min | Tempo real máximo por requisição para o mediador. |
maxPendingPermissionsPerSession | DEFAULT_MAX_PENDING_PER_SESSION = 64 | Contrapressão para agentes de alto volume. |
childEnvOverrides | {} | Adições/remoções de env por handle para o filho do ACP. |
persistApprovalMode, persistDisabledTools | — | Hooks de escrita de configuração para as rotas de mutação da Wave 4. |
contextFilename | do context.fileName em settings.json | Substitui getCurrentGeminiMdFilename. |
statusProvider | (nenhum) | Células de pré-voo hospedadas pelo daemon (DaemonStatusProvider). |
fileSystem | (nenhum) | Adaptador BridgeFileSystem para readTextFile / writeTextFile do ACP. |
permissionPolicy | do policy.permissionStrategy em settings.json | Um de first-responder / designated / consensus / local-only. |
permissionConsensusQuorum | de settings.json | N para política de consenso. |
permissionAudit | createNoOpPermissionAuditPublisher() | Conecta a PermissionAuditRing para a trilha de auditoria. |
channelIdleTimeoutMs | 0 | Mantém o filho do ACP ativo por este número de milissegundos após o fechamento da última sessão. |
Métodos adicionais de bridge
Além das chamadas principais spawnOrAttach, sendPrompt, cancelSession,
respondToPermission, loadSession e resumeSession, a interface
HttpAcpBridge agora inclui estes auxiliares voltados ao daemon:
| Método | Finalidade |
|---|---|
generateSessionRecap(sessionId, context?) | Gerar um resumo de sessão em uma linha. |
generateSessionBtw(sessionId, question, signal?, context?) | Responder a uma pergunta lateral / prompt btw. |
executeShellCommand(sessionId, command, signal?, context?) | Executar um comando shell no host do daemon. |
getSessionContextUsageStatus(sessionId, opts?) | Retornar o uso da janela de contexto. |
getSessionSupportedCommandsStatus(sessionId) | Retornar os comandos de barra disponíveis. |
getSessionTasksStatus(sessionId) | Retornar um snapshot de tarefas em segundo plano. |
getSessionStatsStatus(sessionId) | Retornar estatísticas de uso da sessão. |
setSessionApprovalMode(sessionId, mode, opts, context?) | Atualizar o modo de aprovação de uma sessão. |
detachClient(sessionId, clientId?) | Desanexar um cliente explicitamente. |
addRuntimeMcpServer(name, config, originatorClientId) | Adicionar um servidor MCP em tempo de execução. |
removeRuntimeMcpServer(name, originatorClientId) | Remover um servidor MCP em tempo de execução. |
manageMcpServer(serverName, action, originatorClientId) | Ativar / desativar / autenticar / limpar autenticação. |
generateWorkspaceAgent(description, originatorClientId) | Gerar uma definição de subagente com IA. |
preheat() | Aquecer o processo ACP antes da primeira sessão. |
getSessionLastEventId(sessionId) | Ler o ID monotônico de evento da sessão. |
getWorkspaceToolsStatus() | Retornar o snapshot do registro de ferramentas integradas. |
getWorkspaceMcpToolsStatus(serverName) | Retornar ferramentas para um servidor MCP específico. |
BridgeSpawnRequest.sessionScope foi renomeado de 'per-client' para
'thread'. BridgeRestoredSession agora carrega compactedReplay,
liveJournal e lastEventId. BridgeClientRequestContext é o contexto
de requisição propagado nas chamadas de bridge; ele carrega clientId,
fromLoopback: boolean e promptId.
Ressalvas e Limitações Conhecidas
MCP_RESTART_TIMEOUT_MS = 300_000(5 min) — o timeout da bridge para/workspace/mcp/:server/restarté intencionalmente grande porqueMcpClientManager.MAX_DISCOVERY_TIMEOUT_MSpode chegar a 5 min para servidores stdio. Um prazo menor geraria falsos timeouts enquanto o processo ACP continuasse reconectando em segundo plano.BridgeOptions.eventRingSize > 1_000_000lança exceção na construção.connection.unstable_resumeSessioné exposto através da capacidade de daemon estávelsession_resume;unstable_session_resumecontinua sendo anunciado como um alias de compatibilidade obsoleto para SDKs antigos. Clientes devem fazer detecção de funcionalidade comsession_resume.- O pacote da bridge é
@qwen-code/acp-bridgee é consumido por meio de shims de reexportação emserve/event-bus.ts,serve/status.ts,serve/httpAcpBridge.tspara compatibilidade retroativa com caminhos de importação pré‑F1. Código novo deve importar diretamente.
Referências
packages/acp-bridge/src/bridge.ts(esp.createHttpAcpBridgena linha 350+)packages/acp-bridge/src/bridgeClient.tspackages/acp-bridge/src/bridgeTypes.tspackages/acp-bridge/src/bridgeOptions.tspackages/acp-bridge/src/channel.tspackages/acp-bridge/src/spawnChannel.tspackages/acp-bridge/src/bridgeErrors.ts- Issues: #3803 , #4175 .