Клиент демона TypeScript SDK
Обзор
packages/sdk-typescript/src/daemon/ — это клиент демона TypeScript SDK. Это каноничный способ подключения к работающему демону qwen serve из любого хоста на TypeScript / JavaScript (собственный TUI-адаптер CLI, бэкенды каналов ботов, IDE-компаньон для VS Code, пользовательские скрипты и серверные веб-бэкенды). Все остальные адаптеры зависят от него.
Структура пакета намеренно минималистична:
| Файл | Публичный интерфейс |
|---|---|
index.ts | Публичный barrel (DaemonClient, DaemonSessionClient, DaemonAuthFlow, parseSseStream, редьюсеры событий и типы). |
DaemonClient.ts | Низкоуровневый HTTP/SSE фасад — один метод на каждый маршрут из qwen-serve-protocol.md. |
DaemonSessionClient.ts | Обертка для сессии с отслеживанием повтора SSE. |
DaemonAuthFlow.ts | Высокоуровневый хелпер для OAuth device-flow. |
sse.ts | parseSseStream (парсер фрейминга NDJSON / SSE). |
events.ts | asKnownDaemonEvent, reduceDaemonSessionEvent, reduceDaemonAuthEvent (см. 09-event-schema.md). |
types.ts | DaemonCapabilities, DaemonSession, DaemonEvent, PermissionResponse, PromptResult, типы MCP / агента / памяти / auth. |
Пошаговый пример находится в ../examples/daemon-client-quickstart.md; этот документ служит справочником по архитектуре и контрактам.
Ответственность
- Предоставлять один TypeScript-метод для каждого HTTP-маршрута демона.
- Корректно проставлять bearer-токен и
X-Qwen-Client-Idв каждом запросе. - Компонировать таймауты для каждого вызова с переданным вызывающим кодом
AbortSignal(не прерывая долгоживущие SSE-соединения). - Потоково принимать и парсить SSE-фреймы в типизированные
DaemonEvent. - Отслеживать
lastSeenEventIdдля каждой сессии, чтобы переподключения корректно воспроизводили пропущенные события. - Предоставлять интерфейс аутентификации device-flow, который выполняет опрос с интервалами, заданными демоном.
Архитектура
DaemonClient (DaemonClient.ts)
Конструктор:
new DaemonClient({
baseUrl: string, // default 'http://127.0.0.1:4170'
token?: string,
fetch?: typeof globalThis.fetch, // injectable for tests
fetchTimeoutMs?: number, // 0 = disabled; default DEFAULT_FETCH_TIMEOUT_MS
});Группы методов (каждый метод принимает опциональный clientId для простановки X-Qwen-Client-Id):
| Группа | Методы |
|---|---|
| Служебные | health(), capabilities(), auth (ленивый аксессор DaemonAuthFlow) |
| Сессии | createOrAttachSession, loadSession, resumeSession, listSessions, closeSession, setSessionMetadata, getSessionContext, getSessionSupportedCommands, setSessionApprovalMode, setSessionModel |
| Промпты | prompt, cancel, heartbeat |
| События | subscribeEvents (SSE-генератор), subscribeEventsStream (сырой ответ) |
| Разрешения | respondToPermission, respondToSessionPermission |
| Снимки рабочего пространства | getWorkspaceMcp, getWorkspaceSkills, getWorkspaceProviders, getWorkspaceEnv, getWorkspacePreflight |
| Изменения рабочего пространства | addWorkspace, updateWorkspace, writeWorkspaceMemory, readWorkspaceMemory, rememberWorkspaceMemory, getWorkspaceMemoryRememberTask, forgetWorkspaceMemory, getWorkspaceMemoryForgetTask, dreamWorkspaceMemory, getWorkspaceMemoryDreamTask, listWorkspaceAgents, getWorkspaceAgent, createWorkspaceAgent, updateWorkspaceAgent, deleteWorkspaceAgent, setWorkspaceToolEnabled, setWorkspaceSkillEnabled, restartMcpServer, initWorkspace |
| Файлы | readFile, readFileBytes, writeFile, editFile, listDirectory, globPaths, statPath |
| Аутентификация | startDeviceFlow, pollDeviceFlow, cancelDeviceFlow, getAuthStatus |
fetchWithTimeout
Каждый запрос проходит через fetchWithTimeout. Ключевые детали:
- Чтение тела находится в области действия таймера. В предыдущих реализациях таймер сбрасывался при получении заголовков; если прокси зависал в середине тела,
await res.json()мог зависнуть дольшеfetchTimeoutMs. Текущая реализация передает код чтения тела как колбэк, поэтому таймер покрывает как получение заголовков, так и чтение тела. perCallTimeoutMsпозволяет одному вызову переопределить дефолтное значение для всего клиента. Самый заметный вызывающий код —restartMcpServer: SDK используетMCP_RESTART_DEFAULT_TIMEOUT_MS = 330_000(5 мин 30 сек). СобственныйMCP_RESTART_TIMEOUT_MSдемона равен ровно 300 с; если бы клиент использовал то же значение, перезапуск, завершающийся около 300 с, мог бы проиграть гонку, пока демон сериализует и отправляет свой структурированный ответ, что привело бы к ложному срабатываниюTimeoutError. Дополнительные 30 с покрывают сериализацию, передачу по сети и декодирование на обеих сторонах. Вызывающий код, которому требуется более жесткий лимит, может передатьtimeoutMs; передача0отключает таймаут.AbortSignal.anyкомпонует сигнал, переданный вызывающим кодом, с сигналом таймера для конкретного вызова, поэтому и отмена вызывающим кодом, и таймаут вызова корректно прерывают операцию.AbortController+ отменяемыйsetTimeoutвместоAbortSignal.timeout(), чтобы быстро завершающиеся запросы не оставляли висящие таймеры в цикле событий. Таймер очищается в блокеfinally.- Потоковые эндпоинты (
subscribeEvents) обходят таймаут — долгоживущие SSE не должны прерываться им.
DaemonSessionClient (DaemonSessionClient.ts)
Привязывается к одной сессии и автоматически отслеживает lastSeenEventId, чтобы повтор и переподключение SSE работали без дополнительного состояния со стороны вызывающего кода.
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() проксирует client.subscribeEvents с resume: true по умолчанию — он передает отслеживаемый lastSeenEventId, чтобы при переподключении воспроизведение начиналось с того места, где остановилась предыдущая подписка. Каждое полученное событие увеличивает 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() опрашивает GET /workspace/auth/device-flow/:id с интервалом intervalMs, заданным демоном, пока поток не перейдет в состояние authorized, failed или cancelled. Он лениво создается через client.auth, поэтому клиенты, которые никогда не взаимодействуют с аутентификацией, не несут затрат на выделение памяти.
parseSseStream (sse.ts)
Преобразует Response.body (ReadableStream<Uint8Array>) в AsyncIterable<DaemonEvent>. Обрабатывает:
- Фрейминг LF и CRLF.
- Ограничение переполнения буфера (16 МиБ) — защитный лимит на случай, если демон отправит один абсурдно большой фрейм.
- Интеграция AbortSignal — прерывание закрывает поток и итератор.
- Фреймы только с комментариями и неизвестные типы событий (передаются дальше как
DaemonEvent; потребители SDK уточняют тип ниже по цепочке с помощьюasKnownDaemonEvent).
Типы (types.ts)
Основные экспорты: DaemonCapabilities, DaemonSession ({ sessionId, workspaceCwd, attached, clientId?, createdAt? }), DaemonEvent, DaemonSessionState, DaemonSessionContextStatus, DaemonSessionSupportedCommandsStatus, PermissionResponse, PromptResult, HeartbeatResult, SetModelResult, SessionMetadataResult, а также типы результатов MCP / агента / памяти / аутентификации. К типам задач управляемой памяти рабочего пространства относятся DaemonWorkspaceMemoryRememberTask, DaemonWorkspaceMemoryForgetTask и DaemonWorkspaceMemoryDreamTask.
Хелперы для задач управляемой памяти рабочего пространства:
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-...');Переключатели навыков рабочего пространства доступны в обоих вариантах клиента:
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_toggle'). Типизированный DaemonSkillToggleResult сообщает канонический skillName, изменилось ли состояние на диске (changed), состояние активации (applied, deferred или partial) и количество обновлённых/ошибочных сессий. DaemonWorkspaceSkillStatus.userInvocable — опциональное поле только для false; отсутствие означает, что навык доступен для вызова пользователем.
Для пакетных изменений выполните pre-flight workspace_skill_batch_toggle и вызовите любой из вариантов клиента с тем же контрактом:
await client.setWorkspaceSkillsEnabled(['review', 'deploy'], false, {
clientId: 'dashboard-1',
});
await client
.workspaceByCwd('/work/secondary')
.setWorkspaceSkillsEnabled(['review', 'deploy'], true);DaemonSkillBatchToggleResult содержит упорядоченные успешные results, errors для каждой цели и batch-level счётчики активации/обновления сессий. Демон сохраняет валидные цели вместе и обновляет активные сессии один раз; одна ожидаемая ошибка цели не блокирует остальные валидные цели. Метод выбрасывает исключение только при ответе с кодом, отличным от 200; ответ 200 не означает, что каждая цель была применена, поэтому всегда проверяйте errors перед тем, как считать пакет успешным.
Пакетная активация расширений V2 сохраняет асинхронную модель операций расширений. Выполните pre-flight extension_batch_activation_v2, отправьте глобальный пакет по умолчанию или пакет-переопределение для выбранного рабочего пространства, затем опрашивайте его с помощью существующего хелпера операций:
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);Результат завершающей операции содержит упорядоченные results. Цели не обязаны быть установлены при задании enabled или disabled: демон сохраняет объявление имени и сохраняет эту политику активации, когда расширение с таким именем устанавливается позже. Все изменённые цели разделяют одну генерацию Extension Store и один проход согласования. Глобальные пакеты по умолчанию согласуют каждый зарегистрированный runtime; пакеты рабочего пространства разрешают и согласуют только выбранный доверенный runtime. inherit для рабочего пространства очищает конкретное переопределение, но не создаёт объявление для неизвестного имени; очистка всех неизвестных целей успешно выполняется как no-op без согласования. Единичные методы активации остаются только для установленных расширений.
Отображаемые имена рабочего пространства — это опциональные метаданные для представления. Pre-flight capabilities.features.includes('workspace_display_name'); id рабочего пространства и канонические пути остаются единственными селекторами, дублирующиеся отображаемые имена допустимы.
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 принимает displayName?: string и возвращает его, если он задан. updateWorkspace принимает селектор по id или cwd и { displayName: string | null }; null очищает имя. Имена ограничены 256 символами после обрезки и отклоняют внутренние управляющие символы C0/DEL. Локальное для процесса рабочее пространство сохраняет своё имя только для текущего процесса демона; совпадающие постоянные регистрации обновляются через существующее хранилище. DaemonWorkspaceCapability.displayName остаётся опциональным, чтобы SDK продолжал взаимодействовать со старыми демонами.
Рабочий процесс
Create-or-attach + первый запрос
Подписка с повтором
Аутентификация через device-flow
qwen-oauth — это устаревший идентификатор провайдера v1. Бесплатный тариф Qwen OAuth был отменен 15 апреля 2026 года, поэтому новым клиентам следует использовать актуальный поддерживаемый провайдер аутентификации, если он доступен.
Состояние и жизненный цикл
DaemonClientне поддерживает постоянное соединение; при создании экземпляра ничего не происходит. Каждый метод выполняет новыйfetch-запрос.DaemonSessionClientсохраняетlastSeenEventIdмежду вызовамиevents(); при переподключении повтор (replay) начинается с последнего полученного события.DaemonAuthFlowинициализируется лениво —client.authсоздает его при первом обращении.- Итератор SSE закрывается, когда: (а) демон завершает поток, (б) срабатывает
AbortSignal.abort(), (в) потребитель прерывает циклfor awaitили (г) достигается лимит переполнения буфера (16 МиБ).
Зависимости
globalThis.fetch(встроен в Node 18+, браузер, undici и т.д.). Можно внедрить (inject) для каждогоDaemonClientв тестах.- Нативные
AbortController/AbortSignal.any/setTimeout. - Нет транзитивных зависимостей от
@qwen-code/qwen-code-coreили@qwen-code/acp-bridge— пакет SDK полностью изолирован, чтобы внешние потребители не тянули за собой внутренние компоненты демона.
Подпакет ui/* (#4328 + #4353 )
SDK также экспортирует packages/sdk-typescript/src/daemon/ui/ — независимый от хоста набор примитивов, которые преобразуют события демона в блоки транскрипта:
normalizeDaemonEvent(evt)сопоставляет 53 известных сетевых события демона с 43 удобными для UI значениямиDaemonUiEventType; немоделированные или некорректные события нормализуются вdebug.createDaemonTranscriptState()вместе сreduceDaemonTranscriptEvents(state, events)проецируют UI-события вDaemonTranscriptBlock[].createDaemonTranscriptStore()оборачивает subscribe / dispatch.render.ts/terminal.tsпредоставляют базовые рендереры для HTML и терминала, аtoolPreview.tsформирует сводки вызовов инструментов.- Селекторы включают
selectTranscriptBlocksOrderedByEventId,selectPendingPermissionBlocks,selectCurrentTool,selectApprovalMode,selectToolProgress,selectSubagentChildBlocks,formatMissedRangeиformatBlockTimestamp. - Публичные константы включают
DAEMON_PLAN_TOOL_CALL_ID. conformance.tsсодержит набор тестов на кросс-хостовую согласованность.
Первый production-потребитель — это packages/webui/src/daemon/ через React-провайдер DaemonSessionProvider. Подробную архитектуру, глоссарий, таблицу селекторов и связь с устаревшим DaemonTuiAdapter см. в 14-cli-tui-adapter.md.
Подпакет экспортируется из подпути @qwen-code/sdk/daemon. Существующий код, использующий import { DaemonClient }, не затрагивается.
Переподключение с Last-Event-ID через SDK
Автоматическое отслеживание через DaemonSessionClient
DaemonSessionClient отслеживает lastSeenEventId внутри себя. Каждое возвращаемое событие с числовым id сдвигает курсор. Последующие вызовы events() автоматически передают отслеживаемый id как Last-Event-ID, поэтому переподключение с повтором работает без дополнительного состояния на стороне вызывающего кода:
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);
// First subscription — starts live (or from ring start for new sessions).
for await (const event of session.events()) {
console.log(event.type, event.id);
// session.lastEventId is bumped on each id-bearing frame.
if (shouldStop(event)) break;
}
// Reconnect — automatically sends Last-Event-ID: <last seen id>.
// The daemon replays missed events from the ring, then goes live.
for await (const event of session.events()) {
// Replay frames arrive first, then a synthetic `replay_complete`,
// then live events.
handleEvent(event);
}Ручное переподключение с DaemonClient
Для более низкоуровневого контроля используйте DaemonClient.subscribeEvents напрямую и управляйте курсором самостоятельно:
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170', token });
let cursor: number | undefined; // undefined = live-only on first connect
async function* subscribe(sessionId: string, signal: AbortSignal) {
for await (const event of client.subscribeEvents(sessionId, {
lastEventId: cursor,
signal,
})) {
// Only id-bearing frames advance the cursor.
if (event.id !== undefined) {
cursor = event.id;
}
// Handle ring-eviction gap.
if (event.type === 'state_resync_required') {
// State is stale — reload the daemon's bounded replay snapshot window.
await client.loadSession(sessionId);
continue;
}
if (event.type === 'history_truncated') {
// Informational only. Render a status notice, then continue applying
// the retained replay events; do not trigger another reload.
}
yield event;
}
}Переподключение с циклом повторных попыток
SDK не выполняет автоматические повторные попытки при сетевых сбоях. Реализуйте цикл повторных попыток вокруг 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` (default) passes the tracked lastSeenEventId.
for await (const event of session.events()) {
attempt = 0; // reset on successful event
handleEvent(event);
}
break; // clean stream end
} catch (err) {
const delay = BASE_DELAY_MS * 2 ** Math.min(attempt, 5);
await new Promise((r) => setTimeout(r, delay));
}
}
}При переподключении демон повторяет события с id > lastSeenEventId из своего ограниченного кольцевого буфера (по умолчанию 8000 событий). Если разрыв превышает размер буфера, фрейм state_resync_required сигнализирует клиенту о необходимости вызвать loadSession и восстановиться из текущего ограниченного окна снимка повтора. Этот снимок может начинаться с history_truncated; рассматривайте его как видимый оператору маркер статуса, а не как запрос на повторную синхронизацию.
history_truncated.fullTranscriptAvailable — это булев флаг возможности. Когда он равен true, вызывающий код может листать полный активный сохранённый повтор с помощью DaemonClient.getSessionTranscriptPage(sessionId, { cursor, limit }); когда он равен false, клиенты должны продолжать обычный рендеринг ограниченного повтора.
Когда анонсируется workspace_persisted_transcript, client.workspaceById(workspaceId).getSessionTranscriptPage(sessionId, { cursor, limit }) читает выбранное зарегистрированное рабочее пространство без подключения к ACP. Метод с квалификацией рабочего пространства всегда использует нативный REST, даже если клиент имеет заменяемый транспорт; его курсор истекает при перезапуске демона.
Когда анонсируется workspace_session_export, client.workspaceById(workspaceId).exportSession(sessionId, { format }) или client.workspaceByCwd(workspaceCwd).exportSession(...) экспортирует активный сохранённый транскрипт выбранного доверенного рабочего пространства. Метод возвращает существующий DaemonSessionExportResult, сохраняет опциональную идентификацию клиента и поведение таймаута fetch для всего клиента, и всегда использует нативный REST, даже если клиент имеет заменяемый транспорт. Не выводите поддержку этого метода на сервере из session_export или workspace_qualified_rest_core; старые демоны сохраняют только основной экспорт.
Когда анонсируется workspace_archived_session_export, используйте client.workspaceById(workspaceId).exportArchivedSession(sessionId, { format }) или соответствующий метод workspaceByCwd для экспорта только архивированного сохранённого транскрипта выбранного рабочего пространства. Метод использует тот же тип результата и поведение нативного REST, что и активный экспорт, но никогда не откатывается к активной сессии; поддержку нельзя вывести ни из одной возможности активного экспорта.
Когда анонсируется workspace_session_live_state, client.getWorkspaceSessionLiveState(workspaceCwd) или скоупированные методы client.workspaceById(workspaceId).getSessionLiveState() / client.workspaceByCwd(workspaceCwd).getSessionLiveState() читают снимок живых сессий только из памяти выбранного доверенного рабочего пространства вместе с версией каталога, возвращая DaemonWorkspaceSessionLiveState ({ v: 1, catalogVersion: DaemonSessionCatalogVersion, sessions: DaemonSessionLiveState[] }). Эти методы всегда используют нативный REST с аутентификацией bearer и закодированным селектором рабочего пространства, сохраняют опциональную идентификацию клиента и используют существующий таймаут для коротких запросов. Они не вызывают requireCapability() — проверка возможностей при каждом опросе удвоила бы объём запросов, — поэтому потребители выполняют pre-flight workspace_session_live_state один раз из уже загруженных возможностей и откатываются к существующему опросу каталога, если тег отсутствует. Не выводите поддержку из workspace_qualified_rest_core. Каждый DaemonSessionLiveState несёт опциональный watermark активности updatedAt, который позволяет потребителю обновить актуальность уже имеющейся строки каталога вместо перезагрузки каталога после завершённого хода; он отсутствует до первого терминала выполняющегося хода в текущем бридже и после замены демона или runtime, поэтому потребитель должен сохранять существующий фолбэк на каталог при отсутствующем значении, а не считать отсутствие как неподдерживаемую возможность.
Инициализация lastEventId при создании
Вызывающий код, сохраняющий курсор между перезапусками процесса, может инициализировать его:
const session = new DaemonSessionClient({
client,
session: { sessionId, workspaceCwd, attached: true },
lastEventId: persistedCursor, // resume from persisted position
});Значение должно быть конечным неотрицательным целым числом (проверяется при создании). Неверные значения вызывают ошибку.
Конфигурация
| Параметр | Где | Эффект |
|---|---|---|
baseUrl | конструктор DaemonClient | URL демона; конечные слеши удаляются. |
token | конструктор DaemonClient | Добавляется как Authorization: Bearer. |
fetch | конструктор DaemonClient | Точка внедрения для тестов. |
fetchTimeoutMs | конструктор DaemonClient | Таймаут для каждого вызова; 0 = отключено. |
clientId | необязательный аргумент метода | Заголовок X-Qwen-Client-Id (см. 08-session-lifecycle.md). |
lastEventId | конструктор DaemonSessionClient | Начальное значение курсора для повтора. |
maxQueued | опция для каждой подписки | ?maxQueued=N для SSE-маршрута; сначала pre-flight caps.features.slow_client_warning. |
perCallTimeoutMs | для каждого метода (напр., restartMcpServer) | Переопределяет глобальный таймаут клиента. |
Важные замечания и известные ограничения
fetchTimeoutMsдействует для каждого вызова, а не на уровне соединения. Долгие чтения тела ответа используют общий таймер. Демон, потоково передающий ответы, должен переопределять таймаут для каждого вызова или устанавливать его в0.- SSE обходит таймаут fetch — долгоживущие SSE-соединения не прерываются по
fetchTimeoutMs. ИспользуйтеAbortSignalдля отмены на стороне вызывающего кода. - Лимит буфера
parseSseStreamсоставляет 16 МиБ в качестве защитного ограничения. Одиночный фрейм больше этого размера прерывает итератор (демон никогда легитимно не отправляет такие фреймы). asKnownDaemonEventвозвращаетundefinedдля нераспознанных типов событий. Потребители SDK должны обрабатывать эту ветку, а не предполагать, что объединение типов исчерпывающе; это контракт forward-compatibility. Нераспознанные события увеличиваютDaemonSessionViewState.unrecognizedKnownEventCount.client_evicted,slow_client_warning,stream_errorотсутствуют в кольцевом буфере повтора. Переподключение после выселения (eviction) начинается с текущего состояния кольцевого буфера демона; вы больше не увидите фрейм выселения.DaemonClientне выполняет автоматические повторные попытки. Сетевые сбои проявляются как отклонения (rejections); стратегия переподключения / повтора лежит на вызывающем коде (DaemonSessionClient.events()упрощает повтор, но переподключение всё равно выполняется для каждого вызова).
Ссылки
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- Пошаговое руководство:
../examples/daemon-client-quickstart.md.