Cliente Daemon do SDK TypeScript
Visão Geral
packages/sdk-typescript/src/daemon/ é o cliente daemon do SDK TypeScript. É a maneira oficial de se conectar a um daemon qwen serve em execução a partir de qualquer host TypeScript / JavaScript (o adaptador TUI da própria CLI, backends de bots de canal, o companion de IDE do VS Code, scripts personalizados e backends web server-side). Todos os outros adaptadores dependem dele.
A estrutura do pacote é intencionalmente pequena:
| Arquivo | Superfície |
|---|---|
index.ts | Barrel público (DaemonClient, DaemonSessionClient, DaemonAuthFlow, parseSseStream, redutores de eventos, tipos). |
DaemonClient.ts | Facade HTTP/SSE de baixo nível — um método por rota do qwen-serve-protocol.md. |
DaemonSessionClient.ts | Wrapper com escopo de sessão e rastreamento de replay de SSE. |
DaemonAuthFlow.ts | Helper de device-flow OAuth de alto nível. |
sse.ts | parseSseStream (parser de framing NDJSON / SSE). |
events.ts | asKnownDaemonEvent, reduceDaemonSessionEvent, reduceDaemonAuthEvent (veja 09-event-schema.md). |
types.ts | DaemonCapabilities, DaemonSession, DaemonEvent, PermissionResponse, PromptResult, tipos de MCP / agent / memory / auth. |
O exemplo passo a passo está em ../examples/daemon-client-quickstart.md; este documento é a referência de arquitetura e contrato.
Responsabilidades
- Fornecer um método TypeScript por rota HTTP do daemon.
- Aplicar corretamente o bearer token +
X-Qwen-Client-Idem cada requisição. - Combinar timeouts por chamada com o
AbortSignalfornecido pelo chamador (sem encerrar SSEs de longa duração). - Transmitir e analisar frames SSE em
DaemonEvents tipados. - Rastrear
lastSeenEventIdpor sessão para que as reconexões façam o replay corretamente. - Expor uma interface de autenticação device-flow que faz polling nos intervalos fornecidos pelo daemon.
Arquitetura
DaemonClient (DaemonClient.ts)
Construtor:
new DaemonClient({
baseUrl: string, // padrão 'http://127.0.0.1:4170'
token?: string,
fetch?: typeof globalThis.fetch, // injetável para testes
fetchTimeoutMs?: number, // 0 = desativado; padrão DEFAULT_FETCH_TIMEOUT_MS
});Grupos de métodos (cada método aceita um clientId opcional para aplicar X-Qwen-Client-Id):
| Grupo | Métodos |
|---|---|
| Infraestrutura | health(), capabilities(), auth (lazy DaemonAuthFlow accessor) |
| Sessões | createOrAttachSession, loadSession, resumeSession, listSessions, closeSession, setSessionMetadata, getSessionContext, getSessionSupportedCommands, setSessionApprovalMode, setSessionModel |
| Prompting | prompt, cancel, heartbeat |
| Eventos | subscribeEvents (SSE generator), subscribeEventsStream (raw response) |
| Permissões | respondToPermission, respondToSessionPermission |
| Snapshots de workspace | getWorkspaceMcp, getWorkspaceSkills, getWorkspaceProviders, getWorkspaceEnv, getWorkspacePreflight |
| Mutações de workspace | addWorkspace, updateWorkspace, writeWorkspaceMemory, readWorkspaceMemory, rememberWorkspaceMemory, getWorkspaceMemoryRememberTask, forgetWorkspaceMemory, getWorkspaceMemoryForgetTask, dreamWorkspaceMemory, getWorkspaceMemoryDreamTask, listWorkspaceAgents, getWorkspaceAgent, createWorkspaceAgent, updateWorkspaceAgent, deleteWorkspaceAgent, setWorkspaceToolEnabled, setWorkspaceSkillEnabled, restartMcpServer, initWorkspace |
| Arquivos | readFile, readFileBytes, writeFile, editFile, listDirectory, globPaths, statPath |
| Autenticação | startDeviceFlow, pollDeviceFlow, cancelDeviceFlow, getAuthStatus |
fetchWithTimeout
Toda requisição passa por fetchWithTimeout. Detalhes críticos:
- A leitura do body está dentro do escopo do timer. Implementações anteriores limpavam o timer quando os headers chegavam; se um proxy travasse no meio do body,
await res.json()poderia travar além defetchTimeoutMs. A forma atual passa o código de leitura do body como um callback para que o timer cubra tanto a chegada dos headers QUANTO o consumo do body. perCallTimeoutMspermite que uma única chamada sobrescreva o padrão de todo o cliente. O chamador mais visível érestartMcpServer: o SDK usaMCP_RESTART_DEFAULT_TIMEOUT_MS = 330_000(5 min 30s). O próprioMCP_RESTART_TIMEOUT_MSdo daemon é exatamente 300s; se o cliente correspondesse a esse valor, um restart que completasse perto de 300s poderia perder a corrida enquanto o daemon serializa e envia sua resposta estruturada, causando umTimeoutErrorfalso-positivo. Os 30s extras cobrem serialização, transferência de rede e decodificação em ambos os lados. Chamadores que precisam de um orçamento mais restrito podem passartimeoutMs; passar0desativa o timeout.AbortSignal.anycompõe o signal fornecido pelo chamador com o signal do timer por chamada, para que o cancelamento do chamador e o timeout por chamada abortem de forma limpa.AbortController+setTimeoutcancelável em vez deAbortSignal.timeout(), para que requisições de resolução rápida não vazem timers pendentes no event loop. O timer é limpo nofinally.- Endpoints de streaming (
subscribeEvents) ignoram o timeout — SSEs de longa duração não devem ser encerrados por ele.
DaemonSessionClient (DaemonSessionClient.ts)
Vincula uma sessão e rastreia automaticamente lastSeenEventId para que o replay e a reconexão de SSE funcionem sem estado extra do chamador.
class DaemonSessionClient {
readonly client: DaemonClient;
readonly session: DaemonSession;
readonly state: DaemonSessionState;
private lastSeenEventId: number | undefined;
static createOrAttach(client, req?): Promise<DaemonSessionClient>;
static load(client, sessionId, req?): Promise<DaemonSessionClient>;
static resume(client, sessionId, req?): Promise<DaemonSessionClient>;
events(opts?: DaemonSessionSubscribeOptions): AsyncIterable<DaemonEvent>;
prompt(req: PromptRequest): Promise<PromptResult>;
cancel(): Promise<void>;
respondToPermission(...): Promise<PermissionResponse>;
setModel(modelServiceId): Promise<SetModelResult>;
heartbeat(): Promise<HeartbeatResult>;
setMetadata(metadata): Promise<SessionMetadataResult>;
close(): Promise<void>;
}events() faz o proxy de client.subscribeEvents com resume: true por padrão — ele passa o lastSeenEventId rastreado para que as reconexões façam replay de onde a assinatura anterior parou. Cada evento gerado incrementa lastSeenEventId.
DaemonAuthFlow (DaemonAuthFlow.ts)
class DaemonAuthFlow {
start(opts: { providerId, ... }): Promise<DaemonAuthFlowHandle>;
}
interface DaemonAuthFlowHandle {
deviceFlowId: string;
providerId: string;
expiresAt: string;
verificationUrl: string;
userCode: string;
awaitCompletion(opts?): Promise<DaemonAuthDeviceFlowState>;
cancel(): Promise<void>;
}awaitCompletion() faz polling de GET /workspace/auth/device-flow/:id no intervalMs fornecido pelo daemon até que o flow se torne authorized, failed ou cancelled. Ele é construído lazy via client.auth, então clientes que nunca tocam na autenticação não incorrem em custo de alocação.
parseSseStream (sse.ts)
Transforma um Response.body (ReadableStream<Uint8Array>) em AsyncIterable<DaemonEvent>. Lida com:
- Framing LF e CRLF.
- Limite de estouro de buffer (16 MiB) — limite defensivo contra um daemon emitindo um único frame absurdamente grande.
- Conexão do AbortSignal — o abort fecha o stream e o iterador.
- Frames apenas com comentários e tipos de eventos desconhecidos (passados como
DaemonEvent; os consumidores do SDK refinam downstream viaasKnownDaemonEvent).
Tipos (types.ts)
Exportações notáveis: DaemonCapabilities, DaemonSession ({ sessionId, workspaceCwd, attached, clientId?, createdAt? }), DaemonEvent, DaemonSessionState, DaemonSessionContextStatus, DaemonSessionSupportedCommandsStatus, PermissionResponse, PromptResult, HeartbeatResult, SetModelResult, SessionMetadataResult, além de tipos de resultado de MCP / agent / memory / auth. Os tipos de tarefa de memória de workspace gerenciada incluem DaemonWorkspaceMemoryRememberTask, DaemonWorkspaceMemoryForgetTask e DaemonWorkspaceMemoryDreamTask.
Helpers de tarefas de memória gerenciada de workspace:
await client.rememberWorkspaceMemory('Use strict TypeScript.', {
contextMode: 'workspace',
});
await client.getWorkspaceMemoryRememberTask('remember-...');
await client.forgetWorkspaceMemory('old preference');
await client.getWorkspaceMemoryForgetTask('forget-...');
await client.dreamWorkspaceMemory();
await client.getWorkspaceMemoryDreamTask('dream-...');Toggles de skills de workspace estão disponíveis em ambos os formatos do cliente:
await client.setWorkspaceSkillEnabled('review', false, {
clientId: 'dashboard-1',
});
await client
.workspaceByCwd('/work/secondary')
.setWorkspaceSkillEnabled('review', true, { clientId: 'dashboard-1' });Pre-flight capabilities.features.includes('workspace_skill_settings_toggle'). O DaemonSkillToggleResult tipado reporta o skillName solicitado após trim, se o estado em disco foi alterado (changed), o estado de ativação (applied, deferred, reconciling ou partial) e contagens de sessões atualizadas/com falha. reconciling significa que a escrita foi persistida e o coordenador de workspace enfileirou o refresh do runtime. A escrita é apenas nas configurações e não requer que o nome apareça em DaemonWorkspaceSkillStatus; o campo opcional apenas false userInvocable desse tipo de status continua útil para renderizar o catálogo ao vivo, mas não controla a persistência. A tag obsoleta workspace_skill_toggle descrevia o comportamento anterior validado pelo catálogo e não é anunciada para este contrato.
Para alterações em lote, faça pre-flight de workspace_skill_settings_batch_toggle e chame qualquer forma do cliente com o mesmo contrato:
await client.setWorkspaceSkillsEnabled(['review', 'deploy'], false, {
clientId: 'dashboard-1',
});
await client
.workspaceByCwd('/work/secondary')
.setWorkspaceSkillsEnabled(['review', 'deploy'], true);DaemonSkillBatchToggleResult contém results ordenados, um array de compatibilidade errors e contagens de ativação/refresh de sessão em nível de lote. Os daemons atuais processam cada nome estruturalmente válido na ordem da requisição, persistem todas as alterações de declaração resultantes juntas em no máximo uma escrita bloqueada de configurações, atualizam sessões ativas uma vez quando algo mudou e retornam um array errors vazio sem consultar o catálogo de Skills carregado. Habilitar registra um opt-in explícito de skills.enabled do workspace mesmo para nomes ainda não instalados, de forma que pode sobrescrever a desativação interna da Extensão; uma declaração idêntica repetida continua sendo uma no-op. Os tipos de item de erro permanecem disponíveis para que o SDK ainda possa decodificar respostas de daemons mais antigos. O método lança exceção em resposta não-200.
A ativação em lote de Extensões V2 retém o modelo assíncrono de operações de Extensões. Faça pre-flight de extension_batch_activation_v2, submeta um lote padrão global ou um lote de substituição de workspace selecionado, e então faça poll com o helper de operação existente:
const globalHandle = await client.setExtensionDefaultActivations(
['formatter', 'review-tools'],
'disabled',
'dashboard-1',
);
const workspaceHandle = await client
.workspaceByCwd('/work/secondary')
.setExtensionActivations(
['formatter', 'review-tools'],
'inherit',
'dashboard-1',
);
const operation = await client.waitForExtensionOperation(workspaceHandle);O resultado terminal da operação contém results ordenados. Os alvos não precisam estar instalados ao definir enabled ou disabled: o daemon armazena uma declaração de nome e preserva essa política de ativação quando uma Extensão com esse nome é instalada posteriormente. Todos os alvos alterados compartilham uma geração do Extension Store. Quando extension_activation_explicit_refresh é anunciado, as operações de ativação terminam após o commit durável da política sem atualizar as sessões ativas. Um chamador que precise de aplicação imediata deve então submeter workspace.refreshExtensionRuntime() para cada workspace cujas sessões precisam aplicar a mudança imediatamente; o refresh é uma operação separada e pode ser aguardado ou deixado em background. Um lote padrão global altera a ativação padrão que cada workspace registrado herda, a menos que esse workspace tenha uma substituição exata para o nome (ou corresponda a uma regra de caminho legada), e não possui um único refresh que cubra cada runtime; um lote de workspace altera apenas o runtime confiável selecionado. Daemons mais antigos já fazem o refresh dentro da operação de ativação, então os clientes não devem submeter o refresh extra a menos que a capability esteja presente. O reconciliador de geração de 30 segundos permanece como um caminho independente de convergência eventual para workspaces que o chamador não atualizou. O inherit do workspace limpa a substituição exata, mas não cria uma declaração para um nome desconhecido; uma limpeza totalmente desconhecida é bem-sucedida como no-op. Os métodos de ativação singular permanecem apenas para instalados.
Para trocas de Extension Skill internas ao workspace, faça pre-flight de extension_state e use os métodos REST agrupados por recurso. Estes não escrevem configurações de Skill nem ativam uma Extensão pai desativada:
const workspace = client.workspaceByCwd('/work/secondary');
const state = await workspace.extensionState(extensionId);
const handle = await workspace.setExtensionState(extensionId, {
skills: [
{ name: 'review', state: 'enabled' },
{ name: 'deploy', state: 'disabled' },
],
});
const updated = await client.waitForExtensionOperation(handle);WorkspaceExtensionState reporta os padrões do manifesto, substituições exatas do workspace e o estado efetivo consciente das configurações. A operação retorna resourceStates.skills ordenados e pode ser bem-sucedida com avisos de refresh. Apenas o grupo skills é suportado. Não rebaixe essas chamadas para setWorkspaceSkillEnabled, que escreve configurações de prioridade mais alta.
Nomes de exibição de workspace são metadados de apresentação opcionais. Pre-flight capabilities.features.includes('workspace_display_name'); os ids de workspace e os caminhos canônicos continuam sendo os únicos seletores, e nomes de exibição duplicados são válidos.
const workspace = await client.addWorkspace('/srv/repos/payments', {
persist: true,
displayName: 'Payments Production',
});
await client.updateWorkspace(workspace.id, {
displayName: 'Payments',
});
await client.updateWorkspace(workspace.id, { displayName: null });addWorkspace aceita displayName?: string e o retorna quando definido. updateWorkspace aceita um seletor por ID ou cwd e { displayName: string | null }; null limpa o nome. Os nomes são limitados a 256 caracteres após trim e rejeitam caracteres de controle C0/DEL internos. Um workspace local ao processo mantém seu nome apenas para o processo atual do daemon; registros persistentes correspondentes são atualizados através do store existente. DaemonWorkspaceCapability.displayName continua opcional para que o SDK continue interoperando com daemons mais antigos.
Fluxo de trabalho
Create-or-attach + primeiro prompt
Inscrição com replay
Autenticação device-flow
qwen-oauth é o identificador legado do provedor v1. O nível gratuito do Qwen OAuth foi descontinuado em 15/04/2026, portanto, novos clientes devem preferir um provedor de autenticação atualmente suportado, quando disponível.
Estado e Ciclo de Vida
- O
DaemonClientnão mantém conexão; nada acontece na construção. Cada método abre um novofetch. - O
DaemonSessionClientretém olastSeenEventIdentre as invocações deevents(); as reconexões fazem replay a partir do último evento visto. - O
DaemonAuthFlowé lazy —client.autho constrói no primeiro acesso. - O iterador SSE é fechado quando (a) o daemon encerra o stream, (b)
AbortSignal.abort()é disparado, (c) o consumidor sai dofor awaitou (d) o limite de estouro do buffer (16 MiB) é atingido.
Dependências
globalThis.fetch(nativo no Node 18+, browser, undici, etc.). Injetável porDaemonClientpara testes.AbortController/AbortSignal.any/setTimeoutnativos.- Sem dependências transitivas em
@qwen-code/qwen-code-coreou@qwen-code/acp-bridge— o pacote do SDK é totalmente desacoplado para que consumidores externos não importem os detalhes internos do daemon.
Subpacote ui/* (#4328 + #4353 )
O SDK também exporta packages/sdk-typescript/src/daemon/ui/, um conjunto de primitivas neutras em relação ao host que transformam eventos do daemon em blocos de transcrição:
normalizeDaemonEvent(evt)mapeia os 53 eventos de wire conhecidos do daemon em 43 valoresDaemonUiEventTypeamigáveis para a UI; eventos não modelados ou malformados são normalizados paradebug.createDaemonTranscriptState()junto comreduceDaemonTranscriptEvents(state, events)projeta eventos da UI emDaemonTranscriptBlock[].createDaemonTranscriptStore()encapsula subscribe / dispatch.render.ts/terminal.tsfornecem renderizadores base para HTML e terminal, enquantotoolPreview.tsproduz resumos de chamadas de ferramentas.- Os seletores incluem
selectTranscriptBlocksOrderedByEventId,selectPendingPermissionBlocks,selectCurrentTool,selectApprovalMode,selectToolProgress,selectSubagentChildBlocks,formatMissedRangeeformatBlockTimestamp. - As constantes públicas incluem
DAEMON_PLAN_TOOL_CALL_ID. conformance.tscontém a suite de testes de consistência entre hosts.
O primeiro consumidor em produção é packages/web-shell/client/daemon/ através do DaemonSessionProvider do React. Consulte 14-cli-tui-adapter.md para a arquitetura detalhada, glossário, tabela de seletores e a relação com o legado DaemonTuiAdapter.
O subpacote é exportado a partir do subpath @qwen-code/sdk/daemon. O código existente que faz import { DaemonClient } não é afetado.
Reconexão Last-Event-ID com o SDK
Rastreamento Automático via DaemonSessionClient
O DaemonSessionClient rastreia o lastSeenEventId internamente. Cada evento gerado com um id numérico avança o cursor. Chamadas subsequentes de events() passam automaticamente o id rastreado como Last-Event-ID, para que a reconexão com replay funcione sem estado extra por parte do chamador:
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk/daemon';
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token });
const session = await DaemonSessionClient.createOrAttach(client);
// Primeira assinatura — inicia ao vivo (ou do início do ring para novas sessões).
for await (const event of session.events()) {
console.log(event.type, event.id);
// session.lastEventId é incrementado a cada frame com id.
if (shouldStop(event)) break;
}
// Reconexão — envia automaticamente Last-Event-ID: <último id visto>.
// O daemon faz o replay dos eventos perdidos do ring e depois vai para ao vivo.
for await (const event of session.events()) {
// Frames de replay chegam primeiro, depois um `replay_complete` sintético,
// depois eventos ao vivo.
handleEvent(event);
}Reconexão Manual com DaemonClient
Para um controle de nível mais baixo, use DaemonClient.subscribeEvents diretamente e gerencie o cursor você mesmo:
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token });
let cursor: number | undefined; // undefined = apenas ao vivo na primeira conexão
async function* subscribe(sessionId: string, signal: AbortSignal) {
for await (const event of client.subscribeEvents(sessionId, {
lastEventId: cursor,
signal,
})) {
// Apenas frames com id avançam o cursor.
if (event.id !== undefined) {
cursor = event.id;
}
// Lidar com lacuna de evicção do ring.
if (event.type === 'state_resync_required') {
// O estado está obsoleto — recarregar a janela de snapshot de replay limitado do daemon.
await client.loadSession(sessionId);
continue;
}
if (event.type === 'history_truncated') {
// Apenas informativo. Exibir um aviso de status e continuar aplicando
// os eventos de replay retidos; não disparar outro recarregamento.
}
yield event;
}
}Reconexão com Loop de Retry
O SDK não faz retry automático em caso de falha de rede. Implemente um loop de retry em torno de events():
async function resilientSubscribe(session: DaemonSessionClient) {
const MAX_RETRIES = 10;
const BASE_DELAY_MS = 1000;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
// `resume: true` (padrão) passa o lastSeenEventId rastreado.
for await (const event of session.events()) {
attempt = 0; // reset em caso de evento bem-sucedido
handleEvent(event);
}
break; // fim limpo do stream
} catch (err) {
const delay = BASE_DELAY_MS * 2 ** Math.min(attempt, 5);
await new Promise((r) => setTimeout(r, delay));
}
}
}Na reconexão, o daemon faz o replay de eventos com id > lastSeenEventId a partir de seu ring limitado (padrão de 8000 eventos). Se a lacuna exceder o ring, um frame state_resync_required sinaliza o cliente para chamar loadSession e reconstruir a partir da janela atual de snapshot de replay limitado. Esse snapshot pode começar com history_truncated; trate-o como um marcador de status visível ao operador, não como outra solicitação de resync.
history_truncated.fullTranscriptAvailable é uma flag de capacidade booleana. Quando true, os chamadores podem paginar o replay persistente ativo completo com DaemonClient.getSessionTranscriptPage(sessionId, { cursor, limit }); quando false, os clientes devem continuar renderizando o replay limitado normalmente.
Quando workspace_persisted_transcript é anunciado, client.workspaceById(workspaceId).getSessionTranscriptPage(sessionId, { cursor, limit }) lê o workspace registrado selecionado sem se anexar ao ACP. O método qualificado por workspace sempre usa REST nativo mesmo que o cliente tenha um transport substituível; seu cursor expira quando o daemon reinicia.
Quando workspace_session_export é anunciado, client.workspaceById(workspaceId).exportSession(sessionId, { format }) ou client.workspaceByCwd(workspaceCwd).exportSession(...) exporta a transcrição persistente ativa do workspace confiável selecionado. Retorna o DaemonSessionExportResult existente, preserva a identidade opcional do cliente e o comportamento de timeout de fetch em todo o cliente, e sempre usa REST nativo mesmo que o cliente tenha um transport substituível. Não infera o suporte server-side deste método a partir de session_export ou workspace_qualified_rest_core; daemons mais antigos mantêm exportação apenas primária.
Quando workspace_archived_session_export é anunciado, use client.workspaceById(workspaceId).exportArchivedSession(sessionId, { format }) ou o método correspondente workspaceByCwd para exportar apenas a transcrição persistente arquivada do workspace selecionado. O método usa o mesmo tipo de resultado e comportamento REST nativo da exportação ativa, mas nunca faz fallback para uma sessão ativa; o suporte não pode ser inferido a partir de nenhuma capacidade de exportação ativa.
Quando workspace_session_live_state é anunciado, client.getWorkspaceSessionLiveState(workspaceCwd) ou os métodos com escopo client.workspaceById(workspaceId).getSessionLiveState() / client.workspaceByCwd(workspaceCwd).getSessionLiveState() leem o snapshot de memória somente de sessões ativas do workspace confiável selecionado mais sua versão de catálogo, retornando DaemonWorkspaceSessionLiveState ({ v: 1, catalogVersion: DaemonSessionCatalogVersion, sessions: DaemonSessionLiveState[] }). Esses métodos sempre usam REST nativo com autenticação bearer e um seletor de workspace codificado, preservam a identidade opcional do cliente e usam o timeout existente de requisições curtas. Eles não chamam requireCapability() — uma sondagem de capability em cada poll dobraria o volume de requisições — então consumidores fazem pre-flight de workspace_session_live_state uma vez a partir de suas capabilities já carregadas e usam fallback para o polling de catálogo existente quando a tag está ausente. Não inferir suporte a partir de workspace_qualified_rest_core. Cada DaemonSessionLiveState carrega uma marca d’água de atividade opcional updatedAt que permite ao consumidor atualizar a recência de uma linha de catálogo que já possui em vez de recarregar o catálogo após um turno concluído; ela está ausente antes do primeiro terminal de turno em execução na bridge atual e após uma substituição de daemon ou runtime, então o consumidor deve manter seu fallback de catálogo existente para um valor ausente em vez de tratar a ausência como não suportada.
Inicializando lastEventId na Construção
Chamadores que persistem o cursor entre reinicializações de processo podem inicializá-lo:
const session = new DaemonSessionClient({
client,
session: { sessionId, workspaceCwd, attached: true },
lastEventId: persistedCursor, // retomar a partir da posição persistida
});O valor deve ser um inteiro finito e não negativo (validado na construção). Valores inválidos lançam exceção.
Configuração
| Parâmetro | Onde | Efeito |
|---|---|---|
baseUrl | Construtor do DaemonClient | URL do daemon; barras finais removidas. |
token | Construtor do DaemonClient | Aplicado como Authorization: Bearer. |
fetch | Construtor do DaemonClient | Ponto de injeção para testes. |
fetchTimeoutMs | Construtor do DaemonClient | Timeout por chamada; 0 = desativado. |
clientId | Argumento opcional por método | Cabeçalho X-Qwen-Client-Id (veja 08-session-lifecycle.md). |
lastEventId | Construtor do DaemonSessionClient | Inicializa o cursor de replay. |
maxQueued | Opção por subscribe | ?maxQueued=N para a rota SSE; verifique caps.features.slow_client_warning antes (pre-flight). |
perCallTimeoutMs | Por método (ex.: restartMcpServer) | Sobrescreve o timeout geral do cliente. |
Ressalvas e Limites Conhecidos
- O
fetchTimeoutMsé por chamada, não por conexão. Leituras longas de body compartilham o timer. Um daemon que faz streaming de respostas deve sobrescrever o timeout por chamada ou definir o timeout como0. - O SSE ignora o timeout do fetch — conexões SSE de longa duração não são encerradas pelo
fetchTimeoutMs. UseAbortSignalpara cancelamento controlado pelo chamador. - O limite do buffer do
parseSseStreamé de 16 MiB como uma proteção defensiva. Um único frame maior que isso aborta o iterador (o daemon nunca emite frames legítimos desse tamanho). asKnownDaemonEventretornaundefinedpara tipos de evento não reconhecidos. Os consumidores do SDK devem tratar esse caso em vez de assumir que a união é exaustiva; esse é o contrato de compatibilidade futura (forward-compatibility). Eventos não reconhecidos incrementamDaemonSessionViewState.unrecognizedKnownEventCount.client_evicted,slow_client_warningestream_errornão estão no ring de replay. Reconectar após uma evicção retoma a partir do ring do daemon; você não verá o frame de evicção novamente.- O
DaemonClientnão faz retry automático. Falhas de rede surgem como rejeições; a estratégia de reconexão / replay é responsabilidade do chamador (DaemonSessionClient.events()facilita o replay, mas a reconexão ainda é por chamada).
Referências
packages/sdk-typescript/src/daemon/DaemonClient.tspackages/sdk-typescript/src/daemon/DaemonSessionClient.tspackages/sdk-typescript/src/daemon/DaemonAuthFlow.tspackages/sdk-typescript/src/daemon/sse.tspackages/sdk-typescript/src/daemon/events.tspackages/sdk-typescript/src/daemon/types.ts- Passo a passo de ponta a ponta:
../examples/daemon-client-quickstart.md.