Skip to Content
Руководство для разработчиковHTTP-протокол qwen serve

qwen serve справочник по HTTP-протоколу

Этап 1 дизайна демона qwen-code . Все маршруты находятся под базовым URL демона (по умолчанию http://127.0.0.1:4170).

В этом справочнике описаны HTTP-маршруты и форматирование SSE. Читайте его вместе со схемой типизированных событий для контрактов полезной нагрузки, причин ресинхронизации и якорей истории, руководством по шине событий и воспроизведению для доставки и обратного давления, а также возможностями и версионированием для существующего протокола v1 и правил совместимости. Индекс документации демона содержит ссылки на полный набор контрактов.

Аутентификация

Если демон был запущен с --token или QWEN_SERVER_TOKEN — или привязан к не-loopback интерфейсу без обоих флагов, что генерирует эфемерный bearer-токен и выводит его один раз при запуске — каждый обычный API-маршрут, кроме /health на стандартных loopback-привязках, должен содержать:

Authorization: Bearer <token>

Без настроенного токена на default loopback заголовок является необязательным, и запросы, поступающие через основной слушатель, имеют полные права оператора API. Проверки доверия к рабочему пространству, владения сессией, X-Qwen-Client-Id, разрешений, функций, валидации и ресурсов по-прежнему применяются. Сравнение токенов выполняется за константное время. Ответы 401 единообразны для missing header / wrong scheme / wrong token.

--open-with-auth. Этот режим CLI, отключенный по умолчанию, требует loopback-привязки и доступного Web Shell. Он повторно использует обычный выбор --token через QWEN_SERVER_TOKEN или генерирует 32 случайных байта, закодированных как base64url, перед запуском демона, если этот выбор пуст. Браузер получает выбранный bearer через #token= и хранит его для каждой вкладки; протокол и промежуточное ПО видят обычный настроенный токен. Ключи доставки фрагментов зависят от разрешенного токена, а не от этого флага: любой запуск --open прикрепляет разрешенный bearer — настроенный или сгенерированный — к фрагменту #token= запускаемого URL (видимому локальным пользователям через ps / /proc, как предупреждает лаунчер), поэтому не-loopback привязка с голым --open передает свой сгенерированный bearer браузеру тем же способом; отличительные вклады этого флага — это генерация токена на loopback и резервный вариант ручного URL для сред, не поддерживающих браузер. Только прямые встроенные вызывающие объекты, игнорирующие RunHandle.resolvedToken, и клиенты, которые никогда не запускают браузер, не получают автоматических учетных данных. В средах, не поддерживающих браузер, выводится URL с секретом во фрагменте для ручного открытия. Loopback /health и статические ресурсы Web Shell сохраняют описанные ниже исключения; --require-auth по-прежнему контролирует доступ к /health.

Входящие вебхуки каналов (POST /channels/:channelName/webhooks/:source) отделены от этого контракта bearer во всех режимах. При монтировании они регистрируются до bearerAuth и проходят аутентификацию с помощью настроенного x-qwen-webhook-secret; ротация bearer-токена демона не приводит к ротации секретов источника вебхуков.

Исключение /health (Bctum): на loopback-привязках (127.0.0.0/8 / localhost / ::1 / [::1]) /health регистрируется ДО middleware bearer, поэтому зонды работоспособности внутри пода не должны передавать токен, даже если демон был запущен с --token. Не-loopback привязки (--hostname 0.0.0.0 и т. д.) защищают /health вместе с другими обычными API-маршрутами — см. раздел GET /health для обоснования.

--require-auth (#4175 PR 15). Передайте этот флаг при загрузке, чтобы распространить правило «должен быть токен» также на loopback. Загрузка завершается ошибкой, если не удается разрешить источник токена — на loopback это означает --token, QWEN_SERVER_TOKEN или --open-with-auth (который устанавливает свой собственный сгенерированный токен перед загрузкой, поэтому --require-auth --open-with-auth запускается). Fail-fast применяется только к loopback: не-loopback привязка разрешает эфемерный bearer, который она генерирует, когда ни один из настроенных источников не присутствует, и это удовлетворяет требованию флага. Исключение /health удаляется в любом случае, поэтому /health также требует Authorization: Bearer ….

Когда флаг включен, глобальный middleware bearerAuth защищает каждый обычный API-маршрут — включая /health и /capabilities. Входящие вебхуки каналов остаются независимо аутентифицированными общим секретом, а маршруты документов и ресурсов Web Shell остаются пред-аутентификационными. Таким образом, неаутентифицированный клиент не может выполнить предварительный запрос caps.features, чтобы узнать, требуется ли аутентификация: поверхностью обнаружения для этого случая является само тело ответа 401 (единообразное для маршрутов, защищенных bearer, согласно разделу Аутентификация). Тег возможности require_auth является подтверждением после аутентификации — как только клиент успешно проходит аутентификацию и читает /capabilities, наличие тега подтверждает, что демон был запущен с --require-auth (полезно для интерфейсов аудита/соответствия требованиям и для клиентов SDK, чтобы отобразить «это развертывание усилено» в панели настроек). Строгие маршруты мутаций принимают запросы основного слушателя от доверенного loopback, запросы с bearer-аутентификацией или сопряженные запросы Local Control. Недоверенные встроенные компоненты без токена все еще получают 401 { code: "token_required", error: "…" }; при наличии --require-auth глобальный middleware bearer сначала отклоняет запрос со старым телом Unauthorized.

--allow-origin <pattern> (T2.4 #4514 ). Браузерные клиенты, обращающиеся к демону кросс-доменно, по умолчанию блокируются — запрос, содержащий заголовок Origin, возвращает 403 {"error":"Request denied by CORS policy"}, поскольку клиенты CLI/SDK никогда не отправляют Origin, и демон рассматривает его наличие как признак того, что запрос поступил из браузерного контекста, в который оператор не входил добровольно. Одно исключение предшествует стене на не-loopback привязке с токеном: однодоменный запрос (Origin равен схеме прямого сокета плюс нормализованный авторитет Host) проходит bearer-аутентификацию, и его Origin удаляется на основном слушателе — при действительном bearer статус маршрута следует дальше, при отсутствующем или недействительном bearer возвращается 401, за исключением пред-аутентификационных маршрутов документа Web Shell, /assets/*, /manifest.webmanifest, /sw.js и /mcp-app-sandbox, которые обслуживаются без учетных данных, как и в других режимах — и только кросс-доменные или несоответствующие значения Origin сохраняют конверт 403. Передайте --allow-origin <pattern> (повторяемо) при загрузке, чтобы установить список разрешений вместо стены. Каждый шаблон представляет собой:

  • Литерал * — разрешает любой источник. Рискованно: загрузка завершается ошибкой, если настроен *, но не удалось получить токен авторизации (bearer token). Проверка использует разрешённый токен — --token, сгенерированный токен для loopback-адреса из QWEN_SERVER_TOKEN или --open-with-auth, либо временный bearer-токен, генерируемый при привязке к non-loopback интерфейсу, если ни один из настроенных источников не доступен; таким образом, этот отказ касается только loopback-адресов. При загрузке в stderr выводится предупреждение, если * присутствует в списке. Рекомендация: используйте вместе с --require-auth на loopback-привязках, чтобы доступ к /health также контролировался через bearer-токен — по умолчанию он регистрируется до middleware проверки токена на loopback (чтобы пробы k8s/Compose могли обращаться к нему без токена), а allowlist с * делает его доступным из любого браузера с другим origin. --require-auth всё равно оставляет статические ресурсы Web Shell (/, /assets/*, /manifest.webmanifest, /sw.js, а также навигации документов /session/:id, /plugins, /channels, /live, /scheduled-tasks, /goals и /settings) доступными до авторизации на loopback по замыслу — они монтируются до middleware проверки токена, поэтому при allowlist с * они остаются читаемыми из любого браузера с другим origin; --no-web устраняет эту поверхность. На non-loopback привязках проверка bearer-токена уже обязательна при загрузке, а /health регистрируется после неё. Обычные API-маршруты защищены проверкой bearer-токена, входные данные webhook каналов сохраняют собственный контроль через общий секрет, а статические ресурсы Web Shell (/, /assets/*, /manifest.webmanifest, /sw.js, а также навигации документов /session/:id, /plugins, /channels, /live, /scheduled-tasks, /goals и /settings) остаются доступными до авторизации, если только --no-web не отключает их.
  • Канонический URL источника — <scheme>://<host>[:<port>]. Без завершающего слэша, без пути, без userinfo, без запроса. Загрузка завершается ошибкой InvalidAllowOriginPatternError, если запись не проходит проверку на соответствие канонической форме new URL(pattern).origin === pattern; сообщение об ошибке указывает неверный шаблон и каноническую форму. Строгость по замыслу: тихая нормализация (например, удаление завершающего /) позволила бы проскользнуть опечаткам и принять неоднозначный ввод. Без полученного токена — что после генерации означает привязку к локальному адресу — записи HTTP(S) ограничены хостами локального адреса; браузерный источник с нелокального адреса требует токен, так как иначе он может управлять полным операторским API, включая выполнение кода от имени пользователя демона. Явно указанные источники расширений браузера сохраняют существующий путь локальной автоматизации без токена. При запуске логируется авторизация, предоставленная любому браузерному источнику без токена.

Совпавшие источники получают стандартные заголовки ответа CORS на каждый запрос:

Access-Control-Allow-Origin: <echoed origin> Vary: Origin Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type, X-Qwen-Client-Id, Last-Event-ID, X-Qwen-Event-Epoch Access-Control-Max-Age: 86400 Access-Control-Expose-Headers: Retry-After, X-Qwen-Event-Epoch, X-Qwen-SSE-Stream-Id

Access-Control-Allow-Origin возвращает origin запроса дословно (в нижнем/верхнем регистре, как отправил браузер), а не литерал *, даже при использовании шаблона * — кэш браузера использует его в паре с Vary: Origin в качестве ключа, и возврат значения оставляет возможность добавить Access-Control-Allow-Credentials в будущих релизах без изменения схемы. Открытые заголовки позволяют браузерным клиентам учитывать подсказки о повторных попытках, сохранять эпоху SSE и коррелировать принятые физические потоки. Access-Control-Allow-Credentials сегодня НЕ отправляется: настроенные учетные данные демона используют bearer-in-Authorization, который работает между источниками без credentials: 'include'; доверенная локальная авторизация не требует браузерных учетных данных.

Preflight-запросы OPTIONS (OPTIONS с Access-Control-Request-Method или Access-Control-Request-Headers) завершаются ответом 204 No Content плюс заголовками выше. Это конвенциональный паттерн CORS, и он безопасен — preflight лишь подтверждает, какие методы/заголовки примет демон; фактический последующий запрос все равно проходит через шлюз Host, а затем либо через авторизацию токена/listener, либо через шлюз общего секрета вебхука канала перед чтением или изменением состояния. Простые запросы OPTIONS от совпавших источников продолжают проходить дальше с прикрепленными заголовками CORS.

Источники, которые не совпадают со списком разрешений, все равно получают 403 {"error":"Request denied by CORS policy"} — та же структура ответа, что и у стены по умолчанию, поэтому клиентам, которые уже разбирали ответ стены, не нужно обрабатывать особые случаи для демонов со списком разрешений. Путь отказа не выдает никаких заголовков Access-Control-* (браузер проигнорировал бы их, а выдача косвенно раскрыла бы размер списка разрешений через наличие заголовков).

Настроенный список шаблонов намеренно НЕ возвращается в /capabilities — браузерный клиент уже знает свой собственный origin (в конце концов, он обращался к демону), а отображение списка позволило бы неаутентифицированному читателю /capabilities перечислить все доверенные источники (полезная разведка для неправильно настроенного развертывания). SDK-клиенты проверяют тег caps.features.allow_origin для определения «этот демон поддерживает обращения от браузеров с другими источниками», не нуждаясь в знании конкретных источников.

Запросы с тем же origin на локальном адресе (например, вызовы Web Shell к демону на том же 127.0.0.1:port) обрабатываются отдельным shim’ом удаления Origin, который выполняется ДО middleware CORS и удаляет заголовок Origin для 127.0.0.1:port / localhost:port / [::1]:port / host.docker.internal:port или точного связанного локального адреса и порта. Он также принимает формы без порта, соответствующие схеме, которые браузеры отправляют для портов по умолчанию: http://host на порту 80 и https://host на порту 443. Эти запросы проходят независимо от конфигурации --allow-origin — операторам не нужно указывать порт демона в списке, чтобы Web Shell работал.

Общая форма ошибки

Ответы 5xx содержат исходные поля code и data ошибки, если они присутствуют (стиль JSON-RPC — SDK ACP пересылает {code, message, data} от агента):

{ "error": "Internal error", "code": -32000, "data": { "reason": "model quota exceeded" } }

Некорректный JSON в теле запроса возвращает:

{ "error": "Invalid JSON in request body" }

со статусом 400.

SessionNotFoundError для неизвестного идентификатора сессии возвращает:

{ "error": "No session with id \"<sid>\"", "sessionId": "<sid>", "code": "session_not_found" }

со статусом 404. Конкурентное закрытие использует code: "session_closing".

WorkspaceMismatchError для POST /session, чей cwd не приводится к канонической форме зарегистрированного рабочего пространства, возвращает 400 с:

{ "error": "Workspace mismatch: daemon is bound to \"…\"", "code": "workspace_mismatch", "boundWorkspace": "/path/the/daemon/uses/as-primary", "requestedWorkspace": "/path/in/the/request" }

Используйте это для обнаружения несоответствия перед выполнением: прочитайте workspaceCwd из /capabilities и исключите cwd из POST /session (он вернется к основному рабочему пространству), или когда рекламируется multi_workspace_sessions, выберите один из workspaces[].cwd.

POST /session превышающий лимит демона --max-sessions возвращает 503 с заголовком Retry-After: 5 и:

{ "error": "Session limit reached (20)", "code": "session_limit_exceeded", "limit": 20, "scope": "workspace" }

Когда эффективный общий лимит демона отвергает новую сессию — --max-total-sessions или значение по умолчанию, описанное в limits.maxTotalSessions — возвращается та же форма ответа с "scope": "total".

Подключения к существующим сессиям НЕ учитываются в лимите, поэтому переподключения к простаивающему демону продолжают работать даже при достижении лимита.

Если бюджет инициализации канала ACP истекает до отправки newSession, POST /session возвращает 504 с Retry-After: 5 и:

{ "error": "AcpSessionBridge initialize timed out after 10000ms", "code": "init_timeout", "errorKind": "init_timeout", "retryable": true, "sideEffectPossible": false, "phase": "channel.initialize", "timeoutMs": 10000 }

timeoutMs — и числовой суффикс error — отражают настроенный бюджет демона --initialize-timeout-ms (приведенные выше значения являются значениями по умолчанию). Полная безопасная форма повтора выше выдается только для простого создания сессии, где инициализация канала строго предшествует каждому долговременному изменению: на этом пути поле sideEffectPossible: false является авторитетным, поскольку инициализация предшествует запросу ACP newSession, и клиент, понимающий этот структурированный контракт, может выполнить повторную попытку после указанной задержки без риска дублирования Сессии.

Запросы, несущие branch или worktree, и таймауты инициализации, возвращаемые маршрутами с мутациями, отличными от простого создания (например, POST /session/:id/branch и POST /session/:id/side-task, где зафиксированный форк может пережить неудачное рукопожатие), возвращают тот же 504 с code: "init_timeout", phase и timeoutMs — но БЕЗ Retry-After, retryable или sideEffectPossible. Для них результат мутации неизвестен: подготовка ветки и worktree изменяет git до инициализации канала, а попытка отката при ошибке является best-effort (неудачный откат checkout оставляет рабочую область на новой ветке, а повторная попытка может вызвать конфликт «ветка уже существует»). Клиенты, которые классифицируют все ответы 5xx с мутацией как неоднозначные, остаются консервативно в состоянии fail-closed и должны применять ту же политику к сокращенной форме. POST /session/:id/load и POST /session/:id/resume возвращают ту же сокращенную форму init_timeout 504, когда инициализация канала завершается по таймауту до отправки запроса восстановления (этап ensureChannel); в отличие от session_restore_timeout 504, описанных в их списках ошибок, эта форма не содержит Retry-After, не содержит retryable и не устанавливает блокировку, поскольку восстановление никогда не было отправлено. Таймаут отправки newSession для POST /session является исключением: он возвращает 504 с code: "init_timeout", retryable: true иDerived из бюджета Retry-After, но без phase или sideEffectPossible, поскольку результат создания неоднозначен. Все остальные метки таймаута сохраняют общее отображение.

RestoreInProgressError — генерируется POST /session/:id/load, POST /session/:id/resume или вызываемым id POST /session, когда другая регистрация уже владеет этим id — возвращает 409 и:

{ "error": "Session \"<sid>\" is already being restored via session/<resume|load>; retry session/<load|resume> after it completes", "code": "restore_in_progress", "reason": "restore_in_progress", "retryable": true, "sessionId": "<sid>", "activeAction": "load", "requestedAction": "resume" }

Генерируется, когда для id, который уже имеет активный session/resume (или наоборот), выпускается session/load, или когда гонка spawn с caller-supplied-id конкурирует с любым направлением восстановления. Подождите не менее Retry-After секунд и повторите попытку. Гонки одного действия (load против load, resume против resume) объединяются вместо ошибки, пока восстановление активно.

reason различает две блокировки, которые используют этот код, и заголовок Retry-After отслеживает это:

reasonЗначениеRetry-After
restore_in_progressВыполняется обычное восстановление.5 (соответствует session_limit_exceeded)
awaiting_abandoned_cleanupПубличный вызывающий объект уже получил таймаут (504 для восстановления или init_timeout для инициализации сессии), и некэнцеллируемый запрос ACP вместе с его очисткой еще не завершились.бюджет операции с таймаутом в секундах — восстановление или инициализация для spawn с caller-supplied-id — ограниченный значениями 5–120

Публичный запрос восстановления регулируется параметром limits.sessionRestoreTimeoutMs (по умолчанию 60 с). После 504 id остается заблокированным до завершения позднего запроса ACP и очистки, поэтому клиент, который продолжает повторять попытки с обычным интервалом в 5 секунд, будет бесконечно получать ошибку 409, которую он не может устранить — соблюдайте подсказку, основанную на бюджете, которая приходит вместе с awaiting_abandoned_cleanup.

SessionWorkspaceConflictError — генерируется POST /session/:id/load и POST /session/:id/resume, когда запрошенный cwd указывает на одну зарегистрированную рабочую область, но тот же id сессии уже активен или восстанавливается другим рантаймом — возвращает 409 с:

{ "error": "Session \"<sid>\" is already live or restoring in another workspace runtime.", "code": "session_workspace_conflict", "sessionId": "<sid>", "workspaceCwd": "/requested/workspace", "workspaceId": "requested-workspace-id", "liveWorkspaceCwd": "/live/owner/workspace", "liveWorkspaceId": "live-owner-workspace-id" }

Клиенты должны повторить попытку с рабочей областью-владельцем или дождаться завершения активного восстановления перед восстановлением id в другую рабочую область. Гонки восстановления в той же рабочей области продолжают использовать поведение объединения restore_in_progress моста.

SessionArchivedError генерируется, когда вызывающий объект пытается загрузить или возобновить сессию, чей JSONL находится под chats/archive/:

{ "error": "Session \"<sid>\" is archived. Unarchive it before loading.", "code": "session_archived", "sessionId": "<sid>" }

со статусом 409.

SessionArchivingError генерируется, когда переход архивирования или разархивирования сессии уже выполняется для того же id:

{ "error": "Session \"<sid>\" is being archived or unarchived; retry later.", "code": "session_archiving", "sessionId": "<sid>" }

со статусом 409 и Retry-After: 5.

Возможности

Демон объявляет поддерживаемые им теги функций из реестра возможностей serve. Клиенты должны ограничивать UI через features, а не через mode (согласно §10 дизайна).

['health', 'capabilities', 'session_create', 'session_startup_config', 'session_id_override', 'session_scope_override', 'session_load', 'session_resume', 'session_transcript', 'unstable_session_resume', 'session_list', 'session_catalog_batch', 'session_info', 'session_prompt', 'session_mid_turn_message_mutation', 'session_mid_turn_send_now', 'session_cancel', 'session_events', 'slow_client_warning', 'typed_event_schema', 'session_set_model', 'client_identity', 'client_heartbeat', 'session_permission_vote', 'permission_vote', 'workspace_mcp', 'workspace_skills', 'workspace_skills_config_runtime', 'workspace_providers', 'workspace_acp_preheat', 'workspace_acp_status', 'auth_provider_install', 'workspace_memory', 'workspace_agents', 'workspace_agent_generate', 'workspace_env', 'workspace_preflight', 'session_context', 'session_context_usage', 'session_supported_commands', 'session_tasks', 'scheduled_task_gate', 'autofix_pr_watches', 'session_monitor_tool_correlation', 'session_stats', 'session_lsp', 'session_resources', 'session_status', 'session_close', 'session_metadata', 'session_organization', 'session_archive', 'mcp_guardrails', 'workspace_mcp_manage', 'mcp_guardrail_events', 'mcp_server_runtime_mutation', 'workspace_file_read', 'workspace_file_bytes', 'workspace_file_write', 'workspace_file_upload', 'workspace_file_delete', 'workspace_directory_create', 'workspace_directory_delete', 'session_approval_mode_control', 'workspace_tool_toggle', 'workspace_skill_settings_toggle', 'workspace_skill_settings_batch_toggle', 'workspace_session_refresh', 'extension_batch_activation_v2', 'extension_activation_explicit_refresh', 'extension_state', 'workspace_settings', 'workspace_init', 'workspace_mcp_restart', 'session_recap', 'session_generation', 'session_btw', 'session_btw_stream', 'session_shell_command', 'standalone_sessions_v1', 'standalone_session_options_v1', 'mcp_workspace_pool', 'mcp_pool_restart', 'require_auth', 'allow_origin', 'auth_device_flow', 'permission_mediation', 'prompt_absolute_deadline', 'writer_idle_timeout', 'non_blocking_prompt', 'session_language', 'user_language_sync', 'session_message_deletion', 'session_rewind', 'workspace_hooks', 'session_hooks', 'workspace_extensions', 'session_branch', 'rate_limit', 'workspace_reload', 'workspace_model_reload', 'channel_delivery', 'multi_workspace_sessions', 'multi_workspace_session_rewind', 'multi_workspace_session_shell', 'persistent_workspace_registration', 'workspace_display_name', 'workspace_runtime_removal', 'workspace_runtime', 'workspace_qualified_rest_core', 'workspace_qualified_voice', 'workspace_qualified_memory', 'extension_management_v2', 'extension_git_credentials', 'extension_local_path_install', 'workspace_persisted_transcript', 'workspace_session_export', 'workspace_archived_session_export', 'workspace_session_live_state', 'workspace_session_live_state_batch', 'client_mcp_over_ws', 'cdp_tunnel_over_ws', 'browser_automation_mcp']

Условные теги появляются только тогда, когда выполнено условие развертывания или доступности рантайма (см. таблицу ниже). Тег permission_mediation для F3 всегда включен и несет modes: ['first-responder', 'designated', 'consensus', 'local-only'], чтобы клиенты SDK могли интроспектировать набор, поддерживаемый сборкой; стратегия активного рантайма указана в body.policy.permission.

session_startup_config объявляет опциональный startupConfig: { modelServiceId, reasoningEffort? } при обычном и автономном создании. Всегда выполняйте предпроверку этого тега: старые маршруты обычного создания могут молча игнорировать объект. modelServiceId обязателен (1–256 символов); reasoningEffort принимает none, default, low, medium, high, xhigh или max, если они поддерживаются выбранной моделью. Отсутствие поля применяет только модель, без установщика рассуждений или неявного default. Объект нельзя комбинировать с устаревшим верхним уровнем modelServiceId или явным одиночным scope, и он подразумевает новую нить. При запуске общие настройки по умолчанию никогда не сохраняются; последующее поведение сессии не меняется.

Успех включает modelApplied: true и startupConfigApplied: { modelServiceId, reasoningEffort?, effectiveReasoning? }. Селектор модели является каноническим. Только явные запросы рассуждений включают два поля рассуждений; эффективное состояние — { state: 'disabled' }, { state: 'provider-default' } или { state: 'enabled', effort? } (рассуждения только через переключатель не имеют effort). Неверная структура вызывает 400 invalid_startup_config; отклоненный или неподтвержденный выбор вызывает 422 startup_config_rejected. Определенный отказ автономного режима откатывает принадлежащую ему запись и пытается удалить пустой выходной каталог перед возвратом; откат записи, удаление которого не может быть подтверждено, и временные сбои запуска сохраняют standalone_creation_outcome_unknown, тогда как неудачное удаление каталога только логируется, и определенный отказ все равно возвращается. Сбой подтверждения тела успешного ответа в SDK является ошибкой протокола, а не инструкцией принять восстановленную сессию.

session_scope_override является дескриптором согласования для поля sessionScope на уровне запроса в POST /session (см. ниже). Старые демоны молча игнорируют это поле, поэтому клиенты SDK должны выполнить предпроверку caps.features для этого тега перед его отправкой.

session_id_override является дескриптором согласования для опционального caller-supplied sessionId в метаданных POST /session и ACP session/new. Клиенты должны убедиться, что caps.features содержит этот тег перед отправкой поля, поскольку старые демоны могут молча его игнорировать.

persistent_workspace_registration объявляет долговечную регистрацию для рабочих областей, добавленных во время выполнения. POST /workspaces принимает { "cwd": "/absolute/path", "persist": true }; успех включает persisted: true. Регистрации ограничены канонической основной рабочей областью демона в домашнем каталоге пользователя Qwen и восстанавливаются при следующем запуске демона. Отсутствие persist сохраняет локальную для процесса регистрацию. GET /workspace-registrations перечисляет сохраненный желаемый набор, а DELETE /workspace-registrations/:id забывает запись для следующего перезапуска без горячего удаления активного рантайма.

workspace_display_name объявляет опциональный ввод displayName в POST /workspaces, обновления метаданных рабочей области через PATCH /workspaces/:workspace и опциональные поля отображаемых имен в проекциях рабочей области. Имена не участвуют в поиске или маршрутизации: id и канонический cwd остаются единственными селекторами, и дубликаты имен разрешены.

workspace_runtime_removal объявляет о синхронном горячем удалении через DELETE /workspaces/:workspace. Записи capability workspace добавляют необязательный removable; удаляться могут только строки с removable: true. Удаление также забывает все постоянные псевдонимы регистрации для runtime, но никогда не удаляет файлы, настройки, транскрипты или архивы.

workspace_runtime объявляет GET /workspace/runtime/status, POST /workspace/runtime/ensure и их эквиваленты /workspaces/:workspace/runtime/.... ensure не принимает выбор capability: он запускает или повторно использует ACP runtime выбранного доверенного workspace и возвращает его состояние жизненного цикла и монотонную эпоху runtime. Основные маршруты принадлежат только основному runtime; квалифицированные маршруты разрешают только выбранный зарегистрированный runtime и никогда не выполняют откат. Успешное ensure обновляет окно keepalive runtime на десять минут. Status является только для чтения и никогда не запускает дочерний процесс. Конкурентные ensures разделяют один физический запуск. Идущий запуск или сбой возвращают повторные 503 runtime_still_starting или 503 runtime_initialization_failed. Подготовка capability может продолжаться после истечения бюджета наблюдения ensure; в этом случае ensure всё равно возвращает живой runtime с состоянием capability non-ready для опроса status. Capability объявляется только тогда, когда все активные мосты runtime предоставляют авторитетный снимок жизненного цикла; выбранный легаси внедрённый мост возвращает 501 workspace_runtime_not_supported вместо угаданного состояния или эпохи.

workspace_runtime_stop независимо объявляет об остановке runtime, подтверждённой пользователем. GET /workspaces/runtime-stop-options — это предварительный просмотр только для чтения, глобальный для процесса: он включает количество зафиксированных дочерних процессов, смоделированный потолок дочерних процессов, а также сессии каждого видимого workspace, причины блокировки, ID канала, эпоху, текущий токен остановки и последнюю квитанцию. Обычные загруженные сессии могут быть явно остановлены; независимые клиенты ACP, включённые расписания, ожидающие работы планировщика/управления/запуска, задачи памяти, воркеры, голосовые и специальные purpose-runtimes заблокированы. POST /workspaces/:workspace/runtime/stop использует строгий гейт мутаций и только выбранный доверенный runtime. Отправьте confirmInterruptions: true, expectedChannelId, expectedRuntimeEpoch, expectedStopToken и точный expectedSessionIds из предварительного просмотра. Никогда не выполняйте откат к основному runtime. Предварительный просмотр и каждая квитанция перечисляют живые каналы ACP runtime как channels (channelId, runtimeEpoch, необязательный executionEngine); runtime может иметь по одному каналу на движок выполнения, и остановка покрывает все они. channelId называет первый перечисленный канал, а runtimeEpoch — новейшая перечисленная эпоха, поэтому канал, запущенный после предварительного просмотра, делает подтверждение устаревшим.

Успешный ответ содержит state: "stopped", stopped: true и released: true после признанного закрытия сессий и освобождения реестра процессов для каждого перечисленного дочернего процесса. Регистрация, файлы и сохранённая история остаются. Ответы включают затронутые, прерванные, закрытые и оставшиеся ID сессий. Устаревшее или заблокированное подтверждение — HTTP 409 (workspace_runtime_stop_stale / workspace_runtime_stop_blocked); частичное закрытие — 409 workspace_runtime_stop_incomplete; идущее удаление или неудачное удаление без доказанного освобождения — 503 workspace_runtime_stop_in_progress / workspace_runtime_stop_failed; неподдерживаемое управление — 501 workspace_runtime_stop_not_supported. Неаккуратное удаление, которое позже доказывает освобождение реестра, может вернуть 200 с предупреждением об ошибке; неподтверждённый сброс сессии всё ещё возвращает частичный/ошибочный результат. Общий бюджет наблюдения/закрытия составляет 60 секунд. Если он истекает, пока закрытие сессии всё ещё выполняется, ответ завершается с state: "failed" и 503 workspace_runtime_stop_failed; очистка продолжается отдельно. Выбранный workspace остаётся изолированным, и его дочерний процесс продолжает учитываться до фактического завершения очистки, даже если вызывающий получил ошибку. Опции только для чтения могут позже сообщить stopped после признанных закрытий и доказанного освобождения, или incomplete, когда сохранение было неподтверждённым; уже возвращённый снимок ошибки не меняется. Если бюджет вместо этого исчерпан между закрытиями сессий, ответ завершается с state: "incomplete" и 409 workspace_runtime_stop_incomplete: дальнейшие закрытия или принудительное убийство не начинаются, гейт допуска остановки освобождается (выживший дочерний процесс продолжает учитываться как обычно), и остановка оставшихся сессий требует нового предварительного просмотра и подтверждения. Отказ в сбросе не обходится принудительным убийством оставшихся сессий. Выход корня или падение глобального количества процессов само по себе не устанавливает освобождение выбранного дочернего процесса. На Windows, где реестр не отслеживает группы дочерних процессов, установленный выход корня является доказательством освобождения, поэтому released: true там не доказывает, что дерево процессов исчезло.

Повторное подтверждение последнего использованного токена наблюдает ту же операцию. Новое подтверждение может повторить попытку неполной остановки, ротируя токен; старые использованные токены затем становятся устаревшими. Если ответ потерян, прочитайте опции и сопоставьте последнюю квитанцию по workspace и токену остановки вместо остановки другой цели. Квитанции могут быть недоступны после удаления, изменений доверия или пересоздания демона/моста; отсутствие оставляет результат неизвестным и никогда не авторизует продолжение. SDK runtimeStopOptions() и workspaceById(id).stopRuntime(confirmation) используют REST даже при транспорте ACP и никогда автоматически не повторяют POST. Подписчики сессий получают session_closed с reason: "client_close" и cause: "workspace_runtime_stop"; фатальные выходы, связанные с остановкой, дополнительно помечают persistenceUnconfirmed: true. Обновлённые клиенты Web Shell сохраняют выбранное сохранённое/транскрипт и ждут явного Resume. Это одноразовая остановка, а не постоянное приостановление; клиент, пропустивший событие, может снова конкурировать за ёмкость.

Ответы статуса runtime и ensure используют эту форму:

{ "v": 1, "workspaceCwd": "/work/project", "state": "active", "runtimeLive": true, "runtimeEpoch": 4, "capabilities": { "skills": { "state": "ready", "revision": 2, "runtimeEpoch": 4 } } }

Когда runtime workspace имеет более одного движка выполнения, runtimeLive и runtimeEpoch описывают канал, обслуживающий управление workspace (MCP и Skills): пока этот канал жив, запуск или выход другого движка не меняет их. state сообщает этот канал как cold, starting или stopping, пока он не жив; active и idle считают работу на каждом движке.

capabilities.skills.state — это not_started, starting, ready, stale или error; сбои включают {code, message} в capabilities.skills.error. Каталог Skills актуален только тогда, когда значения верхнего уровня и capability runtimeEpoch совпадают. revision упорядочивает подготовку Skills внутри одной эпохи runtime и не должен сравниваться между эпохами.

workspace_skills_config_runtime объявляет раздельные чтения Skills и мутации конфигурации под /workspace/{config,runtime}/skills и /workspaces/:workspace/{config,runtime}/skills. Web Shell использует эти маршруты для управления Skills и, при составлении новой сессии, для заполнения слэш-команд из конфига непосредственно перед заменой их каталогом runtime с соответствующей эпохой. Когда функция отсутствует, клиенты должны продолжать использовать легаси маршруты Skills и не должны вызывать runtime ensure исключительно для Skills.

session_load и session_resume объявляют маршруты явного восстановления (POST /session/:id/load и POST /session/:id/resume). Старые демоны возвращают 404 для этих путей, поэтому клиенты SDK должны предварительно проверить caps.features перед вызовом. unstable_session_resume всё ещё объявляется как устаревший псевдоним для совместимости с SDK, которые поставлялись, когда базовый метод ACP назывался connection.unstable_resumeSession; новые клиенты должны гейтовать на session_resume.

limits.sessionRestoreTimeoutMs, при наличии, представляет собой бюджет времени реального времени демона для базового запроса ACP loadSession / unstable_resumeSession. Это аддитивное поле v1. TypeScript SDK предоставляет демону 10 секунд запаса на стороне клиента, а watchdog Web Shell — 15 секунд; клиенты, взаимодействующие со старыми версиями демона, должны использовать 70 и 75 секунд соответственно.

session_transcript объявляет GET /session/:id/transcript, доступный только для чтения постраничный просмотр воспроизведения поверх сохраненного JSONL активной сессии. Он отделен от /load: не подключает клиент, не инициализирует живой EventBus, не создает живую сессию и не изменяет окно живого воспроизведения. Клиенты должны использовать его, когда им нужна полная транскрипция на диске для длительной сессии, и продолжать использовать /load только для ограниченного живого воспроизведения при холодном восстановлении UI.

workspace_persisted_transcript объявляет GET /workspaces/:workspace/session/:id/transcript, локальный для демона постраничный просмотрщик только сохраненных данных, который не запускает ACP, не запрашивает состояние живого моста, не загружает настройки, не обнаруживает возможности проекта и не создает устаревший ключ курсора сохраненных данных. Тег является безусловным, так как доверенные первичные рабочие пространства могут использовать маршрут во множественном числе; авторизация доверия для каждого рабочего пространства по-прежнему проверяется при каждом запросе. Зарегистрированные недоверенные вторичные рабочие пространства могут выполнять чтение, тогда как недоверенное первичное рабочее пространство остается заблокированным.

workspace_session_export объявляет GET /workspaces/:workspace/session/:id/export, полный экспорт только для доверенных пользователей активной сохраненной сессии выбранного рабочего пространства. Он независим от session_export и workspace_qualified_rest_core: выпущенные версии демона могут объявлять оба старых тега без реализации маршрута во множественном числе, поэтому клиенты должны предварительно проверять этот тег напрямую. Тег является безусловным, так как доверенное первичное рабочее пространство может использовать маршрут по id или cwd. Экспорт не определяет владельца живой сессии, не запускает ACP, не подключает клиента и не выполняет откат к другому рабочему пространству.

workspace_archived_session_export объявляет GET /workspaces/:workspace/session/:id/archive/export, полный экспорт только для доверенных пользователей из архивного хранилища сохраненных данных выбранного рабочего пространства. Он независим от workspace_session_export и workspace_qualified_rest_core; клиенты должны предварительно проверять этот тег напрямую. Отдельный маршрут предотвращает ситуацию, когда старый демон игнорирует намерение работы с архивом и возвращает активную транскрипцию с тем же id.

workspace_session_live_state объявляет GET /workspaces/:workspace/sessions/live-state, снимок состояния в памяти только для доверенных пользователей, содержащий живые сессии среды выполнения выбранного рабочего пространства, а также версию каталога в памяти, которая сообщает клиентам, когда необходима полная перезагрузка сохраненного каталога. Он независим от workspace_qualified_rest_core: выпущенные версии демона могут объявлять более широкую возможность REST для рабочего пространства без реализации этого маршрута, поэтому клиенты должны предварительно проверять этот тег напрямую. Тег является безусловным, так как доверенное первичное рабочее пространство может использовать маршрут по id или cwd; проверки доверия для каждого рабочего пространства по-прежнему применяются при каждом запросе, и маршрут не распространяет разрешительную политику чтения сохраненного каталога для недоверенных вторичных рабочих пространств на состояние живого моста. Тег означает наличие конечной точки; он не гарантирует, что каждый живой элемент содержит необязательный маркер активности updatedAt, который зависит от жизненного цикла.

workspace_session_live_state_batch объявляет POST /sessions/live-state. Клиенты выполняют предварительную проверку один раз перед пакетированием опросов и сохраняют маршрут для одного рабочего пространства для поддержки старых версий демона. Пакет использует те же семантики снимка состояния в памяти только для доверенных пользователей и версии каталога, что и маршрут для одного рабочего пространства.

Необязательное верхнеуровневое поле /capabilities sessionLiveStatePollIntervalMs объявляет интервал опроса состояния живых данных для всего демона в миллисекундах. Оно определяется один раз из переменной окружения запуска QWEN_SESSION_LIVE_STATE_POLL_INTERVAL_MS независимо от наложений переменных окружения рабочего пространства. Принимаются целочисленные значения от 1000 до 2147483647; отсутствующие или недействительные значения используют 5000. Web Shell использует эту подсказку для всех опросов состояния живых данных рабочего пространства и также выполняет откат к 5000, если поле отсутствует или недействительно в старой или несовместимой версии демона. Это поле не изменяет семантику снимка маршрута, немедленное обновление локального состояния/видимости или опрос полного каталога. Клиенты SDK остаются ответственными за свои собственные таймеры.

slow_client_warning охватывает поведение обратного давления SSE: (a) демон отправляет синтетический кадр потока событий slow_client_warning, когда накопление живых кадров подписчика или накопление сериализованных байтов достигает 75% заполнения, один раз за эпизод переполнения (повторная активация после того, как оба показателя упадут ниже 37.5%); (b) GET /session/:id/events принимает параметр запроса ?maxQueued=N (диапазон [16, 2048]) для предварительного определения размера накопления кадров для каждого подписчика при холодном переподключении к большому кольцу воспроизведения. Ограничение по сериализованным байтам управляется демоном (по умолчанию 2 MiB на подписчика), относится только к живым данным и намеренно не имеет параметра запроса. Размер кольца для всего демона контролируется через --event-ring-size (по умолчанию 8000, согласно #3803 §02). Старые версии демона молча не поддерживают поведение предупреждения/запроса — выполните предварительную проверку этого тега перед включением функции.

typed_event_schema объявляет полезные нагрузки событий демона, соответствующие схеме KnownDaemonEvent SDK. Старые версии демона могут все еще передавать совместимые кадры, но клиенты SDK должны выполнить предварительную проверку этого тега перед предположением о типизированном покрытии событий.

client_heartbeat объявляет POST /session/:id/heartbeat. Старые версии демона возвращают 404; выполните предварительную проверку этого тега перед отправкой периодических сигналов heartbeat.

session_close и session_metadata объявляют DELETE /session/:id и PATCH /session/:id/metadata. Старые версии демона возвращают 404; выполните предварительную проверку этих тегов перед предоставлением возможностей закрытия или переименования.

session_organization объявляет пользовательские группы сессий и закрепление. Он добавляет GET/POST/PATCH/DELETE /workspace/:id/session-groups, PATCH /session/:id/organization и организованный список с опциональным включением GET /workspace/:id/sessions?view=organized. Когда объявлены и session_organization, и workspace_qualified_rest_core, доступна также мутация организации с указанием рабочего пространства PATCH /workspaces/:workspace/session/:id/organization. Устаревшая мутация остается доступной только для первичного рабочего пространства. Старые версии демона возвращают 404 для маршрутов мутации/группы и игнорируют контракт организованного просмотра, поэтому клиенты WebShell/SDK должны выполнить предварительную проверку этих тегов перед отображением соответствующего интерфейса группировки или закрепления.

session_archive объявляет API архивирования состояния директории v1: POST /sessions/archive, POST /sessions/unarchive и GET /workspace/:id/sessions?archiveState=active|archived. Архивированные сессии не могут быть загружены или возобновлены до их разархивирования. session_archive_idle_guard объявляет аддитивную опцию архивирования requireIdle: среда выполнения, владеющая сессией, проверяет, что активная работа подтверждена как простаивающая непосредственно перед началом закрытия, и в противном случае оставляет активную транскрипцию на месте. session_storage_conflict_repair объявляет аддитивную опцию запроса resolveConflicts и корзину ответа resolvedConflicts, описанные ниже.

workspace_qualified_rest_core объявляет множественные основные REST-маршруты под /workspaces/:workspace/.... Селектор сначала разрешается как точный идентификатор рабочего пространства, затем как URL-кодированный абсолютный путь к текущему рабочему каталогу после канонизации. Новые однопространственные демоны включают основной рантайм в workspaces[] даже при отсутствии multi_workspace_sessions, что позволяет клиентам обнаруживать идентификатор, требуемый маршрутами с привязкой к рабочему пространству; для старых демонов, не содержащих массив, клиенты должны использовать запасной вариант capabilities.workspaceCwd. Маршруты статуса доверия и запроса доверия доступны для зарегистрированных недоверенных рабочих пространств; квалифицированное чтение статуса хуков (GET /workspaces/:workspace/hooks) также доступно для зарегистрированных недоверенных рабочих пространств, так как оно сообщает о ожидающем решения доверии к проектным хукам, соответствуя квалифицированной мутации подтверждения; маршруты чтения файлов следуют существующей политике чтения файловой системы. Зарегистрированные вторичные недоверенные рабочие пространства также предоставляют каталоги только сохраненных сессий и групп сессий: эти операции чтения не подключаются к сессии, не запускают ACP и не объединяют состояние живого моста. Запись файлов, изменения каталогов и другие множественные основные маршруты требуют доверенного рабочего пространства, если отдельная возможность не документирует более узкую политику: workspace_persisted_transcript (только для чтения) или мутация предоставления доверия, которая принимается для зарегистрированного недоверенного рабочего пространства независимо от того, рекламируется ли workspace_trust_grant — этот тег сообщает только о том, настроена ли горячая перезагрузка доверия, поэтому записанное решение может достичь запущенного рантайма без перезапуска демона; он никогда не сообщает, обслуживается ли маршрут. Недоверенное основное рабочее пространство продолжает получать 403 { code: "untrusted_workspace" } из множественных маршрутов каталога и транскриптов; устаревшие единичные основные маршруты сохраняют свою существующую совместимость. Этот тег охватывает основные поверхности файлов, статуса, настроек, разрешений, доверия, жизненного цикла, управления MCP, переключателей инструментов и навыков, памяти, CRUD агентов рабочего пространства и хранения сессий. Он не охватывает аутентификацию, голос, расширения, транспорт ACP/WebSocket, маршрутизацию worker-каналов или экспорт сессий с привязкой к рабочему пространству; предварительно проверьте workspace_session_export или workspace_archived_session_export отдельно. Доверие к рабочему пространству не является ACL: клиент, имеющий токен демона, может читать все поверхности зарегистрированного рабочего пространства, разрешенные этой политикой.

workspace_qualified_voice объявляет голосовые маршруты, выбираемые доверенным рантаймом рабочего пространства: GET и POST /workspaces/:workspace/voice, POST /workspaces/:workspace/voice/transcribe и WS /workspaces/:workspace/voice/stream. Он объявляется только тогда, когда включены оба: многопространственные рантаймы и общий слушатель WebSocket для ACP/Voice. Селектор следует тем же правилам id-or-encoded-absolute-cwd, что и другие множественные маршруты. Для REST неизвестный селектор возвращает 400 { code: "workspace_mismatch" }, а недоверенный селектор возвращает 403 { code: "untrusted_workspace" }; отказ при обновлении WebSocket раскрывает соответствующий HTTP-статус 400/403 без структурированного JSON-конверта. Ни один из транспортов не использует запасной вариант на основной. Устаревшие /workspace/voice, /workspace/voice/transcribe и /voice/stream остаются только для основного. Клиенты используют workspace_qualified_voice для всех квалифицированных голосовых модальностей и позволяют выбранному рантайму сообщать об ошибках, специфичных для конфигурации. Устаревшие теги workspace_voice, workspace_voice_transcription и voice_transcribe описывают только маршруты, привязанные к основному, и не должны скрывать квалифицированную вторичную конфигурацию.

workspace_qualified_memory объявляет маршруты управляемой памяти с привязкой к рабочему пространству: POST /workspaces/:workspace/memory/{remember,forget,dream} для постановки задач в очередь и GET /workspaces/:workspace/memory/{remember,forget,dream}/:taskId для их чтения обратно. Он объявляется только тогда, когда включены оба: HTTP-транспорт ACP и многопространственные рантаймы. Селектор следует тем же правилам id-or-encoded-absolute-cwd, что и другие множественные маршруты. Каждое зарегистрированное рабочее пространство получает свою собственную полосу задач; квалифицированная полоса основного пространства является тем же экземпляром, что и единичная поверхность /workspace/memory, поэтому задача, поставленная в одной, читается в другой. Разрешение строго выполняется для выбранного рантайма без запасного варианта на основной: неизвестный селектор возвращает 400 { code: "workspace_mismatch" }, недоверенный селектор возвращает 403 { code: "untrusted_workspace" }, а неактивный или завершающий работу рантайм возвращает 503 { code: "workspace_runtime_unavailable" }. Операции чтения никогда не выделяют полосу, поэтому опрос рабочего пространства без задач возвращает 404 { code: "<kind>_task_not_found" }. Идентификаторы задач ограничены их полосой и не выживают после переконфигурации рабочего пространства или замены рантайма; устаревший идентификатор возвращает 404, а не условие потери данных. Когда HTTP-транспорт ACP отключен, тег не объявляется, и неквалифицированный запрос к вторичному пространству возвращает неретраибельный 501 { code: "workspace_memory_unavailable" }, в то время как квалифицированный маршрут основного пространства продолжает работать через локально принадлежащую полосу.

session_lsp объявляет GET /session/:id/lsp, снимок статуса LSP в структурированном виде только для чтения для клиентов демона. Старые демоны возвращают 404; предварительно проверьте этот тег перед раскрытием удаленного статуса LSP.

session_resources объявляет GET /session/:id/resources, пару снимков Skill и MCP в очищенном виде только для чтения, построенных из Config выбранной живой сессии. Маршрут ограничен владельцем живой сессии: он никогда не использует запасной вариант на основной рантайм и не выводит ресурсы сессии из статуса рабочего пространства. Вложенные объекты skills и mcp повторно используют соответствующие полезные нагрузки статуса рабочего пространства, но исключают дополнения аутентификации MCP, пула, бюджета рабочего пространства и ошибок обнаружения рабочего пространства. Статус, обнаружение и учет, сообщаемые менеджером MCP выбранной сессии, остаются присутствующими. Старые демоны возвращают 404; предварительно проверьте этот тег перед отображением каталога ресурсов сессии.

session_status объявляет GET /session/:id/status, сводку живого моста для одной сессии по идентификатору. Помимо clientCount и hasActivePrompt, живые сессии раскрывают isWaitingForPermission, isWaitingForUserQuestion, pendingInteractionCount и сохраненный turnError после неудачного хода. Ошибка очищается, когда следующий промпт действительно начинается. Живая сессия, которая завершила выполняющийся ход в текущем мосту, также несет updatedAt, тот же водяной знак активности, документированный в маршруте состояния живого режима; поскольку этот маршрут возвращает сводку моста напрямую, значение не объединяется с mtime сохраненного транскрипта и может быть раньше того, которое сообщает список сессий. Как ответ статуса одиночной сессии, так и списки сессий рабочего пространства включают turnError и pendingInteractions: действия разрешения, готовые к рендерингу, или вопросы ask_user_question плюс requestId и выбираемые варианты, требуемые существующими маршрутами голосования за разрешения. Каждый пользовательский вопрос имеет answerKey; голосуйте с помощью answers, например { "0": "Polling" }, используя это значение в качестве ключа. Сохраненные только сессии опускают состояние рантайма, потому что рантайм не существует. Старые демоны возвращают 404; предварительно проверьте этот тег перед опросом статуса одиночной сессии вместо сканирования полного списка сессий.

session_info объявляет GET /workspace/:id/session-info и его близнец /workspaces/:workspace/session-info. Ответ агрегирует количество активных и архивированных сохраненных сессий без гидратации метаданных списка. Это явное O(n) сканирование диска, и его нельзя опрашивать; клиенты должны рассматривать truncated: true как результат нижней границы.

session_approval_mode_control, workspace_tool_toggle, workspace_skill_settings_toggle, workspace_skill_settings_batch_toggle, extension_batch_activation_v2, workspace_init и workspace_mcp_restart объявляют маршруты управления мутациями, описанные ниже. Управление режимом одобрения сохраняет свой нестрогий шлюз совместимости. Остальные элементы управления проходят строгую проверку авторитета оператора: проходят запросы от доверенного локального обратного адреса (trusted-loopback) основного процесса, аутентифицированные по bearer-токену или сопряженные запросы Local Control. Основной запрос без токена, достигающий строгого шлюза без авторитета доверенного локального обратного адреса, возвращает 401 token_required; отсутствующие или недействительные настроенные учетные данные и несопряженные учетные данные Local Control отклоняются ранее промежуточным ПО bearer с простым ответом 401 Unauthorized. Демоны, у которых отсутствует один из этих маршрутов, возвращают 404. Специфичные для настроек теги Skill отличаются: демоны поколения с устаревшими тегами объявляют workspace_skill_toggle и workspace_skill_batch_toggle и обслуживают свой контракт, проверенный каталогом, по тем же путям. Устаревший маршрут с одной целью может вернуть HTTP 404 skill_not_found или 409 skill_not_toggleable; устаревший пакетный маршрут возвращает HTTP 200 и помещает ошибки, полученные из каталога, в errors[]. Выполняйте предпроверку каждого тега перед раскрытием его функциональности и не пытайтесь определить специфичный для настроек контракт Skill путем проверки доступности маршрутов. Пути маршрутов и тела запросов не изменились.

mcp_guardrails (issue #4175  PR 14) охватывает поверхность бюджета MCP: поля clientCount / clientBudget / budgetMode / budgets[] в GET /workspace/mcp, поле disabledReason в ячейках отдельных серверов и флаги CLI --mcp-client-budget / --mcp-budget-mode. Старые демоны полностью пропускают новые поля; клиенты SDK выполняют предпроверку этого тега перед использованием семантики budgets[]. Дескриптор реестра также содержит modes: ['warn', 'enforce'] для будущего раскрытия режимов функций — пока что клиенты определяют режим на основе поля budgetMode снимка состояния. Отказ сервера в режиме enforce детерминирован порядком объявления Object.entries(mcpServers); будущий слой приоритетов области действия (если qwen-code его внедрит) изменит это на «сначала наименьший приоритет», чтобы соответствовать соглашению claude-code plugin < user < project < local.

Область действия определяется возможностями. При наличии mcp_workspace_pool сессии внутри одного рабочего пространства разделяют пул транспортов и WorkspaceMcpBudget, а снимок состояния выдает budgets[0].scope: 'workspace'. Различные рабочие пространства владеют независимыми пулами. Без тега каждая ACP-сессия использует свой устаревший McpClientManager, снимок состояния выдает scope: 'session', и N сессий могут каждая потреблять настроенный лимит.

workspace_file_read охватывает маршруты рабочих файлов workspace text/list/stat/glob (GET /file, GET /list, GET /glob, GET /stat). workspace_file_bytes охватывает GET /file/bytes, который был добавлен позже, чтобы клиенты могли выполнить предварительную проверку поддержки сырых байтовых окон против демонов эпохи PR19. workspace_file_write охватывает маршруты мутаций текста с учетом хеша (POST /file/write, POST /file/edit). Тег write означает, что контракт маршрута существует; это не означает, что текущее развертывание открыто для анонимных мутаций. Write/edit — это строгие маршруты мутаций, требующие авторитет оператора; доверенный loopback без токена подходит. workspace_file_upload охватывает POST /file/upload, маршрут бинарного входа: тело application/octet-stream размером до MAX_UPLOAD_BYTES (50 МиБ) записывается в workspace без перезаписи — занятое имя автоматически нумеруется (name (1).ext, name (2).ext, …). Это также строгий маршрут мутации.

workspace_file_delete охватывает только POST /workspaces/:workspace/file/delete; маршрут /file/delete для устаревшего основного процесса не существует. Тело JSON представляет собой { "path": "relative/file.txt" }, а успешное выполнение возвращает { "kind": "file_delete", "path": "relative/file.txt" }. Этот строгий маршрут мутации навсегда удаляет один обычный файл из выбранного доверенного локального рабочего пространства. Каталоги и символьные ссылки отклоняются; неподдерживаемые фабрики (включая SSH) возвращают 422. Возможность описывает поддержку протокола, а не разрешение на удаление во всех рабочих пространствах. Клиенты также должны выполнить предпроверку workspace_qualified_rest_core.

Когда объявляется workspace_qualified_rest_core, та же файловая поверхность также доступна по адресам /workspaces/:workspace/file, /workspaces/:workspace/file/bytes, /workspaces/:workspace/stat, /workspaces/:workspace/list, /workspaces/:workspace/glob, /workspaces/:workspace/file/write, /workspaces/:workspace/file/edit, /workspaces/:workspace/file/upload и /workspaces/:workspace/file/delete (последний маршрут дополнительно требует workspace_file_delete).

workspace_directory_create и workspace_directory_delete независимо объявляют POST /workspaces/:workspace/directory/create и POST /workspaces/:workspace/directory/delete. Выполняйте предпроверку тега операции вместе с workspace_qualified_rest_core; старые квалифицированные демоны могут не иметь этих маршрутов. Оба являются строгими мутациями, принадлежащими доверенному активному выбранному локальному исполняемому окружению, без резервного варианта для основного процесса или аналога для устаревшего основного процесса. Создание является эксклюзивным при существующем родительском элементе; удаление является постоянным и рекурсивным.

Тот же тег также раскрывает CRUD проектных агентов с указанием рабочего пространства по адресам /workspaces/:workspace/agents и /workspaces/:workspace/agents/:agentType. Эти множественные маршруты читают или изменяют только агентов уровня проекта для выбранного рабочего пространства; запросы с областью действия global и user возвращают 400 { code: "global_scope_not_supported_for_workspace_route" }. Маршруты /workspace/agents без указания рабочего пространства сохраняют свое существующее поведение для основного рабочего пространства и остаются единственной REST-поверхностью для области действия агентов пользователя.

extension_management_v2 объявляет каталог расширений уровня пользователя и поверхность мутаций по адресу /extensions/*, а также проекции активации рабочего пространства по адресу /workspaces/:workspace/extensions/*. Артефакты являются глобальными; маршруты рабочего пространства раскрывают только чтение проекций, точные переопределения активации и обновление среды выполнения. Чтения могут быть направлены на недоверенное зарегистрированное рабочее пространство, тогда как активация, обновление и установка с областью действия рабочего пространства требуют доверенной цели. Медленные мутации используют локальные для демона операции по адресу /extensions/operations/:operationId; генерация хранилища, а не история операций, является авторитетной после перезапуска и между демонами. Опубликованная возможность workspace_extensions и маршруты /workspace/extensions/* остаются адаптером совместимости для основного рабочего пространства. Клиенты должны выполнить предпроверку extension_management_v2 и не должны определять её на основе режима демона или workspace_qualified_rest_core.

extension_git_credentials объявляет аутентифицированные HTTPS-установки Git как по адресу POST /workspace/extensions/install, так и по адресу POST /extensions/install. Клиенты должны выполнить предпроверку этого тега перед отправкой userinfo URL или credentialPersistence; старые демоны отклоняют учетные данные в URL. Тег описывает поддержку протокола бэкенда, а не наличие хранилища ключей: режим хранения сообщает о выбранном бэкенде в результате терминальной операции.

extension_local_path_install объявляет локальные для демона источники расширений как по адресу POST /workspace/extensions/install, так и по адресу POST /extensions/install. Значение source должно быть абсолютным путем, существующим на хосте демона. Относительные пути остаются неподдерживаемыми, чтобы рабочий каталог процесса демона не мог изменить идентичность источника или затенить сокращение GitHub owner/repo. Существующая операция установки копирует расширение в управляемое хранилище; она не создает ссылку на источник. Клиенты должны выполнить предпроверку этого тега, поскольку старые демоны отклоняют локальные источники.

extension_batch_activation_v2 добавляет PUT /extensions/activation и PUT /workspaces/:workspace/extensions/activation. Оба принимают от 1 до 100 имён в extensionNames, удаляют дубликаты без учёта регистра, сохраняя порядок первого появления, фиксируют изменённые цели за одну генерацию и возвращают один дескриптор операции 202. Цель не обязана быть установленной при задании enabled или disabled: её имя создаёт декларацию желаемого состояния, которая сохраняется при установке Extension с этим именем. Глобальный маршрут принимает state: "enabled" | "disabled" и записывает V2 defaultActivation; рабочий маршрут также принимает "inherit" и применяет или сбрасывает точные переопределения для выбранного доверенного рантайма. inherit не декларирует неизвестное имя, а сброс всех неизвестных имён сообщает updated: false.

extension_activation_explicit_refresh означает, что одиночные и пакетные операции активации завершаются после надёжной фиксации политики без прямого обновления активных сессий. Вызывающие стороны, которым требуется немедленное применение, должны дождаться успешной активации, а затем отправить либо синхронный запрос POST /workspace/extensions/refresh для основного рабочего пространства, который напрямую возвращает счётчики обновлений, либо асинхронный запрос POST /workspaces/:workspace/extensions/refresh для выбранного рабочего пространства, который возвращает отдельный дескриптор операции. Эти две формы не взаимозаменяемы: асинхронная операция записывает применённую генерацию по завершении полной сверки рантайма, тогда как синхронный маршрут ничего не записывает, поэтому согласователь генераций всё ещё считает это рабочее пространство ожидающим и снова обновляет его сессии на следующем проходе. Вызывающие стороны, способные обратиться к конкретному рабочему пространству, должны предпочесть асинхронную форму. Сбой обновления не приводит к откату или понижению результата активации. Демоны без этой возможности уже включают обновление рантайма в процесс активации, поэтому совместимые клиенты не должны отправлять второе обновление. Независимый согласователь генераций с интервалом 30 секунд остаётся включённым и обычно применяет зафиксированную политику на следующем проходе; сбои сверки повторяются на последующих проходах.

Контракт протокола управления Extension V2

Все маршруты используют правила bearer-аутентификации демона, описанные выше. X-Qwen-Client-Id необязателен для маршрутов мутации V2; если он указан, он должен идентифицировать клиента, зарегистрированного с одним из целевых рантаймов рабочего пространства мутации. :extensionId — это идентификатор расширения в виде строчных шестнадцатеричных символов длиной 64. :workspace разрешается сначала как точный идентификатор рабочего пространства, а иначе — как URL-кодированный абсолютный путь cwd после канонизации.

Метод и путьУспех
GET /extensions200 глобальный каталог артефактов
PUT /extensions/activation202 пакетная операция глобальной активации по умолчанию
PUT /extensions/:extensionId/activation202 операция глобальной активации по умолчанию
POST /extensions/install202 операция установки
POST /extensions/check-updates202 операция проверки обновлений
POST /extensions/:extensionId/update202 операция обновления
DELETE /extensions/:extensionId202 операция удаления или идемпотентный 204, если расширение отсутствует
GET /extensions/operations/:operationId200 снимок состояния операции
GET /workspaces/:workspace/extensions200 проекция активации рабочего пространства
GET /workspaces/:workspace/extensions/:extensionId/state200 состояние ресурсов рабочего пространства (extension_state)
PUT /workspaces/:workspace/extensions/:extensionId/state202 операция состояния ресурсов рабочего пространства (extension_state)
PUT /workspaces/:workspace/extensions/activation202 пакетная операция точной активации рабочего пространства
PUT /workspaces/:workspace/extensions/:extensionId/activation202 операция точной активации рабочего пространства
DELETE /workspaces/:workspace/extensions/:extensionId/activation202 операция сброса переопределений
POST /workspaces/:workspace/extensions/refresh202 операция обновления рантайма

Явное обновление Extension

Тег возможности: workspace_session_refresh для опционального выбора сессий. Оба варианта POST /workspace/extensions/refresh (основной рантайм) и POST /workspaces/:workspace/extensions/refresh (выбранный рантайм) принимают:

{ "sessionIds": ["session-1", "session-2"] }

Отсутствие sessionIds сохраняет полное обновление рантайма. Предоставленный массив дедуплицируется в порядке запроса, а [] не обновляет ни сессии, ни bootstrap-кеши. Идентификаторы должны быть непустыми строками. Некорректные форматы запросов возвращают HTTP 400; неизвестные идентификаторы, включая сессии, принадлежащие другому рабочему пространству, возвращают HTTP 404 до начала работы. Принадлежность сессии проверяется только внутри разрешённого рантайма, без возврата к основному рантайму. Применяются существующие ограничения аутентификации, строгой мутации, доверия к рабочему пространству и идентификации клиента.

Основной маршрут по-прежнему возвращает HTTP 200 с { refreshed, failed }; квалифицированный маршрут по-прежнему возвращает HTTP 202 с дескриптором операции. Счётчики описывают выбранные сессии. Обновление с sessionIds не продвигает применённую генерацию всего рабочего пространства, даже если выборка охватывает все текущие сессии. Автоматическая полная сверка остаётся неизменной и может позже обновить другие сессии. Выбор не создаёт политику активации уровня сессии: общие кеши рантайма могут изменяться, а события рабочего пространства остаются общими для всего рабочего пространства.

Клиенты должны предварительно проверить поддержку workspace_session_refresh перед передачей этого поля, включая пустой массив; старые демоны могут молча игнорировать его. Существующие вызовы без этого поля не требуют новой проверки поддержки.

Состояние ресурсов рабочего пространства

Предварительно проверьте поддержку extension_state независимо. Она поддерживает только Skills; она не подразумевает управление ресурсами MCP. Оба маршрута выбирают зарегистрированный рантайм рабочего пространства и никогда не возвращаются к основному. GET следует существующим правилам чтения с проверкой доверия; PUT требует доверенного рабочего пространства.

{ "skills": [ { "name": "review", "state": "enabled" }, { "name": "deploy", "state": "disabled" } ] }

PUT принимает от 1 до 100 записей, включая пакет из одного элемента. Он отвергает некорректные записи, дубликаты имён без учёта регистра и неподдерживаемые группы перед постановкой в очередь. Все имена должны принадлежать установленному целевому Extension, включая в настоящее время отключённые Skills; некорректная цель приводит к сбою операции без частичной записи. Один заблокированный коммит хранилища объединяет перечисленные переопределения для точного канонического рабочего пространства. Неперечисленные Skills и другие рабочие пространства сохраняются. Нет записи настроек, декларации будущих имён или неявной активации родительского Extension.

GET возвращает v: 1, workspaceId, workspaceCwd, extensionId, name и skills. Каждый Skill имеет name, defaultEnabled, nullable workspaceEnabled, effectiveEnabled и опциональные disabledReason/lockedScope. Опциональный skillStates манифеста задаёт значения по умолчанию; отсутствующие значения по умолчанию считаются включёнными. Для активного родителя приоритет следующий: жёсткое отключение в настройках, явное включение в настройках, отключение по умолчанию в настройках, внутреннее переопределение рабочего пространства, значение по умолчанию из манифеста. Внутреннее отключение использует disabledReason: "default" и никогда не блокирует настройки. Отключённый или удалённый родитель не может быть восстановлен переопределением Skill.

Операция set_extension_state возвращает status: "updated" с упорядоченными result.resourceStates.skills; result.states сохраняет своё значение проверки обновлений. Сохранённое состояние не является доказательством того, что каждая сессия была обновлена. Демон обновляет только Skills и их контекст команд/моделей для целевой среды выполнения, включая загрузочные и активные сессии, без перезапуска несвязанных MCP/LSP/хуков. Сбои обновления после фиксации приводят к предупреждениям без отката состояния. Переопределения сохраняются после перезапуска и обновлений Extension и удаляются при деинсталляции. Клиенты не должны использовать резервный вариант API настроек Skill, который записывает настройку более высокого приоритета.

Глобальный каталог

Ответ глобального каталога имеет следующий вид:

{ "v": 1, "generation": 12, "extensions": [ { "id": "<64 lowercase hex characters>", "name": "demo", "version": "1.2.3", "installType": "npm", "defaultActivation": "enabled", "workspaceOverrideCount": 1 } ] }

installType опускается, если метаданные установки недоступны. defaultActivation может быть enabled или disabled. workspaceOverrideCount исключает сохранённые записи inherit.

Ответ проекции рабочего пространства имеет следующий вид:

{ "v": 1, "workspaceId": "workspace-id", "workspaceCwd": "/absolute/workspace", "trusted": true, "desiredGeneration": 12, "appliedGeneration": 11, "extensions": [ { "extensionId": "<64 lowercase hex characters>", "name": "demo", "version": "1.2.3", "defaultActivation": "enabled", "workspaceActivation": "disabled", "effectiveActivation": "disabled", "activationSource": "workspace_override" } ] }

workspaceActivation может быть enabled, disabled или null для наследования. activationSource может быть default, workspace_override, legacy_path_rule или cli_override. desiredGeneration — это поколение долговременного хранилища; appliedGeneration — последнее поколение, которое контроллер зафиксировал как применённое к этой среде выполнения рабочего пространства, и оно может временно отставать.

Установка требует явного согласия и начальной активации:

{ "source": "@scope/demo", "consent": true, "activation": { "scope": "user" }, "ref": "optional-git-ref", "autoUpdate": true, "allowPreRelease": false, "registry": "https://registry.npmjs.org" }

Для начальной активации только в рабочем пространстве используйте { "scope": "workspace", "workspaceId": "target-workspace-id" }; цель должна существовать и быть доверенной. Установки демона принимают источники GitHub, Git и npm. Когда поддерживается extension_local_path_install, они также принимают абсолютный путь, существующий на хосте демона. ref неприменимо к npm, ref и autoUpdate неприменимы к локальным источникам, а registry применимо только к npm. ref, autoUpdate, allowPreRelease и registry являются необязательными.

Когда поддерживается extension_git_credentials, HTTPS-источник Git может включать userinfo, например https://username:token@git.example.com/org/repository.git. credentialPersistence допустимо только с таким источником. Это может быть stored или one_time и по умолчанию равно one_time, если опущено. Режим хранения сохраняет учётные данные через гибридное хранилище секретов демона и оставляет только чистый URL репозитория в метаданных установки, поэтому extension остаётся обновляемым. Одноразовый режим не сохраняет ни URL репозитория, ни учётные данные и создаёт необновляемый snapshot; autoUpdate: true отвергается для этого режима. Предоставление поля без учётных данных в URL, предоставление недопустимых учётных данных или использование учётных данных с источниками npm, архивами, локальными путями, SSH или не-Git приводит к 400.

Ответы операций установки с учётными данными и сами операции раскрывают credentialPersistence и могут раскрывать credentialStorage как keychain или encrypted_file. Одноразовые операции опускают source; операции хранения могут вернуть чистый источник. Записи каталога/статуса снимка опускают источник, устанавливают credentialPersistence равным one_time и сообщают not updatable. Обновление завершается ошибкой extension_not_updatable; недоступный сохранённый секрет вызывает ошибку до доступа к сети с кодом extension_credential_unavailable.

Запросы активации глобальных и рабочих пространств PUT используют одинаковое тело запроса:

{ "state": "enabled" }

state может быть enabled или disabled. Запросы обновления, деинсталляции, проверки обновлений, сброса активации и обновления не имеют обязательного тела запроса.

Пакетные запросы активации используют имена Extension:

{ "extensionNames": ["formatter", "review-tools"], "state": "disabled" }

Пакетная операция для рабочего пространства также принимает "state": "inherit". Терминальные глобальные результаты содержат name и defaultActivation; результаты для рабочего пространства содержат name, workspaceActivation (null для наследования) и effectiveActivation. Некорректные имена отвергают запрос; конфликты с существующими идентификаторами Store приводят к атомарному сбою без частичной фиксации. Неизвестная цель inherit не сохраняется, поскольку сброс переопределения не должен создавать декларацию активации по умолчанию или заменять последующее согласие на установку.

Каждая принятая асинхронная мутация возвращает:

HTTP/1.1 202 Accepted Location: /extensions/operations/<operation-id> Retry-After: 1 Content-Type: application/json {"accepted":true,"operationId":"<operation-id>"}

Мутации с указанием рабочего пространства используют тот же глобальный путь опроса /extensions/operations/:operationId. История операций является локальной для процесса, хранит только ограниченное количество терминальных записей и теряется при перезапуске демона; клиенты должны повторно прочитать каталог или проекцию рабочего пространства и сравнить поколения, когда идентификатор операции исчезает.

Снимок операции имеет следующую структуру:

{ "v": 1, "operationId": "<operation-id>", "operation": "install", "status": "running", "phase": "preparing", "createdAt": 1750000000000, "updatedAt": 1750000000100, "source": "owner/repository", "name": "demo" }

status переходит из queued в running, затем в succeeded, succeeded_with_warnings или failed. Во время выполнения phase может быть preparing, committing или reconciling. Успешное завершение может включать result со значением status, равным installed, enabled, disabled, updated, uninstalled, checked или refreshed; результаты согласования могут дополнительно содержать refreshed, failed и error, тогда как результаты пакетной активации содержат упорядоченные results. Проверки обновлений возвращают result.states, ключом которого является имя extension, со значениями, такими как checking for updates, update available, up to date, not updatable или error. Учётные данные и заголовки авторизации никогда не являются полями операции.

Долговременная фиксация, за которой следует неполная очистка или несогласованность среды выполнения, не сообщается как неудачная мутация. Она возвращает succeeded_with_warnings и сохраняет зафиксированный результат:

{ "v": 1, "operationId": "<operation-id>", "operation": "uninstall", "status": "succeeded_with_warnings", "createdAt": 1750000000000, "updatedAt": 1750000000200, "result": { "status": "uninstalled", "name": "demo", "refreshed": 2, "failed": 0 }, "warnings": [ { "workspaceId": "workspace-id", "workspaceCwd": "/absolute/workspace", "code": "reconcile_slow", "error": "Runtime reconciliation took 31000ms." } ] }

Предупреждения workspaceId и code являются необязательными; workspaceCwd и error всегда присутствуют. Клиенты должны отображать предупреждения, обновлять свой каталог/проекцию и не должны слепо повторять долговременную мутацию.

Ошибки валидации и авторизации являются синхронными HTTP-ошибками, использующими { "error": "...", "code": "..." }, когда существует стабильный код. Важными случаями являются 400 invalid_extension_id, 400 invalid_extension_names, 400 invalid_extension_name, 400 invalid_extension_activation, 400 workspace_mismatch, 403 untrusted_workspace, 404 extension_operation_not_found и 429 extension_queue_full. Валидация установки также возвращает 400 для недопустимых параметров источника/ref/registry, отсутствия согласия или отсутствия/недопустимой начальной активации. Мутация, которая терпит неудачу после 202, представляется, оставаясь в истории операций, с status: "failed", error и необязательным стабильным code; распространённые коды включают extension_prepare_timeout и extension_conflict. HTTP-код 404 для операции не подразумевает откат, поскольку история операций не является долговременной.

daemon_status объявляет GET /daemon/status, консолидированный снимок диагностических данных оператора только для чтения, описанный ниже.

daemon_update объявляет глобальную для процесса поверхность GET /daemon/update, POST /daemon/update/prepare и POST /daemon/update/restart.

scheduled_task_gate объявляет необязательный gate на POST и PATCH /scheduled-tasks, а также gate и gateStatus на представлениях задач. Gate — это командная оболочка, которую владеющая сессия запускает при каждом срабатывании вне модели: выход 0 с выводом доставляет срабатывание с этим выводом, выход 0 без чего-либо в stdout пропускает его, а ненулевой выход сообщается, когда gate начинает давать сбои, а не при каждом срабатывании. gateStatus.lastOutcome может быть quiet, delivered, failed или blocked.

  • Gate требует повторяющейся задачи: POST с recurring: false или PATCH, которые устанавливают gate на одноразовой задаче или превращают задачу с gate в одноразовую, отвечают 400 gate_requires_recurring; задача, уже находящаяся в таком состоянии, сохраняет свои другие изменения. Неверно сформированный gate (пустой на POST, только комментарий, более 2048 символов, символ NUL или непарный суррогат UTF-16, обертка оболочки, такая как bash -c '…', или любая часть, запущенная в фоне с помощью &) отвечает 400 invalid_gate. Проверка & является лексической и выполняется по принципу «лучшее усилие»; при сомнениях она отказывает (& в комментарии, теле heredoc или (( … ))), а gate, прошедший эту проверку, сообщается как failed, если он завершается без вывода в stdout, пока фоновая часть все еще удерживает свой stdout (вне песочницы bwrap shell, пространство PID которой завершает эту часть вместе с командой). На PATCH, gate: null или gate: "" очищает gate и его condition.
  • Устаревшая задача, защищенная condition, отклоняет любой ключ gate в PATCH с кодом 409 task_legacy_unsupported, поэтому клиент отправляет gate только если изменяет его.
  • Gate, записанный через эти маршруты, не получает подтверждения от сессии. Он выполняется без присмотра только там, где вызов monitor той же команды выполнился бы без запроса: под сохраненным правилом allow Monitor(...) или Bash(...), как команда только для чтения или в сессии auto или yolo. Правило deny блокирует его во всех режимах, а режим plan блокирует его; заблокированное срабатывание завершается состоянием blocked.
  • Клиенты не должны отправлять gate в демон без этого тега: такой демон отбрасывает его на POST и отвечает только для шлюза PATCH с кодом 400 empty_patch.

autofix_pr_watches объявляет GET /autofix-pr/watches (основной workspace) и GET /workspaces/:workspace/autofix-pr/watches (зарегистрированный, доверенный). Каждый отвечает на /autofix-pr наблюдения клона workspace так, как перечислено в qwen autofix-pr status, считывая файлы наблюдений из основного рабочего дерева клона (.qwen/tmp), каждое открывается без следования ссылкам и без блокировки. Блокировка не берется, GitHub не опрашивается, и не выполняется ни одна команда git, кроме git worktree list, которая находит клон. Ответ — { v: 1, available, root?, now?, watches, unreadable? }: available ложно, без наблюдений, когда для workspace не найден git-репозиторий (или git не может там работать), а root — это основное рабочее дерево клона, одинаковое для двух workspace одного клона. Наблюдение содержит поля из status (Раздел 6.10 дизайна /autofix-pr), но без thisSession, worktreePath, pushTarget, gitTrusted и worktreeWork; его session — это сессия, владеющая им, которая может отсутствовать в этой демоне. Остановка наблюдения — это ответственность навыка (/autofix-pr stop <n> в владеющей сессии); эти маршруты ничего не меняют.

Условные теги. Эти функциональные теги объявляются только тогда, когда активны их переключатель развертывания, интеграция во время выполнения или условие доступности. Наличие тега означает, что документированное поведение доступно; отсутствие означает либо более старый демон, предшествующий тегу, либо текущий демон, где это условие ложно. В настоящее время:

ТегОбъявляется, когда …
agent_collaboration_v1хотя бы одно зарегистрированное рабочее пространство выбрало постоянное сотрудничество Agent Workspace через experimental.agentCollaboration (любой охват, включая рабочее пространство) или QWEN_CODE_ENABLE_AGENT_COLLABORATION=1. Присутствие означает /workspaces/:workspace/agent/* и то, что маршруты транспорта Agent Host смонтированы; workspaces[].agentCollaborationEnabled идентифицирует состояние выбора каждого рабочего пространства, и клиенты должны показывать или вызывать поверхность сотрудничества только для включенных записей. Более старые демоны пропускают это поле рабочего пространства, поэтому клиенты могут вернуться к этому верхнеуровневому тегу для совместимости. Отсутствие означает, что сотрудничество не обслуживается в этом ответе — тег пересчитывается для каждого запроса, а не снимается при загрузке — поэтому клиент должен убрать точку входа, а не отображать ее и позволять вызовам возвращать 404. Первое включение требует перезапуска; переключение для рабочего пространства, когда другое уже имеет его включенным, вступает в силу при следующем запросе. Отлично от безусловного тега workspace_agents, который представляет CRUD определений субагентов.
session_message_deletionпрофиль private Hosted Harness не активен. Перечисляет и удаляет группы сообщений через маршруты владельцев живых сессий; обычные локальные сессии демона поддерживают физическое удаление, тогда как Managed authoritative storage и SSH-рабочие пространства отвергают его.
hosted_harness_private_v1профиль private Hosted Harness активен; доступен только его аутентифицированный API сессий.
managed_session_journal_delta_v1профиль private Hosted Harness активен (то же условие, что и для hosted_harness_private_v1), и эта сборка может открывать журналы Managed Session, содержащие записи message.delta. Плоскость управления требует этот тег при согласовании возможностей, поэтому откат Harness, предшествующий этим записям, отвергается там вместо того, чтобы приводить к сбою при каждом открытии Session (managed_session_open_failed).
require_authдемон был запущен с --require-auth (или requireAuth: true через встроенный API). Bearer-токен обязателен для каждого обычного маршрута API, включая /health на привязках loopback; входной канал webhook сохраняет свою независимую аутентификацию по общему секрету, а маршруты документов и ресурсов Web Shell остаются до аутентификации.
mcp_workspace_poolобщий пул транспортов MCP активен. Пропускается, когда QWEN_SERVE_NO_MCP_POOL=1 отключает пул.
mcp_pool_restartобщий пул транспортов MCP активен; ответы на перезапуск могут включать многозаписные формы, учитывающие пул.
external_tool_guardqwen serve завершило рукопожатие запуска для --external-tool-guard-mode=required; каждый порожденный канал ACP должен подтвердить установленный обратный вызов перед созданием Session, и каждый поддерживаемый вызов управляемого инструмента ACP верхнего уровня, достигающий финальной границы выполнения, должен получить одно внешнее предварительное разрешение на выполнение. Более ранние отказы разрешений/хуков не приводят к запросу провайдера. Вложенное выполнение AgentCore находится вне v1 и отвергается, пока этот внешний режим провайдера активен. Тег отражает только внешнего провайдера: независимо от него каждый демон применяет встроенную защиту перемещения Git к управляемым инструментам, которые содержат командную строку оболочки (run_shell_command и monitor), поэтому отсутствие этого тега не означает отсутствие предварительных отказов в выполнении.
allow_originT2.4 (#4514 ). Демон был запущен как минимум с одним --allow-origin <pattern> (или allowOrigins: [...] через встроенный API). Междоменные запросы от совпадающих источников получают корректные заголовки ответа CORS; несовпадающие источники по-прежнему получают стандартную ошибку 403. Настроенный список шаблонов намеренно НЕ выводится в /capabilities, чтобы избежать утечки набора доверенных источников для неаутентифицированных читателей — браузерный клиент уже знает свой собственный источник.
prompt_absolute_deadline--prompt-deadline-ms / QWEN_SERVE_PROMPT_DEADLINE_MS / ServeOptions.promptDeadlineMs установлено в положительное целое число.
writer_idle_timeout--writer-idle-timeout-ms / QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS / ServeOptions.writerIdleTimeoutMs установлено в положительное целое число.
workspace_settingsдемон создан с доступным сохранением настроек.
user_language_syncдемон создан с доступным сохранением настроек (то же условие, что и у workspace_settings), поэтому безсессионный маршрут POST /language зарегистрирован.
workspace_voiceсохранение настроек доступно, поэтому маршруты Voice для основного рабочего пространства активны.
workspace_voice_transcriptionв основном рабочем пространстве настроена модель транскрипции Voice.
session_shell_commandвыполнение команд в сессии явно включено и активно при аутентификации через bearer или доверенном loopback; вызовы по-прежнему требуют привязанный к сессии идентификатор клиента.
standalone_sessions_v1демон установил полный автономный runtime для сессий, координатор жизненного цикла, надежный журнал удаления, реализацию управляемых директорий и семейство маршрутов /standalone/sessions. Прямые встраивания без полного графа зависимостей пропускают как маршруты, так и этот тег.
standalone_session_options_v1установлен полный автономный runtime для сессий (то же условие, что и у standalone_sessions_v1), поэтому только для чтения, безсессионный маршрут GET /standalone/session-options зарегистрирован во внутреннем runtime Conversations.
session_artifacts_persistenceсохранение артефактов сессий подключено для runtime.
session_artifact_libraryсохранение артефактов сессий подключено; поддерживает чтение/удаление живых, холодных и архивированных регистраций доверенным владельцем рабочего пространства через /workspaces/:workspace/session/:id/artifact-library.
workspace_artifact_libraryСохраненные запросы артефактов уровня рабочего пространства, пакетные запросы сессий и удаление явных пар через /workspaces/:workspace/artifact-library.
workspace_artifact_library_streamМетаданные сессий при всех чтениях библиотеки артефактов и опциональный конечный SSE для полных и выбранных запросов сессий; требует сохранения артефактов.
session_sourcesсохранение источников сессий подключено для runtime. Регистрирует файлы метаданных рабочего пространства, загруженные вложения и ссылки HTTP(S) через активного владельца сессии.
session_generationвспомогательные функции генерации сессий доступны.
session_btw_streamКаждый зарегистрированный мост runtime поддерживает возобновляемое потоковое BTW. Клиенты должны проверить этот флаг перед вызовом /session/:id/btw/stream; в противном случае используйте существующий JSON интерфейс BTW.
scheduled_task_session_reuseНадежное управление сессиями запланированных задач активно, и каждый управляемый демоном runtime установил обратный вызов, позволяющий задаче явно привязаться к своей текущей существующей сессии.
workspace_generationВспомогательные функции генерации уровня рабочего пространства доступны.
rate_limit--rate-limit / QWEN_SERVE_RATE_LIMIT=1 / ServeOptions.rateLimit включено.
workspace_reloadПоддержка перезагрузки рабочего пространства доступна в конфигурации встроенных маршрутов.
workspace_trust_grantГорячая перезагрузка доверия подключена (то же условие, что и у workspace_trust_hot_reload). Оба маршрута предоставления доступа доверяют только привязанной папке, сохраняя существующее правило TRUST_FOLDER или TRUST_PARENT. После сохранения 200 возвращает статус v1 только если политика хоста и отчетный статус являются доверенными; в противном случае 409 { error, code: "trust_grant_ineffective" } указывает проверенное состояние/источник в error, включая отказ загрузки политики или IDE/файла. Сбои сохранения файла доверия возвращают 500 trusted_folders_invalid для FatalConfigError, иначе 500 internal_error. Чтение файлов доверия принимает комментарии и завершающие запятые писателя JSONC. Маршруты обслуживаются также без горячей перезагрузки: собственное предоставление API демона немедленно обновляет кэшированный статус v1, но стартовые привязанные гейты требуют перезапуска (requiresDaemonRestartForChanges: true). Внешние правки терминала/файлов требуют перезапуска демона для обновления этого кэшированного статуса.
workspace_trust_hot_reloadМониторинг политики доверия рабочего пространства и согласование генерации runtime подключены, поэтому изменения доверия вступают в силу без перезапуска демона, а статус доверия v2 сообщает о конвергенции.
channel_reloadМенеджер рабочих процессов каналов, управляемых демоном, включен и может перезагрузить свой текущий выбор.
channel_controlУправление runtime рабочих процессов каналов, управляемых демоном, подключено.
channel_managementНастройки каналов уровня рабочего пространства, жизненный цикл и управление сопряжением подключены.
channel_delete_config_loss_convergenceУправление каналами рабочего пространства подключено (то же условие, что и у channel_management). DELETE /workspaces/:workspace/channels/:name приводит в соответствие канал, конфигурация которого отсутствует в объединенном представлении настроек, которое разрешает Worker (системная + пользовательская + области рабочего пространства): runtime должен молчать о канале или подтвердить ровно одного зафиксированного владельца Worker в этом рабочем пространстве, который останавливается перед удалением сохраненного выбора запуска в этой области, вместо сообщения channel_instance_not_found. Канал, конфигурация которого все еще содержится в объединенном представлении, но область разрешенных настроек каналов рабочего пространства не владеет им — настроенный только на уровне пользователя или системы, тогда как рабочее пространство является проектной директорией — сообщает channel_instance_not_found с нетронутым runtime; канал, который настроен в разрешенной области, удаляется нормально, а рабочее пространство, чья директория является домашней директорией пользователя, разрешает эту область в общий файл пользователя, поэтому канал уровня пользователя настроен — и удален — там.
multi_workspace_sessionsЗарегистрировано более одного runtime рабочего пространства, поэтому создание сессии может выбрать доверенный runtime по cwd.
multi_workspace_session_rewindЗарегистрировано более одного runtime рабочего пространства; единичные маршруты перемотки живых сессий разрешают owning runtime.
multi_workspace_session_shellЗарегистрировано более одного runtime рабочего пространства, и выполнение команд в сессии явно включено при аутентификации через bearer или доверенном loopback; единичный REST shell разрешает owning runtime.
dynamic_workspace_registrationФабрика runtime рабочего пространства подключена к демону, поэтому существующая доверенная директория может быть зарегистрирована как вторичный runtime во время выполнения.
persistent_workspace_registrationХранилище регистрации рабочих пространств подключено к демону. Производственный runQwenServe автоматически предоставляет хранилище уровня пользователя; прямые встраивания createServeApp должны явно внедрять одно и владеть восстановлением своего реестра рабочих пространств при запуске.
scratch_workspace_registrationдоступно создание управляемого временного рабочего пространства — фабрика среды выполнения, проверенный корень управляемого временного хранилища и удаление среды выполнения настроены, и каждая управляемая среда выполнения соблюдает границу корня временного хранилища.
git_workspace_cloneна хосте демона есть бинарный файл git; POST /workspaces с { kind: "git", url, target } запускает асинхронную задачу клонирования, маршруты которой сообщают о прогрессе, отмене и каноническом cwd. Хосты без git пропускают этот тег.
workspace_runtime_removalудаляемые динамические или восстановленные из сохранения вторичные среды выполнения могут быть опорожнены и удалены через маршрут управления.
workspace_directoriesдемон сохраняет настройки рабочего пространства, и каждый доверенный локальный runtime может применять дополнительные директории к своим активным сессиям: /workspaces/:workspace/directories перечисляет, добавляет и удаляет записи, сохраненные в области рабочего пространства (context.includeDirectories). Рабочие пространства SSH, managed и standalone отвергаются даже при наличии тега, а экспериментальные демоны с парными движками пропускают тег и маршрут, поскольку мутация в области рабочего пространства не может достичь управляемого дочернего процесса.
native_directory_pickerхост демона может открыть нативный системный выбор директории (osascript на macOS, PowerShell на Windows, zenity на Linux-хосте с дисплеем). Безголовые хосты пропускают тег, поэтому клиенты скрывают нативный выбор демона вместо того, чтобы показывать гарантированную ошибку. Web Shell скрывает кнопку Обзор, если выбор недоступен; выбор директории хостом встраивания может предоставить его без этого тега.
workspace_runtime_stopдемон владеет учетом процессов ACP, полными наблюдениями за независимой активностью и запланированными задачами, а также мостом среды выполнения, поддерживающим подтвержденную остановку и снимки квитанций. Неподдерживаемые отдельные среды выполнения отключены в предпросмотре.
workspace_runtimeкаждый активный мост рабочего пространства предоставляет авторитетный снимок жизненного цикла среды выполнения; смешанные или устаревшие внедренные мосты пропускают тег.
workspace_skills_config_runtimeкаждое активное рабочее пространство поддерживает авторитетный жизненный цикл среды выполнения и разделенные маршруты конфигурации/среды выполнения для Skills, используемые управлением Skills и композитором нового сеанса.
skill_archive_installдоступны авторитетный жизненный цикл рабочего пространства, установка архивов Skill в исходном виде и квитанции попыток, ограниченные владельцем.
skill_zip_file_sourceдоступны авторитетный жизненный цикл рабочего пространства, локальное копирование ZIP демоном и квитанции попыток, ограниченные владельцем.
workspace_local_openхост демона может открыть директорию рабочего пространства в файловом менеджере ОС хоста (open на macOS, explorer.exe на Windows, xdg-open на Linux-хосте с дисплеем). Безголовые хосты пропускают тег, поэтому клиенты скрывают возможность «Открыть локально» вместо того, чтобы показывать гарантированный сбой запуска. Открываемый путь всегда является разрешенным зарегистрированным cwd рабочего пространства, через POST /workspaces/:workspace/open; маршрут принимает необязательное JSON-тело { "target": "terminal" } (отсутствует/другое = папка) и отвечает { kind: 'workspace-local-open', opened: true, target } с target, установленным в folder или terminal.
workspace_local_terminalхост демона может открыть окно терминала в директории рабочего пространства (open -a Terminal на macOS, wt.exe с резервным вариантом cmd.exe на Windows, gnome-terminal/konsole/xterm на Linux-хосте с дисплеем). Безголовые хосты пропускают тег, поэтому клиенты скрывают возможность «Открыть в терминале» вместо того, чтобы показывать гарантированный сбой запуска. Обслуживается через POST /workspaces/:workspace/open с телом { "target": "terminal" }.
workspace_qualified_acpHTTP-соединения ACP и многопространственные среды выполнения активны, поэтому множественная конечная точка ACP может выбрать вторичную среду выполнения.
workspace_qualified_voiceмногопространственные среды выполнения и общий слушатель WebSocket ACP/Voice активны, поэтому каждая модальность Voice, привязанная к рабочему пространству, доступна для вторичной среды выполнения.
workspace_qualified_memoryHTTP-соединения ACP и многопространственные среды выполнения активны, поэтому маршруты управляемой памяти, привязанные к рабочему пространству, могут выбирать отдельную дорожку задач для каждого рабочего пространства для операций remember, forget и dream.
client_mcp_over_wsдемон принимает MCP-серверы, размещенные клиентом, через WebSocket ACP. Это явное включение, не требуемое для пути туннеля CDP.
cdp_tunnel_over_wsдемон предоставляет обратный WebSocket-туннель /cdp, либо по явному включению, либо потому что разрешено происхождение расширения Chrome. Это означает лишь наличие туннеля; это не означает, что инструменты MCP Chrome DevTools зарегистрированы.
browser_automation_mcpHTTP-соединения ACP включены, cdp_tunnel_over_ws активен, токен носителя не блокирует /cdp, и QWEN_CDP_MCP_COMMAND указывает на внешний stdio-адаптер MCP. Основной пакет CLI не включает адаптер автоматизации браузера; без этого тега чат в боковой панели расширения Chrome может работать, но инструменты консоли/сети/скриншота/клика не регистрируются по умолчанию.
voice_transcribeконечная точка WebSocket Voice смонтирована; для успешной транскрипции все еще требуется настроенная модель Voice.
realtime_voiceдемон WebShell для macOS имеет включенный Live Voice, и нативный Host включил его с помощью QWEN_SERVE_LIVE_NATIVE_HOST=1 (по умолчанию выключено; см. ниже). /live/status сообщает о готовности, но возможность отзывается до включения функции.
realtime_voice_webLive Voice включен, и страница Web Shell сама может быть аудио-конечной точкой через WS /live/web на любой платформе, и это конечная точка по умолчанию (нативный Host доступен только на macOS и требует включения). Маршрут проходит аутентификацию как /voice/stream (подпротокол bearer), требует доверенного основного рабочего пространства и использует протокол Live Host с сокращенным приветствием: разрешение на микрофон плюс самопроверки ввода/вывода аудио. Сохраняется одна аренда Host: нативный Host заменяет браузер (4010), браузер никогда не вытесняет нативный Host (4009), и вторая вкладка перехватывает управление только с ?takeover=1. /live/status сообщает host.kind: "browser"; захват экрана требует общего доступа к экрану (см. ниже), а изменение горячих клавиш сохраняется для следующего нативного Host, а не регистрируется немедленно.
web_terminalHTTP-соединения ACP включены, поэтому доступная аутентифицированная конечная точка Web Terminal.
user_skill_activationДоступно сохранение пользовательских настроек. PUT /skills/activation сохраняет предпочтения пользователя с точным именем и ставит в очередь асинхронное распространение.

Live Voice по умолчанию использует конечную точку Web Shell (realtime_voice_web, WS /live/web) на всех платформах. Нативный Host для macOS требует включения: только демон macOS, запущенный с QWEN_SERVE_LIVE_NATIVE_HOST=1, объявляет realtime_voice, регистрирует WS /live/host и устанавливает Host при включении Live. Без него /live/setup сообщает nativeHost: false с ошибкой install, не подлежащей повтору, а POST /live/setup/install и POST /live/setup/launch отвечают 409 live_native_host_unavailable.

Браузерный Host отвечает на host.capture_visual только пока пользователь делится экраном со страницей, о чем страница сообщает в своем приветствии как selfChecks.screenShare. Совместное использование — это отдельное, явное действие — модель спрашивает посреди хода, и getDisplayMedia требует жеста пользователя — поэтому страница удерживает поток и берет один кадр на запрос. При отсутствии совместного использования она немедленно отвечает success: false, вместо того чтобы дать запросу истечь по таймауту.

Поскольку страница не может писать в файловую систему демона, она отправляет только JPEG. Демон игнорирует любые screenshotPath, о которых сообщает браузерная аренда, и сам сохраняет изображение под выбранным им именем в том же приватном каталоге захвата, куда пишет нативный Host. Нативный Host по-прежнему сохраняет свой собственный PNG и передает путь, как и раньше.

mcp_guardrails не входит в эту условную таблицу — это всегда активный тег, объявляемый всякий раз, когда бинарный файл поддерживает новые поля бюджета /workspace/mcp, независимо от того, настроил ли оператор бюджет. Операторы, которые не установили --mcp-client-budget, также получают новые поля (со значениями budgetMode: 'off', budgets: []).

mcp_guardrail_events (issue #4175  PR 14b) объявляет типизированные события push SSE, которые отражают пересечения состояний бюджета MCP без цикла опроса. Два типа кадров поступают на GET /session/:id/events:

  • mcp_budget_warning — срабатывает один раз при пересечении вверх порога 75% для reservedSlots.size / clientBudget. Повторно активируется только после того, как отношение упадет ниже 37,5% (MCP_BUDGET_REARM_FRACTION). Зеркалирует гистерезис slow_client_warning из PR 10, но на уровне менеджера, а не уровня очереди подписчика. Полезная нагрузка: { liveCount, reservedCount, budget, thresholdRatio: 0.75, mode: 'warn' | 'enforce' }. Срабатывает в режимах warn и enforce; никогда не срабатывает в режиме off.
  • mcp_child_refused_batch — срабатывает в конце каждого прохода discoverAllMcpTools*, если одному или нескольким серверам было отказано, И как пакет длиной 1 на пути отказа ленивого запуска readResource. Полезная нагрузка: { refusedServers: [{ name, transport, reason: 'budget_exhausted' }, ...], budget, liveCount, reservedCount, mode: 'enforce' }. mode является литералом 'enforce', поскольку режим warn никогда не отказывает.

Оба события находятся в кольце воспроизведения SSE для каждой сессии (они несут id), поэтому клиент, переподключающийся с помощью Last-Event-ID, возобновляет работу через них; снимок на GET /workspace/mcp остается источником истины для состояния после длительного отключения. Всегда активен после объявления — нет условного переключателя. Состояние редуктора SDK (DaemonSessionViewState) предоставляет mcpBudgetWarningCount, lastMcpBudgetWarning, mcpChildRefusedBatchCount, lastMcpChildRefusedBatch для адаптеров, которым нужен простой интерфейс отображения задержки.

Маршруты

Клиенты могут выполнять feature-detection для session_turn_status и опрашивать GET /session/:id/turns/current или GET /session/:id/turns/:promptId. Эти маршруты требуют активной владеющей Session и никогда не загружают и не сканируют другое рабочее пространство. Завершенные результаты являются best-effort записями транскрипта, считываемыми из активной ветки с ограниченным сканированием; prompt_not_found означает, что результат не был найден в живой очереди, терминальном оверлее на 64 записи или ограниченном активном окне. resultText — это сырой финальный ответ родительской модели после последней границы инструмента, перед необязательным переписыванием сообщения, и он может отсутствовать. Результаты более 32 768 кодовых единиц UTF-16 включают resultTruncated: true и resultCode: "RESULT_TEXT_TRUNCATED".

GET /health

Проба живости. Форма по умолчанию возвращает 200 {"status":"ok"}, если слушатель запущен — дешево, без доступа к мосту, подходит для высокочастотных проб живости k8s/Compose.

Передайте ?deep=1 (также принимает ?deep=true или просто ?deep) для пробы живости всего демона, которая агрегирует счетчики моста по всем управляемым средам выполнения рабочих пространств, включая рабочее пространство, которое все еще дренируется (информационно, не является истинной проверкой живости):

{ "status": "ok", "workspaceCount": 2, "sessions": 3, "pendingPermissions": 1, "activePrompts": 1, "activeWork": true, "activeWorkReporting": "full", "activeWorkStaleMs": 4200, "connectedClients": 2, "channelAlive": true, "lastActivityAt": "2026-07-15T08:30:00.000Z", "idleSinceMs": 120000 }

sessions, pendingPermissions и activePrompts являются суммами. activeWork равно true, если любая среда выполнения имеет принятый, но незавершенный prompt (включая prompt, ожидающий в FIFO), работающий фоновый Agent, уведомление терминала queued/in-progress Agent, фоновую оболочку или работу workflow, управляемую Session, или ход Session, принадлежащий дочернему процессу. Агрегированное удержание session охватывает обработку целей и cron, мутацию истории и continuations Monitor в очереди или выполняемые; foreground prompts остаются собственностью демона. Работающие Monitors, предложения follow-up и внешние процессы, которые реестр shell больше не может отслеживать, остаются вне поля. Это session-scoped: работа на уровне канала, еще не привязанная к сессии — запуск в полете, ожидающее восстановление, обнаружение или аутентификация MCP — не учитывается, поэтому activeWork может читать false, даже если демон все еще отказывается освобождать этот канал. Не интерпретируйте это поле как «демон можно освободить»; оно описывает только работу, принадлежащую сессии. activeWorkReporting показывает, какая часть этого булева значения действительно подтверждена: full, когда каждая живая сессия покрыта свежим отчетом от дочернего процесса, который сообщает обо всех требуемых категориях, none, когда ни одна сессия не согласовала отчетность, и partial для всего промежуточного — включая устаревший снимок или согласованный дочерний процесс, который пропускает требуемую категорию. Снимок старше трех интервалов отчетности перестает считаться покрытием: это не отчет о том, что сессия простаивает, поэтому сессия снова читается как сохраняемая, точно так же, как если бы дочерний процесс никогда не сообщал. Обычная автоматическая очистка также отключена для согласованного, но неполного дочернего процесса; дочерний процесс, который не понимает shell или session, не может безопасно авторизовать условное закрытие согласно полному текущему предикату. Полностью неподдерживаемые исторические дочерние процессы сохраняют поведение legacy cleanup, а явные close, kill, shutdown и выход из канала остаются force operations. activeWorkStaleMs — это возраст самого старого снимка, на котором основано булево значение среди покрытых сессий, и равен 0, когда ни одна сессия не покрыта; это диагностическое значение, потому что свежесть уже оценена в activeWorkReporting демоном (только демон знает согласованный ритм каждого канала). Оценка вычисляется один раз для всех управляемых сред выполнения, а не для каждой среды отдельно, а затем комбинируется — среда выполнения без сессий является вакуумно полной, и трактовка этого как доказательства позволила бы пустому рабочему пространству поручиться за несообщаемые сессии другого рабочего пространства. lastActivityAt — это последнее ненулевое время активности рабочего пространства, а idleSinceMs выводится из того же снимка. channelAlive означает, что хотя бы один канал управляемого рабочего пространства активен; это не означает, что каждое рабочее пространство здорово. connectedClients и необязательный rateLimitHits остаются счетчиками всего демона, а не суммами по рабочим пространствам.

Контроллеры перезапуска должны считать демон занятым, когда:

const busy = health.activePrompts > 0 || health.activeWork || health.activeWorkReporting !== 'full';

Отбрасывание третьего члена делает activeWork === false неотличимым от «дочерний процесс мне ничего не сообщил», что является единственным случаем, когда действие на основе этого небезопасно. Неизвестные ответы и неудачные пробы также должны предотвращать перезапуск. activePrompts остается независимым сигналом совместимости.

Эти поля являются кэшем наблюдений, а не арендой перезапуска: даже свежий, полностью оцененный, пустой ответ описывает момент выборки, и работа может начаться сразу после него. Приведенное выше правило значительно снижает риск неверного перезапуска, но не устраняет его — строгая безопасность требует забора prepare-restart, который останавливает прием новой работы, подтверждает дренаж и только затем выполняет выключение.

⚠️ Глубокое зондирование носит информационный характер, а не является реальной проверкой работоспособности или атомарным освобождением аренды. Согласованные дочерние процессы ACP публикуют снимки активной работы по всему каналу с согласованной периодичностью, и демон оценивает их актуальность в activeWorkReporting — но он никогда не завершает канал из-за отсутствия отчета, поскольку молчание одной сессии не является доказательством того, что процесс завершился. Живость транспорта и обнаружение зависших агентов — это отдельные механизмы. connectedClients подсчитывает соединения REST SSE, а не каждый транспорт ACP. Используйте повторные выборки и корректное завершение работы для освобождения простаивающих ресурсов; используйте аутентифицированный /daemon/status для диагностики транспорта и отдельных рабочих пространств. Если любой геттер управляемой среды выполнения вызывает исключение, глубокая проверка состояния завершается ошибкой с 503 {"status":"degraded","reason":"aggregation_failed"} вместо возврата частичных итогов, и журнал демона указывает на среду выполнения рабочего пространства, вызвавшую ошибку. Во время начальной загрузки, до готовности реестра сред выполнения, возвращается 503 {"status":"degraded","reason":"bootstrap"} с Retry-After: 1. Для проверки живости слушателя используйте стандартный /health без ?deep.

Авторизация: требуется при привязке к не-loopback интерфейсам и когда loopback защищен с помощью --require-auth. При обычной привязке к loopback (127.0.0.0/8, localhost, ::1, [::1]) маршрут /health регистрируется перед middleware bearer-токена, чтобы зонды k8s/Compose внутри пода могли работать без передачи токена. При привязке к не-loopback (например, --hostname 0.0.0.0) или защищенном loopback маршрут регистрируется после middleware bearer-токена и возвращает 401 без действительного токена — иначе неаутентифицированный вызывающий объект мог бы зондировать произвольные адреса, чтобы подтвердить существование qwen serve, что представляет собой утечку информации низкой степени критичности, плохо сочетающуюся со сканированием портов. Запрет CORS + белый список Host все еще применяются к исключению для обычного loopback.

GET /daemon/update, POST /daemon/update/prepare, and POST /daemon/update/restart

Эти аутентифицированные маршруты управляют установкой Qwen Code для демона независимо от выбранного рабочего пространства. Они всегда используют REST, включая запросы от SDK-клиентов с транспортом ACP-сессий. Возможность daemon_update объявляет протокол; доступность определяется возвращаемым состоянием.

GET проверяет наличие релизов без их загрузки или активации. Успешные проверки кэшируются на 15 минут, ошибки — на одну минуту; запрос ?refresh=true инициирует новую проверку. Параллельные проверки используют один и тот же поиск. Ответ содержит state (available, up-to-date, installing, ready, restarting, unavailable или error), canInstall, необязательные currentVersion и latestVersion, а также необязательные instructions и message. currentVersion идентифицирует запущенный демон, в том числе после подготовки обновления. Неподдерживаемые методы установки могут возвращать available вместе с canInstall: false и инструкциями по ручной установке.

Автоматические обновления требуют реального жизненного цикла CLI с POSIX process.execve, автономной установки или глобальной установки через npm/pnpm/yarn/bun, запускающей опубликованный лаунчер (префикс npm, принадлежащий root, поддерживается, так как этап подготовки никогда не записывает в глобальный префикс), а также настроек оператора, разрешающих general.enableAutoUpdate. Встроенные серверы, Windows, неподдерживаемые среды выполнения Node и режим разработки возвращают unavailable. Применяются только глобальные/системные настройки; рабочие пространства не могут включить установку. Соединение должно использовать основной слушатель и его токен runtime демона, либо явно доверенное loopback-развертывание без токена. Сопряженные браузеры и соединения Local Control возвращают unavailable, поскольку их учетные данные или слушатели не переживут перезапуск; их мутации обновления возвращают 403 update_connection_unsupported.

Оба POST-маршрута требуют строгих прав оператора демона для мутаций и не принимают параметров. Сервер выбирает релиз, место назначения и лаунчер; клиенты не могут указать версию или команду.

  • POST /daemon/update/prepare запускает фоновую загрузку и возвращает 202 с state: "installing". Это не изменяет активную установку. Опрашивайте GET, пока состояние не станет ready; повторные попытки подготовки используют ту же операцию, а уже подготовленное обновление возвращает 200. Если автоматическое обновление недоступно, возвращается 409 update_unavailable.
  • POST /daemon/update/restart требует подготовленного обновления, иначе возвращается 409 update_not_ready. Он отвечает с помощью 202 и state: "restarting" перед активацией обновления, корректным завершением работы демона и заменой процесса обновленным лаунчером. Повторные нажатия используют один и тот же перезапуск. После ответа демон повторно определяет текущий релиз: если он новее подготовленного и одношаговое обновление охватывает его, новый релиз подготавливается, а ранее подготовленный пакет отбрасывается, поэтому долгоживущий демон переходит на актуальную последнюю версию, а не на ту, которая была последней на момент первой подготовки. Новый пакет фиксируется до отбрасывания устаревшего, и это отбрасывание выполняется по принципу «best-effort»: сбой отбрасывания приводит к утечке дискового пространства, но никогда не оставляет новый пакет сиротой и не откатывается к пакету, который может быть удален частично. Затем latestVersion сообщает о новой целевой версии, при этом state остается в состоянии restarting. При сбое повторного определения или подготовки применяется подготовленный релиз; нажатие никогда не зависает на запущенной версии из-за сбоя реестра. Замена сохраняет PID, рабочую директорию, параметры CLI, привязанный порт и эффективный токен демона, очищает старые закрепления версий и не открывает заново браузер. Клиент опрашивает GET, пока currentVersion не изменится, затем перезагружает документ по URL, захваченному при запросе обновления. Поскольку подготовка выполняется после 202 и может занимать минуты, клиент отсчитывает дедлайн перезапуска от непрерывной недоступности, а не от момента нажатия: демон, который продолжает отвечать, все еще работает. Щедрый абсолютный лимит (20 минут) страхует демон, который остается доступным, но никогда не покидает состояние restarting. Этот лимит охватывает всю цепочку подготовки, а не отдельную константу тайм-аута: загрузка автономного архива последовательно пытается скачать с OSS, затем с GitHub, выделяя по пять минут на каждый источник, а получение контрольных сумм и подписей добавляет еще две минуты, поэтому одна переподготовка может потратить около двенадцати минут на скачивание и проверку перед активацией; управляемый дочерний процесс npm ограничен десятью минутами. Каждый путь отказа оставляет на экране элемент, допускающий повторную попытку, потому что зависший демон продолжает сообщать restarting, который обычно не отображается в элементах управления. После подтверждения перезапуска restarting становится постоянным до замены процесса, поэтому доступный демон, сообщающий любое другое состояние с неизменным currentVersion, означает, что замена не завершилась на новой сборке; клиент прекращает ожидание, публикует это финальное состояние и сообщает об ошибке вместо бесконечного ожидания изменения версии. Эта проверка применяется только к опросу, инициированному после подтверждения: запросы статуса и перезапуска независимы, поэтому опрос, начатый ранее, может позже доставить снимок состояния до перезапуска, и интерпретация этого как финального состояния демона привела бы к ложной ошибке и подавила бы перезагрузку страницы, которую успешное обновление все еще должно пользователю. Проверка автономного лаунчера выполняется до закрытия сервисов; неудачная проверка оставляет демон работающим. Сбои после начала завершения работы или во время замены процесса не гарантируют восстановления.

Web Shell автоматически проверяет и подготавливает доступные поддерживаемые обновления в фоновом режиме и отображает кнопку обновления рядом с версией, когда подготовленное обновление готово или когда доступное обновление содержит ручные инструкции. В состоянии с ручными инструкциями кнопка открывает всплывающее окно с инструкциями из проверки и никогда не выполняет подготовку или перезапуск. Подготовка и готовность сохраняются при перезагрузке среды выполнения рабочего пространства. Подготовленное обновление фиксирует свою цель: опрос GET, включая ?refresh=true, продолжает сообщать о подготовленном latestVersion и никогда не загружает повторно, поэтому демон, который остается активным между несколькими релизами, не вызывает лишних загрузок в фоне. Только перезапуск заново определяет цель. Обычное завершение работы демона отбрасывает подготовленную загрузку. Сбои активации оставляют демон работающим и сообщают error; следующая проверка после кэша ошибок длительностью одна минута может подготовить новую загрузку для повторной попытки. Отключение автоматических обновлений удаляет состояние готовности и очищает загрузку перед началом перезапуска. Как только перезапуск запущен, он не прерывается: пока состояние равно restarting, маршруты статуса, подготовки и перезапуска возвращаются без обращения к настройкам оператора, а очистка при завершении работы является операцией без действия, поэтому этап переключения, занимающий несколько минут, не может потерять свой пакет данных. Нажатие кнопки в состоянии готовности явно перезапускает весь демон и прерывает его активные сеансы.

GET /daemon/status

Диагностика оператора только для чтения. В отличие от /health, это обычный API демона: он регистрируется после проверки токена доступа и ограничения частоты запросов, включая привязки к локальному интерфейсу (loopback). Параметр запроса:

  • detail=summary (по умолчанию) читает только состояние демона в памяти.
  • detail=full также включает диагностику активных сеансов, диагностику соединений ACP, счетчики устройства потока авторизации и разделы статуса рабочего пространства.
  • любое другое значение detail возвращает 400 { "code": "invalid_detail" }.

summary намеренно не запрашивает методы статуса рабочего пространства, не запускает дочерний процесс ACP и не создает сеанс. full независимо запрашивает каждый раздел рабочего пространства; тайм-аут или исключение помечает только этот раздел как unavailable и добавляет проблему workspace_status_unavailable.

Формат ответа:

{ "v": 1, "detail": "summary", "generatedAt": "2026-06-16T00:00:00.000Z", "status": "ok", "issues": [], "daemon": { "pid": 12345, "uptimeMs": 3600000, "mode": "http-bridge", "workspaceCwd": "/repo", "qwenCodeVersion": "0.18.1", "daemonId": "serve-..." }, "security": { "tokenConfigured": true, "requireAuth": false, "loopbackBind": true, "allowOriginConfigured": false, "allowOriginMode": "none", "sessionShellCommandEnabled": false }, "limits": { "maxRegisteredWorkspaces": 256, "maxChannelControlWorkspaces": 25, "maxSessions": 32, "maxTotalSessions": 800, "maxPendingPromptsPerSession": 5, "listenerMaxConnections": 256, "eventRingSize": 8000, "compactedReplayMaxBytes": 4194304, "promptDeadlineMs": null, "writerIdleTimeoutMs": null, "channelIdleTimeoutMs": 0, "sessionIdleTimeoutMs": 1800000, "acpConnectionCap": 64 }, "runtime": { "sessions": { "active": 0 }, "permissions": { "pending": 0, "policy": "first-responder" }, "channel": { "live": false }, "channelWorker": { "enabled": false, "state": "disabled", "channels": [] }, "transport": { "restSseActive": 0, "acp": { "enabled": true, "connections": 0, "connectionStreams": 0, "sessionStreams": 0, "sseStreams": 0, "wsStreams": 0, "pendingClientRequests": 0 } }, "perf": { "eventLoop": { "meanMs": 0, "p50Ms": 0, "p99Ms": 0, "maxMs": 0 }, "promptQueueWait": { "count": 0, "meanMs": 0, "maxMs": 0, "lastMs": null }, "pipe": { "inbound": { "count": 0, "totalBytes": 0, "maxBytes": 0 }, "outbound": { "count": 0, "totalBytes": 0, "maxBytes": 0 } } }, "activity": { "activePrompts": 0, "pendingPrompts": 0, "queuedPrompts": 0, "lastActivityAt": null, "idleSinceMs": null } } }

Ответы для нескольких рабочих пространств также включают верхнеуровневые строки workspaces[] с { id, cwd, displayName?, primary, trusted }. Необязательное отображаемое имя опускается, если оно не задано, и остается только для представления; потребители статуса должны продолжать использовать id или cwd для корреляции сред выполнения.

runtime.perf необязательно. При наличии он сообщает о задержке цикла событий процесса демона, выборках времени ожидания очереди FIFO подсказок и счетчиках байтов канала связи дочернего процесса демона; задержка цикла событий дочернего процесса ACP не включается в /daemon/status.

status равен error, если любая проблема имеет серьезность ошибки, warning, если любая проблема имеет серьезность предупреждения, иначе ok. Коды проблем стабильны и включают session_capacity_high, connection_capacity_high, pending_permissions, acp_channel_down, preflight_error, mcp_budget_warning, mcp_budget_exhausted, rate_limit_hits, channel_worker_exited, channel_worker_partial_connect, channel_restore_failed и workspace_status_unavailable. В короткий промежуток времени после готовности слушателя, но до полного монтирования среды выполнения, /daemon/status может сообщить daemon_runtime_starting; если асинхронное монтирование среды выполнения завершается ошибкой, он сообщает daemon_runtime_failed, тогда как маршруты среды выполнения, не связанные со статусом, возвращают 503.

runtime.activity сообщает о активности промптов в масштабе демона. activePrompts подсчитывает сессии с выполняющимся промптом. pendingPrompts подсчитывает все принятые промпты, которые еще не завершились, включая текущий выполняющийся промпт и промпты, ожидающие в очереди FIFO. queuedPrompts подсчитывает промпты, ожидающие в очереди FIFO, которые были приняты, но не отправлены на выполнение. lastActivityAt — это временная метка ISO 8601 последнего начала/завершения промпта или запуска сессии; null если демон не обрабатывал никакой активности с момента загрузки. idleSinceMs вычисляется из lastActivityAt во время формирования ответа.

limits.memory является аддитивным и сообщает разрешенные значения памяти демона: обязательное enforced: false, объект childHeap (mode: off, observe или admit; admissionEnforced, истинно только для управляемых admit; maxConcurrentChildren и perChildCeilingMb, оба null под mode: 'off', который ничего не моделирует — и perChildCeilingMb дополнительно null везде, где раздел не может быть смоделирован в пределах modeled.minChildHeapMb — либо пул не может покрыть один дочерний процесс при этом минимальном значении, либо максимальное значение окажется ниже него после ограничения до modeled.legacyChildCeilingMb, что равно floor(available / 2) и поэтому падает ниже минимального значения на хостах с менее чем 1024 МБ. Оно никогда не равно 0, и maxConcurrentChildren равно 0 в этих случаях, поскольку хост, который не моделирует раздел, дает вычисленный ответ, а не отсутствующую модель; и refusals, запуски, которые превысили бы смоделированное ограничение), configuredBudgetMb, effectiveBudgetMb (настроенное значение, ограниченное разрешенной памятью cgroup/хоста), budgetSource (flag / derived), availableMemoryMb, availableMemorySource (constrained / host), insufficientMemory и объект modeled, содержащий rootReserveMb, childPoolMb, minChildHeapMb, maxChildHeapMb и legacyChildCeilingMb (консервативная модель максимального значения, которое дочерний процесс ACP получает сегодня, и которое может быть ниже реального значения). runtime.memory дополнительно сообщает registeredWorkspaces (количество регистраций — записи рабочего пространства, не удаленные, включая находящиеся в процессе завершения, переходящие или заблокированные; это не количество активных дочерних процессов), activeAcpChildren (дочерние процессы ACP, управляемые демоном, с живым незавершаемым каналом — включает переходящие или заблокированные записи, но исключает рабочее пространство, для которого начато завершение процесса, даже если дочерний процесс еще не вышел; не рабочие процессы каналов, потомки MCP или незарегистрированные резервирования запусков), childRssCoverage (active_children — каждый дочерний процесс ACP с живым каналом, то есть набор, который подсчитывает activeAcpChildren; более старые демоны отправляют primary_only), объект children, описанный ниже, и объект modeled, содержащий recommendedShareAtRegisteredMb (null, когда не зарегистрировано ни одно рабочее пространство) и recommendedShareAtActiveMb (null, когда ни один дочерний процесс не активен). Каждая доля ограничена устаревшим максимальным значением для дочернего процесса и установлена как минимум по размеру кучи дочернего процесса только тогда, когда максимальное значение это позволяет — на малых хостах максимальное значение находится ниже минимального, поэтому доля × количество может превышать пул дочерних процессов. Воспринимайте долю как рекомендательную, а не как разделение пула. Определение размера кучи дочернего процесса остается рекомендательным: ни один аргумент запуска дочернего процесса не выводится из этих значений. Опциональный admit применяет смоделированное количество одновременных дочерних процессов, тогда как по умолчанию observe только фиксирует потенциальные отказы. runtime.memory.committedAcpChildren подсчитывает управляемые резервирования ACP и присоединенные дочерние процессы, включая завершающиеся дочерние процессы до освобождения реестра; он равен null, когда управляемый реестр недоступен. childHeap моделирует фиксированное разделение modeled.childPoolMb — каждый дочерний процесс получил бы одинаковый perChildCeilingMb, поэтому смоделированная сумма остается внутри пула, а не накапливается, как это было бы с долей на запуск. Воспринимайте refusals только как давление допуска: значение 0 не означает, что разделение безопасно применять, потому что дочерние процессы работают на гораздо большем максимальном значении, полученном от хоста, поэтому нагрузка, требующая больше старого пространства, чем perChildCeilingMb, здесь считается здоровой и терпела бы неудачу только после применения разделения. Две дополнительные причины, почему ненулевое количество не обязательно означает проблему с емкостью: решение о допуске учитывает завершающийся дочерний процесс до его выхода, поэтому на демоне, уже достигающем maxConcurrentChildren, каждая замена канала бронирует отказ во время окна перекрытия; и на хосте, слишком малом для моделирования раздела, maxConcurrentChildren равно 0, поэтому refusals равно общему количеству запусков ACP, а поле insufficientMemory объясняет это. В admit модель с нулевым количеством слотов приводит к сбою при запуске. Полностью управляемый демон, столкнувшийся с исчерпанием емкости, может освободить один наименее недавно использованный пустой ACP без живых сессий или ожидающих задач, дождаться отслеживаемого освобождения, затем повторить попытку допуска один раз перед запуском. Загруженные сессии остаются защищенными; регистрация рабочего пространства и сохраненная история сохраняются. Счетчик refusals подсчитывает попытки отказа в допуске, включая первоначальный отказ, за которым последовало успешное освобождение, а не окончательные видимые пользователю ошибки. Исчерпанная емкость возвращает REST HTTP 503 с code: "acp_child_capacity_exhausted", maxConcurrentChildren и committedAcpChildren (исключая отклоненное резервирование), без Retry-After. JSON-RPC ACP использует -32603 с data.errorKind, несущим этот код, data.httpStatus: 503 и те же счетчики. Проверенный откат создания автономного процесса до отправки сохраняет standalone_creation_rolled_back, его идентификатор сессии и возможность повторной попытки, и вкладывает код емкости и счетчики в capacity; неопределенные результаты создания сохраняют свою существующую семантику восстановления. Клиентам следует прекратить автоматические повторные попытки из-за проблем с емкостью и сохранить черновик или выбранную сессию для ручной повторной попытки. На обычном пути runQwenServe бюджет определяется до создания приложения bootstrap, поэтому limits.memory уже заполнено во время окна bootstrap. Оно равно null только на путях, которые не определяют бюджет (например, прямое встраивание, обходящее runQwenServeImpl). Тип SDK допускает null, поэтому корректные клиенты справляются с этим.

runtime.memory.children является аддитивным в рамках этого блока и сообщает агрегированный RSS среди дочерних процессов, указанных в childRssCoverage: rssBytes (их суммированный самостоятельно сообщаемый RSS), sampled (сколько из них дали показания) и oldestReadingAgeMs (возраст самого старого показания в сумме, чтобы вызывающий мог понять, насколько далеко друг от друга были взяты части). Знаменатель для sampled — это соседний activeAcpChildren, а не повторяется внутри блока; когда sampled ниже, rssBytes представляет собой нижнюю границу, а не общее значение. Выборка выполняется только при наличии активного наблюдателя SSE/WS, поэтому запрос статуса к демону, с которым никто не устанавливает потоковое соединение, сообщает sampled: 0 даже при наличии живых дочерних процессов — activeAcpChildren рядом с ним делает этот разрыв видимым, и rssBytes: 0 вместе с sampled: 0 никогда не означает измеренный ноль. oldestReadingAgeMs равно null, когда ничего не было выборочно проверено, а также когда каждый источник данных является мостом, предшествующим этому полю, поэтому оно никогда не означает «актуальное». Воспринимайте сумму одновременно как завышенную и заниженную оценку: суммирование RSS каждого процесса дважды учитывает страницы, разделяемые дочерними процессами, при этом каждый дочерний процесс сообщает только о своем собственном процессе, поэтому его потомки MCP и все рабочие процессы каналов отсутствуют. Это не память дерева демона. Поле является необязательным в зеркале SDK, потому что демоны, сообщающие primary_only, никогда не отправляют его.

runtime.memory.children.heap аддитивен в рамках этого блока и сообщает о максимальных значениях V8 old-generation за время жизни каждого дочернего процесса ACP, агрегируемых как максимум, а не сумма: peakOldGenerationBytes, peakLiveSetBytes, peakTotalHeapBytes, majorGcCount, majorGcMs, unclassifiedSpaceNames и reported. Потолок кучи применяется к каждому дочернему процессу отдельно, и пики достигались в разное время, поэтому общее значение не ответило бы ни на один вопрос; каждое поле представляет собой независимый максимум по всем отчитывающимся дочерним процессам, а не портрет одного дочернего процесса, и потолок для каждого дочернего процесса оценивается по каждой оси отдельно. reported показывает количество из sampled, которые внесли вклад, и это число меньше, если некоторые дочерние процессы были запущены до появления этих полей. Все значения в байтах относятся к old generation — тому, что фактически ограничивает --max-old-space-size, — а не только к old_space, поскольку дочерний процесс может исчерпать свой потолок при нескольких мегабайтах old_space, в то время как large_object_space содержит всё остальное. peakOldGenerationBytes — это зарезервированные байты, которые растут вместе с потолком, выделенным дочернему процессу, поэтому воспринимайте их как верхнюю границу потребности нагрузки, а не как её точную потребность; peakLiveSetBytes — это то, что выживает после полной сборки мусора (major GC) и не зависит от потолка, что делает это значение показателем того, что дочерний процесс не помещается в него; воспринимайте его как верхнюю границу, а не как точный набор живых объектов, так как события GC поступают асинхронно, и всё, что было выделено между сборкой и чтением, учитывается. peakLiveSetBytes равно 0 до тех пор, пока не будет зафиксирована полная сборка мусора, что является отсутствием данных, а не измерением. unclassifiedSpaceNames — это объединение пространств кучи, которые ни один отчитывающийся дочерний процесс не смог классифицировать; V8 переименовывает и добавляет пространства между версиями, неизвестные пространства исключаются из сумм, и такое исключение приводит к занижению значений, поэтому непустой массив означает, что значения в байтах неполны и их нельзя рассматривать как полное измерение. Весь объект равен null, никогда не обнуленному объекту, когда ни один выборочный дочерний процесс не предоставил данных; если наблюдатель SSE/WS не подключен, ничего не выбирается, поэтому это обычное состояние, а не краевой случай. Всё это носит наблюдательный характер: ничто здесь не определяет размер дочернего процесса, не отказывает в запуске и не перемещает limits.memory.enforced с false.

runtime.memory.pressure аддитивен в рамках этого блока и сообщает о давлении памяти самого корневого процесса демона: mode (off / observe), level (normal / soft / hard / critical), source (rss / heap / unknown), ratio и шесть исходных значений, из которых рассчитываются коэффициенты — rssBytes, rssRatio, availableBytes, heapUsedBytes, heapRatio, heapLimitBytes. ratio — это большее из rssRatio и heapRatio, а source указывает, какое именно из них было больше; при равенстве сообщается rss. availableBytes — это limits.memory.availableMemoryMb в байтах — намеренно обнаруженное значение cgroup/хоста, а не effectiveBudgetMb, потому что процесс завершает реальный лимит, а не политическое значение оператора. source: "unknown" означает, что ни один знаменатель не был измерим, и это не следует трактовать как здоровое состояние; level равно normal в этом случае только потому, что классифицировать нечего. Значения относятся только к корневому процессу демона: это собственные показатели memoryUsage() данного процесса, поэтому рост дочерних процессов не влияет на них. runtime.memory.children сообщает об этих показателях отдельно, и ни одно из значений не отражает память всего дерева процессов. Оба режима сообщают весь блок; только observe дополнительно поднимает предупреждение daemon_memory_pressure без пути в сводку статуса, поэтому off оставляет верхнеуровневый status неизменным. Ничто не исправляется ни в одном из режимов. Поле является необязательным в зеркале SDK, потому что демоны, выпущенные до runtime.memory, отправляют блок без него.

limits.maxRegisteredWorkspaces аддитивен и сообщает об установленном пределе регистрации пользователей (по умолчанию 256, настраиваемый от 1 до 256). limits.maxChannelControlWorkspaces сообщает о независимом пределе владельцев управления/восстановления, равном 25, для стандартного демона, даже когда каналы отключены. Пользовательские контроллеры объявляют это поле только тогда, когда они применяют его. Эти поля присутствуют в ответах bootstrap и ready; более старые демоны могут их опускать. Контроллер канала отвергает переходные объединения владельцев, превышающие его предел, с помощью 409 channel_control_workspace_limit_reached перед созданием кандидатов рабочих процессов; начальное объединение во время загрузки, превышающее предел, приводит к сбою запуска до публикации слушателя, поэтому у него нет HTTP-интерфейса. Емкость регистрации не определяет таймаут канала SDK.

limits.maxTotalSessions аддитивен. null означает, что эффективный глобальный предел новых сессий для демона отключен. Когда установленная емкость регистрации (по умолчанию 256, настраиваемая через QWEN_SERVE_MAX_WORKSPACES или встроенный maxRegisteredWorkspaces) превышает 25, а --max-total-sessions опущен, стандартный демон использует фиксированное общее значение 800, даже с одним рабочим пространством при запуске. При емкостях регистрации 25 или меньше несколько рабочих пространств при запуске/восстановлении с конечным maxSessionsPerWorkspace вычисляют эффективное общее значение один раз как maxSessionsPerWorkspace * workspaceCount по тому же количеству запуск+восстановление; одно рабочее пространство при запуске или восстановлении сохраняет неограниченный параметр по умолчанию. Явные общие пределы, включая отключенные значения, имеют приоритет. Последующая динамическая регистрация не пересчитывает общий предел. Прямые внедрения createServeApp должны предоставлять свою собственную политику совместного допуска. Когда установлено, оно ограничивает создание новых сессий во всем демоне и сообщает о сбоях общего предела с существующей формой ошибки session_limit_exceeded плюс scope: "total".

runtime.channel.live сообщает о канале моста ACP внутри демона. Это не рабочий процесс адаптера каналов. Каналы, управляемые демоном, используют runtime.channelWorker, чей state является одним из disabled, starting, running, exited, failed или stopped. Когда рабочий процесс достигает running, а затем завершается, /daemon/status оставляет демон в сети и сообщает код проблемы предупреждения channel_worker_exited.

Запуск рабочего процесса канала, управляемого демоном, от явного qwen serve --channel ... остается быстрым отказом (fail-fast) и имеет приоритет над сохраненными настройками запуска. Запуск без флагов восстанавливает serve.channels из каждого доверенного зарегистрированного рабочего пространства, каждое из которых предоставляет список в своих настройках области рабочего пространства. Восстановленные имена образуют один набор выбора, владельцы которого определяются обычным образом, при этом рабочее пространство, перечислившее имя, разрывает иначе неоднозначную связь владения. Эта атрибуция запуска действует столько, сколько работает демон: PUT /workspace/channel, повторно включающий такое имя, определяет его так же, как при запуске, даже если набор выбора был остановлен между тем. Имя, предоставленное не основным рабочим пространством, отбрасывается с журналом, идентифицирующим его, когда оно не может быть определено, чтобы устаревшая запись одного рабочего пространства не блокировала другие; имя, перечисленное основным рабочим пространством, все еще приводит к общему сбою восстановления, независимо от того, перечисляет ли его другое рабочее пространство тоже. all остается только для основного рабочего пространства: он игнорируется и сообщается где угодно еще.

Доверенное не основное рабочее пространство, зарегистрированное после запуска, также восстанавливает свои собственные serve.channels, загружая среду выполнения канала и резервируя аренду сервиса канала, если ничто другое не сделало этого. Это восстановление:

  • происходит один раз за запуск демона — записывается, как только рабочее пространство запрашивает что-либо, — поэтому последующее удаление и повторная регистрация или повторное материализование доверия не отменяют действие оператора, который остановил один из этих каналов между тем;
  • не происходит при явном выборе --channel, который не расширяется ни одним рабочим пространством, зарегистрированным позже;
  • не происходит после того, как DELETE /workspace/channel перестал размещать, до тех пор, пока PUT /workspace/channel снова не зафиксирует выбор или демон не перезапустится;
  • оставляет зафиксированный выбор all без изменений;
  • добавляет имена этого рабочего пространства в зафиксированный выбор одним изменением, восстанавливая по одному имени за раз. В отличие от загрузки, которая отбрасывает имя, которое не может быть размещено, но сохраняет остальные, имя, которое невозможно атрибутировать, приводит к тому, что весь список этого рабочего пространства не восстанавливается, а ошибка регистрируется и сообщается так, как описано ниже.

Регистрация вообще не ожидает управления каналами. Восстановление ставится в очередь на полосу управления каналами, а ответ отправляется без ожидания завершения этой операции; то же самое касается сверки (reconcile), которую вызывает изменение регистрации или доверия для каналов, уже размещаемых демоном. Поэтому воркер, который никогда не становится готовым, задерживает только работу с каналами, поставленную в очередь после него, но никогда — другую регистрацию или сверку доверия. Обратная сторона: 201 из POST /workspaces не означает, что каналы этого рабочего пространства запущены, или что уже размещаемый канал, которым он владеет, завершил перезапуск; для этого читайте GET /workspace/channel.

Без явного выбора, восстановления при загрузке serve.channels или регистрации, подобной описанной выше, загрузка среды выполнения каналов остается ленивой.

Сохраненные имена запуска должны быть непустыми, не содержать пробелов в начале или конце и не включать небезопасные управляющие или невидимые символы. Недействительные записи пропускаются по отдельности и логируются по индексу массива; запуск не обрезает их в другие имена экземпляров и не переписывает настройки. Аргументы воркера используют --channel=<value>, сохраняя ведущий дефис как часть имени.

Недействительное поле запуска или ошибка валидации либо аренды (lease) до начала работы воркеров пропускает автоматическое восстановление, при этом в лог записывается идентификатор serve.channels, а несвязанные настройки остаются в силе. Неудачный запуск воркера позволяет демону продолжить работу после успешной очистки. Глобальные таймауты запуска среды выполнения и неподтвержденные остановки воркеров следуют существующему пути обработки ошибок запуска; аренда сохраняется, пока завершение работы воркера не подтверждено. Управление каналами сообщает об сохраненных настройках запуска и фактическом состоянии среды выполнения.

Некоторые имена serve.channels, которые демон не запустил, не просто логируются, а репортятся. Такое имя никогда не присоединяется к зафиксированному выбору, поэтому ни один снимок состояния воркера его не содержит. Сообщаются три случая:

  • настроенный выбор запуска, воркер которого не смог запуститься, на пути загрузки, который оставляет демон обслуживающим запросы без своих каналов;
  • имя, отброшенное разрешителем прав собственности при загрузке во время атрибуции рабочему пространству, репортится против каждого рабочего пространства, которое его перечислило;
  • каждое имя, все еще не размещенное после сбоя восстановления поздней регистрации.

Они появляются в двух местах:

  • /daemon/status сообщает одно предупреждение channel_restore_failed на каждое рабочее пространство, называя каждый канал вместе с его ошибкой. Это полный набор данных, который присутствует также в ответе bootstrap, единственном доступном во время записи записей загрузки. На каждое рабочее пространство называется максимум 64 канала, каждое имя ограничено 128 символами, с and <n> more, если список длиннее;
  • в GET /workspaces/:workspace/channels этого рабочего пространства канал указан с runtime: { state: "error", lastError } вместо stopped, где lastError — это собственная ошибка запуска адаптера, когда воркер сообщил об ошибке для этого канала, иначе — ошибка попытки. Этот набор данных формируется из собственной области настроек каналов рабочего пространства, поэтому он может содержать только те имена, которые определяет эта область — имя, которое не конфигурирует ни одно зарегистрированное рабочее пространство, или чья конфигурация находится в другом рабочем пространстве, здесь не имеет строки и репортится только через /daemon/status.

Все остальное, что оставляет имя serve.channels неразмещенным, диагностируется только в логе демона: имя, потерянное из-за того, что весь список был нечитаемым или недействительным, список вторичного рабочего пространства, отброшенный потому, что первичное выбрало all, позднее восстановление, пропущенное потому, что зафиксированный выбор равен all, и список удаленного (SSH) рабочего пространства. Для этих случаев проверяйте логи демона.

Отчет действует до тех пор, пока канал не будет размещен — этим рабочим пространством или более поздним восстановлением из другого — до тех пор, пока его рабочее пространство не перестанет быть зарегистрированным, или пока оператор не предпримет действие: запуск, остановка или перезапуск POST на канале, PUT/DELETE /workspace/channels/:name, или PUT/DELETE /workspace/channel, изменяющий выбор. PUT /workspace/channels/:name/startup не очищает отчет: он переписывает serve.channels, ничего не говоря о неудачной попытке. Отчет хранится в памяти: перезапущенный демон снова выполняет восстановление и сообщает о результатах этой попытки. Текст ошибки очищается от учетных данных и ограничивается так же, как это делает лог демона.

После того как воркер достиг состояния готовности, неожиданные выходы перезапускаются супервизором serve в рамках ограниченной политики: до 3 попыток перезапуска в окне 5 минут, с задержками 1с, затем 5с, затем 15с. Воркер отправляет IPC heartbeat каждые 15 секунд; если heartbeat не наблюдается в течение 45 секунд, супервизор считает воркер устаревшим (stale), убивает его, записывает staleHeartbeatAt и использует тот же путь перезапуска.

runtime.channelWorker может включать дополнительные операционные поля: requestedChannels, pid, startedAt, exitCode, signal, error, restartCount, lastExitAt, lastRestartAt, nextRestartAt, lastHeartbeatAt, staleHeartbeatAt, startupFailures и startupFailuresTruncated. Каждая ошибка запуска содержит channel, phase (в настоящее время connect), необязательное поле code, предоставляемое адаптером, и message с удаленными учетными данными. Для текущей генерации воркера сохраняется не более 64 ошибок; флаг усечения означает, что было зафиксировано больше ошибок. Поле code является диагностическим и не представляет собой стабильную классификацию между адаптерами. Поле restartCount — это общее количество попыток перезапуска, выполненных этим процессом serve; запущенный воркер с restartCount > 0 считается исправным, если нет других проблем. Запущенный воркер, чьи requestedChannels включают имена, отсутствующие в channels, сообщает о channel_worker_partial_connect.

В демоне с несколькими рабочими пространствами (при повторении --workspace) поле runtime дополнительно включает channelWorkers[] — по одной записи на каждое рабочее пространство-владелец, каждая из которых является снимком channelWorker с аннотациями workspaceId, workspaceCwd и primary. Поле channelWorker остается заполненным как снимок основного рабочего пространства для обеспечения совместимости. Демоны с одним рабочим пространством не содержат поле channelWorkers[].

Управление каналами под управлением демона

Возможность channel_control объявляет ресурс выбора среды выполнения. Этот ресурс является общедоступным для всего демона, хотя его путь совместимости использует единичный префикс /workspace. Выборы среды выполнения не сохраняются постоянно и не изменяют параметр --channel времени загрузки демона.

GET /workspace/channel возвращает неизменяемый снимок менеджера:

{ "enabled": true, "selection": { "mode": "names", "names": ["telegram", "feishu"] }, "pendingSelection": { "mode": "names", "names": ["telegram"] }, "transition": "reconciling", "workers": [ { "workspaceId": "primary-id", "workspaceCwd": "/work/primary", "primary": true, "enabled": true, "state": "running", "channels": ["telegram"], "pid": 1234 } ] }

Поле selection равно null при отключенном состоянии. Поле pendingSelection присутствует только во время изменения. Поле transition принимает одно из значений: idle, starting, reconciling, stopping или rolling_back.

PUT /workspace/channel строго контролируется и принимает ровно один выбор:

{ "selection": { "mode": "all" } }
{ "selection": { "mode": "names", "names": ["telegram", "feishu"] } }

Имена обрезаются и дедуплицируются без сортировки; пустой массив имен недопустим. Поле all остается доступным только для основного рабочего пространства. Переход от отключенного к включенному состоянию возвращает 201; идемпотентный запрос PUT или замена возвращают 200. Ответ имеет тип { changed, replaced, partial, state }. Равный выбор оставляет исправные воркеры на месте, но восстанавливает равный выбор, если его воркер остановлен или завершился с ошибкой.

Операция DELETE /workspace/channel строго контролируется и идемпотентна. Она возвращает { changed, state }; успешное состояние — отключено. Операция POST /workspace/channel/reload также строго контролируется, повторно считывает настройки, заново разрешает группы рабочих пространств и принудительно согласовывает зафиксированный выбор. При отключенном состоянии она возвращает 409 channel_worker_not_enabled. Возможность channel_reload динамически объявляется только тогда, когда менеджер имеет зафиксированный, перезагружаемый выбор.

Каждая операция включения, замены, перезагрузки, остановки и завершения работы демона попадает в одну очередь жизненного цикла FIFO. Метод GET не ожидает эту очередь. Группы рабочих пространств, чей упорядоченный выбор не изменился, остаются онлайн. При сбоях замены предпринимается попытка остановить недавно запущенные воркеры и восстановить предыдущий зафиксированный выбор. Клиенты должны проверять rolledBack, rollbackError и state, так как очистка или восстановление также могут завершиться ошибкой. Демон сохраняет аренду PID канала обслуживания на протяжении всей транзакции и не освобождает ее до подтверждения выхода всех соответствующих дочерних процессов.

Стабильные ошибки управления:

  • 400 invalid_channel_selection, channel_workspace_mismatch или ambiguous_channel_workspace
  • 403 untrusted_workspace
  • 409 channel_service_conflict, channel_worker_not_enabled или channel_control_workspace_limit_reached
  • 500 channel_worker_stop_failed
  • 502 channel_worker_start_failed, с rolledBack и необязательным очищенным от учётных данных rollbackError
  • 503 daemon_draining

Строгие записи из первичного запроса без токена, который достигает шлюза без авторитетной обратной связи через loopback, возвращают 401 token_required до выполнения кода управления. Отсутствующие или недействительные настроенные учетные данные и неподтвержденные учетные данные Local Control отклоняются раньше с простой ошибкой 401 Unauthorized. Первичные запросы с авторитетной обратной связью через loopback выполняются нормально. После начала обработки запроса отключение HTTP-клиента не отменяет транзакцию жизненного цикла; клиенты могут безопасно повторить тот же запрос PUT.

Для 502 channel_worker_start_failed ответ может также включать startupFailures[] и startupFailuresTruncated. Каждый сбой добавляет доверенное значение workspaceCwd для попытки запуска воркера. Эти поля описывают неудачную транзакцию, тогда как state описывает текущее состояние после отката; последующий запрос GET не сохраняет информацию о неудачной попытке. Частично подключенный воркер вместо этого возвращает успех и раскрывает свои сбои в снимке воркера. Явный запуск с --channel без подключенного адаптера приводит к ошибке запуска. Сбой запуска, вызванный настройками, фиксируется в журнале и позволяет демону продолжить работу после успешной очистки с учетом глобального таймаута запуска среды выполнения. Невозможность подтвердить очистку сохраняет обычное поведение при сбое запуска и аренду сервиса.

Файл qwen channel status без --daemon-url продолжает читать метаданные pidfile; с --daemon-url он читает GET /workspace/channel. Во время окна перезапуска принадлежащий serve файл pidfile остается зарезервированным, но поле workerPid опускается, чтобы клиенты не отображали устаревший процесс воркера. В демоне с несколькими рабочими пространствами файл pidfile также содержит дополнительный массив workers[] (для каждого рабочего пространства: workspaceId / workspaceCwd / channels / активный workerPid), в то время как верхнеуровневые поля channels (объединение) и workerPid (основной) остаются заполненными для старых читателей; демоны с одним рабочим пространством сохраняют исходную форму одного воркера. Стандартный вывод и ошибки воркера перенаправляются в журнал демона с удалением токенов доступа, чувствительных значений переменных окружения воркера и учетных данных URL прокси.

Управление каналами рабочего пространства

Возможность channel_management объявляет конфигурацию каналов и управление средой выполнения в масштабе рабочего пространства. Маршруты с единичным префиксом /workspace направлены на основную среду выполнения. Поле /workspaces/:workspace разрешает точную зарегистрированную доверенную среду выполнения и никогда не выполняет резервный переход к основной среде выполнения.

Обнаружение только для чтения использует:

  • GET /workspace/channel-types
  • GET /workspace/channels
  • GET /workspace/channels/events
  • GET /workspaces/:workspace/channel-types
  • GET /workspaces/:workspace/channels
  • GET /workspaces/:workspace/channels/events

GET .../channels/events возвращает журнал событий канала (Channel) для разрешенного рабочего пространства в виде { "events": [...], "truncated": boolean } с Cache-Control: no-store, а также необязательный элемент channelLinkMeta: карту от имени экземпляра канала к метаданным ссылки без секретов (type и baseUrl, каждый из которых присутствует только если настроен как строка, а для каналов GitHub — разрешенный webOrigin — источник веб-интерфейса, который клиенты должны напрямую конкатенировать вместо повторного вывода из корневого URL API). Метаданные разрешаются из объединенных системных, пользовательских и рабочих настроек, с которыми работают воркеры каналов — локальный список управления областью не содержит каналы, настроенные в другой области — и фильтруются по каналам, чей разрешенный cwd принадлежит запрошенному рабочему пространству, поэтому одно рабочее пространство никогда не получает метаданные каналов другого рабочего пространства. Это позволяет клиентам связывать строки журнала (например, ссылку на PR или issue GitHub) даже для каналов, унаследованных из пользовательских или системных настроек. Элемент полностью опускается, когда ни один канал не разрешается, и старые демоны никогда его не отправляют; сбой чтения настроек приводит к отсутствию этого элемента, а не к сбою чтения журнала. Каждое событие записывает действие канала — принятое или пропущенное входящее сообщение (с машинной причиной: reason_filter, mention_required, duplicate, отклонение по фильтру отправителя, github_api_error), ветку GitHub, обработка которой завершилась ошибкой и которую он прочитает снова (failed с github_api_error: ошибка GitHub или локальная ошибка, когда канал не может читать или писать свое собственное состояние, либо одна из задач ветки не завершена), результат публикации, переход входящей задачи или результат опроса — и никогда не содержит текст сообщения. Опрос GitHub, который фильтрует более 20 уведомлений, записывает первые 20 отдельно и одну дополнительную строку reason_filter, где detail.filtered подсчитывает остальные, чтобы закрепленный курсор опроса не переполнял журнал повторами одного и того же пакета. Пока курсор удерживается, комментарий, повторно считанный опросом, записывается как duplicate или mention_required один раз за окно, а сбоящая ветка — как github_api_error при первом сбойном опросе в окне и на каждом десятом сбойном опросе (при ограничении частоты или сбое учетных данных: первый и каждый десятый опрос), и один раз, когда она пропускается. Строки «один раз за окно» отслеживаются под максимум 8192 ключами; строка, чей ключ больше не помещается, записывается при каждом опросе. Параметры запроса: limit (1–500, по умолчанию 100), type (разделенные запятыми inbound, outbound, task, poll), channel (повторите параметр один раз для каждого имени экземпляра) и since (ISO 8601). Маршрут не требует прав оператора. Чтения журнала выполняются асинхронно и с ограничениями: текущий журнал плюс один ротированный файл на канал, 256 КиБ на файл, 4 МиБ на запрос, 32 файла на запрос, самые свежие первыми. Метаданные ссылок дополнительно синхронно читают снимок объединенных настроек без побочных эффектов (несколько небольших JSON-файлов, разрешенных относительно эффективной среды выполнения); это декорация best-effort, поэтому любой сбой опускает элемент вместо сбоя чтения журнала. truncated равно true, если лимит записей или любое ограничение чтения обрезали результат, включая случай, когда журнал выше окна хвоста на файл. Рабочее пространство без журнала пока возвращает пустой список; директорию области, которую невозможно прочитать, отвечает 5xx вместо пустого списка.

Каталог отмечает типы, поддерживаемые этим API управления, с помощью manageable: true. Снимки экземпляров включают ревизию, отредактированные метаданные наличия секретов, состояние запуска и состояние выполнения; литеральные секреты никогда не возвращаются. Снимки каналов используют Cache-Control: no-store.

Дескрипторы полей могут раскрывать метаданные вложенных объектов через properties. Числовые дескрипторы могут использовать exclusiveMinimum для открытой нижней границы и maximum для включительной верхней границы. Сохраненное число вне вновь объявленной границы все еще принимается, когда клиент повторно отправляет его без изменений, поэтому объявление границы не блокирует несвязанные правки существующего канала; значение, которое клиент действительно изменил, должно удовлетворять обеим границам. Дескрипторы строк и секретов могут использовать multiline, чтобы предложить клиентам многострочную текстовую область; типы дескрипторов позволяют это только для полей верхнего уровня. Клиенты, которые не отображают рекламируемый тип поля, должны сохранять существующее конфигурационное значение вместо принудительного преобразования или удаления, а клиент, отображающий поле multiline в однострочном элементе управления, должен сохранять сохраненное значение дословно вместо записи обратно входного значения с удаленными символами новой строки. Объектные поля не могут быть обязательными, а вложенные свойства не могут быть секретами или полями, разрешаемыми через окружение; эти протоколы управления остаются только на верхнем уровне. Вложенное свойство required проверяется только тогда, когда родительский объект присутствует в записи; опускание родительского объекта оставляет его вложенные требования непроверенными. Записи конфигурации заменяют сохраненное значение каждого поля целиком, поэтому сохранение объекта означает повторную отправку сохраненного объекта; демон не объединяет частичные объекты.

Записи конфигурации используют оптимистичное конкурентное управление и строгий шлюз авторитета оператора:

  • PUT /workspace/channels/:name
  • DELETE /workspace/channels/:name
  • PUT /workspace/channels/:name/startup
  • эквивалентные маршруты /workspaces/:workspace/...

Каждая мутация настроек включает expectedRevision. Запросы Upsert содержат объект config и могут содержать явные операции с секретами: preserve, replace или clear. Конфигурация канала не может выбрать рабочую директорию вне разрешенного рабочего пространства.

Действия во время выполнения строго ограничены запросами POST к .../channels/:name/start, stop или restart. Они выполняются только над воркером, принадлежащим разрешенному рабочему пространству.

Управление сопряжением доступно только для экземпляров, настроенных с политикой отправителя pairing или групповой политикой:

  • GET .../channels/:name/pairing-requests
  • POST .../channels/:name/pairing-requests/approve с { "code": "..." }
  • GET .../channels/:name/pairing-approvals
  • DELETE .../channels/:name/pairing-approvals с использованием либо { "senderId": "..." }, либо { "groupId": "..." }

Все маршруты сопряжения требуют строгих прав оператора и используют Cache-Control: no-store. Запросы, одобрения и отзывы ограничены выбранным экземпляром канала и рабочим пространством. Ожидающие запросы включают типизированную сущность пользователя или группы; групповые запросы также сохраняют информацию об отправителе, инициировавшем запрос. Снимки одобрений содержат senderIds и groupIds, так как списки разрешений не сохраняют отображаемые имена. Отзыв неизвестного пользователя или группы возвращает 404 channel_pairing_approval_not_found.

Доставка канала и Notify

channel_delivery объявляет поддержку немедленной доставки по принципу «максимальных усилий». Это возможность протокола, а не сигнал о состоянии воркера. Доставка никогда не запускает отсутствующий воркер, не переключается на другое рабочее пространство, не выполняет повторные попытки, не сохраняет исходящие сообщения и не воспроизводит исторические уведомления.

Прямой Notify обходит Agent и Session и ожидает одной попытки отправки:

POST /workspace/notify POST /workspaces/:workspace/notify Authorization: Bearer <token> Content-Type: application/json { "text": "service unavailable", "delivery": { "kind": "channel", "target": { "channelName": "dingtalk", "type": "user", "id": "platform-user-id" } } }

Оба маршрута используют строгий мутационный шлюз. Квалифицированный маршрут разрешает только зарегистрированное доверенное рабочее пространство. Успехом является 200 {delivered:true,deliveryId}. delivered:true означает, что промис отправки в канале был выполнен; это не доказывает принятие провайдером, получение пользователем или подтверждение прочтения. Специфичная для провайдера валидация ответа и согласованная семантика причин ошибок между адаптерами IM находятся вне рамок этого контракта V1. Ошибками являются 400 channel_delivery_invalid, 503 channel_worker_unavailable или channel_delivery_queue_full, 504 channel_delivery_timeout, а также 502 channel_delivery_rejected или channel_delivery_failed. Тайм-аут имеет неизвестный результат и не повторяется. Отдельная конечная точка для проверки соединения сознательно отсутствует: обычный вызов Notify служит сквозным тестом.

Воспроизводимое событие результата содержит только корреляцию и очищенный статус:

{ "type": "channel_delivery_result", "promptId": "prompt-1", "data": { "sessionId": "session-1", "deliveryId": "prompt-1", "source": "prompt", "status": "failed", "promptId": "prompt-1", "code": "channel_worker_unavailable", "error": "Channel worker is not running." } }

Пустой успешный финальный Prompt опускает поля ошибки:

{ "type": "channel_delivery_result", "promptId": "prompt-1", "data": { "sessionId": "session-1", "deliveryId": "prompt-1", "source": "prompt", "status": "skipped", "promptId": "prompt-1" } }

source — это prompt или scheduled; status — это delivered, failed или skipped. skipped означает, что соответствующий ход успешно завершен, но последний блок ответа ассистента без инструментов был пустым или содержал только пробельные символы. Демон потребляет разрешение на доставку и публикует событие без разрешения воркера канала. Для корреляции по расписанию используются taskId и firedAt. Событие никогда не содержит идентификаторов целей, текста сообщений, учетных данных или секретов вебхуков.

Безопасность: ответ никогда не включает токены доступа (bearer tokens), идентификаторы клиентов, полные идентификаторы соединений ACP, коды пользователей потока устройства или URL-адреса верификации. Оба уровня детализации могут включать дополнительные поля daemon.runId, daemon.logMode и daemon.logHealth. summary исключает путь к журналу демона и детали потерь; full может включать logPath, logIssues, logDroppedRecords и logDroppedBytes для аутентифицированных операторов. Деградированное журналирование файлов добавляет предупреждение daemon_log_degraded без пути к обычному сводному статусу.

GET /capabilities

{ "v": 1, "protocolVersions": { "current": "v1", "supported": ["v1"] }, "mode": "http-bridge", "features": [ "health", "daemon_status", "capabilities", "multi_workspace_sessions", "..." ], "limits": { "maxRegisteredWorkspaces": 256, "maxChannelControlWorkspaces": 25, "maxPendingPromptsPerSession": 5, "maxSessionsPerWorkspace": 32, "maxTotalSessions": 800, "sessionRestoreTimeoutMs": 60000 }, "modelServices": [], "workspaceCwd": "/canonical/path/to/primary-workspace", "workspaces": [ { "id": "stable-workspace-id", "cwd": "/canonical/path/to/primary-workspace", "primary": true, "trusted": true }, { "id": "stable-secondary-workspace-id", "cwd": "/canonical/path/to/secondary-workspace", "displayName": "Payments Production", "primary": false, "trusted": true } ] }

Стабильный контракт: когда v увеличивается, структура кадров изменилась несовместимым с обратной совместимостью образом.

protocolVersions описывает версии протокола serve, которые демон умеет использовать. current — это предпочтительная версия протокола демона, а supported — совместимый набор. Клиенты, которым требуется конкретная версия протокола, должны проверять supported; специфичный для функций UI должен по-прежнему зависеть от features. Аддитивно к v=1: более старые демоны v=1 опускают это поле, поэтому SDK-клиенты, ориентированные на старые сборки, должны считать его необязательным.

modelServices всегда равно [] на Этапе 1. Агент использует свой единственный сервис модели по умолчанию и не перечисляет его по сети. На Этапе 2 это поле будет заполняться из зарегистрированных адаптеров моделей, чтобы SDK-клиенты могли строить выборщики сервисов; до этого момента НЕ полагайтесь на то, что это поле непустое.

workspaceCwd — это канонический абсолютный путь к основному рабочему пространству демона. Используйте его, чтобы опускать cwd в POST /session (маршрут возвращается к этому основному пути) и сохранять совместимость со старыми однорабочими клиентами. Аддитивно к v=1: демоны v=1 до §02 опускают поле — клиенты, ориентированные на старые сборки, должны выполнять проверку на null перед использованием.

workspaces[] перечисляет все зарегистрированные среды выполнения. Более новые однорабочие демоны включают основную среду выполнения даже при отсутствии multi_workspace_sessions, чтобы клиенты могли обнаружить стабильный идентификатор, требуемый маршрутами с указанием рабочего пространства; более старые демоны могут опускать массив. Каждая запись представляет собой { id, cwd, displayName?, primary, trusted, removable? }. displayName предназначен только для отображения и опускается, если не установлен. Первое/основное рабочее пространство по-прежнему отражается в workspaceCwd; новые клиенты выбирают неосновную среду выполнения, передавая её cwd в POST /session. Недоверенные рабочие пространства объявляются для диагностики, но отклоняют создание новых сессий с помощью 403 untrusted_workspace до изменения доверия. removable присутствует у демонов, поддерживающих удаление сред выполнения, и истинно только для вторичных сред выполнения, динамически создаваемых процессом или восстановленных из сохранения.

session_worktree_persistence_v1 означает, что демон может сохранять и проверять владение worktree из Части 4A. Успешные ответы на создание worktree и ответы на восстановление, дочерний элемент которых либо перемещен в простое, либо уже сообщает проверенный cwd worktree, содержат метаданные worktree плюс worktreeState: "persisted-v1". Легасовое восстановление best-effort или холодное восстановление, где запрос на восстановление был запущен, а не отложен (suppressWorktreeContextRestore выключен, поэтому маршрут запросил у моста отсутствие отсрочки), и которое поэтому сообщает об активном запросе без текущего cwd, может всё ещё возвращать worktree без этого аттестата. Клиенты, запрашивающие изоляцию, должны предварительно проверить этот тег и проверить каждый ответ; сам объект worktree не является доказательством долговременного владения.

session_worktree_reset_v1 означает, что демон поддерживает передачу владения worktree: POST /session/:id/worktree-reset перемещает владение checkout сохраненной сессии worktree в новую замену сессии. Ответы на восстановление получают три типизированные классификации 409 вместе с ним: worktree_session_superseded (sidecar сессии содержит ссылку supersededBy — классификация определяется только по этой ссылке, до любого чтения маркера, и ссылка записывается до переключения маркера, поэтому replacementSessionId в теле является редиректом для проверки, а не доказательством владения: в прерванном состоянии до коммита он называет сессию, которая не является владельцем маркера, не может быть сама восстановлена и удаляется повторенным сбросом), worktree_marker_missing (маркер checkout отсутствует; сбросьте задачу для его воссоздания — повторное восстановление не может этого сделать, потому что ни один путь восстановления не пишет маркер) и worktree_reset_interrupted (предыдущая передача завершилась аварийно в середине процесса, и ссылки sidecar совпадают; повторите сброс). Прерванная классификация проверяется первой: отсутствующий маркер, чьи ссылки supersedes/supersededBy совпадают, отображается как worktree_reset_interrupted, никогда как worktree_marker_missing. См. раздел маршрутов ниже для протокола передачи и таксономии сбоев.

Теги функций рабочего пространства и workspaces[] являются динамическими. Клиенты, добавляющие рабочее пространство, должны повторно получить /capabilities после завершения мутации; демон не рассылает изменения возможностей клиентам, закэшировавшим более ранний ответ. Забывание сохранения не выгружает активную среду выполнения, поэтому эта среда выполнения продолжает объявляться до перезапуска.

GET /brand

Продуктовый брендинг Web Shell для хостов, желающих выполнить white-labeling оболочки. Разрешается из ui.brand в областях настроек оператора. Ограничено тегом функции web_shell_brand; демон без маршрута отвечает 404.

{ "name": "QiuQiu Code", "logoDataUri": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E" }

Оба поля необязательны, и {} является нормальным ответом. Отсутствие поля означает «использовать встроенный бренд клиента» — Web Shell отображает свое собственное имя и встроенный логотип. Клиенты не должны рассматривать пустое тело как ошибку.

Обработчик всегда отвечает 200, даже если настроенный логотип был отклонен. Отсутствующий файл, символическая ссылка, файл с жесткой ссылкой, каталог, не-SVG-документ или содержимое более 32 КиБ приводят к телу без logoDataUri, а причина записывается в stderr демона как qwen serve: GET /brand: ui.brand.logoPath …. Поэтому клиент не может отличить «бренд не настроен» от «логотип оператора был отклонен»; канал для оператора — это журнал демона. Одно предупреждение мягче отказа, и их два: корневой элемент <svg> без пригодного viewBox и без положительного, не процентного width/height (некорректный или нулевой viewBox считается непригодным), который браузер может отобразить пустым при фиксированном размере боковой панели; и корневой элемент с префиксом пространства имен, у которого элементы без префикса не имеют привязки к пространству имен по умолчанию, что делает их невидимыми. Промежуточное ПО перед обработчиком отвечает до его запуска — 401, когда требуется bearer-аутентификация, но она отсутствует, и 429, когда включен необязательный ограничитель частоты запросов. Демон в режиме завершения работы не отклоняет этот маршрут: ограничитель частоты запросов разрешителен во время завершения работы, поэтому обработчик продолжает отвечать 200, пока слушатель не закроется и клиент не увидит ошибку соединения. Запуск не дает 503 на этом маршруте: на пути отложенной среды выполнения по умолчанию запрос удерживается до готовности среды выполнения, после чего возвращается ответ 200, поэтому ранний сбой может произойти из-за короткого таймаута клиента. Ответ 503 с code: "daemon_runtime_starting" достигает этого маршрута только в конфигурациях, отвечающих до готовности среды выполнения (например, --open), и является повторяемым; ответ 503 с code: "daemon_runtime_failed" является терминальным до перезапуска демона и не содержит Retry-After — не повторяйте его.

Настройки рабочего пространства никогда не участвуют. Маршрут загружает настройки с помощью skipWorkspaceSettings, поэтому .qwen/settings.json репозитория не может переименовать продукт или указать имя файла для чтения и встраивания демоном во все подключенные браузеры. Читаются только System Defaults, User и System, в таком порядке приоритета. По той же причине значение бренда отклоняется с предупреждением в stderr демона всякий раз, когда подстановка плейсхолдеров изменяет его: подстановка берет значения из общепроцессного окружения, которое заполняется блоком .qwen/.env или env рабочего пространства первым при загрузке. Неразрешимый плейсхолдер (переменная не установлена) сохраняется дословно, поэтому опечатка в имени переменной отображается как литеральный текст, а не молча заменяется запасным значением.

logoDataUri должен отображаться как изображение, никогда не внедряйтесь как разметка. Демон не очищает прочитанный SVG. SVG, загруженный через src элемента img или href фавикона, не может выполнять скрипты; SVG, внедренный в документ, может. Web Shell всегда присваивает его только контексту изображения, и именно эта инвариантность делает отсутствие санитайзера безопасным.

Глобально для процесса. Маршрут не принимает селектор рабочего пространства и идентификатор сессии: значение происходит из глобальной конфигурации пользователя, поэтому оно одинаково для всех рабочих пространств, обслуживаемых демоном. Он регистрируется после bearerAuth и ограничителя частоты запросов и перед резервным вариантом SPA Web Shell, поэтому он отвечает JSON для любого заголовка Accept.

POST /workspaces

Зарегистрируйте дополнительную среду выполнения рабочего пространства. Путь должен быть существующим, доступным абсолютным каталогом, который не дублирует и не вкладывает другое зарегистрированное рабочее пространство. Регистрация локальна для процесса, если клиент не отправляет persist: true; клиенты должны выполнить предварительную проверку persistent_workspace_registration перед запросом сохранения. Когда поддерживается workspace_display_name, запрос также может включать необязательный displayName.

Когда поддерживается git_workspace_clone, тот же маршрут также принимает { "kind": "git", "url": "...", "target": "/absolute/new/path" }. Эта форма требует абсолютного target, который еще не существует, запускает асинхронное клонирование, ничего не регистрирует и возвращает 202 { "kind": "git-workspace-clone", "jobId": "..." }. Ответы 201 и 200 ниже описывают синхронные формы cwd и scratch; git-форма ничего не регистрирует, а прогресс клонирования и отмена используют GET/DELETE /workspace-git-clone-jobs/:jobId.

{ "cwd": "/canonical/path/to/secondary-workspace", "persist": true, "displayName": "Payments Production" }

Ново созданная среда выполнения возвращает 201; повышение уже активной вторичной рабочей области до постоянной возвращает 200. Успешное сохранение включает persisted: true:

{ "id": "stable-workspace-id", "cwd": "/canonical/path/to/secondary-workspace", "displayName": "Payments Production", "primary": false, "trusted": true, "persisted": true }

displayName должен быть строкой длиной не более 256 символов после удаления пробелов по краям. Пустой результат трактуется как отсутствие имени, а внутренние управляющие символы C0 (U+0000–U+001F) или DEL (U+007F) отклоняются. Значение JSON null не является значением создания и возвращает 400 invalid_display_name; опустите поле, чтобы не задавать начальное имя. Дублирующиеся отображаемые имена разрешены. Имя, предоставленное при регистрации, локальной для процесса, действует только для этого процесса демона; persist: true сохраняет его вместе с постоянной регистрацией, чтобы его можно было восстановить после перезапуска. Повторный запрос для уже постоянного рабочего пространства идемпотентен и не переименовывает его.

Ошибки включают 400 invalid_path / invalid_persist_flag / invalid_persist_target / invalid_display_name, 409 workspace_exists / workspace_nested / workspace_limit_reached / workspace_registration_store_too_large, 500 workspace_registration_store_error / runtime_creation_failed и 501 persistence_not_available / not_implemented.

PATCH /workspaces/:workspace

Обновите активный ресурс рабочего пространства, выбранный по ID рабочего пространства или URL-кодированному абсолютному cwd. Конечная точка в настоящее время поддерживает только метаданные отображаемого имени:

{ "displayName": "Payments Production" }

Отправьте { "displayName": null }, чтобы очистить имя. Здесь null — это маркер удаления только для обновления; ненулевые значения следуют тем же правилам нормализации строк, что и POST /workspaces. Ответом является обновленная проекция рабочего пространства { id, cwd, displayName?, primary, trusted, removable? }. Метаданные среды выполнения всегда обновляются. Если у среды выполнения есть совпадающие постоянные идентификаторы регистрации, каждый алиас обновляется атомарно через существующее хранилище регистраций схемы v1; конечная точка никогда не создает и не повышает постоянную регистрацию.

Неподдерживаемые поля завершаются ошибкой вместо того, чтобы молча игнорироваться. Ошибки включают 400 empty_patch / invalid_display_name / unsupported_field / workspace_mismatch, 409 workspace_registration_in_progress, 500 workspace_registration_store_error и 503 daemon_shutting_down.

DELETE /workspaces/:workspace

Удалите одну удаляемую вторичную среду выполнения. Селектор следует правилам маршрутизации множественных рабочих пространств и принимает либо ID рабочего пространства, либо URL-кодированный абсолютный cwd. Необязательное тело JSON — это { "force": boolean }; его отсутствие запрашивает удаление без принуждения.

Удаление без принуждения возвращает 409 workspace_busy со снимком activity, когда замороженная среда выполнения имеет сессии, подсказки, ожидающие запуски, соединения ACP, задачи памяти или рабочие процессы каналов рабочего пространства. Отправка { "force": true } запрашивает завершение этих ресурсов. Удаление из хранилища является точкой фиксации: последующая очистка ограничена и выполняется по возможности, сбои очистки логируются, и логическое удаление все равно сходится, вместо восстановления среды выполнения. Успешный ответ выглядит так:

{ "removed": true, "workspaceId": "stable-workspace-id", "workspaceCwd": "/canonical/path/to/secondary-workspace", "forced": true, "persistedRegistrationRemoved": true, "activity": { "sessions": 2, "activePrompts": 1, "pendingSessionStarts": 0, "acpConnections": 1, "memoryTasks": 0, "channelWorkers": 0, "voiceSessions": 0 } }

Немедленно занятый запрос без принуждения возвращает быстрый снимок активности до начала завершения работы. После начала завершения работы ответ «занято» или «успех» содержит окончательный снимок, сделанный после закрытия шлюзов приема и завершения ACP и перед началом очистки. Ошибки включают 400 invalid_force_flag / workspace_mismatch, 409 workspace_busy / primary_workspace_removal_forbidden / static_workspace_removal_forbidden / workspace_removal_in_progress / workspace_registration_in_progress, 500 workspace_persist_failed / workspace_runtime_removal_failed, 501 workspace_runtime_removal_unsupported и 503 daemon_shutting_down.

GET /workspace-registrations

Список сохраненного желаемого набора рабочих пространств для данного основного рабочего пространства. Записи остаются видимыми с active: false, если сохраненный каталог не удалось восстановить при текущем запуске. Запись остается active: true, пока ее среда выполнения завершает работу (draining), поскольку среда выполнения все еще владеет активными ресурсами до завершения удаления. Записи включают необязательное поле displayName, если оно есть в постоянном реестре.

{ "schemaVersion": 1, "primaryWorkspace": "/canonical/path/to/primary-workspace", "entries": [ { "id": "stable-registration-id", "cwd": "/canonical/path/to/secondary-workspace", "displayName": "Payments Production", "active": true, "persisted": true } ] }

Возвращает 501 persistence_not_available, если хранилище регистраций не настроено, и 500 workspace_registration_store_error, если хранилище недоступно для чтения.

DELETE /workspace-registrations/:id

Забывает одну сохранённую регистрацию. Это не выгружает активную среду выполнения и не завершает её сессии; restartRequired: true означает, что активная среда выполнения исчезнет при следующем перезапуске демона.

{ "removed": true, "active": true, "restartRequired": true }

Возвращает 404 workspace_registration_not_found, 500 workspace_registration_store_error или 501 persistence_not_available. Как и другие маршруты мутаций, этот эндпоинт требует аутентификации мутаций, если включена аутентификация демона.

Маршруты статуса среды выполнения только для чтения

Эти маршруты сообщают снимки среды выполнения на стороне демона. Это аддитивные маршруты v1, они не изменяют состояние и не меняют версию протокола serve. Маршруты статуса рабочего пространства сознательно не запускают дочерний процесс ACP только потому, что клиент опрашивает GET-маршрут. GET /workspace/mcp возвращает initialized: false с пустым снимком, когда демон простаивает и не хранит наблюдаемых данных MCP; если живой дочерний процесс отвечал ранее, чтение в режиме простоя воспроизводит это наблюдение с source: "cache" и его исходным значением initialized. Вместо этого GET /workspace/skills предоставляет сохраненное наблюдение демона или локальную перечисленную демоном информацию и сообщает initialized: false с записью errors (повторно доступная недоступность, а не пустой каталог) только тогда, когда сохраненное наблюдение отсутствует и оба источника недоступны (или холодное чтение ограничено частотой) — см. GET /workspace/skills. На GET /workspace/providers отвечает локально демон из настроек рабочего пространства и окружения, поэтому он не зависит от живого дочернего процесса ACP, а acpChannelLive сообщает о работоспособности. Его initialized равен true на успешном пути и false с записью errors, когда настройки рабочего пространства или конфигурация модели не могут быть разрешены; демоны, сконструированные без локального построителя провайдера демона, возвращаются к снимку живого ACP, а локальный ответ демона опускает модели, зарегистрированные во время выполнения. Маршруты статуса сессии требуют активной сессии и возвращают 404 { code: "session_not_found", ... } для неизвестных идентификаторов.

Теги возможностей:

  • workspace_mcp → GET /workspace/mcp
  • workspace_skills → GET /workspace/skills
  • workspace_providers → GET /workspace/providers
  • workspace_acp_status → GET /workspace/acp/status
  • workspace_env → GET /workspace/env
  • workspace_preflight → GET /workspace/preflight
  • session_context → GET /session/:id/context
  • session_supported_commands → GET /session/:id/supported-commands
  • session_tasks → GET /session/:id/tasks
  • session_resources → GET /session/:id/resources
  • session_monitor_tool_correlation → записи монитора из GET /session/:id/tasks включают toolUseId для корреляции транскрипта с задачей
  • session_status → GET /session/:id/status
  • session_info → GET /workspace/:id/session-info и GET /workspaces/:workspace/session-info
  • session_transcript → GET /session/:id/transcript
  • workspace_persisted_transcript → GET /workspaces/:workspace/session/:id/transcript
  • workspace_session_export → GET /workspaces/:workspace/session/:id/export
  • workspace_archived_session_export → GET /workspaces/:workspace/session/:id/archive/export
  • workspace_session_live_state → GET /workspaces/:workspace/sessions/live-state
  • workspace_qualified_memory → POST /workspaces/:workspace/memory/{remember,forget,dream} и GET /workspaces/:workspace/memory/{remember,forget,dream}/:taskId

workspace_acp_status сообщает о текущей работоспособности канала ACP основного рабочего пространства как { channelLive: boolean }. Обработчик не создает канал, но достижение маршрута среды выполнения может сначала запустить отложенную среду выполнения демона, чья настроенная политика запуска может независимо предварительно нагревать ACP. Снимок не является арендой: клиенты должны позволить созданию сессии повторно проверить или запустить канал.

Прогрев ACP

Тег возможности: workspace_acp_preheat.

POST /workspace/acp/preheat?timeoutMs=N пытается инициализировать канал ACP основного рабочего пространства по принципу «максимальных усилий». Значение по умолчанию для timeoutMs равно 5000 и должно быть положительным целым числом, не превышающим 60000. Конкурентные вызывающие стороны и создание сессии разделяют одну и ту же инициализацию моста. Тайм-аут запроса завершает только это HTTP-ожидание; он не отменяет общую инициализацию.

interface WorkspaceAcpPreheatResult { ready: boolean; channelLive: boolean; durationMs: number; reason?: 'timeout' | 'error'; error?: string; }

ready всегда равно channelLive. Живой ответ не содержит полей reason и error; в противном случае поле reason принимает значение timeout или error. Поле durationMs измеряет длительность текущего HTTP-вызова, а не полный жизненный цикл инициализации, к которой присоединился вызов. Операционный тайм-аут или сбой возвращают HTTP 200. Неверное значение timeoutMs возвращает код 400, тогда как ошибки аутентификации, ограничения частоты запросов и сбои отложенного выполнения сохраняют свои обычные ответы.

Оба маршрута рабочего пространства ACP являются единичными и относятся только к основному рабочему пространству. Клиенты не должны использовать их для вторичных рабочих пространств или интерпретировать любой из ответов как гарантию постоянной готовности.

Общая ячейка статуса:

type DaemonStatus = | 'ok' | 'warning' | 'error' | 'disabled' | 'not_started' | 'unknown'; type DaemonErrorKind = | 'missing_binary' | 'blocked_egress' | 'auth_env_error' | 'init_timeout' | 'restore_timeout' | 'protocol_error' | 'missing_file' | 'parse_error'; interface DaemonStatusCell { kind: string; status: DaemonStatus; error?: string; errorKind?: DaemonErrorKind; hint?: string; }

errorKind — это закрытое перечисление, общее для /workspace/preflight, /workspace/env и (в конечном итоге) защитных механизмов MCP, чтобы клиенты SDK могли отображать исправления по категориям вместо разбора сообщений в свободной форме. Исходные семь литералов статуса были взяты из #4175; restore_timeout был добавлен отдельно для запросов восстановления сессии. blocked_egress остается зарезервированным до внедрения пробы исходящего трафика.

Полезные нагрузки статуса никогда не раскрывают значения переменных окружения MCP, заголовки, детали OAuth/сервисных аккаунтов, API-ключи провайдеров, baseUrl / envKey провайдеров, тело навыка, пути файловой системы навыков, определения хуков или значения секретных переменных окружения. /workspace/env сообщает только о наличии разрешенных переменных окружения; URL-адреса прокси очищаются от учетных данных и сокращаются до host:port перед отправкой по сети.

GET /workspace/mcp

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "discoveryState": "completed", "servers": [ { "kind": "mcp_server", "status": "ok", "name": "docs", "mcpStatus": "connected", "transport": "stdio", "disabled": false, "description": "Documentation server", "extensionName": "docs-ext" } ] }

discoveryState принимает одно из значений: not_started, in_progress или completed. transport принимает одно из значений: stdio, sse, http, websocket, sdk или unknown. Поле errors опускается при успешном обнаружении.

source имеет значение "live", пока дочерний процесс, создавший наблюдение, активен. Когда демон простаивает, но живой дочерний процесс ранее ответил, ответ воспроизводит то наблюдение с полем source: "cache" и исходными значениями дочернего процесса, включая initialized: true и его список серверов. Кэшированное наблюдение очищается только при начале нового обнаружения MCP; завершение работы дочернего процесса не приводит к его удалению, поэтому простаивающий GET /workspace/mcp может сообщать каталог, который в данный момент не обслуживается ни одним живым дочерним процессом.

Защитные механизмы клиента MCP (проблема #4175 ). Текущие демоны расширяют полезную нагрузку четырьмя дополнительными полями и ячейкой бюджета с областью действия:

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "discoveryState": "completed", "clientCount": 3, "clientBudget": 2, "budgetMode": "enforce", "budgets": [ { "kind": "mcp_budget", "scope": "workspace", "status": "error", "errorKind": "budget_exhausted", "hint": "Raise --mcp-client-budget or remove servers from mcpServers config.", "liveCount": 2, "budget": 2, "mode": "enforce", "refusedCount": 1, }, ], "servers": [ { "kind": "mcp_server", "status": "ok", "name": "a", "mcpStatus": "connected", "transport": "stdio", "disabled": false, }, { "kind": "mcp_server", "status": "ok", "name": "b", "mcpStatus": "connected", "transport": "stdio", "disabled": false, }, { "kind": "mcp_server", "status": "error", "name": "c", "mcpStatus": "disconnected", "transport": "stdio", "disabled": false, "disabledReason": "budget", "errorKind": "budget_exhausted", "hint": "...", }, ], }

budgetMode принимает одно из значений enforce, warn или off. Поле clientBudget отсутствует, если бюджет не был установлен. Поле budgets[] всегда является массивом у демонов, объявляющих поддержку mcp_guardrails (возможно пустым, когда budgetMode === 'off'); старые демоны полностью пропускают это поле. Когда поддерживается mcp_workspace_pool, ячейка содержит scope: 'workspace' и охватывает общий пул выбранной среды рабочего пространства. Когда этот тег отсутствует, включая случай QWEN_SERVE_NO_MCP_POOL=1, устаревший менеджер выдает scope: 'session'. Потребители ДОЛЖНЫ допускать дополнительные нераспознанные значения области действия.

Поле disabledReason в ячейках на уровне сервера позволяет отличить отключение оператором ('config' — disabledMcpServers конфигурационный список) от отказа из-за бюджета ('budget' — обнаружен, но никогда не подключен из-за режима enforce). Отказы детерминированы порядком объявления в Object.entries(mcpServers). Поле status: 'error', errorKind: 'budget_exhausted' на уровне сервера перекрывает исходное значение mcpStatus: 'disconnected' (которое истинно, но не отражает уровень серьезности для оператора).

Применение бюджета управляется возможностями. При наличии mcp_workspace_pool сессии внутри одной среды рабочего пространства разделяют транспорты и один WorkspaceMcpBudget; разные среды рабочего пространства никогда не разделяют пул или бюджет. Без этого тега каждый McpClientManager сессии ACP применяет свою собственную копию ограничения, и снимок представляет этот устаревший вид сессии.

Обнаружение давления бюджета. Два интерфейса, оба заполняются после PR-14b:

  • Push-события (объявляются через mcp_guardrail_events): подпишитесь на GET /session/:id/events и фильтруйте кадры mcp_budget_warning / mcp_child_refused_batch через KnownDaemonEvent. Машина состояний срабатывает один раз при каждом пересечении порога 75% вверх (сбрасывается ниже 37,5%); отказы объединяются один раз за проход обнаружения в режиме enforce.

  • Опрос снимков (объявляется через mcp_guardrails): выполните GET /workspace/mcp и проверьте ячейку бюджета (budgets[0]) вместе с mcp_workspace_pool, чтобы определить ее область действия:

  • budgets[0].status === 'warning' ⇔ liveCount >= 0.75 * clientBudget (соответствует порогу гистерезиса, который будет использовать push-событие из PR 14b).

  • budgets[0].status === 'error' ⇔ refusedCount > 0 (один или более серверов были отклонены в этом проходе обнаружения).

  • budgets[0].status === 'ok' ⇔ ниже порога 75% И нет отказов.

Рекомендуемая частота опроса: согласованная с тем, что уже опрашивает /workspace/mcp; снимок дешев, а ячейка бюджета не несет дополнительных затрат на обнаружение. Клиенты SDK, подписанные на push-события, по-прежнему получают выгоду от снимка для состояния после длительного отключения (глубина кольца воспроизведения SSE конечна — --event-ring-size, по умолчанию 8000 — поэтому клиент, находящийся в автономном режиме дольше покрытия кольца, переходит на повторную синхронизацию по снимку).

GET /workspace/skills

Маршруты инвентаризации основного и квалифицированного по рабочему пространству немедленно возвращают сохраненный снимок, выполняя ревалидацию в фоновом режиме. Запуски ограничены частотой один раз в секунду, отсчитываемой от начала задачи, и параллельные читатели используют одну задачу. Фоновая ревалидация освобождает слот после пяти секунд; тайм-аут позволяет выполнить повторную попытку, но не отменяет наблюдение или работу ACP. Холодные чтения соблюдают настроенный тайм-аут запроса ACP (по умолчанию десять секунд), затем выполняют локальную перечисление демоном, если ACP недоступен. Они не навязывают фоновый дедлайн ни одному из источников. Холодное чтение, завершившееся ошибкой в течение одной секунды от своего начала, оставляет остаток этого окна возвращающим статус недоступности немедленно; более медленные сбои не оставляют окна троттлинга, поэтому клиентам требуется собственная стратегия отката при повторных попытках. Позднее успешное фоновое наблюдение может быть опубликовано, пока ожидает повторная попытка, но не может заменить наблюдение, начатое позже и уже опубликованное. Аннулированные задачи не могут публиковать результаты; успешно аннулированное наблюдение все еще может ответить своему исходному вызывающему объекту. Явное аннулирование удаляет известный недействительный инвентарь и сбрасывает ограничение частоты. Успешный предварительный нагрев ACP, завершающийся до истечения тайм-аута, также аннулирует сохраненный инвентарь, в том числе когда дочерний процесс уже был активен, так что последующее чтение видит дочерний процесс. Предварительный нагрев, который завершается по тайм-ауту, а затем находит канал активным, также сообщает ready: true — готовность всегда равна channelLive — но сохраняет инвентарь, поскольку запуск может быть еще незавершенным; следующее чтение вернет эти сохраненные данные, а фоновая ревалидация заменит их, как только односекундное ограничение запуска позволит. Удаление навыка обходит снимок и ограничение частоты, но требует свежего перечисленного каталога: оно использует локальный резервный вариант и никогда не отвечает из сохраненного снимка. Чтения идентичности включения обходят снимок и ограничение частоты, ожидая новое наблюдение с локальным резервным вариантом, а затем сохраненный снимок, когда оба источника терпят неудачу.

Ответы инвентаризации, содержащие сохраненное наблюдение демона, дополнительно включают необязательные метаданные снимка. Прямые чтения статуса конфигурации/среды выполнения, инвентари ресурсов сессии и старые демоны опускают их. Отмененное успешное холодное чтение возвращает более новый сохраненный снимок, если он доступен; в противном случае оно возвращает свое собственное некэшированное наблюдение без этих метаданных. Только инициализированные наблюдения без ошибок могут заменить сохраненный снимок:

{ "snapshot": { "observedAt": 1792486800000, "ageMs": 1500, "stale": true, "refreshing": true } }

observedAt — это время начала успешного наблюдения в миллисекундах Unix; ageMs пересчитывается для каждого ответа, консервативно включая время сбора. stale означает, что наблюдению как минимум одна секунда. refreshing сообщает, занимает ли повторная валидация общий слот; наблюдения с истекшим таймаутом могут завершиться после освобождения слота. Неудачные чтения, таймауты слотов и воспроизведение после остановки среды выполнения не обновляют временную метку; последующая успешная публикация записывает собственное время начала наблюдения. Клиенты могут отображать этот возраст, но он не является ревизией файловой системы или доказательством актуальности всех сессий. Если инициализированный снимок отсутствует и оба источника недоступны (или холодное чтение ограничено), ответ содержит initialized: false, запись errors и отсутствующие метаданные snapshot. Это повторяемая недоступность, а не пустой каталог; допустимый пустой каталог имеет initialized: true. Чтения статуса не вызывают команды обновления Skill, влияющие на сессию. Закрытые или завершающие работу среды выполнения рабочего пространства отклоняют чтения; сохраненные данные переживают удаление дочернего канала, но не вывод из эксплуатации среды выполнения.

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "skills": [ { "kind": "skill", "status": "ok", "name": "review", "description": "Review code", "level": "project", "modelInvocable": true, "userInvocable": false, "installedPath": "/home/alice/project/.qwen/skills/review/SKILL.md", "argumentHint": "[path]" }, { "kind": "skill", "status": "ok", "name": "database-review", "description": "Review database changes", "level": "extension", "modelInvocable": true, "installedPath": "/home/alice/.qwen/extensions/alibabacloud-database-suite/skills/database-review/SKILL.md", "extensionName": "alibabacloud-database-suite", "extensionDisplayName": "Alibaba Cloud Database Suite" } ] }

level принимает одно из значений: project, user, extension или bundled. Поле userInvocable (логическое, необязательное) опускается для обычных навыков (означает true) и присутствует только как false, если навык нельзя вызвать вручную. Оно не блокирует описанные ниже маршруты переключателя Skill, доступные только через настройки. modelInvocable независимо: false означает, что навык остается доступным для ручного вызова, но скрыт от вызова моделью. installedPath — существующий абсолютный путь к SKILL.md навыка; демон возвращает его в том виде, в каком он сохранен, без отдельного разрешения симлинков или канонизации. Текущие демоны выводят его для каждого навыка, тогда как клиенты должны допускать его отсутствие у старых демонов v1. Тела навыков, хуки, skillRoot и другая конфигурация навыков остаются исключены. errors опускается при успешном обнаружении.

Для навыков, принадлежащих расширениям, extensionName — это каноническое имя манифеста, безопасное для использования в качестве идентификатора владельца. extensionDisplayName — необязательное локализованное значение для отображения, которое может быть неуникальным. Новые клиенты должны отображать extensionDisplayName ?? extensionName; старые демоны опускают поле отображения.

Повторные чтения обслуживаются из последнего зафиксированного снимка рабочего пространства, периодически проверяемого по памяти дочернего процесса. В устойчивом состоянии чтения не сканируют каталоги навыков и не повторно разбирают файлы SKILL.md; резервное локальное холодное чтение демона может выполнять перечисление с диска. Дочерний процесс проверяет неизменность своих источников расширений — один readdir каталога расширений плюс stat для каждой записи, файл включения и состояние активации хранилища — и обновляет их только при изменениях, поэтому расширение, установленное или переключенное вне демона, будет обнаружено при повторной валидации и появится при последующем чтении после публикации этой валидации. Теплое чтение всегда возвращает сохраненное наблюдение, даже если оно запускает фоновую работу. Безопасный режим и режим без расширений пропускают проверку, соответствуя своему исключению расширений.

GET /workspace/providers

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "current": { "authType": "qwen", "modelId": "qwen3(qwen)" }, "providers": [ { "kind": "model_provider", "status": "ok", "authType": "qwen", "current": true, "models": [ { "modelId": "qwen3(qwen)", "baseModelId": "qwen3", "name": "Qwen 3", "description": null, "contextLimit": 4096, "isCurrent": true, "isRuntime": false } ] } ] }

Модели сгруппированы по типу авторизации. Диагностика подключения провайдера находится в ячейке providers объекта /workspace/preflight; предварительная проверка окружения находится в /workspace/preflight и /workspace/env (ниже). errors опускается при успешном построении снимка.

GET /workspace/env

Сообщает о среде выполнения процесса демона, платформе, песочнице, прокси и наличии секретных переменных окружения из белого списка. Всегда отвечает на основе состояния process.* — демон никогда не запускает дочерний процесс ACP для обслуживания этого маршрута, и ответ идентичен независимо от того, запущен ACP или простаивает. Поле acpChannelLive носит исключительно информационный характер.

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "acpChannelLive": false, "cells": [ { "kind": "runtime", "name": "node", "status": "ok", "value": "22.4.0" }, { "kind": "platform", "name": "darwin", "status": "ok", "value": "arm64" }, { "kind": "sandbox", "name": "SANDBOX", "status": "disabled", "present": false }, { "kind": "proxy", "name": "HTTPS_PROXY", "status": "ok", "present": true, "value": "proxy.internal:1080" }, { "kind": "proxy", "name": "NO_PROXY", "status": "disabled", "present": false }, { "kind": "env_var", "name": "OPENAI_API_KEY", "status": "ok", "present": true }, { "kind": "env_var", "name": "ANTHROPIC_BASE_URL", "status": "disabled", "present": false } ] }

Форма ячейки:

type DaemonEnvKind = | 'runtime' // name: 'node' | 'bun' | 'unknown'; value: process.versions.node | 'platform' // name: process.platform; value: process.arch | 'sandbox' // name: 'SANDBOX' | 'SEATBELT_PROFILE'; value optional | 'proxy' // name: HTTP_PROXY | HTTPS_PROXY | NO_PROXY | ALL_PROXY; value: redacted host | 'env_var'; // presence-only; value field is ALWAYS omitted interface DaemonEnvCell extends DaemonStatusCell { kind: DaemonEnvKind; name: string; present?: boolean; value?: string; }

Политика редактирования. Ячейки kind: 'env_var' никогда не включают поле value; клиенты видят только present: boolean. Ячейки kind: 'proxy' пропускают исходное значение переменной окружения через редактирование учетных данных (redactProxyCredentials), а затем через разбор URL, так что в сети передается только host:port. NO_PROXY проходит редактирование дословно, поскольку это список хостов, а не URL. Белый список перечисленных секретных переменных окружения в настоящее время включает OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, DASHSCOPE_API_KEY, OPENROUTER_API_KEY и QWEN_SERVER_TOKEN. Другие переменные окружения не перечисляются, поэтому случайно установленные секреты остаются невидимыми.

GET /workspace/preflight

Сообщает о проверках готовности демона. Ячейки уровня демона (node_version, cli_entry, workspace_dir, ripgrep, git, npm) всегда заполняются из process.* и node:fs. Ячейки уровня ACP (auth, mcp_discovery, skills, providers, tool_registry, egress) требуют активного дочернего процесса ACP — когда демон простаивает, они выводят заполнители status: 'not_started'. Маршрут никогда не запускает ACP исключительно для заполнения ячеек; соответствующие ячейки возвращают значения по умолчанию not_started.

Форма ячейки:

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "acpChannelLive": false, "cells": [ { "kind": "node_version", "status": "ok", "locality": "daemon", "detail": { "version": "22.4.0", "required": ">=22" } }, { "kind": "cli_entry", "status": "ok", "locality": "daemon", "detail": { "path": "/usr/local/bin/qwen", "source": "process.argv[1]" } }, { "kind": "workspace_dir", "status": "ok", "locality": "daemon", "detail": { "path": "/canonical/path" } }, { "kind": "ripgrep", "status": "ok", "locality": "daemon" }, { "kind": "git", "status": "ok", "locality": "daemon", "detail": { "version": "2.45.0" } }, { "kind": "npm", "status": "ok", "locality": "daemon", "detail": { "version": "10.7.0" } }, { "kind": "auth", "status": "not_started", "locality": "acp", "hint": "spawn a session to populate" }, { "kind": "mcp_discovery", "status": "not_started", "locality": "acp", "hint": "spawn a session to populate" }, { "kind": "skills", "status": "not_started", "locality": "acp", "hint": "spawn a session to populate" }, { "kind": "providers", "status": "not_started", "locality": "acp", "hint": "spawn a session to populate" }, { "kind": "tool_registry", "status": "not_started", "locality": "acp", "hint": "spawn a session to populate" }, { "kind": "egress", "status": "not_started", "locality": "acp", "hint": "egress probing lands in PR 14 (#4175)" } ] }

Форма ячейки:

type DaemonPreflightKind = | 'node_version' | 'cli_entry' | 'workspace_dir' | 'ripgrep' | 'git' | 'npm' | 'auth' | 'mcp_discovery' | 'skills' | 'providers' | 'tool_registry' | 'egress'; interface DaemonPreflightCell extends DaemonStatusCell { kind: DaemonPreflightKind; locality: 'daemon' | 'acp'; detail?: Record<string, unknown>; }

Семантика errorKind:

  • missing_binary — версия Node ниже требуемой, отсутствует QWEN_CLI_ENTRY, ripgrep / git / npm не находятся в PATH (предупреждения, а не ошибки для необязательных бинарных файлов).
  • missing_file — boundWorkspace не существует или не является каталогом; ошибка разбора навыка, указывающая на отсутствующий или нечитаемый файл.
  • parse_error — сбой разбора SKILL.md, некорректный JSON конфигурации.
  • auth_env_error — validateAuthMethod вернул ненулевую строку ошибки, или подкласс ModelConfigError был распространен из разрешения провайдера.
  • init_timeout — отклонение withTimeout в мосте (фактический таймаут при ожидании roundtrip ACP). Распознается через типизированный класс BridgeTimeoutError. Примечание: преходящая ячейка mcp_discovery warning со значением connecting > 0 НЕ относится к этому типу — это нормальное состояние выполняющегося рукопожатия, отличное от реального таймаута.
  • restore_timeout — загрузка или возобновление сессии превысило выделенный бюджет восстановления. Ответ REST имеет код 504 и может быть повторен; он отличается от инициализации дочернего процесса и от ограничений окна воспроизведения.
  • protocol_error — запрос ACP extMethod отклонен, так как канал закрылся во время выполнения запроса, или из-за неожиданного отсутствия реестра инструментов.
  • blocked_egress — зарезервировано для PR 14 (#4175). В PR 13 ячейка egress остается со значением status: 'not_started'.

Если мост не может связаться с дочерним процессом ACP при обработке предварительного запроса (например, закрытие канала во время запроса), массив errors в конверте содержит единственный объект ServeStatusCell, описывающий ошибку, а ячейки возвращают заполнители ACP not_started. Ячейки уровня демона все равно возвращаются.

GET /workspace/tools

Возвращает каталог инструментов, предоставленный дочерним процессом ACP основного рабочего пространства. Это устаревший маршрут для основного рабочего пространства: он не имеет селектора рабочего пространства и не должен использоваться для определения инструментов среды выполнения, отличной от основной.

{ "v": 1, "workspaceCwd": "/canonical/path", "initialized": true, "acpChannelLive": true, "tools": [ { "name": "ReadFile", "displayName": "Read", "description": "Read a file", "enabled": true } ] }

Когда ни один дочерний процесс ACP не активен, маршрут по-прежнему возвращает 200 с acpChannelLive: false, пустым массивом tools и записью not_started в необязательном массиве errors. Неожиданные ошибки моста используют стандартный ответ об ошибке моста 500. Метод TypeScript SDK называется workspaceTools(); специального тега возможности нет, поэтому клиенты, поддерживающие старые версии демонов, должны считать 404 неподдерживаемым.

Маршруты файлов рабочего пространства

Устаревшие файловые маршруты разрешают пути через основное рабочее пространство; маршруты /workspaces/:workspace/... разрешают пути через выбранную среду выполнения рабочего пространства. Ответы используют относительные пути рабочего пространства и никогда не возвращают абсолютные пути файловой системы в обычных успешных случаях. Успешные ответы файловых операций включают:

Cache-Control: no-store X-Content-Type-Options: nosniff

Ошибки файловой системы используют этот формат JSON:

{ "errorKind": "hash_mismatch", "error": "expected sha256:..., found sha256:...", "hint": "re-read the file and retry with the latest hash", "status": 409 }

Значения errorKind включают path_outside_workspace, symlink_escape, path_not_found, binary_file, file_too_large, untrusted_workspace, permission_denied, parse_error, hash_mismatch, file_already_exists, text_not_found и ambiguous_text_match.

GET /file

Читает текстовый файл. Параметры запроса: path (обязательно), maxBytes, line, limit и cursor. Демон отклоняет бинарные файлы. Файлы, превышающие лимит полного снимка в 256 КиБ, требуют указания хотя бы одного явного параметра окна (line, limit или maxBytes); запрос без них остается file_too_large. Такое окно передается потоково, и его возвращаемое содержимое UTF-8 ограничено 256 КиБ. maxBytes всегда применяется к байтам ответа UTF-8 после декодирования, включая случаи, когда источник использует другую поддерживаемую кодировку в пределах лимита полного снимка.

Смещения строк определяются путем сканирования с начала файла, поэтому окно также отклоняется с кодом file_too_large, если достижение его требует чтения более 8 МиБ (MAX_TEXT_SCAN_BYTES). Используйте GET /file/bytes, чтобы напрямую достичь более глубокого смещения. Большой текст в кодировке, которую маршрут не может декодировать, возвращает binary_file, а не file_too_large — повторная попытка с меньшим окном не поможет, и readBytes является тем же решением, которое уже применяется для бинарных файлов.

Для файлов в пределах лимита полного снимка ответ включает hash, SHA-256 дайджест сырых байтов на диске для всего файла, даже если line, limit или maxBytes вернули фрагмент. Большие частичные окна опускают hash, сохраняют полный sizeBytes, устанавливают truncated: true и возвращают originalLineCount: null, если поток останавливается до EOF.

Постраничная загрузка с помощью cursor

Требует возможность workspace_file_read_cursor. Ответ, который имеет больше данных для передачи, возвращает hasMore: true и, если смещение байта файла выводимо, токен nextCursor. Передача его обратно как cursor возобновляет чтение за O(1), тогда как глубокое смещение line требует сканирования от байта 0 и отклоняется за пределами 8 МиБ.

GET /file?path=big.log&limit=500 → { content, nextCursor, hasMore: true } GET /file?path=big.log&limit=500&cursor=… → next page

cursor и line взаимоисключающи (parse_error) — оба указывают начальную точку. Некорректный или слишком длинный курсор вызывает parse_error; курсор, для которого файл был заменен или усечен, вызывает hash_mismatch (409). Добавление данных не инвалидирует существующий курсор, что является случаем, ради которого эта функция существует.

content опускает завершающую новую строку последней строки, как и любое другое чтение, поэтому клиент, собирающий страницы вместе, соединяет их с помощью \n. hasMore не является пересказом nextCursor: небольшой не-UTF-8 файл, прочитанный с помощью limit, имеет больше содержимого, но не имеет выводимого смещения байта, поэтому он сообщает hasMore: true с nextCursor: null. Курсор также равен null, когда лимит байтов обрезает текущую строку, потому что возобновление с этого смещения вернуло бы неполную строку. Для многих коротких строк уменьшите limit, пока страница не закончится до лимита байтов и не вернет курсор. Для одной слишком большой строки явно запросите следующую строку (например, line=2 при начале со строки 1), затем продолжайте с курсорами; используйте GET /file/bytes, когда требуется полная слишком большая строка.

{ "kind": "file", "path": "src/index.ts", "content": "export {};\n", "encoding": "utf-8", "bom": false, "lineEnding": "lf", "sizeBytes": 11, "returnedBytes": 11, "truncated": false, "hash": "sha256:...", "matchedIgnore": null, "originalLineCount": null }

GET /file/bytes

Читает сырые байты из файла без декодирования. Параметры запроса: path (обязательно), offset (по умолчанию 0) и maxBytes (по умолчанию 65536, максимум 262144). Этот маршрут поддерживает ограниченные окна для больших бинарных файлов без загрузки всего файла целиком. Ответ включает hash только тогда, когда возвращаемое окно охватывает весь файл.

{ "kind": "file_bytes", "path": "assets/logo.png", "offset": 0, "sizeBytes": 3912, "returnedBytes": 3912, "truncated": false, "contentBase64": "...", "hash": "sha256:..." }

GET /stat

Возвращает метаданные для одного пути в основном рабочем пространстве. Параметр запроса path обязателен.

{ "kind": "stat", "path": "src/index.ts", "type": "file", "sizeBytes": 128, "modifiedMs": 1700000000123 }

type может быть file, directory, symlink или other. Маршрут использует границы файлов рабочего пространства и общую оболочку ошибок файловой системы, описанную выше. Метод TypeScript SDK называется fileStat().

GET /list

Список каталогов основного рабочего пространства. Параметр запроса path обязателен; includeIgnored=1 (или true) включает записи, соответствующие правилам игнорирования. Ответ ограничен 2000 записями и устанавливает truncated: true, если их больше.

{ "kind": "list", "path": ".", "entries": [{ "name": "src", "kind": "directory", "ignored": false }], "truncated": false, "matchedIgnore": null }

Поле kind каждой записи имеет значение file, directory, symlink или other. Метод TypeScript SDK — dirList().

GET /glob

Соответствие путям внутри основного рабочего пространства. Параметр запроса pattern обязателен. Необязательный параметр cwd сужает поиск, includeIgnored=1 (или true) включает игнорируемые пути, а maxResults — целое число от 1 до 50 000 (по умолчанию 5 000).

{ "kind": "glob", "pattern": "**/*.ts", "cwd": "", "matches": ["src/index.ts"], "count": 1, "truncated": false, "durationMs": 4 }

Совпадения являются относительными для рабочего пространства. Неверные значения запроса возвращают 400; ошибки доверия к рабочему пространству, containment, отсутствующего пути и непредвиденные сбои используют общий формат ошибок файловой системы. Метод TypeScript SDK — glob().

POST /file/write

Создает или заменяет текстовый файл. Это строгий маршрут мутации: авторизован запрос без токена из доверенного loopback-соединения в основном рабочем пространстве. Запрос без токена, достигший шлюза без полномочий доверенного loopback-соединения, возвращает 401 { "code": "token_required" }. Ошибки настроенного токена и учетных данных Local Control отвергаются ранее с простой ошибкой 401 Unauthorized; при наличии --require-auth глобальный bearer middleware отвергает неаутентифицированные запросы до выполнения маршрута.

Тело:

{ "path": "src/new.ts", "content": "export const value = 1;\n", "mode": "create" }
{ "path": "src/existing.ts", "content": "export const value = 2;\n", "mode": "replace", "expectedHash": "sha256:..." }

Поле mode должно иметь значение create или replace. Поле create никогда не перезаписывает существующий файл (409 file_already_exists). Поле replace требует expectedHash; отсутствующие или некорректные хеши приводят к ошибке 400 parse_error, а устаревшие хеши — к ошибке 409 hash_mismatch. Поле expectedHash состоит из sha256: плюс 64 символов нижнего регистра в шестнадцатеричном формате, вычисленных по необработанным байтам на диске.

Могут быть переданы поля bom, encoding и lineEnding. При замене профиль кодировки существующего файла сохраняется по умолчанию; явные поля переопределяют его. Запись бинарных данных находится вне области действия.

Демон записывает данные во временный файл со случайным именем в целевом каталоге, выполняет fsync там, где это поддерживается, повторно проверяет текущий хеш непосредственно перед rename(), затем переименовывает файл на место. Это предотвращает наблюдение частичной записи файла и сериализует записи, инициированные демоном, в один и тот же файл, но это не является межпроцессным сравнением и обменом на уровне ядра: внешний редактор может создать гонку в крошечном окне между финальной проверкой хеша и переименованием.

{ "kind": "file_write", "path": "src/existing.ts", "mode": "replace", "created": false, "sizeBytes": 24, "hash": "sha256:...", "encoding": "utf-8", "bom": false, "lineEnding": "lf", "matchedIgnore": null }

POST /file/edit

Применяет одну точную текстовую замену к существующему текстовому файлу. Это также строгий маршрут мутации, требующий expectedHash.

{ "path": "src/config.ts", "oldText": "timeout: 30000", "newText": "timeout: 60000", "expectedHash": "sha256:..." }

Поле oldText должно быть непустым и встречаться ровно один раз. Отсутствие совпадений возвращает 422 text_not_found; множественные совпадения возвращают 422 ambiguous_text_match. Маршрут сохраняет кодировку, BOM и окончания строк, а также повторно проверяет expectedHash непосредственно перед атомарным переименованием.

Явные записи/изменения игнорируемых путей разрешены, так как аутентифицированный вызывающий объект указал путь. Успешные ответы и события аудита включают matchedIgnore: "file" | "directory" | null.

{ "kind": "file_edit", "path": "src/config.ts", "replacements": 1, "sizeBytes": 128, "hash": "sha256:...", "encoding": "utf-8", "bom": false, "lineEnding": "lf", "matchedIgnore": null }

POST /workspaces/:workspace/directory/create

Требуются workspace_qualified_rest_core и workspace_directory_create, строгие полномочия мутации и активное доверенное локальное рабочее пространство. Тело запроса:

{ "path": "src/new-folder" }

Родительский каталог должен существовать. Создание является эксклюзивным: существующий файл или каталог возвращает 409 file_already_exists; конечная символическая ссылка возвращает 400 symlink_escape. Успех означает статус 201 { "kind": "directory_create", "path": "src/new-folder" }. Метод SDK — WorkspaceDaemonClient.createWorkspaceDirectory(path, clientId?).

POST /workspaces/:workspace/directory/delete

Требуются workspace_qualified_rest_core и workspace_directory_delete в соответствии с той же политикой строгой мутации и доверенного локального выполнения. Тело запроса:

{ "path": "src/old-folder" }

Рекурсивно удаляет пустой или непустой каталог. Корни рабочего пространства, обычные файлы и конечные символические ссылки отвергаются; вложенные записи символических ссылок удаляются без перехода по их целям. Отсутствующая цель возвращает 404 path_not_found. Успех означает статус 200 { "kind": "directory_delete", "path": "src/old-folder" }. Метод SDK — WorkspaceDaemonClient.deleteWorkspaceDirectory(path, clientId?).

Оба маршрута для каталогов сохраняют лексическое конечное имя, разрешая его родителя внутри выбранного рабочего пространства. Пути не должны заканчиваться разделителем; подозрительные шаблоны путей возвращают 400 path_outside_workspace, а некорректные формы конечных элементов возвращают 400 parse_error. Неподдерживаемые пользовательские фабрики возвращают 422; SSH-рабочие пространства отвергаются границей SSH с ошибкой 501. Неизвестные, недоступные, удаленные или недоверенные владельцы никогда не возвращаются к основному рабочему пространству.

Удаление является постоянным, и сбои могут привести к частичному удалению. Конкурентный писатель может вызвать ENOTEMPTY, возвращаемый как 503 io_error; повторите ту же операцию после остановки писателя. Блокировки по путям не сериализуют каталог относительно каждой дочерней записи, и внешнюю замену каталога невозможно сделать атомарной с помощью переносимых проверок на основе путей. Явные изменения игнорируемых каталогов разрешены, и события аудита аннотируют matchedIgnore, когда применимо; создание записывает sizeBytes: 0.

GET /session/:id/context

{ "v": 1, "sessionId": "<sid>", "workspaceCwd": "/canonical/path", "state": { "models": {}, "modes": {}, "configOptions": [] } }

Для сеанса верхнего уровня поле state отражает те же структуры модели/режима/параметров конфигурации ACP, которые используются в POST /session, POST /session/:id/load и POST /session/:id/resume. Виртуальный идентификатор сеанса с префиксом subagent. разрешается относительно родительской среды выполнения и возвращает пустой объект state.

GET /session/:id/supported-commands

{ "v": 1, "sessionId": "<sid>", "availableCommands": [ { "name": "init", "description": "Initialize the project", "input": null, "_meta": { "source": "builtin" } } ], "availableSkills": ["review"] }

Поле availableCommands представляет собой тот же снимок команд, который используется в SSE-уведомлении available_commands_update. Поле availableSkills перечисляет только имена навыков; клиенты не должны ожидать тела навыков или пути через этот маршрут.

GET /session/:id/tasks

{ "v": 1, "sessionId": "<sid>", "now": 1700000000000, "tasks": [ { "kind": "agent", "id": "agent-1", "label": "reviewer: check failure", "description": "check failure", "status": "running", "startTime": 1699999999000, "runtimeMs": 1000, "outputFile": "/tmp/agent-1.jsonl", "isBackgrounded": true, "subagentType": "reviewer" }, { "kind": "agent", "id": "agent-2", "label": "general-purpose: run the failing test", "description": "run the failing test", "status": "running", "startTime": 1699999999500, "runtimeMs": 500, "outputFile": "/tmp/agent-2.jsonl", "isBackgrounded": false, "subagentType": "general-purpose", "parentAgentId": "agent-1", "parentName": "reviewer", "depth": 1 } ] }

Этот маршрут представляет собой снимок вне полосы пропускания только для чтения. Он намеренно не является подсказкой (prompt) и может запрашиваться во время потоковой передачи сеанса. Ответ содержит только разрешенные метаданные из реестров задач агента, оболочки и монитора; контроллеры, таймеры, смещения, ожидающие сообщения и необработанные объекты реестра никогда не раскрываются.

Задачи агента, порожденные другим суб-агентом (вложенные суб-агенты, ограниченные maxSubagentDepth), содержат три необязательных поля происхождения: parentAgentId (id задачи агента, породившего его), parentName (subagentType породившего агента, зафиксированный при регистрации, чтобы он сохранился после удаления родителя из реестра) и depth (глубина запуска, начиная с 0; 0 = запущен верхнеуровневой сессией). Агенты, запущенные верхнеуровневой сессией, не имеют полей parentAgentId и parentName; клиенты должны считать все три поля необязательными и использовать плоский список при их отсутствии.

GET /session/:id/tasks/:taskId/runtime

Детальная информация по запросу для одной shell-задачи. GET /session/:id/tasks содержит только факты жизненного цикла; этот маршрут читает захваченный файл вывода и исследует дерево процессов, поэтому открытая панель деталей запрашивает это отдельно.

Метод ACP — это _qwen/session/task_runtime с параметрами sessionId и taskId. Оба транспорта определяют владельца активной сессии; ACP также требует, чтобы соединение владело сессией.

taskId пути разрешается относительно собственного реестра shell-сессии; вызывающие стороны никогда не передают путь к файлу.

{ "v": 1, "sessionId": "<sid>", "taskId": "bg_0123abcd", "runtime": { "memoryBytes": 104857600, "outputBytes": 65536, "outputModifiedAt": 1700000000000, "outputTail": "ready in 120ms\n", "outputTailTruncated": true } }

runtime равно null, если задача неизвестна, была удалена или не является shell-задачей. memoryBytes — это резидентный набор дерева процессов shell, опрашиваемый только во время выполнения задачи; общие страницы учитываются один раз на процесс, а песочница контейнера может показывать только обертку процесса. outputTail ограничен тем же лимитом в 8 КиБ, что используется в уведомлении о завершении, а outputTailTruncated отмечает хвост, начинающийся посреди файла. outputError устанавливается, если файл невозможно прочитать; поля из успешно прочитанной части могут присутствовать.

POST /session/:id/tasks/:taskId/workflow-action

Управляет запуском рабочего процесса или начинает новый. Метод ACP — _qwen/session/tasks/workflow_action.

{ "action": "run-script", "script": "export const meta = { name: 'daily-audit', description: 'Audits yesterday' }\nreturn await agent(args.question)", "args": { "question": "which tables grew?" }, "sourceRef": { "id": "definition-7", "revision": "rev-3" } }

Что означает taskId зависит от действия:

actiontaskIdЗапуск процесса
pause, resume, retry, rerunидентификатор процессаделают retry и rerun
delete-historyидентификатор процессанет
run-savedимя сохранённого рабочего процесса, <extension>:<name> для расширенияда
run-scriptключ запуска, выбираемый вызывающей сторонойда

args, sourceRef и script читаются только через run-saved и run-script. args привязан к глобальной переменной args скрипта и может быть любым значением JSON; sourceRef — это собственный {id, revision} вызывающей стороны, записываемый в запуск, его журнал и снимок; script — это исходный run-script, который запускается и требуется им. Повторная попытка или перезапуск воспроизводят собственные args и sourceRef исходного запуска — именно это делает его тем же запуском — поэтому эти поля игнорируются вместе с управляющими действиями.

Начатый запуск принадлежит сессии: он выполняется в фоновом режиме без запроса подтверждения, и его завершение достигает канала завершения сессии. После этого он становится целью retry/rerun, как любой другой запуск.

retry и rerun также принимают запуск, восстановленный из истории (isHistorical в GET /session/:id/tasks), например тот, который был прерван перезапуском демона: запуск снова стартует из скрипта своего снимка, args и sourceRef, регистрируется в сессии, отправившей действие, и сообщает ей о своем завершении. retry возобновляет журнал запуска под тем же идентификатором запуска и применяется к запуску failed, над которым больше не работает ни один процесс; rerun начинает новый идентификатор запуска из любого завершенного запуска. Запись истории, содержащая argsOmitted, была запущена с слишком большим args для сохранения, поэтому ни одно из действий к ней не применимо. Также неприменимо к снимку, написанному до того, как сохранялись args, который не содержит ни argsOmitted, ни argsRecorded: журнал запуска ключуется от хеша его args, поэтому перезапуск того, чей args история не может назвать, ничего не воспроизведет и заново диспетчеризует всех агентов. Оба отказа являются workflow_args_unavailable, и GET /session/:id/tasks сообщает оба как argsUnavailable в записи, чтобы клиент мог скрыть два действия вместо обнаружения отказа путем выполнения вызова. argsOmitted остается рядом с ним как причина для клиента, который хочет указать, какая именно. Сами args никогда не передаются по сети.

run-script не передает имя определения, поэтому запуск помечается собственным export const meta скрипта — компилируемый скрипт должен объявить его, иначе запуск покажет только свой id.

Ответом является {"changed": true, "status": "running", "taskId": "<runId>"} для начатого запуска, {"changed": true, "status": "<status>"} для управляющего действия, которое вступило в силу, и {"changed": false}, когда ничего не произошло: Workflow недоступен для сессии (отключен, базовый режим, недоверенная папка), рабочее пространство недоверенное, сохраненное имя workflow неизвестно, id запуска неизвестен или другой запуск уже выполняется под тем же taskId. Дедуплицируются только пересекающиеся запуски: два одновременных вызова run-script под одним taskId начинают один запуск. Как только этот запуск вернется, ключ освобождается; повторная отправка может начать другой запуск, даже если первый еще активен. Ключ не обеспечивает идемпотентность повторных попыток после потери ответа.

Отклоненный параметр — это 400 (-32602 через ACP), а не начатый запуск: неизвестный action, run-script без script, sourceRef, который не является {id, revision} непустых строк, или скрипт, отклоненный перед запуском из-за синтаксической ошибки или нарушения детерминизма. Ошибки входных данных при запуске Workflow содержат workflow_invalid_params и сохраняют сообщение об отказе.

Четыре отказа исходят из сохраненного состояния запуска, а не из запроса. Каждый является -32602 через ACP, содержащим свой errorKind и свой data.httpStatus. Три из них — это 409; четвертый, workflow_not_recorded, является 503, потому что ничего не было начато, и тот же вызов можно просто повторить: retry записывает, что запуск снова выполняется, перед началом — эта запись предотвращает запуск другим процессом повторно — но запись не удалось сохранить.

code / errorKindHTTPКогдаЧто делать
workflow_journal_unavailable409retry, у которого нет журнала на диске или он не читаетсяrerun его, что запустит его с самого начала
workflow_args_unavailable409retry или rerun записи истории, помеченной как argsUnavailable: её args были слишком велики для сохранения, или снимок создан до внедрения этой функциизапустите заново с помощью run-saved или run-script и её args
workflow_run_live_elsewhere409retry запуска, контрольная точка которого указывает на процесс, завершение которого не зафиксировано — всё ещё работающий, находящийся на другой машине или с повторно использованным pidrerun его, что присвоит новый идентификатор запуска
workflow_not_recorded503retry, запись о повторном запуске которого не удалось сохранить, поэтому он не началсяповторите тот же вызов; запуск не затронут

workflowToolFeatures в GET /session/:id/supported-commands объявляет поддержку runSavedArgs и runScript; демон без них не принимает ни входные данные запуска, ни действие run-script. Он также содержит поле nameOnly, равное true, когда модель сессии может запускать только именованные рабочие процессы (tools.workflowNameOnly): собственные вызовы модели script и scriptPath отклоняются, тогда как все перечисленные выше действия, включая run-script, по-прежнему инициируют запуски. Поле retryHistorical равно true, когда retry и rerun принимают запуск, восстановленный из истории; более старый демон отвечает на них ошибкой {"changed": false}, поэтому клиент должен предлагать эти элементы управления для записи истории только если это поддерживается.

GET /session/:id/lsp

{ "v": 1, "sessionId": "<sid>", "workspaceCwd": "/canonical/path", "enabled": true, "configuredServers": 1, "readyServers": 1, "failedServers": 0, "inProgressServers": 0, "notStartedServers": 0, "servers": [ { "name": "typescript", "status": "READY", "languages": ["typescript", "javascript"], "transport": "stdio", "command": "typescript-language-server" } ] }

status — одно из значений NOT_STARTED, IN_PROGRESS, READY или FAILED. Необязательный параметр error присутствует у сбойных серверов, если он доступен. Отключенный LSP (включая базовый режим) возвращает HTTP 200 с enabled: false, нулевыми счетчиками и servers: []. Включенный LSP без настроенных серверов возвращает enabled: true, configuredServers: 0 и servers: []. Если инициализация завершается неудачей до создания клиента, ответ может содержать initializationError; если активный клиент не может предоставить снимок состояния, ответ включает statusUnavailable: true.

Этот маршрут предоставляет только стабильные поля, видимые клиенту. Он намеренно исключает внутренние отладочные данные, такие как идентификаторы процессов, аргументы запуска, хвосты stderr, корневые URI и пути папок рабочего пространства.

GET /session/:id/resources

{ "v": 1, "sessionId": "<sid>", "workspaceCwd": "/canonical/session/path", "skills": { "v": 1, "workspaceCwd": "/canonical/session/path", "initialized": true, "skills": [] }, "mcp": { "v": 1, "workspaceCwd": "/canonical/session/path", "initialized": true, "discoveryState": "completed", "servers": [] } }

Этот маршрут владельца активной сессии читает Config выбранной сессии через собственное соединение ACP. Он не объединяет маршруты статуса глобального процесса или уровня рабочего пространства, не инициализирует холодную среду выполнения, не подключает клиента и не использует резервный вариант основного рабочего пространства. Неизвестные сессии и сессии, существующие только в сохраненном виде, возвращают существующий ответ session_not_found.

Вложенные объекты используют точные контракты статуса GET /workspace/skills и GET /workspace/mcp. Их поля workspaceCwd идентифицируют Config, создавший снимок, и совпадают со значением верхнего уровня. Действующие правила редактирования данных сохраняются: учетные данные и заголовки MCP, значения переменных окружения, тела Skill и необработанные настройки никогда не появляются в ответе. Расширения аутентификации MCP, пула, бюджета рабочего пространства и ошибок обнаружения рабочего пространства отсутствуют, поскольку их базовое состояние принадлежит рабочему пространству или ключуется только по имени сервера, а не по сессии. Статус, обнаружение и учет от собственного менеджера MCP выбранной сессии остаются доступными.

Автономный жизненный цикл сессии (standalone_sessions_v1)

Когда /capabilities.features содержит standalone_sessions_v1, демон предоставляет семейство маршрутов глобального процесса для автономных сессий верхнего уровня, принадлежащих его выделенной среде выполнения Conversations. Эти маршруты никогда не принимают селектор рабочего пространства и никогда не возвращаются к основному рабочему пространству. Прямые встраивания, которые не могут построить полную графовую зависимость владения, среды выполнения, каталога, жизненного цикла и журнала удаления Conversations, исключают как функцию, так и все перечисленные ниже маршруты.

МаршрутЗапросУспех
POST /standalone/sessions{ "sessionId": "<UUID>", "modelServiceId"?: string, "startupConfig"?: { "modelServiceId": string, "reasoningEffort"?: ReasoningSelection }, "approvalMode"?: ApprovalMode }200 с автономной сессией, context: { "kind": "standalone" } и её управляемым выходным каталогом без проекта. Создание выполняется без подсказки.
GET /standalone/session-optionsнет; любое поле запроса отклоняется с 400200 с { v, initialized, current?, approvalMode?, providers, errors? }; внутренний путь рабочего пространства и состояние канала ACP опущены
GET /standalone/sessionsQuery: cursor?, size? (1-100), archiveState? (active или archived)200 { sessions, nextCursor?, liveMergeFailed?, truncated? }
GET /standalone/sessions/:idнет202 { sessionId, state: "creating" } пока локальное создание выполняется, иначе 200 с точной сводкой.
POST /standalone/sessions/:id/loadТолько существующие параметры восстановления: historyPageSize?, liveReplayMode?, compactedReplayMode?, hideInheritedHistory?, approvalMode?; идентичность клиента остаётся в X-Qwen-Client-Id.200 восстановленная автономная сессия.
POST /standalone/sessions/:id/resumeТе же параметры восстановления, что и у load.200 восстановленная автономная сессия без воспроизведения истории загрузки.
POST /standalone/sessions/:id/repair-directoryПустое тело или {}200 с проверенным или воссозданным управляемым каталогом.
PATCH /standalone/sessions/:id/metadata{ "displayName": string }200 { sessionId, displayName }
GET /standalone/sessions/:id/exportQuery: format=html, format=md, format=json или format=jsonl (по умолчанию html).Существующий тип содержимого экспорта, имя файла и тело.
POST /standalone/sessions/archive{ "sessionIds": ["<UUID>", ...] }200 { archived, alreadyArchived, notFound, errors }
POST /standalone/sessions/unarchive{ "sessionIds": ["<UUID>", ...] }200 { unarchived, alreadyActive, notFound, errors }
POST /standalone/sessions/delete{ "sessionIds": ["<UUID>", ...] }200 { removed, notFound, errors, fileCleanupPending }

Когда POST /standalone/sessions несёт устаревший modelServiceId, ответ включает modelApplied: false означает, что переключение модели при запуске не удалось (также отображается через событие сессии model_switch_failed), и сессия работает на модели по умолчанию агента — само создание всё равно завершается успешно, чтобы вызывающий код мог предупредить, освободить ресурсы или явно повторить попытку.

Тела должны быть JSON-объектами без неизвестных полей. Идентификаторы являются значениями RFC UUID v1-v5; демон канонизирует их в нижний регистр. Пакетные запросы содержат 1-100 строк и проверяются и дедуплицируются перед изменением. Сбой пакета сообщается как { sessionId, code, message } и не откатывает успешные операции для других ID. fileCleanupPending означает, что удаление транскрипта зафиксировано, но очистка sidecar или управляемого каталога, авторизованная журналом, должна быть повторена посредством сверки; сессия уже логически удалена.

Видимы только явные автономные транскрипты и документированная форма совместимости верхнего уровня для устаревших данных. Дочерние записи, Live, проекты, worktree, неоднозначные, нечитаемые или записи с журналом удаления терпят неудачу безопасно. Создание продолжается, если его HTTP-ответ разрывается; зафиксированная сессия не удаляется, и клиент ответа отсоединяется. Восстановитесь с помощью точного GET, за которым следует загрузка/возобновление, вместо повторной попытки создания как присоединения.

Архивация, разархивация, восстановление, переименование и удаление разделяют один и тот же допуск жизненного цикла для каждой сессии, как загрузка/возобновление и подсказки. Удаление использует отвязку транскрипта как свою долговременную точку фиксации и частный журнал плюс атомарное размещение управляемого каталога для восстановления после сбоя. Восстановление возвращает каталог, если транскрипт остаётся целым, и завершает очистку, когда транскрипт отсутствует; любая несовпадающая идентичность, конфликтующий путь, внешний владелец или неоднозначный транскрипт возвращают структурированную ошибку безопасного отказа.

POST /session

Необязательный контракт startupConfig описан в разделе Capabilities. В отличие от устаревшего выбора модели best-effort, отвергнутый выбор при запуске приводит к сбою создания.

Запустить нового агента или присоединиться к существующему (в рамках sessionScope: 'single', по умолчанию).

Запрос:

{ "cwd": "/absolute/path/to/workspace", "modelServiceId": "qwen-prod", "sessionId": "550e8400-e29b-41d4-a716-446655440000", "sessionScope": "thread", "worktree": { "slug": "feature-a" } }
ПолеОбязательноПримечания
cwdнетАбсолютный путь, соответствующий одному зарегистрированному рабочему пространству. Если пропущено, маршрут возвращается к основному рабочему пространству (прочитайте это из /capabilities.workspaceCwd). Несоответствующее непустое cwd возвращает 400 workspace_mismatch. Когда features содержит multi_workspace_sessions, клиенты могут передавать любой доверенный workspaces[].cwd; иначе принимается только основное рабочее пространство. Пути рабочих пространств канонизируются через realpathSync.native (с резервным вариантом только разрешения для несуществующих путей), чтобы файловые системы без учёта регистра не отклоняли сессии из-за написания.
modelServiceIdнетВыбирает, какой настроенный сервис моделей будет использовать агент (бэкенд-провайдер — Alibaba ModelStudio, OpenRouter и т.д.). Если пропущено, агент использует свой вариант по умолчанию. Если в рабочем пространстве уже есть сессия, это вызывает setSessionModel для существующей и рассылает model_switched. Отлично от modelId в POST /session/:id/model, который выбирает модель внутри уже привязанного сервиса. Массив modelServices в /capabilities зарезервирован для объявления настроенных сервисов; на Этапе 1 он всегда равен [] (сервис агента по умолчанию используется и не перечисляется по HTTP).
startupConfigнет{ modelServiceId: string, reasoningEffort?: ReasoningSelection }; требует session_startup_config, создаёт новую ветку и подтверждает запрошенный выбор без записи общих значений по умолчанию. Нельзя смешивать с верхним уровнем modelServiceId или явной одиночной областью.
sessionIdнетRFC-вариант UUID v1-v5, выбранный вызывающим кодом. Демон нормализует его в нижний регистр и всегда создаёт свежую сессию ветки; он никогда не трактует это поле как идемпотентное присоединение. Подтвердите, что caps.features содержит session_id_override, прежде чем отправлять его, поскольку более старые демоны могут игнорировать неизвестные поля. null эквивалентно пропуску.
sessionScopenoПереопределение для совместного использования сессии на уровне запроса. Значение 'single' (общий параметр по умолчанию для демона) заставляет вторую POST /session в том же рабочем пространстве повторно использовать существующую сессию (attached: true); значение 'thread' принудительно создает новую уникальную сессию при каждом вызове. Если поле опущено, используется общий параметр по умолчанию для демона. Значения вне перечисления возвращают 400 { code: 'invalid_session_scope' }. Старые демоны (до PR 5 из #4175) молча игнорируют это поле — выполните предварительную проверку caps.features.session_scope_override перед отправкой. В настоящее время в продакшене общий параметр по умолчанию для демона жестко задан как 'single'; в #4175 может быть добавлен флаг CLI --sessionScope в последующих обновлениях.
worktreenoСоздает новую потоковую сессию в Git worktree с именем пользователя. Необязательный параметр slug использует валидацию имен worktree демона. Выполните предварительную проверку session_worktree_persistence_v1; клиенты не должны делать вывод о надежной изоляции на основе более старых ответов, связанных с worktree. Создание worktree не является операцией подключения и не может сочетаться с созданием ветки. Маршрут возвращает успех только после перемещения, создания маркера эксклюзивного владения, сохранения sidecar-файла и финальной проверки поколения среды выполнения.

Ответ:

{ "sessionId": "<uuid>", "workspaceCwd": "/canonical/path", "attached": false, "worktree": { "slug": "feature-a", "path": "/canonical/path/.qwen/worktrees/feature-a", "branch": "worktree-feature-a" }, "worktreeState": "persisted-v1" }

attached: true означает, что сессия для этого рабочего пространства уже существовала, и теперь вы разделяете её.

Поле worktreeState присутствует только для аттестованной сессии worktree. Устаревшее восстановление best-effort может вернуть worktree без worktreeState. Холодное восстановление Part 4A, где промпт восстановления был запущен, а не поставлен в очередь (suppressWorktreeContextRestore выключен, поэтому маршрут запросил у моста отсутствие отсрочки), также возвращает непроверенные метаданные worktree до завершения промпта: дочерний процесс читается как активный без указания cwd, перемещение невозможно при активном промпте, и маршрут сохраняет сессию вместо уничтожения той, которую вызывающий код только что восстановил. Ни один из ответов не является доказательством надежного корня выполнения. Клиент, запросивший изоляцию через worktree, должен требовать worktreeState: "persisted-v1" и ожидаемый канонический путь перед маршрутизацией промпта; начальная загрузка может быть повторена после завершения активного промпта. При повторном подключении SDK результат зависит от того, содержит ли ответ все еще метаданные worktree: ответ, содержащий их с отсутствующей аттестацией или измененным путем, является терминальным для этого клиента — новое подключение отсоединяется, и промпт не повторяется — тогда как ответ вообще без объекта worktree исцеляется: клиент сбрасывает свой кэшированный запрос на worktree, сохраняет новое подключение и повторяет промпт в том же вызове. Исцеление не блокирует этот повтор, поэтому один ход все еще может выполняться в директории, которую клиент больше не может аттестовать (обычной причиной является exit_worktree, который клиент никогда не видел, и клиент не может отличить его от очищенной ассоциации в памяти). Поэтому вызывающий код, учитывающий изоляцию, должен повторно проверять саму аттестацию worktree перед отправкой промпта; сброшенный запрос заставляет такого вызывающего код fail-closed на собственном шлюзе идентификации при следующей загрузке или выборе.

ID, предоставленные вызывающим кодом, уникальны среди всех зарегистрированных сред выполнения рабочих пространств и всех живых поколений мостов, включая заменяемые процессы в режиме слива (draining). Дубликат живой, ожидающей, активной, архивированной или основанной на worktree сессии возвращает 409 session_id_conflict. Недопустимые значения возвращают 400 invalid_session_id; недоступная проверка владельца в реальном времени или сохраненного состояния возвращает повторную ошибку 503 session_id_admission_unavailable. Повторите попытку с ограниченным отступом после изменений в состоянии моста или хранилища; retryable означает, что другая попытка безопасна, а не то, что немедленный повтор будет успешным. Если нижестоящий агент возвращает другой ID, демон удаляет этот осиротевший объект и возвращает 500 session_id_not_honored. После неоднозначного ответа загружайте или возобновляйте известный ID вместо повторного создания как операции присоединения.

Интеграции с несколькими клиентами, которым нужны независимые диалоги, должны отправлять sessionScope: "thread" при каждом POST /session. Используйте область по умолчанию single только когда клиенты намеренно разделяют одну совместную сессию; общие сессии сериализуют запросы через один FIFO, что видно через /daemon/status как runtime.activity.pendingPrompts и runtime.activity.queuedPrompts.

Параллельные вызовы POST /session для одного и того же рабочего пространства объединяются в одно порождение — оба вызывающих получают тот же самый sessionId, и ровно один сообщает attached: false. Если базовое порождение завершается ошибкой (таймаут инициализации, некорректный вывод агента, OOM), все объединенные вызывающие получают ту же ошибку — слот в процессе обработки очищается, чтобы последующий вызов мог начать заново.

⚠️ Отказ modelServiceId для новой сессии молчаливо обрабатывается в HTTP-ответе. Неправильный modelServiceId (опечатка, ненастроенный сервис) НЕ вызывает ошибку 500 при создании — сессия остается работоспособной на модели агента по умолчанию, чтобы вызывающий объект мог получить sessionId и повторить переключение модели (через POST /session/:id/model). Видимым сигналом сбоя является событие model_switch_failed в потоке SSE сессии, возникающее между рукопожатием при запуске и вашей первой подпиской. Подписчики, которым нужно наблюдать это событие, должны передавать Last-Event-ID: 0 при первом GET /session/:id/events, чтобы воспроизвести события от самых старых доступных в кольце (это охватывает model_switch_failed во время запуска, даже если подписка происходит через несколько миллисекунд после ответа на создание).

GET /session/:id/status

Верните живую сводку из среды выполнения, владеющей сессией. Этот маршрут не загружает сессию, существующую только в сохраненном виде, и никогда не использует резервную основную среду выполнения. Предварительная проверка caps.features.session_status.

{ "sessionId": "<sid>", "workspaceCwd": "/canonical/path", "createdAt": "2026-09-10T08:00:00.000Z", "clientCount": 1, "hasActivePrompt": true, "isWaitingForPermission": false, "isWaitingForUserQuestion": false, "pendingInteractionCount": 0, "pendingInteractions": [] }

Ответ имеет формат передачи данных DaemonSessionSummary. Необязательные поля включают отображаемые и метаданные источника, activeWorkState, updatedAt, turnError, метаданные worktree или ветки, а также привязки PR. Значение 404 означает отсутствие активного владельца; владелец в состоянии загрузки, завершения работы или недоступности возвращает 503 вместо использования резервного варианта. Недоверенный неосновной владелец возвращает 403 untrusted_workspace, а id, активный в нескольких рабочих пространствах, возвращает 500 ambiguous_session_owner. Метод TypeScript SDK называется sessionStatus().

ACP session/new ID, предоставленный вызывающим кодом

Клиенты ACP запрашивают такое же поведение через поле метаданных расширения:

{ "jsonrpc": "2.0", "id": 1, "method": "session/new", "params": { "cwd": "/absolute/path/to/workspace", "_meta": { "qwen-code/sessionId": "550E8400-E29B-41D4-A716-446655440000" } } }

Ответ содержит нормализованный ID в нижнем регистре. Основные и квалифицированные по рабочему пространству монтирования ACP разделяют допуск с REST, включая session/load и session/resume. Недопустимые ID используют ошибку ACP INVALID_PARAMS с data.httpStatus=400 и data.errorKind="invalid_session_id"; конфликты используют data.httpStatus=409; недоступные проверки владельца в реальном времени или сохраненного состояния используют data.httpStatus=503 и data.retryable=true.

Сессия, созданная через ACP, которая никогда не получает запрос, не оставляет сохраненного следа, и демон удаляет её, когда закрывается владеющее ею соединение с нулевым количеством подключенных сессий. После такого удаления тот же ID может быть создан снова — это жизненный цикл соединения, а не повторное использование ID: пока соединение (или любое подключение) активно, проверка допуска отклоняет дубликат.

POST /session/:id/load

liveReplayMode и compactedReplayMode принимают значения full (по умолчанию) или summary. Они проецируют liveJournal и compactedReplay соответственно, не изменяя сохраненную историю. Выбор страницы предшествует фильтрации, поэтому количество видимых элементов может быть меньше historyPageSize. Недопустимые значения возвращают 400 invalid_live_replay_mode или 400 invalid_compacted_replay_mode. Автономная загрузка принимает оба варианта, но сообщает о недопустимых значениях как 400 invalid_request.

Восстановите сохраненную сессию ACP по id и воспроизведите её историю через SSE. Путь id является авторитетным; любое поле sessionId в теле игнорируется. Предварительная проверка caps.features.session_load — более старые демоны возвращают 404 для этого маршрута.

Запрос:

{ "cwd": "/absolute/path/to/workspace" }
ПолеОбязательноПримечания
cwdнетТа же канонизация, что и для POST /session, с более строгим контрактом отсутствия для восстановления: явный cwd без записи в реестре (удалена или никогда не регистрировалась) возвращает 404 workspace_not_found — постоянное отсутствие, которое вызывающие стороны могут классифицировать как утраченное, — тогда как рабочее пространство, запись которого существует в обратимом состоянии (дренирование, переход, блокировка), все еще возвращает 400 workspace_mismatch. Пропустите, чтобы унаследовать /capabilities.workspaceCwd. Когда features содержит multi_workspace_sessions, вызывающие стороны могут передавать любой доверенный зарегистрированный workspaces[].cwd; недоверенные неосновные рабочие пространства возвращают 403 untrusted_workspace. mcpServers намеренно НЕ принимается здесь — глобальный MCP демона управляется настройками (совпадает с POST /session).

Ответ:

{ "sessionId": "persisted-1", "workspaceCwd": "/canonical/path", "attached": false, "state": { "models": { ... }, "modes": { ... }, "configOptions": [ ... ] } }

state отражает LoadSessionResponse из ACP — models представляет собой SessionModelState, modes — SessionModeState, configOptions — массив SessionConfigOption. Отсутствующие поля определяются агентом. Поздние подключающиеся стороны (пути attached: true ниже) получают ТОТ ЖЕ снимок state, который видел первоначальный вызов загрузки — демон кэширует его в записи; изменения во время выполнения (например, model_switched) доставляются через поток SSE, а не в ответах последующих подключений.

attached: true означает, что сессия уже была активной (либо из-за предыдущего вызова session/load/session/resume, либо потому, что объединенный параллельный вызов опередил этот).

Когда сохраненная сессия владеет рабочим деревом Part 4A, загрузка/возобновление проверяет его sidecar, каноническое включение и точный маркер checkout. Простой дочерний процесс перемещается перед ответом; активный дочерний процесс удостоверяется только если его сообщаемый cwd уже равен рабочему дереву, а активный дочерний процесс, чей сообщаемый cwd отсутствует или находится в другом месте, завершается ошибкой вместо перемещения под свой запрос — за исключением холодного восстановления, которое не могло отложить свой restore-prompt (suppressWorktreeContextRestore выключено — любое восстановление, эффективный источник которого не принадлежит Channel — или мост без API отсрочки): такая форма все еще возвращает worktree без worktreeState, без перемещения, и сессия сохраняется. Ответы с перемещением и удостоверением возвращают worktree плюс worktreeState: "persisted-v1". Типизированные классификации 409 на стороне восстановления (анонсируемые через session_worktree_reset_v1): worktree_session_superseded, когда sidecar сессии содержит ссылку supersededBy — классификация читает только эту ссылку, под блокировкой владения для каждого checkout и перед любым чтением маркера, так что передача, которая откатывается, пока загрузка ожидает, не может перенаправить вызывающую сторону на сессию, которую тот же демон удаляет; однако ссылка записывается перед переключением маркера, поэтому replacementSessionId в теле является перенаправлением для проверки, а не доказательством того, что владение перешло: после завершенной передачи вызывающие стороны должны загружать этот id и обновлять свою учетность, но в прерванном состоянии до коммита он называет замену, которая не является владельцем маркера, чье собственное восстановление возвращает worktree_reset_interrupted, и которую затем убирает повторный сброс — поэтому replacementSessionId не должен сохраняться как id сессии задачи, пока загрузка этого id не завершится успешно; worktree_reset_interrupted, когда ссылка supersedes восстановленной сессии и ссылка supersededBy указанной сессии совпадают, но маркер никогда не перемещался или отсутствует — предыдущий сброс упал посередине передачи, и повторный сброс является исправлением; и worktree_marker_missing, когда маркер checkout отсутствует без этой пары согласующихся ссылок — сброс задачи является исправлением, потому что ни один путь восстановления не воссоздает маркер, а повторная попытка восстановления возвращает ту же ошибку 409. Классификация прерывания проверяется первой, поэтому пара согласующихся ссылок никогда не появляется как worktree_marker_missing. Всякий раз, когда эффективный источник восстановления принадлежит Channel, маршрут подавляет наилучшие усилия агента по очистке для Part 4A или неквалифицированного состояния sidecar, поэтому устаревшее, чужое, неоднозначное или несогласованное по владению сохраненное состояние сохраняется и приводит к ошибке запроса. Сохраненные метаданные источника имеют приоритет; когда они отсутствуют, запрос загрузки/возобновления предоставляет эффективный источник. Структурно корректный legacy sidecar без поля Part 4A workspaceCwd сохраняет существующее восстановление агента на основе наилучших усилий, может быть очищен этим путем и может вернуть worktree без worktreeState; клиенты не должны делать вывод об изоляции из этих совместимых метаданных. За исключением этого явного случая обратной совместимости с legacy, только сессии, чей эффективный источник восстановления не принадлежит Channel, сохраняют существующую очистку на основе наилучших усилий перед проверкой маршрута. Отсутствие sidecar не возвращает удостоверение рабочего дерева; клиенты, учитывающие изоляцию, должны отвергать такой ответ, а не повторно привязывать сессию к общему рабочему пространству.

Воспроизведение истории через SSE. Пока loadSession выполняется на стороне агента, агент может отправлять уведомления session_update для сохраненных ходов или возвращать пакетные обновления воспроизведения в метаданных ответа. Демон внедряет эти события в ограниченное окно снимка воспроизведения сессии до возврата ответа маршрута. Для активных сессий POST /session/:id/load обещает только это ограниченное окно (compactedReplay, liveJournal, необязательный lifecycleReplay, lastEventId), а не полную транскрипцию. Когда бездействующая активная сессия обновляется из ограниченной сохраненной страницы, lifecycleReplay дополняет эту страницу до 1024 кадров session_update только с метаданными, содержащими _meta.executionLifecycle или _meta.toolLifecycle из окна в памяти. Дополнение может перекрывать сохраненные записи жизненного цикла; клиенты должны обрабатывать его идемпотентно и не должны делать вывод о том, что воспроизведенные начала все еще выполняются. lifecycleReplayIncomplete: true сообщает о недоступном, деградированном или усеченном покрытии в памяти; false не обещает полной транскрипции. Эти необязательные поля не являются отдельно сохраняемым хранилищем, и более старые демоны могут их опускать. Окно ограничено по байтам параметром --compacted-replay-max-bytes (по умолчанию 4 МиБ, максимум 256 МиБ); если более старые записи воспроизведения были удалены, compactedReplay[0] представляет собой маркер history_truncated без идентификатора. Выполняющийся liveJournal отдельно ограничен параметрами --max-journal-events (по умолчанию 10 000 записей воспроизведения) и --max-journal-bytes (по умолчанию 8 МиБ сериализованных исходных событий). Это базовые ограничения на сессию. Когда выполняющийся ход превышает их, демон сначала пытается адаптировать рост: он увеличивает лимиты этой сессии вдвое (до жесткого лимита на сессию в 256 МиБ, количество записей масштабируется пропорционально, ограничиваясь оставшимся запасом пула), пока предоставленный рост во всех активных сессиях помещается в один общесистемный пул роста размером 5% от эффективного бюджета памяти демона — значение --memory-budget-mb, если передано, ограничено разрешенной доступной памятью, иначе 50% автоматически определенной памяти — ограничено 1024 МБ. Учет ведется в масштабе всего демона — многопространственный демон запускает один мост на каждое рабочее пространство, и все они разделяют единый пул. Рост происходит по требованию и только в пределах, допускаемых пулом; закрепленный оператором --max-journal-events или --max-journal-bytes отключает его, как и хост, чей эффективный бюджет падает ниже минимума в 1024 МБ (insufficientMemory): пул равен 0, и адаптивный рост полностью отключен. Последовательные совместимые исходные события agent_message_chunk или agent_thought_chunk разделяют одну запись воспроизведения, до 256 исходных событий на запись, при этом границы инструментов, атрибуции, происхождения и дискретных сообщений остаются нетронутыми. Когда журнал все еще превышает свои (возможно, увеличенные) лимиты после роста, допускаемого пулом — включая случаи, когда запас не предоставляется или предоставление покрывает только часть превышения — самые старые записи удаляются целиком (так что сохраненный хвост может быть значительно меньше байтового лимита), и в начало добавляется маркер history_truncated с scope: 'live_journal'; его поля truncatedEvents и retainedEvents подсчитывают исходные события, а не записи воспроизведения, а его поля maxBytes / maxEvents отражают действующие лимиты (которые могли уже вырасти). Клиенты должны отображать этот маркер как статус и продолжать применять сохраненные события. Полный доступ к сохраненной транскрипции предоставляется отдельно через GET /session/:id/transcript.

Лимиты байтов окна воспроизведения применяются после того, как дочерний процесс реконструировал сохраненную транскрипцию; они не ограничивают чтение JSONL с диска. Восстановление, превышающее бюджет демона, возвращает 504 с Retry-After, производным от бюджета восстановления (ограниченным 5–120 с), и {code: "session_restore_timeout", errorKind: "restore_timeout", retryable: true, sessionId, action, timeoutMs}. Демон изолирует все еще выполняющийся запрос ACP и очищает любую позднюю сессию вместо ее регистрации. Повторная попытка для того же id возвращает 409 restore_in_progress с reason: "awaiting_abandoned_cleanup" и Retry-After, равным бюджету восстановления (ограниченным 5–120 с), пока эта очистка не завершится. Если поздняя очистка восстановления неопределена или заброшенное восстановление не завершилось за полный бюджет восстановления после истечения срока, новые сессии в этом рабочем пространстве возвращают 503 acp_channel_unavailable с reason: "restore_cleanup_failed" или "restore_settlement_overdue". Инициализация сессии с тайм-аутом следует той же политике отказа при закрытии для более старого дочернего процесса ACP, который завершается поздно: неопределенная очистка возвращает reason: "new_session_cleanup_failed", тогда как запрос, остающийся незавершенным еще на один бюджет инициализации, возвращает reason: "new_session_settlement_overdue". Состояние просрочки урегулирования сбрасывается сразу после завершения поздней ошибки или после того, как позднее успешное выполнение завершит очистку точного ID; неопределенная очистка переходит в соответствующее состояние сбоя очистки вместо этого. Состояния сбоя очистки сохраняются до тех пор, пока канал рабочего пространства не будет освобожден и переработан. Уже активные сессии остаются пригодными для использования в любом случае.

Ошибки:

  • 404 — сохраненный id сессии не существует (SessionNotFoundError).
  • 404 — workspace_not_found, когда явный cwd вообще не имеет записи в реестре (удалена или никогда не регистрировалась): ПОСТОЯННОЕ отсутствие, отличное от обратимых состояний ниже. Тело: { error, code: "workspace_not_found", boundWorkspace, requestedWorkspace } — без workspaceCount. Клиент, читавший старое соглашение «404 означает, что сессия исчезла», должен рассматривать этот код как ошибку маршрутизации, которую можно обойти (например, повторно зарегистрировав рабочее пространство), а не как доказательство удаления сессии.
  • 400 — workspace_mismatch (та же форма, что и POST /session), включая обратимые состояния жизненного цикла: рабочее пространство, запись которого существует в реестре, но находится в процессе дренажа, перехода или заблокировано. Повторите попытку, когда рабочее пространство стабилизируется; не классифицируйте его как исчезнувшее.
  • 403 — untrusted_workspace, когда cwd нацелено на недоверенное неосновное рабочее пространство.
  • 503 — session_limit_exceeded (учитывается против --max-sessions; выполняющиеся восстановления также учитываются).
  • 504 — session_restore_timeout; повторная попытка возможна, с Retry-After, производным от бюджета восстановления (ограниченным 5–120 с), так как тот же id сессии остается изолированным до завершения поздней очистки.
  • 504 — init_timeout; НЕ подлежит повторной попытке, без Retry-After, без sideEffectPossible, изоляция не установлена. Выдается, когда инициализация канала завершается по тайм-ауту до отправки запроса на восстановление (этап ensureChannel); восстановление никогда не предпринималось, поэтому ни один id сессии не изолирован и очистка не ожидается.
  • 503 — acp_channel_unavailable, когда канал рабочего пространства закрыт для новой работы сессий. reason объясняет причину: restore_cleanup_failed, если брошенное восстановление не удалось однозначно очистить; restore_settlement_overdue, если брошенное восстановление так и не завершилось в течение одного полного бюджета восстановления после дедлайна; new_session_cleanup_failed, если сессию, созданную после истечения таймаута публичной инициализации, не удалось однозначно закрыть; или new_session_settlement_overdue, если прерванная по таймауту инициализация так и не завершилась в течение еще одного бюджета инициализации после дедлайна. Существующие сессии остаются доступными. В среде выполнения рабочего пространства с более чем одним движком исполнения любой из этих карантинных режимов изолирует канал этого движка вместо общего канала: его сессии также не принимают новую работу (запросы, продолжения, побочные запросы, команды оболочки, сообщения во время хода, а также запуск Goal или workflow возвращают тот же 503), завершенные сессии закрываются, и новые сессии этого движка остаются запрещенными до тех пор, пока дерево процессов канала не исчезнет, при этом возвращается channel_exit_unverified, пока это нельзя подтвердить. Там workspace_change_unacknowledged означает, что канал не подтвердил изменение рабочего пространства, влияющее на сессию (правила разрешений, перезагрузка настроек, шлюз Session Workflow, провайдеры моделей или Skills); позднее подтверждение каждого отсутствующего изменения досрочно снимает этот карантин. Состояние просрочки завершения расчетов очищается после того, как поздний сбой завершится или поздний успех выполнит очистку по точному ID; неоднозначная очистка переходит в соответствующее состояние сбоя очистки, которое требует опустошения и переработки канала рабочего пространства. Тело содержит retryAfterSeconds, а заголовок — соответствующий выведенный из бюджета Retry-After, чтобы клиенты использовали отложенную повторную попытку масштаба операционного бюджета вместо опроса с обычным интервалом 5 секунд.
  • 409 — restore_in_progress (для того же id уже выполняется операция session/resume, или новый запуск предоставил id, принадлежащий восстановлению). Возвращается Retry-After: 5, пока восстановление активно; подсказка, выведенная из бюджета, как только оно будет ограничено состоянием awaiting_abandoned_cleanup. Гонки одинаковых действий (два одновременных session/load для одного и того же id) объединяются — ровно один возвращает attached: false, остальные возвращают attached: true с тем же state.
  • 409 — session_workspace_conflict, когда тот же id сессии уже активен или восстанавливается другой средой выполнения рабочего пространства.
  • 409 — session_archived, когда id существует только под chats/archive/; вызовите POST /sessions/unarchive перед load или resume.
  • 409 — session_archiving, когда архивирование или разархивирование выполняется для того же id. Retry-After: 5.
  • 409 — session_conflict, когда id существует и в chats/, и в chats/archive/; удалите сессию с помощью POST /sessions/delete перед загрузкой.

GET /session/:id/transcript

Возвращает одну страницу кадров воспроизведения session_update без id, реконструированных из активного сохраненного транскрипта JSONL. Предварительная проверка caps.features.session_transcript — старые демоны возвращают 404 для этого маршрута.

Параметры запроса:

ПолеОбязательноПримечания
cursorнетНепрозрачный курсор base64url, возвращенный предыдущей страницей. Пропустите для первой страницы. Курсор выпускается демоном и защищен от подделки; его модификация возвращает 400 invalid_transcript_cursor. Он привязан к идентичности файла транскрипта и замороженному размеру байтов первой страницы; удаление, усечение, замена или архивирование файла аннулируют его и возвращают 409.
limitнетЦелевое количество активных записей ChatRecord на странице. По умолчанию 100, максимум 500. Обратная страница может расшириться до максимум 3 * limit записей, чтобы сохранить границы ходов и вызовов инструментов/результатов. Одна запись может породить несколько кадров воспроизведения, поэтому events.length может быть еще больше. Неверные значения возвращают 400 invalid_transcript_limit.
directionнетЕдинственное принимаемое значение — backward, которое начинает страницу с самых новых записей. Пропустите параметр для листания вперед на первой странице или для продолжения направления, замороженного в cursor. Нельзя комбинировать с cursor, beforeRecordId, atRecordId или snapshot.
beforeRecordIdнетНачать обратную страницу перед этим id записи. Подразумевает обратное направление, поэтому не отправляйте также direction. Нельзя комбинировать с cursor или atRecordId; может сочетаться с snapshot.
compactedReplayModeнетfull (по умолчанию) или summary. Проецирует events после выбора страницы, поэтому видимое количество может быть меньше limit. Не изменяет сохраненную историю или выбор курсора. Также принимается маршрутом транскрипта с указанием рабочего пространства. Неверные значения возвращают 400 invalid_compacted_replay_mode.

Ответ:

{ "v": 1, "sessionId": "persisted-1", "events": [ { "v": 1, "type": "session_update", "data": { "sessionUpdate": "user_message_chunk", "content": { "type": "text", "text": "..." } } } ], "nextCursor": "opaque", "hasMore": true, "startTime": "2026-07-08T00:00:00.000Z", "lastUpdated": "2026-07-08T00:01:00.000Z" }

events содержат только кадры воспроизведения: { v: 1, type: "session_update", data: SessionUpdate }. Они не несут идентификаторов EventBus, и ответ никогда не включает lastEventId. Вызов этого маршрута не вызывает /load, не подключает клиента, не заполняет живой EventBus, не создает живую сессию и не изменяет текущее окно живого воспроизведения. Живые и неактивные активные сессии обе реконструируются методом статуса только для чтения на стороне дочернего процесса, так что воспроизведение использует те же настройки рабочего пространства, выходной каталог среды выполнения, эмиттеры и семантику истории /load без изменения состояния сессии демона.

Первая страница замораживает текущий размер снимка JSONL. Последующие страницы читают только этот префикс байтов, поэтому добавления после страницы 1 не изменяют набор результатов. Если файл исчезает, усекается ниже замороженного размера, заменяется другим inode или перемещается в архив, следующая страница возвращает 409, и клиент должен начать заново со страницы 1 или попросить пользователя повторно открыть транскрипт.

Для защиты памяти демона и задержки снимки выше предела индексирования транскриптов терпят неудачу до того, как демон сканирует JSONL. Клиенты получают 413 transcript_too_large и должны перейти на экспорт/обработку в автономном режиме или попросить пользователя сократить/архивировать старую историю.

partial: true и replayError могут появиться, если конвертация воспроизведения терпит неудачу после производства некоторых кадров. Частичные ответы никогда не включают nextCursor, поэтому клиенты не могут незаметно листать страницы за пределы незаконвертированных записей.

Ошибки:

  • 400 — неверная форма limit, cursor, direction, beforeRecordId или id сессии. Параметры листания взаимоисключающие: cursor нельзя комбинировать с beforeRecordId, atRecordId или snapshot; snapshot требует atRecordId или beforeRecordId; atRecordId требует snapshot; и backward нельзя комбинировать с курсором или любым якорем записи.
  • 404 — активный сохраненный id сессии не существует при запросе первой страницы.
  • 409 — session_archived, session_archiving или session_conflict из тех же проверок загружаемости, что и /load.
  • 409 — снимок транскрипта недоступен, потому что файл был удален, усечен, заменен или архивирован после выпуска курсора; это также применяется, когда предварительная проверка больше не может найти активный файл для запроса с курсором.
  • 413 — transcript_too_large, когда замороженный снимок транскрипта превышает предел индексирования демона.
  • 413 — transcript_page_too_large, когда одна агрегированная запись превышает бюджет страницы с указанием рабочего пространства или сериализованная страница превышает свой бюджет ответа.

GET /session/:id/export

Скачайте транскрипт активной сохраненной сессии основного рабочего пространства. Необязательный запрос format может принимать значения html (по умолчанию), md, json или jsonl. Предварительная проверка caps.features.session_export.

Успешные ответы представляют собой вложения с очищенным именем файла, Cache-Control: no-store и X-Content-Type-Options: nosniff. Тип контента соответствует выбранному формату и равен text/html, text/markdown, application/json или application/jsonl. Маршрут читает только сохраненное хранилище: он не определяет активного владельца, не запускает ACP и не подключает клиента. Используйте указанный ниже маршрут с указанием рабочего пространства, если цель может находиться в неосновном рабочем пространстве. Недопустимые форматы возвращают 400 invalid_export_format; отсутствующие активные сессии возвращают 404; архивированные, переходные или конфликтующие хранилища возвращают 409. Метод TypeScript SDK называется exportSession().

GET /workspaces/:workspace/session/:id/transcript

Возвращает ту же проекцию DaemonSessionTranscriptPage, что и одиночный маршрут, из активной сохраненной JSONL выбранного зарегистрированного рабочего пространства. Предварительная проверка workspace_persisted_transcript; эта возможность независима от multi_workspace_sessions и работает для доверенного основного рабочего пространства, выбранного по id или cwd.

Параметры селектора и запроса следуют существующим правилам для множественных рабочих пространств и транскрипций. Чтение разрешено для доверенных основных и вторичных сред выполнения, а также для недоверенных вторичных сред выполнения. Недоверенное основное рабочее пространство возвращает 403 untrusted_workspace. Архивированное содержимое не возвращается.

Для этого маршрута с привязкой к рабочему пространству limit представляет максимальное количество записей для прямых страниц. Обратная страница может расширяться максимум до 3 * limit записей, чтобы диалоги и пары вызовов/результатов инструментов оставались целостными. Страница может завершиться раньше при достижении лимита источника в 4 МиБ и вернуть курсор продолжения. Сериализованные ответы ограничены 32 МиБ, а курсоры — 64 КиБ. Если состояние воспроизведения превысит лимит курсора, страница вернет успешно преобразованные события с partial: true, hasMore: false и без nextCursor.

Прямые страницы и страницы с курсором реализованы полностью внутри процесса демона. Первая обратная страница для живой сессии сначала проходит барьер сброса моста рабочего пространства на одну запись, чтобы актуализировать конец сохраненных данных; этот барьер может запустить ACP и загрузить настройки рабочего пространства. Само чтение сохраненной страницы не анализирует агентов или навыки, определенные проектом, и не создает/восстанавливает session-transcript-cursor-key. Кадры инструментов используют сохраненные имена и описания инструментов без обращения к реестру инструментов среды выполнения. Его ключ HMAC для курсора существует только в памяти демона, изолирован для каждого рабочего пространства и ротируется при перезапуске; курсор из предыдущего процесса демона возвращает 400 invalid_transcript_cursor.

GET /workspaces/:workspace/session/:id/export

Экспортируйте активную сохраненную сессию выбранного зарегистрированного рабочего пространства как вложение. Предварительная проверка workspace_session_export; не предполагайте поддержку на основе session_export или workspace_qualified_rest_core. Селектор сначала пытается разрешить точный id рабочего пространства, затем абсолютный URL-кодированный cwd после канонизации. И основные, и вторичные среды выполнения должны быть доверенными. Недоверенная среда выполнения возвращает 403 untrusted_workspace перед проверкой сессии или формата.

Необязательный запрос format может быть равен html (по умолчанию), md, json или jsonl. Тело, MIME-тип, очистка имени файла, Cache-Control: no-store, X-Content-Type-Options: nosniff и параметр расположения вложения соответствуют GET /session/:id/export. Устаревший маршрут остается привязанным к основному хранилищу.

Множественный маршрут читает только активную сохраненную JSONL выбранного рабочего пространства под управлением существующего общего координатора архивирования. Он не сканирует другие хранилища рабочих пространств, не выполняет резервный возврат к основному, не определяет активного владельца, не вызывает мост рабочего пространства, не запускает ACP, не подключает клиента и не загружает настройки. Id сессии, существующий только в другом рабочем пространстве, возвращает 404 { code: "session_not_found" }; архивированные сессии возвращают 409 session_archived. Неверные форматы возвращают 400 invalid_export_format, а гонки в хранилище сохраняют существующие ошибки session_archiving и session_conflict.

GET /workspaces/:workspace/session/:id/archive/export

Экспортируйте архивированную сохраненную сессию выбранного зарегистрированного рабочего пространства как вложение. Предварительная проверка workspace_archived_session_export; поддержка не может быть выведена из возможностей экспорта активных сессий или множественного ядра. Разрешение селектора рабочего пространства и проверки доверия выполняются перед проверкой id сессии и формата.

Вызывающие стороны TypeScript SDK используют WorkspaceDaemonClient.exportArchivedSession(sessionId, options). Метод всегда использует нативный REST и возвращает существующую проекцию вложения DaemonSessionExportResult.

Необязательный запрос format, тело ответа, MIME-тип, очищенное имя файла, политика кэширования, заголовок безопасности и параметр расположения вложения идентичны экспорту активного рабочего пространства. Исходная архивированная JSONL ограничена 256 МиБ перед реконструкцией; больший файл возвращает 413 transcript_too_large с sessionId, snapshotSize и maxBytes. Экспорт активных сессий сохраняет существующее поведение по размеру.

Маршрут читает только chats/archive/<id>.jsonl в выбранном доверенном рабочем пространстве под арендой общего координатора архивирования. Он не проверяет активное содержимое для резервного возврата, не сканирует другое рабочее пространство, не определяет активного владельца, не вызывает мост, не запускает ACP, не подключает клиента и не загружает настройки. Id, существующий только среди активных сессий, возвращает 409 { code: "session_not_archived" }; отсутствующий id возвращает 404 { code: "session_not_found" }; одновременное наличие активных и архивированных файлов возвращает 409 session_conflict; а переход в архив возвращает 409 session_archiving с Retry-After: 5.

POST /session/:id/resume

Восстановите сохраненную сессию ACP по id БЕЗ воспроизведения истории через SSE. Контекст модели восстанавливается внутренне на стороне агента (через geminiClient.initialize, читающий config.getResumedSessionData); поток SSE остается чистым для клиентов, которые уже отобразили историю. Предварительная проверка caps.features.session_resume; unstable_session_resume остается устаревшим псевдонимом совместимости для старых клиентов.

Принимает те же поля cwd, approvalMode, sourceType и sourceId, что и /load. Поля воспроизведения только для загрузки historyPageSize, liveReplayMode и compactedReplayMode здесь не анализируются, не проверяются и не передаются дальше — они молча игнорируются, что соответствует опубликованной схеме запроса возобновления. Та же форма ответа — state отражает ResumeSessionResponse из ACP. Тот же формат ошибок, включая 409 restore_in_progress (который срабатывает, когда выполняется session/load; session/resume, конкурирующий с другим session/resume, объединяется).

Используйте /load, если у клиента нет отображенной истории (холодное переподключение, выбор → открытие). Используйте /resume, если у клиента уже есть диалоги на экране и ему нужно только получить дескриптор со стороны демона.

⚠️ Почему все еще рекламируется unstable_session_resume? HTTP-маршрут демона и возможность session_resume стабильны для v1, но мост по-прежнему вызывает connection.unstable_resumeSession ACP. Старый тег сохраняется только для того, чтобы SDK, выпущенные до session_resume, могли продолжать работать.

POST /session/:id/worktree-reset

Передача прав на checkout сохранённой сессии worktree из Части 4A новой замещающей сессии. Предварительная проверка session_worktree_reset_v1 — более старые демоны возвращают 404 для этого маршрута. Этот маршрут позволяет сбросить именованную задачу Канала /clear без удаления файлов worktree: замещающая сессия получает совершенно новый диалог, а вытесненная сессия сохраняет свою историю в каталоге, но больше никогда не может быть восстановлена.

Тело запроса (все поля необязательные):

{ "cwd": "/canonical/path", "modelServiceId": "service-id", "approvalMode": "plan", "sourceType": "channel", "sourceId": "dingtalk-main" }

Поле cwd следует тому же правилу разрешения, что и POST /session/:id/load, и может быть опущено для основного рабочего пространства демона. Поля метаданных помечают замещающую сессию теми же соглашениями об области потока и источнике, что и при создании worktree.

При успешном выполнении маршрут возвращает 200 с ответом в формате создания сессии для замещающей сессии — новый sessionId, workspaceCwd, currentCwd, равный проверенному каноническому пути worktree, метаданные worktree и worktreeState: "persisted-v1". Вызывающие стороны отбрасывают дескриптор старого id и используют id замещающей сессии; последующее восстановление по старому id возвращает worktree_session_superseded с replacementSessionId, что клиенты также могут использовать для самостоятельного исправления устаревших записей — но только после успешной загрузки этого id. Классификация определяется исключительно по ссылке сайдкара, а эта ссылка записывается до переключения маркера, поэтому прерванная передача перед коммитом вернет замещающую сессию, которая не является владельцем маркера, чье собственное восстановление возвращает worktree_reset_interrupted, и которую последующий повторный сброс уничтожит.

Этот маршрут никогда не читает X-Qwen-Client-Id, поэтому замещающая сессия запускается без привязки — но успешный ответ не означает отсутствие регистрации. Новая передача выпускает owner-style clientId для этого запуска, регистрирует его на замещающей сессии и сообщает в теле ответа; идемпотентное возобновление завершенной передачи ничего не сообщает. Выпущенный id не является привязкой (количество привязок замещающей сессии не меняется, поэтому killSession с requireZeroAttaches по-прежнему видит её как непривязанную), но активная регистрация клиента именно то, что предотвращает очистку простоя демона: отвяжите id, который вы больше не используете, иначе замещающая сессия останется активной неопределенно долго — а замещающая сессия, которая все еще активна без маркера, представляет собой состояние, которое последующий сброс откажется демонтировать. Относитесь к ответу как к идентичности замещающей сессии и привяжите её через обычные интерфейсы POST /session/:id/load или /resume, когда требуется зарегистрированный клиент.

Передача сериализуется по checkout относительно восстановления worktree и других сбросов, отказывается выполняться, пока любая из сессий занята (выполняется промпт или ожидает взаимодействия), и устанавливает барьер допуска на вытесненной сессии: пока он активен, девять типов писателей получают отказ worktree_reset_active на этапе допуска — POST /session/:id/prompt, /rewind, /cd, /branch, /fork, /shell, /goal, /tasks/:taskId/workflow-action и /messages/delete. Остальные восемь изолированы, потому что каждый из них перемещает рабочий каталог сессии или начинает работу в нем без прохождения допуска промпта: командная оболочка выполняется в эффективном рабочем каталоге сессии, который для перемещенной сессии worktree является самим checkout; агент ветвления выполняет свои инструменты в этом каталоге; откат восстанавливает файлы относительно него; cd перемещает его, включая в подкаталог checkout; ветвление изменяет сохраненную историю вытесненной сессии и порождает производную сессию, пока права собственности находятся в переходном состоянии; цель resume продвигает очередь хода и ставит в очередь продолжение; удаление сообщений переписывает сохраненную историю; и действие workflow запускает сохраненный workflow или скрипт, предоставленный вызывающей стороной, или перезапускает активное выполнение через собственный реестр инструментов сессии. POST /session/:id/continue не является десятым барьером: он направляет принятое продолжение через изолированный допуск промпта и получает отказ там. Этот список составляет весь барьер, и барьер — это не всё, что достигает дочернего процесса — пути освобождения и остановки остаются доступными во время передачи по замыслу (отмена задачи, очистка цели, отвязка клиента, завершение сессии), и внутренняя постановка фонового уведомления демоном — подтверждение завершения подсессии родителю, к которому не обращается ни один HTTP-маршрут — также не изолирована, поскольку она ставит в очередь уведомление, а не начинает ход. Последний шаг передачи разрывает регистрации клиентов вытесненной сессии, очищает её ассоциацию с worktree в памяти и сообщает, действительно ли вытесненная сессия удалена. Выживший процесс — дочерний процесс, который все еще выполняет фоновую работу, поэтому закрытие по простою, инициированное последней отвязкой, отклоняется и откладывается — отображается явно, а не маскируется: демон записывает это в лог, барьер остается активным на этой записи как единственный оставшийся барьер, а тело 200 содержит supersededSessionLive: true, чтобы вызывающая сторона знала, что старый id все еще активен и может быть повторно привязан внутри checkout, права на который только что перешли. Безопасность при сбое зависит от окна, и порядок записи определяет эти окна: сначала записывается ссылка supersededBy старого сайдкара, затем сайдкар замещающей сессии со ссылкой supersedes, и в последнюю очередь переключается маркер. Сбой до переключения оставляет старую сессию авторитетной в одной из трех форм, которые различает повторная попытка — отсутствие ссылок вообще (замещающая сессия является обычным сиротой, и повторная попытка просто начинает новую передачу), только обратная ссылка при отсутствии сайдкара замещающей сессии (повторная попытка завершается ошибкой с кодом worktree_reset_invalid_state и оставляет прерванное состояние нетронутым для ручного исправления оператором) или обе ссылки присутствуют (повторная попытка откатывает частичную передачу и завершает новую — если только прерванную замещающую сессию нельзя удалить, потому что к ней все еще подключен клиент, в этом случае откат завершается ошибкой с кодом worktree_reset_invalid_state и сохраняет обе ссылки, так что последующая повторная попытка сходится после исчезновения этого клиента). Сбой после переключения оставляет замещающую сессию авторитетной, и повторная попытка возобновляется как пустая операция. Старая сессия никогда не удаляется, а checkout worktree никогда не удаляется при сбросе.

Ошибки:

  • 404 — session_not_found, если запись сессии не существует.
  • 409 — worktree_reset_unsupported, если у сессии нет корректного сайдкара Части 4A (это не сессия worktree).
  • 409 — worktree_reset_active, если сессия, участвующая в передаче, занята; повторите попытку, когда она освободится. Допуск любого писателя, покрытого барьером — промпта, отката, изменения рабочего каталога, ветвления, форка, командной оболочки, управления целью или действия задачи workflow — на сессии во время передачи завершается ошибкой с тем же кодом.
  • 409 — worktree_reset_invalid_state при повреждении sidecar-файла, недействительном маркере, ошибке проверки вложенности или несогласованной паре ссылок на замену. Не разрушает то, что было начато этим запросом: любая частичная передача, начатая этим запросом, откатывается до того, как её увидит вызывающий код, тогда как ветки возобновления с защитой от ошибок (fail-closed) — недействительный маркер, пара ссылок на замену, которая не согласуется (включая обратную ссылку, у которой отсутствует заменяющий sidecar), неоднозначный владелец маркера и замена, которая всё ещё активна на этом демоне при отсутствии маркера — оставляют существующее прерванное состояние нетронутым для ремонта оператором, чтобы эта форма оставалась видимой для последующего сброса или восстановления. Повторная попытка против одного из этих состояний повторно считывает его и возвращает ту же ошибку 409: она не сходится, поэтому состояние требует ремонта оператором, а не цикла повторных попыток. Конкретная причина записывается в лог демона, а не передается по сети.
  • 500 — внутренние ошибки с защитой от ошибок (fail-closed) (например, мост без поддержки сброса или отказ перемещения после отката); не подлежат повторному выполнению в текущем виде. Одно 500 отличается по существу: передача маркера, собственный хвостовой процесс после фиксации которой завершился неудачей после надежной фиксации переключения, сообщает о переходе права собственности, никогда не выполняет разрушительный откат и исправляется повторным выполнением сброса, который идемпотентно возобновляет зафиксированную передачу.

GET /workspace/:id/session-info and GET /workspaces/:workspace/session-info

Возвращает агрегированные количества сохраненных сессий для выбранного рабочего пространства без изменения пути к списку сессий с пагинацией:

{ "active": 450, "archived": 30, "total": 480, "live": 2, "expensive": true, "cost": "disk_scan" }

active, archived и total подсчитывают локальные JSONL-сессии, видимые пользователю; внутренние хост-сессии демона и сессии задач агента рабочего пространства (sourceType agent-host и agent) исключены из этих подсчетов и из любого списка сессий; пользователь все равно может открыть сессию агента по id. live — это соответствующее количество мостов в памяти, видимых пользователю, и оно опускается для зарегистрированного недоверенного вторичного рабочего пространства, поскольку это чтение только из хранилища не должно запрашивать живое состояние. expensive всегда равно true, а cost всегда равно "disk_scan"; клиенты должны вызывать эту конечную точку редко, а не опрашивать её. Если сканирование достигает своего предела безопасности или не может классифицировать каждый кандидатский файл, ответ добавляет "truncated": true, а количества сохраненных данных являются нижними границами. Отсутствие хранилища возвращает нулевые количества сохраненных данных. Множественный маршрут использует тот же селектор рабочего пространства и политику доверия, что и множественный каталог сессий; недоверенное первичное рабочее пространство все равно возвращает 403 untrusted_workspace.

TypeScript SDK демона предоставляет множественный маршрут через workspaceById(...) или workspaceByCwd(...), за которым следует getWorkspaceSessionInfo().

Библиотека артефактов сессий рабочего пространства

workspace_artifact_library дополнительно поддерживает запросы в масштабе рабочего пространства и пакетные операции:

МетодМаршрутВходные данные
GET/workspaces/:workspace/artifact-libraryВсе активные, выгруженные и архивированные сессии в рабочем пространстве
POST/workspaces/:workspace/artifact-library/query{ "sessionIds": ["session-id"] }
POST/workspaces/:workspace/artifact-library/delete{ "artifacts": [{ "sessionId": "session-id", "artifactId": "artifact-id" }] }

Оба тела POST представляют собой строгие объекты, содержащие от 1 до 100 записей. Дублирующиеся идентификаторы или пары обрабатываются один раз. Ответы на запросы имеют вид { "v": 1, "sessions": [<session artifact envelope>], "errors": [<member error>] }. Каждая ошибка участника содержит sessionId, status, code и message. Полное обнаружение пропускает отсутствующие/чужие сессии; явный запрос сообщает о них. Другие сбои участников остаются явными, при этом успешные сессии возвращаются.

При наличии workspace_artifact_library_stream каждая оболочка сессии библиотеки артефактов (включая существующий GET одной сессии) дополнительно содержит:

{ "session": { "displayName": "Task title", "createdAt": "2026-10-26T00:00:00.000Z", "archiveState": "archived", "sourceType": "scheduled_task", "sourceId": "task-id" } }

displayName и sourceId необязательны. createdAt — это исходное время создания сессии, сохраняемое даже после возобновления; это не временная метка артефакта. archiveState может быть active или archived. Типы источников являются открытыми строками, а отсутствие атрибуции становится default. Такие источники, как resource_inspection, сохраняются, чтобы клиенты могли явно исключить их. Идентификатор источника связан с его исходным типом источника. Названия следуют текущему живому заголовку или сохраненному пользовательскому заголовку/первому запросу. Общий ответ /session/:id/artifacts остается неизменным.

Полный GET и выбранный POST также принимают Accept: text/event-stream. Без этого согласования они возвращают JSON-ответ выше. Для POST используйте потоковую передачу на основе fetch; проверяйте возможность и тип содержимого ответа на более старых демонах. Конечная последовательность SSE выглядит так:

СобытиеДанные
start{ "v": 1, "total": <candidate sessions> }
sessionОдин полный конверт-структура библиотеки артефактов, включая session
session_error{ "v": 1, "sessionId": "...", "status": 404, "code": "...", "message": "..." }
complete{ "v": 1, "total": 10, "succeeded": 8, "failed": 1, "skipped": 1 }
errorФатальная { "v": 1, "status": 503, "code": "...", "message": "..." }

Результаты поступают в порядке завершения с одновременным чтением до четырех сессий; медленная сессия не блокирует других работников. JSON сохраняет порядок выбора. Отсутствующие/чужие сессии при полном запросе считаются skipped; сбои при явном запросе выдают session_error. total подсчитывает кандидатов, а не артефакты. Только complete отмечает завершенное сканирование. Фатальная error или отключение без complete означает частичные результаты; курсор воспроизведения, автоматическое переподключение или подписка отсутствуют. Во время ожидания могут возникать комментарии heartbeat. Начальные сбои используют обычные коды статуса HTTP; после заголовков SSE фатальные ошибки становятся событиями потока. Отключенный запрос прекращает диспетчеризацию новых чтений; уже выполняемые загрузки общего кэша могут завершиться. Обратное давление ограничивает ожидающие кадры результатов и не удерживает барьеры архива во время ожидания клиента.

Пакетное удаление возвращает { "v": 1, "results": [...] }. Каждый результат содержит sessionId и artifactId, плюс либо существующую оболочку мутации (v, changes, необязательные warnings/warningDetails), либо error: { status, code, message }. Отсутствующие артефакты приводят к идемпотентным пустым изменениям. Клиенты должны проверять каждый результат и предупреждение о сохранении; пакетный ответ 200 не означает, что все участники выполнены успешно. Удаление группируется по сессиям, не является транзакционным между сессиями, сохраняет фактические файлы и требует тех же строгих прав оператора, что и удаление одного артефакта. Неверный ввод возвращает 400 invalid_artifact_library_request; ошибки уровня рабочего пространства сохраняют обычные коды статуса HTTP.

Живые артефакты и заголовки поступают из моста; ограниченно сохраненные метаданные идентичности сохраняют исходное время создания и атрибуцию источника. Незагруженные метаданные, включая пустые результаты, кэшируются для каждой среды выполнения с бюджетом сериализованного размера 32 МиБ. Неизмененные файлы не воспроизводятся при повторных запросах или после истечения TTL. Идентичности метаданных транскрипта и владельца проверяются при каждом чтении; измененные записи перестраиваются. Обнаружение кандидатов кэшируется по идентичности директории и обновляется при первом запросе не менее чем через две секунды после завершенного сканирования, включая случаи, когда временные метки директории не меняются. Это обновление только по имени файла сохраняет неизмененные кэши артефактов. Одновременные холодные чтения разделяют загрузку; каждый запрос обрабатывает до четырех сессий одновременно. Допущенные неизмененные записи сохраняются; новые результаты, превышающие бюджет, возвращаются без кэширования, чтобы повторные полные сканирования не вытесняли весь кэш. Эти некэшированные сессии или перезапуск демона требуют реконструкции. Это позволяет избежать повторного анализа JSONL для кэшированных записей, но проверки файловой системы и размер ответа все еще масштабируются с рабочим пространством. Доступность холодных файлов сохраняет существующую семантику только для метаданных.

Существующий GET библиотеки артефактов одной сессии разделяет этот же кэш с полными и пакетными запросами, добавляя только сводку session к своему ответу и сохраняя существующие форматы ошибок. Прогрев одного маршрута выгоден остальным; одиночное и пакетное удаление аннулируют общую запись. Добавления транскриптов только для заголовков и перемещения архивов также обновляют метаданные; живые заголовки считываются при каждом запросе без ожидания сохранения на диск. Живые сохраненные метаданные и холодные оболочки разделяют тот же бюджет 32 МиБ, а переходы живое/холодное перестраивают запись. generatedAt кэшированной оболочки — это время создания ее проекции.

session_artifact_library объявляет эти эндпоинты, ограниченные сохраненным рабочим пространством:

МетодМаршрутРезультат
GET/workspaces/:workspace/session/:id/artifact-libraryКонверт артефактов сессии, включая сохраненные регистрации для выгруженных и архивированных сессий.
DELETE/workspaces/:workspace/session/:id/artifact-library/:artifactIdРезультат мутации артефакта. Уже отсутствующая регистрация возвращает changes: [].

Селектор рабочего пространства разрешается в доверенный рантайм. Оба маршрута отклоняют неоднозначных живых владельцев и живого владельца в другом рантайме перед доступом к артефактам. Они участвуют во внутреннем барьере активности рантайма; чтения удерживают общий барьер архива сессий, а удаления — эксклюзивно. Ни один маршрут не загружает агента и не разархивирует сессию. Удаление требует прав оператора демона через строгий гейт мутаций; оно не требует X-Qwen-Client-Id. Существующие эндпоинты артефактов, принадлежащие клиенту, сохраняют свои правила авторизации.

Холодные чтения воспроизводят сохраненные записи артефактов и возвращают метаданные и локаторы, такие как относительные к рабочему пространству workspacePath, managedId или url. Они сохраняют записанные временные метки и сообщают о предупреждениях совместимости для каждого артефакта, не помечая успешное сохранение как недоступное. limits.maxArtifacts описывает лимит регистрации живого хранилища; холодная проекция не вытесняет исторические регистрации для соблюдения этого лимита.

Холодное удаление добавляет долговременную явную надгробную метку (tombstone). Оно сохраняет файлы рабочего пространства и освобождает ссылки на снимки веб-превью, управляемые демоном и не имеющие ссылок, используя существующую очистку best-effort. Живое удаление может вернуть HTTP 200 с changes: [], warnings и warningDetails, если сохранение не удалось; вызывающие стороны должны проверить результат и сохранить регистрацию. Ошибки записи в холодном режиме возвращаются как ошибки.

Оба маршрута возвращают 409 artifact_library_unavailable, когда сохранение артефактов отключено. Неизвестные сессии возвращают 404 session_not_found; хранилище чужого проекта трактуется как не найденное, а неоднозначное или нерегулярное хранилище возвращает 409 session_storage_conflict. Неполные записи диалога или неавторитетный журнал мутаций возвращают 409 session_transcript_corrupt, отличающийся от session_transcript_changed параллельного писателя. Чтения свыше 64 МиБ возвращают 413. Холодная мутация управляемого выполнения возвращает 409 artifact_mutation_requires_managed_authority. Также применяются существующие ответы об ошибках владения рабочим пространством и жизненного цикла.

GET /workspace/:id/sessions and GET /workspaces/:workspace/sessions

Возвращает список сессий, чей канонический рабочий процесс соответствует :id или :workspace. Параметр пути сначала разрешается как точный идентификатор рабочего процесса, а затем как URL-кодированный абсолютный путь текущей рабочей директории (cwd). Основные рабочие процессы включают существующее объединение сохраненных и активных данных: по умолчанию возвращается список активных сессий из chats/; передайте archiveState=archived, чтобы получить список архивированных сессий из chats/archive/. Доверенные неосновные рабочие процессы включают активные сохраненные сессии из их собственного хранилища chats/ и объединяют соответствующие живые сводки без дубликатов; если активных сохраненных сессий нет, маршрут сохраняет предыдущее поведение курсора только для живых данных. Доверенные неосновные рабочие процессы также поддерживают archiveState=archived, организованный список view=organized и фильтры group, читая данные из своих собственных хранилищ chats/, chats/archive/ и организации сессий; комбинированный запрос view=organized&archiveState=archived возвращает только архивированные сессии без объединения с живыми данными. Зарегистрированные недоверенные неосновные рабочие процессы поддерживают те же форматы списков, фильтров и пагинации, но возвращают только сохраненные записи: демон не запрашивает живой мост и не заполняет ожидающие взаимодействия, ошибки хода или состояние клиента из среды выполнения. Сохраненные значения по умолчанию, такие как clientCount: 0 и hasActivePrompt: false, остаются присутствующими для совместимости протокола. При отсутствии хранилища возвращается пустой список. Маршрут во множественном числе по-прежнему возвращает 403 { code: "untrusted_workspace" } для недоверенного основного рабочего процесса; устаревшие маршруты для основного рабочего процесса сохраняют свое существующее поведение совместимости. archiveState=all не поддерживается в v1. Списки основных и сохраненных данных сохраняют существующую числовую семантику cursor; резервный вариант только для живых данных доверенного неосновного рабочего процесса сохраняет свой существующий непрозрачный живой курсор.

curl http://127.0.0.1:4170/workspace/$(jq -rn --arg c "$PWD" '$c|@uri')/sessions curl http://127.0.0.1:4170/workspace/$(jq -rn --arg c "$PWD" '$c|@uri')/sessions?archiveState=archived curl http://127.0.0.1:4170/workspaces/<workspace-id>/sessions

Когда workspace_qualified_rest_core объявлен, пакетные операции с сессиями в рамках рабочего процесса, CRUD-операции с группами и мутации организации сессий доступны под /workspaces/:workspace/sessions/{delete,archive,unarchive}, /workspaces/:workspace/session-groups и /workspaces/:workspace/session/:id/organization. Для недоверенного вторичного рабочего процесса GET группы остается доступным; каждая мутация групп, сессий и организации остается ограниченной проверкой доверия. Маршруты пакетных операций и мутаций организации без указания рабочего процесса остаются доступными только для основного рабочего процесса для обеспечения совместимости.

Параметры запроса:

ПолеОбязательноПримечания
archiveStateнетactive (по умолчанию) или archived. Любое другое значение возвращает 400 { code: "invalid_archive_state" }.
cursorнетКурсор пагинации из предыдущего ответа.
sizeнетРазмер страницы. Недействительные значения возвращают 400 { code: "invalid_cursor" } или существующую ошибку проверки размера страницы.
viewнетОпустите для устаревшего списка последних элементов. organized включает серверную сортировку закрепленных/групповых элементов и добавляет необязательные поля организации. Любое другое значение возвращает 400 { code: "invalid_session_view" }.
groupнетИмеет смысл только вместе с view=organized. all (по умолчанию), pinned, ungrouped или пользовательский идентификатор группы. Неизвестные идентификаторы групп возвращают 404 { code: "group_not_found" }.

Ответ:

{ "sessions": [ { "sessionId": "<uuid>", "workspaceCwd": "/canonical/path", "createdAt": "2026-05-17T08:30:00.000Z", "displayName": "My Session", "clientCount": 2, "hasActivePrompt": false, "isArchived": false } ], "nextCursor": 1772251200000 }

При наличии view=organized демон читает <Storage.getProjectDir(cwd)>/session-organization.v1.json, возвращает закрепленные сессии первыми, затем сортирует по времени активности по убыванию, а затем использует sessionId для стабильной привязки при равенстве. Организованный курсор представляет собой непрозрачный JSON в формате base64url и не должен повторно использоваться с устаревшим списком последних элементов. pinned — это виртуальный фильтр, а не группа. groupId: null означает «без группы». Архивированные сессии сохраняют свои метаданные организации, но archiveState=archived&view=organized по-прежнему возвращает только архивированные сессии.

Курсоры, отсортированные по активности — организованный вид и отфильтрованные списки parentSessionId / sourceType — не изолированы от снимков состояния, и доверенный активный список упорядочивает строки по более позднему значению между временем изменения транскрипта (mtime) и водяным знаком живой активности. Водяной знак живой активности хранится только в памяти, поэтому ключ сессии может регрессировать к своему mtime, когда живая запись завершается между двумя выборками страниц. Курсор компенсирует это: он содержит идентификаторы, уже выданные по ключу, полученному из живых данных — сохраняя их, пока строка отсутствует в коллекции страницы и пока изменение закрепления может снова их включить — и исключает их на остальную часть прохода, так что движение ключа, полученного из живых данных, возвращает сессию не более одного раза за проход. Гарантия ограничена переносом: она ограничена 64 идентификаторами (избыточные идентификаторы за один проход приводят к дубликату «не более одного раза», а не к ошибке), и сохраненная только строка, выданная до изменения ее состояния закрепления, никогда не переносится, поэтому снятие закрепления между выборками может вернуть эту строку второй раз точно так же, как это было до появления этого поля. Вызывающие стороны, накапливающие страницы, должны поэтому всегда использовать ключ sessionId для строк, а не только в случае превышения 64 элементов. Строки все еще могут перемещаться или пропускаться при одновременной активности, точно так же, как и раньше; вызывающая сторона, которой нужен согласованный вид, должна перезагружать данные с первой страницы после изменения активности.

Дополнительные поля могут появляться в каждой сессии, когда view=organized:

{ "isPinned": true, "pinnedAt": "2026-07-04T12:00:00.000Z", "groupId": "018f..." }

Доверенные активные списки включают поля наложения живого демона, такие как clientCount, hasActivePrompt и activeWorkState. Списки недоверенных вторичных рабочих процессов и архивированные списки используют только хранилище: поля наложения живых данных отсутствуют или имеют значение false, а архивированные записи устанавливают isArchived в значение true. Пустой массив (а не 404), когда сессии отсутствуют — интерфейс выбора сессии не должен выдавать ошибку только потому, что рабочий процесс простаивает.

POST /sessions/catalog

Читайте независимо пагинируемые каталоги сессий для нескольких зарегистрированных рабочих процессов одним запросом. Выполните предварительную проверку session_catalog_batch один раз; старые демоны сохраняют маршруты списков сессий и групп с указанием рабочего процесса. Каждый элемент пакета имеет владельца сохраненного рабочего процесса: аутентификация и прием запроса являются глобальными для процесса, тогда как каждый элемент разрешается и читается только в своем рабочем процессе-владельце. Он никогда не регистрирует рабочий процесс, не создает сессию, не запускает ACP и не выполняет откат к основному рантайму.

{ "workspaces": [ { "workspace": "workspace-a", "cursor": "previous-a-cursor" }, { "workspace": "/canonical/workspace-b" } ], "options": { "size": 20, "archiveState": "active", "view": "organized", "group": "all" }, "includeGroups": true }

workspaces — это упорядоченный массив из 1–20 записей { workspace, cursor? }, или "all" для выбора всех публичных зарегистрированных рабочих процессов. Селекторы разрешаются как точные идентификаторы перед каноническими абсолютными путями. all исключает внутренние «Conversations» и удаленные записи, включает временно недоступные записи и завершается ошибкой, если будет выбрано более 20 записей; вызывающие стороны должны затем разделить явные выборы. Пустой публичный реестр возвращает пустой результат. Явные селекторы внутренних элементов возвращают ошибку записи без запуска внутреннего рантайма.

Общий options принимает size (целое число от 1 до 100, по умолчанию 20), archiveState, view, group, parentSessionId, sourceType и sourceId с существующими фильтрами списков и комбинациями. group и parentSessionId ограничены 256 символами. includeGroups по умолчанию равно false. У каждого рабочего процесса есть свой собственный курсор. Страницы по умолчанию для пакетных операций используют сортировку по активности и непрозрачные курсоры, включая для сессий только с живыми данными; эти курсоры не взаимозаменяемы с устаревшими числовыми курсорами списков. Продолжайте с помощью возвращенных для этой записи cwd и nextCursor, сохраняя фильтры. Глобальная сортировка или глобальный курсор пагинации отсутствуют.

{ "workspaces": [ { "workspace": "workspace-a", "workspaceId": "workspace-a", "cwd": "/canonical/workspace-a", "sessions": [], "groups": { "groups": [], "colorOptions": ["red", "orange", "yellow", "green", "blue", "purple"] } }, { "workspace": "/canonical/workspace-b", "error": { "code": "workspace_not_found", "message": "Workspace is not registered with this daemon.", "status": 404 } } ] }

Результат сохраняет порядок выбора и исходный селектор как workspace. Успешные записи включают канонические cwd, workspaceId и существующие поля страницы (sessions, необязательные nextCursor, truncated и liveMergeFailed). Поле workspaceCwd каждой возвращенной сессии нормализуется до канонического значения cwd соответствующей записи. При запросе groups содержит полный каталог групп; идентификаторы групп ограничены областью действия их рабочего пространства. Чтение групп сохраняет существующую нормализацию и резервный вариант «предупреждение и пустой результат» для поврежденных или нечитаемых хранилищ организаций. Неудачные записи содержат error: { code, message, status } и разрешенный идентификатор, если он доступен, без успешного пустого списка sessions. Действительные пакеты возвращают HTTP 200, даже если некоторые записи завершаются ошибкой. Некорректные конверты запросов возвращают HTTP 400 перед чтением из хранилища.

Неизвестные, внутренние и удаленные рабочие пространства приводят к 404 workspace_not_found; недоступные или измененные поколения среды выполнения приводят к 503 workspace_runtime_unavailable; недоверенное основное рабочее пространство приводит к 403 untrusted_workspace. Недоверенные вторичные рабочие пространства сохраняют существующую политику каталога только для сохраненных данных без чтения живого моста, записи исправлений или ведения журнала отладочных сессий. Исключения при чтении курсора и групп сохраняют свои существующие коды; другие исключения при чтении преобразуются в 500 session_catalog_failed. Доверенные активные записи могут объединять существующее состояние живого моста; архивированные записи остаются только в хранилище.

Демон выполняет не более четырех чтений рабочих пространств одновременно на пакет, ограничивает селекторы 4096 символами, а курсоры — 16384 символами, и устанавливает лимит на сериализованную успешную запись в размере 512 КиБ. Слишком большая запись возвращает 413 catalog_response_too_large; уменьшите size или исключите группы перед повторной попыткой. Это ограничивает ответ пакета примерно 10 МиБ плюс накладные расходы на конверт/ошибки. Отключение клиента отменяет ожидающие чтения и предотвращает запуск дополнительных чтений рабочих пространств. Существующие ограничения демона на размер JSON-тела и лимиты сканирования сохраненных данных также применяются. Запросы каталога используют уровень ограничения частоты чтения.

POST /sessions/live-state

Читайте снимки состояния живых сессий для 1–20 явно выбранных зарегистрированных рабочих пространств в одном запросе. Строгое тело представляет собой { "workspaces": ["workspace-id", "/absolute/workspace/path"] }; селекторы разрешаются как точные идентификаторы рабочих пространств перед каноническими абсолютными путями. Ответ представляет собой { "workspaces": [...] } в порядке запроса. Каждая успешная запись включает свой исходный селектор workspace, канонические workspaceId и cwd, а также тот же снимок v: 1, catalogVersion и полный снимок sessions, что и в маршруте для одного рабочего пространства ниже. Снимки независимы; отсутствует атомарность между рабочими пространствами или объединенная лента сессий. Оба маршрута разделяют раскрытие версии каталога и инвалидацию кэша, поэтому чередование между ними сохраняет контракт согласования каталога для одного рабочего пространства. Действительный пакет возвращает HTTP 200 и Cache-Control: no-store, даже если некоторые записи завершаются ошибкой.

Каждая неудачная запись содержит workspace, необязательные разрешенные workspaceId и cwd, и error: { code, message, status } без успешного пустого списка sessions. Неизвестные, внутренние или удаленные селекторы возвращают 404 workspace_not_found; недоступные или замененные поколения среды выполнения возвращают 503 workspace_runtime_unavailable; недоверенные основные или вторичные рабочие пространства возвращают 403 untrusted_workspace; неожиданные сбои моста возвращают 500 session_live_state_failed. Успешная запись, превышающая 512 КиБ, возвращает 413 live_state_response_too_large. Некорректные конверты, лишние ключи, пустые массивы, более 20 селекторов и селекторы длиннее 4096 символов возвращают HTTP 400 invalid_session_live_state_batch_request перед любым чтением моста. Маршрут читает только память живого моста каждого выбранного активного рантайма, никогда не обращаясь к сохраненным каталогам или дочерним процессам ACP; чтение членов выполняется последовательно, а POST-запрос учитывается в уровне ограничения частоты чтения демона.

GET /workspaces/:workspace/sessions/live-state

Возвращает снимок живых сессий выбранного рабочего пространства, хранящийся только в памяти, вместе с версией каталога в памяти, чтобы клиенты могли прекратить опрос сохраненного каталога по адресу GET /workspaces/:workspace/sessions для таких волатильных состояний, как hasActivePrompt, флаги ожидания и clientCount. Предварительная проверка workspace_session_live_state; тег независим от workspace_qualified_rest_core, поэтому более старые демоны, рекламирующие более широкую возможность REST для рабочих пространств, не реализуют этот маршрут. Селектор сначала разрешается как точный идентификатор рабочего пространства, затем как URL-кодированный абсолютный рабочий каталог после канонизации, что соответствует другим множественным маршрутам сессий. Маршрут предназначен только для доверенных основных и вторичных сред выполнения: он никогда не возвращается к основной среде выполнения и не использует разрешительную политику сохраненного каталога, которая предоставляет недоверенному вторичному рабочему пространству ограниченные чтения каталога. Конечная точка не имеет параметров запроса и не выполняет операций хранения сессий, настроек, внешних команд или обменов с ACP, поэтому ее стоимость не зависит от количества сохраненных сессий и размера JSONL; ограничение на количество живых сессий по умолчанию обеспечивает ограниченность ответа, а при отключении этого ограничения стоимость остается пропорциональной только количеству живых сессий.

Ответ:

{ "v": 1, "catalogVersion": { "generation": "7eca3164-bce1-4f50-94d8-c842c480f213", "revision": 17 }, "sessions": [ { "sessionId": "session-123", "clientCount": 1, "hasActivePrompt": true, "activeWorkState": "active", "isWaitingForPermission": false, "isWaitingForUserQuestion": false, "updatedAt": "2026-08-18T08:12:30.123Z" } ] }

v — это версия схемы ответа. Каждый успешный ответ включает Cache-Control: no-store. sessions — это полный, не разбитый на страницы, неупорядоченный набор сессий, которые в настоящее время активны в выбранной среде выполнения; пустая живая среда выполнения возвращает 200 с sessions: []. clientCount, hasActivePrompt, isWaitingForPermission и isWaitingForUserQuestion являются обязательными полями протокола, а отсутствующие необязательные значения моста проецируются в 0 или false. activeWorkState является дополнением к протоколу и отсутствует в более старых демонах: active означает, что демон владеет незавершенной работой или дочерний процесс отправил свежий непустой снимок удержания; idle генерируется только для свежего пустого снимка, охватывающего все требуемые категории; unknown означает, что согласованная отчетность устарела или неполна; а unsupported означает, что дочерний процесс не согласовал отчетность. Это не изменяет hasActivePrompt: фоновая оболочка, цикл cron или ожидающее уведомление терминала представляют собой активную работу, не становясь приглашением foreground. Статические поля каталога, такие как отображаемое имя, время создания, организация и метаданные источника, намеренно исключены и остаются собственностью полного каталога. Отсутствие строки состояния живой сессии только очищает волатильные поля известной строки каталога; оно никогда не удаляет сохраненную строку каталога.

updatedAt — это необязательная метка времени активности, наблюдаемая демоном; она присутствует, когда промпт, достигший состояния выполнения, опубликовал формальный терминал в текущем мосте. Она увеличивается ровно один раз для каждого такого терминала — независимо от того, был ли он успешным, ошибочным, отмененным или прерван по таймауту, — записывается перед публикацией терминального события и строго возрастает для каждой живой сессии, даже если два терминала попадают в одну миллисекунду реального времени или часы идут назад; поэтому скачок часов вперед сохраняется до тех пор, пока реальное время не сравняется с ним. Она никогда не бывает раньше createdAt сессии: первое увеличение фиксируется на моменте создания, поэтому откат системных часов между созданием и первым терминалом не может создать запись с временной меткой позади createdAt, под которым она уже была перечислена. Прием промптов, ожидание в очереди, потоковые обновления, отмена только из очереди, сигналы heartbeat и ожидание взаимодействия никогда не увеличивают её. Клиенты используют её для обновления актуальности строки каталога, которая у них уже есть, вместо полной перезагрузки каталога после завершения хода. Это не подтверждение сохранения данных: регистратор асинхронно записывает результаты хода, поэтому значение доказывает лишь то, что демон наблюдал завершение попытки выполнения. Оно отсутствует до первого терминала выполнения в поколении моста — включая сессии, восстановленные с диска, — поэтому отсутствие не является проверкой поддержки, и оно исчезает, когда перезапуск демона или замена среды выполнения рабочего пространства устанавливают новый мост. Когда для одной сессии существуют как живое, так и сохраненное резюме, полные ответы каталога сообщают о более позднем корректном временном штампе, поэтому GET /session/:id/status, который возвращает резюме моста напрямую без этого слияния, может сообщить более раннее значение, чем ответ списка.

catalogVersion — это токен равенства для изменений каталога, наблюдаемых демоном. generation — это случайный UUID, создаваемый при каждом экземпляре моста, который меняется при перезапуске демона или замене среды выполнения рабочего пространства; revision начинается с нуля и монотонно увеличивается внутри поколения. Единственная поддерживаемая операция — проверка равенства всей пары: одинаковое поколение и ревизия означают отсутствие наблюдаемых демоном изменений каталога, а любое различие означает необходимость перезагрузить полный каталог. Клиенты не должны выполнять арифметику ревизий или сравнивать ревизии между поколениями, консервативные дополнительные увеличения разрешены. Версия охватывает изменения членства в каталоге и статических метаданных, наблюдаемые демоном; обычная активность хода, жизненный цикл промпта, подключение/отключение и переходы состояний ожидания не увеличивают её, поскольку живой снимок уже содержит соответствующие изменчивые поля. Поэтому изменение updatedAt при неизменной версии является корректным и ожидаемым, и оно не инвалидирует кэши сохраненного списка демона. Два значения изменчивого наложения сознательно исключены из обоих сигналов: состояние ошибки хода (hasTurnError/turnError) и количество/содержимое ожидающих взаимодействий (pendingInteractionCount/pendingInteractions) не увеличивают версию и не появляются в снимке, поэтому клиенту, которому они нужны, необходимо продолжать чтение потока событий для каждой сессии или полного каталога, а не полагаться на этот маршрут; любое из этих полей может быть добавлено аддитивно на уровне протокола, когда появится конкретный потребитель. Изменения, записанные напрямую другим демоном, TUI или внешним процессом, не наблюдаются, поэтому, как только клиент прекращает периодический опрос полного каталога, эти записи не имеют ограниченного времени обнаружения и становятся видны только после явной полной перезагрузки, другого наблюдаемого изменения каталога, переподключения или замены демона/среды выполнения.

Клиенты согласовывают пакет каталога с помощью рукопожатия с двумя чтениями: читают живое состояние A, загружают полный список сессий (плюс GET /workspaces/:workspace/session-groups, если клиент потребляет session_organization), затем читают живое состояние B. Одинаковые версии A и B принимают пакет; различные версии помечают каталог как устаревший и объединяют не более одной последующей перезагрузки вместо входа в цикл повторных попыток. Каждый принятый запрос каталога должен быть инициирован после A — запрос или дедуплицированное обещание, начатое до A, не может удовлетворить согласование. Перезагрузки, управляемые версией, выполняются однократно для каждого рабочего пространства и соблюдают ненулевой минимальный интервал фонового процесса, поэтому устойчивые изменения каталога не могут вызывать одно полное сканирование каталога на каждый опрос живого состояния; явные локальные изменения все еще могут запрашивать немедленное обновление через ту же операцию однократного выполнения.

Ошибки:

  • 400 — существующее поведение проверки селектора или workspace_mismatch для неизвестного, неправильно сформированного, вложенного или незарегистрированного селектора; маршрут никогда не разрешает неизвестный селектор в основную среду выполнения.
  • 403 — untrusted_workspace для любой недоверенной среды выполнения, включая недоверенную основную.
  • 503 — workspace_runtime_unavailable с Retry-After для среды выполнения, находящейся в состоянии загрузки, перехода, дренажа, блокировки или удаления, либо поколения среды выполнения, которое закрывается во время обработки запроса.
  • 500 — неожиданные локальные ошибки используют существующее сопоставление ошибок моста.

GET /workspace/:id/session-groups

Список пользовательских групп сессий для рабочего пространства. GET-селектор в единственном числе принимает любой зарегистрированный id рабочего пространства или URL-кодированный канонический cwd. Псевдоним GET во множественном числе также доступен для недоверенной вторичной среды выполнения и читает только sidecar организации. Мутации групп во множественном числе остаются ограниченными уровнем доверия, тогда как мутации групп в единственном числе сохраняют свое совместимое поведение «только для основной». Предварительная проверка caps.features.includes('session_organization').

Ответ:

{ "groups": [ { "id": "018f...", "name": "Frontend", "color": "blue", "order": 0, "createdAt": "2026-07-04T12:00:00.000Z", "updatedAt": "2026-07-04T12:00:00.000Z" } ], "colorOptions": ["red", "orange", "yellow", "green", "blue", "purple"] }

Цвета являются токенами протокола; клиенты локализовывают отображаемые имена. Группы с именами цветов по умолчанию не создаются.

POST /workspace/:id/session-groups

Создать пользовательскую группу сессий. Строгий контроль мутаций. Предварительная проверка caps.features.includes('session_organization').

Запрос:

{ "name": "Frontend", "color": "blue" }

name обрезается, должен содержать 1–64 символа, не может содержать управляющие символы и уникален в пределах рабочего пространства при сравнении без учета регистра и с обрезкой пробелов. Дубликаты имен возвращают 409 { code: "group_name_conflict" }. color должен быть одним из возвращаемых colorOptions.

Ответ:

{ "group": { "id": "018f...", "name": "Frontend", "color": "blue", "order": 0, "createdAt": "...", "updatedAt": "..." } }

PATCH /workspace/:id/session-groups/:groupId

Обновить пользовательскую группу сессий. Строгий контроль мутаций. Предварительная проверка caps.features.includes('session_organization'). Поля тела необязательны: { "name"?: string, "color"?: string, "order"?: number }. Неизвестные id групп возвращают 404 { code: "group_not_found" }; дубликаты/некорректные имена и цвета используют те же ошибки, что и при создании.

DELETE /workspace/:id/session-groups/:groupId

Удалить пользовательскую группу сессий. Строгий контроль мутаций. Предварительная проверка caps.features.includes('session_organization'). Ссылки на группу в сессиях очищаются до groupId: null; закрепленное состояние сохраняется. Ответ — { "deleted": true }, если группа была удалена, и { "deleted": false }, если id не существовал.

POST /sessions/delete

Жестко удалить один или несколько сохраненных файлов JSONL сессий. Демон сначала пытается закрыть живые сессии, затем удаляет активный или архивный JSONL. Если для одного id существуют обе копии (активная и архивная), удаляются обе. Sidecars worktree с обеих сторон очищаются; история файлов, транскрипты субагентов и sidecars среды выполнения намеренно сохраняются.

Запрос:

{ "sessionIds": ["<uuid>"] }

Ответ:

{ "removed": ["<uuid>"], "notFound": [], "errors": [] }

POST /sessions/archive

Архивирование одной или нескольких сессий. Архивирование — это переход состояния, а не удаление: файл JSONL перемещается из chats/<id>.jsonl в chats/archive/<id>.jsonl. История файлов, транскрипты субагентов и служебные sidecar-файлы остаются на месте. Если сессия активна, демон сначала выполняет строгое закрытие и требует от обработчика закрытия ACP-агента сброса записи чата; если закрытие или сброс завершаются ошибкой, файл JSONL не перемещается. Предварительная проверка caps.features.session_archive.

Запрос:

{ "sessionIds": ["<uuid>"], "resolveConflicts": true, "requireIdle": true }

sessionIds должен быть непустым массивом строк, содержащим не более 100 идентификаторов. Дубликаты удаляются.

Ответ:

{ "archived": ["<uuid>"], "alreadyArchived": [], "resolvedConflicts": ["<uuid>"], "notFound": [], "errors": [] }

resolveConflicts является необязательным параметром и по умолчанию равен false. По умолчанию активные и архивные файлы с одинаковым идентификатором отображаются в errors, и ни одна копия не перемещается, не удаляется и не перезаписывается. При архивировании активной сессии описанное выше строгое закрытие выполняется до определения конфликта, поэтому при закрытии могут быть сброшены отложенные записи в активный транскрипт. С параметром resolveConflicts: true архивирование устраняет конфликт только в том случае, если обе копии являются обычными файлами транскриптов, которые выбранное рабочее пространство может обслуживать, включая принадлежащие ему пустые или поврежденные транскрипты. Сохраняется архивная копия, активная копия удаляется, а идентификатор отображается в обоих полях archived и resolvedConflicts. Этот параметр не обходит проверки прав собственности; смешанные локальные/внешние или иным образом неоднозначные права собственности отображаются в errors, и ни одна копия не перемещается. Элементы errors имеют поле { "sessionId": "<uuid>", "error": "message" }.

requireIdle является необязательным параметром и по умолчанию равен false. Клиенты должны выполнить предварительную проверку session_archive_idle_guard перед отправкой запроса. С параметром requireIdle: true активный владелец, состояние работы которого является активным, неизвестным или неподдерживаемым, возвращает эту сессию в errors; демон не закрывает её и не перемещает её транскрипт. Проверка и переход в состояние закрытия происходят в одном цикле управления среды выполнения владельца, поэтому новая работа не может начаться между ними. Сохраненная сессия, дочерний процесс которой уже был завершен, остается доступной для архивирования.

Сессия, существующая только в архиве, возвращается в alreadyArchived после того, как демон получит блокировку записи или обслуживания для завершения очистки отложенных sidecar-файлов. Если другой процесс все еще удерживает блокировку записи, идентификатор архивной сессии отображается в errors до тех пор, пока блокировка не станет доступной.

Перемещение транскрипта или устранение конфликта не откатывается, если последующая проверка прав собственности на очистку завершается ошибкой. В этом случае идентификатор может появиться только в errors, даже если состояние архива уже изменилось, и может отсутствовать в archived и resolvedConflicts. Повторите тот же запрос жизненного цикла для того же рабочего пространства, прежде чем считать ошибку доказательством того, что активная копия или конфликт сохраняются. Когда сервис может проверить сохраненный идентификатор транскрипта и получить необходимую блокировку записи или обслуживания, повторный запрос сообщает авторитетное состояние alreadyArchived даже для пустых или поврежденных транскриптов, которые пропускаются при перечислении сессий, и возобновляет отложенную очистку sidecar-файлов. Транскрипты, сохраненный идентификатор которых не может быть проверен, продолжают сообщать errors при повторном запросе и требуют ручного осмотра; повторные запросы, которые не могут получить необходимую блокировку, остаются в errors до тех пор, пока она не станет доступной.

Конфликты жизненного цикла являются результатами элементов пакета: маршруты без указания рабочего пространства и с указанием рабочего пространства возвращают HTTP 200 с конфликтом в errors. Это заменяет ранее использовавшийся формат ответа HTTP 409 session_conflict для маршрутов с указанием рабочего пространства; клиенты, вызывавшие этот маршрут, должны анализировать ответ пакета. Пакеты REST внутренней среды выполнения сохраняют безопасное сообщение о конфликте, продолжая скрывать другие детали ошибок для каждой сессии.

POST /sessions/unarchive

Восстановление архивированных сессий в активный каталог. Это само по себе не возобновляет сессию; оно лишь перемещает chats/archive/<id>.jsonl обратно в chats/<id>.jsonl. После успешного восстановления клиенты могут вызвать POST /session/:id/load или POST /session/:id/resume.

Запрос:

{ "sessionIds": ["<uuid>"], "resolveConflicts": true }

Ответ:

{ "unarchived": ["<uuid>"], "alreadyActive": [], "resolvedConflicts": ["<uuid>"], "notFound": [], "errors": [] }

resolveConflicts является необязательным параметром и по умолчанию равен false. По умолчанию одновременное наличие активных и архивных файлов JSONL приводит к конфликту в errors, и ни одна копия не перемещается, не удаляется и не перезаписывается; сессия, существующая только в активном состоянии, возвращается в alreadyActive после того, как демон получит блокировку записи или обслуживания для завершения очистки отложенных sidecar-файлов. Если активная сессия все еще удерживает блокировку записи, идентификатор активной сессии отображается в errors до тех пор, пока эта сессия не будет закрыта. С параметром resolveConflicts: true восстановление из архива устраняет конфликт только в том случае, если обе копии являются обычными файлами транскриптов, которые выбранное рабочее пространство может обслуживать, включая принадлежащие ему пустые или поврежденные транскрипты. Сохраняется активная копия, архивная копия удаляется, а идентификатор отображается в обоих полях unarchived и resolvedConflicts. Этот параметр не обходит проверки прав собственности; смешанные локальные/внешние или иным образом неоднозначные права собственности отображаются в errors, и ни одна копия не перемещается. Если для того же идентификатора уже выполняется операция архивирования или восстановления из архива, возвращается 409 session_archiving перед началом обработки пакета.

Перемещение транскрипта или устранение конфликта не откатывается, если последующая проверка прав собственности на очистку завершается ошибкой. В этом случае идентификатор может появиться только в errors, даже если состояние архива уже изменилось, и может отсутствовать в unarchived и resolvedConflicts. Повторите тот же запрос жизненного цикла для того же рабочего пространства, прежде чем считать ошибку доказательством того, что архивная копия или конфликт сохраняются. Когда сервис может проверить сохраненный идентификатор транскрипта и получить необходимую блокировку записи или обслуживания, повторный запрос сообщает авторитетное состояние alreadyActive даже для пустых или поврежденных транскриптов, которые пропускаются при перечислении сессий, и возобновляет отложенную очистку sidecar-файлов. Транскрипты, сохраненный идентификатор которых не может быть проверен, продолжают сообщать errors при повторном запросе и требуют ручного осмотра; повторные запросы, которые не могут получить необходимую блокировку, в том числе потому, что активная сессия все еще удерживает её, остаются в errors до тех пор, пока блокировка не станет доступной.

ACP-over-HTTP использует те же тела запросов и ответов через методы поставщика _qwen/sessions/archive и _qwen/sessions/unarchive. Таблица маршрутов REST сопоставляет POST /sessions/archive и POST /sessions/unarchive с этими методами для транспортов ACP.

Маршрутизация активных сессий в многопользовательских рабочих пространствах

Когда поддерживается multi_workspace_sessions, операции с активными сессиями определяют свое рабочее пространство из sessionId; клиенты не добавляют селектор рабочего пространства в URL. Помимо существующих операций жизненного цикла, маршрутизируемых по владельцу, это применяется к PATCH /session/:id/metadata, POST /session/:id/recap, POST /session/:id/generate, POST /session/:id/btw, POST /session/:id/btw/stream, DELETE /session/:id/btw/stream/:requestId, POST /session/:id/mid-turn-message, GET /session/:id/mid-turn-messages, POST /session/:id/mid-turn-messages/send-now, DELETE /session/:id/mid-turn-messages/:messageId, POST /session/:id/tasks/:taskId/cancel, POST /session/:id/goal/clear, POST /session/:id/continue, POST /session/:id/language, POST /session/:id/artifacts, DELETE /session/:id/artifacts/:artifactId, GET /session/:id/sources, POST /session/:id/sources и DELETE /session/:id/sources/:sourceId. Демон направляет каждый запрос доверенной среде выполнения, владеющей активной сессией. Недоверенный неосновной владелец возвращает 403 untrusted_workspace, отсутствие активного владельца возвращает 404 session_not_found, а неоднозначный владелец приводит к отказу с возвратом 500 ambiguous_session_owner.

Это правило действует только для живых сессий и не делает каждую сессию без рабочего пространства осведомлённой о нескольких рабочих пространствах. Сохранённые или архивные операции используют свои документированные маршруты, привязанные к рабочему пространству. POST /session/:id/branch, POST /session/:id/fork и POST /session/:id/cd намеренно остаются доступными только в основной сессии и возвращают non_primary_session_route_not_supported для владельцев неосновных сессий.

Сообщения во время хода

POST /session/:id/mid-turn-message принимает { "message": "...", "messageId": "<optional-message-id>" }. Успешное принятие возвращает { "accepted": true, "messageId": "<id>" } и передаёт право собственности демону: сообщение извлекается в активный ход или продвигается в обычную очередь запросов (FIFO), когда сессия становится свободной. Клиенты, использующие session_mid_turn_message_query, отправляют стабильный messageId; повторная отправка идемпотентна, пока сообщение находится в очереди, ожидает обработки или находится в ограниченных кольцах сверки. Отклонение запроса, который демон проверил, возвращает { "accepted": false } и никогда не передаёт право собственности: открытая сессия, в которую не было принято ни одного запроса в FIFO и нет активного хода Goal, сообщает { "accepted": false, "reason": "session_idle" }, чтобы клиент мог повторно отправить сообщение как обычный запрос вместо отображения ошибки. Для действительно нового принятия блок content, ссылающийся на вложение, которое сессия больше не хранит, или имеющий недопустимую ссылку, отклоняется до вердиктов о состоянии простоя и очереди: на запрос отвечает 410 session_attachment_gone (или 400 invalid_session_attachment_reference) с телом { "error", "code" }, которое не содержит reason, даже если сессия простаивает. Два более ранних условия имеют приоритет над ним: повторяющийся messageId, чья полезная нагрузка всё ещё совпадает, завершается идемпотентно с { "accepted": true, "messageId" }, даже если вложение было удалено после этого, а сессия, которая закрывается или авторизует закрытие, получает отказ с { "accepted": false } без причины. Вердикт описывает только то, что может извлечь сообщение во время хода, а не всё, что может хранить сессия — снимок сессии по-прежнему может сообщать hasActivePrompt: true для него, например, когда отложенный запрос восстановления находится в режиме ожидания. Отказ без причины имеет другую причину — очередь полна, сессия закрывается или авторизует закрытие, бюджет встроенных вложений в очереди исчерпан, или повторяющийся messageId больше не совпадает с полезной нагрузкой, которую хранит демон. Демон сохраняет уже принятую полезную нагрузку, а не заменяет её, но это не обещание доставки: продвинутое сообщение, удалённое клиентом, немедленно исчезает из снимков ожидающих запросов — то, которое не начало выполняться, отбрасывается там, где стоит, а то, которое уже выполняется, скрывается до завершения прерванного хода — поэтому ответ на удаление является единственным removed = true, который когда-либо наблюдает клиент. Закрывающаяся сессия никогда не продвигает то, что осталось в очереди, а ход, завершающийся во время авторизации закрытия, передаёт очередь шлюзу принятия, который отклоняет её, поэтому эти сообщения отбрасываются, а не продвигаются, и не попадают ни в одно кольцо. Закрытие, которое дочерний процесс затем отклоняет, оставляет сессию активной с тем, что осталось в очереди, перечисленным, и ничего не запланировано для продвижения до тех пор, пока не закончится какой-либо позднейший ход. Эти сообщения по-прежнему принадлежат демону, поэтому стандартным способом восстановления является ожидание: завершение следующего хода продвигает накопленные сообщения, ничего не теряется и ничего не доставляется дважды. Клиент, который не может ждать — следующий ход не планируется, и пользователь наблюдает за очередью, которая не опустеет — может вместо этого форсировать извлечение, и единственный поддерживаемый способ — освободить каждую полезную нагрузку перед повторной отправкой. Оба пути начинаются с одного и того же набора: построите его из собственной записи клиента о том, что он поставил в очередь в этом окне, минус то, для чего у него есть положительное доказательство доставки, а не из молчания списка. Запрос не является инвентаризацией того, что хранит демон — он пропускает запрос, уже помеченный как удалённый, оба кольца ограничены, и запрос, отправленный без идентификатора клиента, вообще не отслеживается на этой поверхности — поэтому идентификатор, нигде не указанный, мог истечь после доставки, быть отброшенным недоставленным или никогда не попасть в список. Положительным доказательством доставки является эхо mid_turn_message_injected, pending_prompt_started для этого идентификатора или членство в любом из колец.

Среднетуровый POST также принимает необязательный eventDetailMode: full (по умолчанию) или summary; недопустимые значения возвращают 400 invalid_event_detail_mode. Демон сохраняет этот режим вместе с сообщением и использует его, если недренированное сообщение становится новым запросом. Дренирование в существующий тур не меняет режим этого тура. Пока сообщение находится в очереди или продвинуто и ожидает обработки, повторная попытка с тем же messageId должна совпадать по тексту, медиа и эффективному режиму (отсутствие поля равнозначно полному). Web Shell отправляет свой текущий режим провайдера. REST-маршрут по-прежнему отклоняет новые приемы в состоянии простоя с кодом reason: "session_idle"; вызывающие стороны затем отправляют обычный запрос.

Чтобы форсировать извлечение, DELETE /session/:id/mid-turn-messages/:messageId каждый идентификатор в этом наборе, который запрос всё ещё сообщает как находящийся в очереди, и только затем повторно отправьте текст под новым messageId, потому что повторяющийся идентификатор подтверждается идемпотентно и ничего не перестраивает. Порядок важен и применяется ко всему набору, а не к одному идентификатору за раз: полезная нагрузка, всё ещё находящаяся в очереди и принадлежащая демону, продвигается следующим завершением хода, поэтому повторная отправка идентификатора, чья полезная нагрузка никогда не была освобождена, доставит это сообщение дважды — один раз из этого продвижения и один раз из повторной отправки. Если session_mid_turn_message_mutation не рекламируется, нет способа освободить полезную нагрузку, поэтому ожидание является единственной безопасной опцией. Ни один вердикт DELETE сам по себе не устанавливает факт доставки. { "removed": false } покрывает идентификатор, который уже внедрён или завершён — доставлен, поэтому его нельзя повторно отправлять — и, согласно маршруту DELETE ниже, два других случая. Один — это идентификатор, который извлечение всё ещё удерживает, который также нельзя повторно отправлять. Позднейший запрос перечислит его в settledMessageIds, но это не окончательный статус: если извлечение не удастся, идентификатор покинет кольцо и вернётся в очередь без события. Параграф «отправить сейчас» ниже описывает, когда сообщения тайм-аута извлечения теряются. Другой случай — это идентификатор, который не найден, что для того, который запрос только что перечислил как находящийся в очереди, означает, что он покинул очередь между двумя вызовами и мог быть отброшен недоставленным путём авторизации закрытия выше; решайте этот случай на основе собственных данных клиента. { "removed": true } освобождает полезную нагрузку, но для идентификатора, продвинутого завершением хода в этом окне, она покрывает как запись FIFO, вырезанную до того, как она когда-либо была отправлена, которую безопасно повторно отправить, так и прерывание продвинутого запроса, который уже начался, который при повторной отправке будет послан второй раз. Продвинутый идентификатор также остаётся удаляемым через тот же маршрут до завершения хода, хотя список больше не сообщает его как находящийся в очереди. Сама повторная отправка является приёмом во время хода и может быть отклонена { "accepted": false, "reason": "session_idle" }; это не ошибка, а сигнал, описанный выше, что сессия теперь принимает обычный запрос, и отправка через обычный маршрут запускает ход, завершение которого продвигает всё, что всё ещё находится в очереди — что является той же причиной, почему каждый идентификатор в наборе должен быть сначала освобождён.

Отсутствие reason поэтому не является свидетельством занятой сессии. Демоны, существовавшие до появления reason, всегда его пропускают, поэтому клиенты сохраняют собственное определение простоя параллельно с ним. Новые клиенты, подключающиеся к более старому демону, обнаруживают отсутствие этой возможности и сохраняют свой устаревший локальный резервный механизм.

GET /session/:id/mid-turn-messages возвращает общесессионную очередь, принадлежащую демону, а также ограниченные кольца settledMessageIds и promotedMessageIds. Идентификаторы в состоянии settled были внедрены или явно удалены; идентификаторы в состоянии promoted попали в обычную очередь FIFO для промптов. Идентификатор, находящийся в любом из колец, не должен быть отправлен повторно.

Когда сообщение из очереди извлекается в активный ход, демон публикует mid_turn_message_injected, содержащий согласованные массивы messages и messageIds (а также promptId текущего выполняющегося хода, если он известен). Это преходящий сигнал дедупликации, а не элемент транскрипта: клиенты завершают колбэки выполнения, зарегистрированные под этими идентификаторами сообщений, и удаляют любые локальные ожидающие строки для них. Более старые демоны дополнительно включают originatorClientId в полезную нагрузку. Пропущенное эхо восстанавливается из кольца settled через запрос выше.

Агент извлекает сообщения из очереди каждый раз, когда завершается пакет инструментов, поэтому сообщение, принятое во время потоковой передачи ответа модели, ожидает этот ответ и его пакет инструментов. Когда заявлена поддержка session_mid_turn_send_now, подключенный клиент сессии может вызвать POST /session/:id/mid-turn-messages/send-now, чтобы доставить сообщения из очереди немедленно: если передний план (включая продолжение цели) выполняет потоковую передачу ответа модели, агент немедленно извлекает очередь, прерывает этот ответ, сохраняет уже переданную часть в разговоре и продолжает тот же ход с новыми сообщениями. Ответ, который начал вызов инструмента, объявленный провайдером, даже если аргументы все еще передаются потоково, не прерывается, и выполняющиеся инструменты никогда не прерываются: сообщения остаются в очереди и доставляются вместе с результатами инструментов или повышаются до следующего промпта, когда инструмент завершает ход. Некоторые провайдеры объявляют вызов инструмента только после завершения его аргументов, и такой вызов все еще может быть прерван. В некоторых случаях сообщения уже покинули очередь, но не получают ответа в этом ходу. Если извлечение «отправить сейчас» уже выполняется, когда провайдер объявляет вызов инструмента, который затем завершает ход, или когда ход останавливается, сообщения сохраняются в разговоре и транскрипте, но не повышаются. Если само извлечение «отправить сейчас» завершается по таймауту (агент ждет 2 секунды, и демон может все еще читать вложения), сообщения, переданные демоном после этого, прерывают ответ, если он все еще передается без вызова инструмента, в противном случае они доставляются при последующем извлечении, возможно, в следующем ходу; как и в случае любого извлечения с таймаутом, они теряются, если демон отвечает более чем через 30 секунд после таймаута, или если ход останавливается во время разрешения вложений при последующем извлечении. Во всех случаях их идентификаторы переходят в состояние settled так же, как у любого извлеченного сообщения: повторная отправка одного из них подтверждается без повторного помещения в очередь, а клиент, желающий получить ответ на сообщения, сохраненные в разговоре, отправляет новый промпт. Внутреннее управление только очередью никогда не вызывает прерывание и не обрабатывается извлечением «отправить сейчас»; оно отправляется со следующим запросом к модели или через путь завершения, управляемый вызывающим кодом, если ход завершается первым. Канальные, cron-фоновые и фоновые ходы сохраняют доставку на границе инструментов. Ответ содержит { "requested": true }, если пользовательские сообщения ожидали и агента просили их доставить, и { "requested": false }, если ни одно не ожидало (они были уже доставлены, удалены или повышены). Агент, существовавший до введения этого запроса, отвечает -32601 и сохраняет доставку на границе инструментов.

Когда заявлена поддержка session_mid_turn_message_mutation, подключенный клиент сессии может вызвать DELETE /session/:id/mid-turn-messages/:messageId. Он удаляет сообщение либо из очереди внутри хода, либо из его состояния повышенного ожидающего промпта; удаление повышенного сообщения, которое уже выполняется, прерывает этот ход, что соответствует обычному удалению ожидающего промпта. Добавления и удаления в очереди, принадлежащей демону, публикуют существующие события сессии pending_prompt_added и pending_prompt_completed, чтобы подключенные клиенты обновляли оба авторитетных снимка очереди. { "removed": false } означает, что сообщение было уже внедрено, завершено или не найдено. Сообщение, извлеченное процессом извлечения, но еще не переданное агенту, также отвечает { "removed": false }. Если это извлечение завершается ошибкой, сообщение возвращается в очередь без события очереди, и его идентификатор покидает кольцо settled. Последующее извлечение или завершение хода затем заберет его, но если извлечение завершится ошибкой после того, как ход был завершен, сообщение останется в очереди до следующего хода.

GET /session/:id/pending-prompts

Возвращает текущий выполняемый запрос и запросы, ожидающие в FIFO живого сеанса. Запрос может включать X-Qwen-Client-Id; если он присутствует, он должен идентифицировать подключенного клиента.

{ "pendingPrompts": [ { "promptId": "<prompt-id>", "text": "Explain the failure", "queuedAt": 1700000000123, "state": "running", "originatorClientId": "<client-id>" } ] }

state равен running для запроса, который отправляется, и queued для ожидающих запросов. content появляется, когда запрос включает структурированный контент, такой как изображения. Это маршрут владельца живого сеанса: 404 означает отсутствие живого владельца, а 503 означает временную недоступность владельца. Недоверенный неосновной владелец возвращает 403 untrusted_workspace, а id, активный в нескольких рабочих пространствах, возвращает 500 ambiguous_session_owner. Специальный тег возможности отсутствует; старые демоны возвращают 404. Метод TypeScript SDK — это getPendingPrompts().

POST /session/:id/prompt

eventDetailMode принимает full (по умолчанию) или summary; недопустимые значения возвращают 400 invalid_event_detail_mode. Режим Summary фильтрует детали вложенных субагентов перед сохранением в кольце и доставкой через SSE, удаляя запросы Agent, встроенные инструменты и счетчики токенов в процессе выполнения, сохраняя при этом корневой контент и использование основной модели. Результаты завершенных агентов Agent сохраняют общие суммы токенов tokenCount и executionSummary (включая ошибки/отмены) для метрик тура; кадры использования вложенных процессов остаются отфильтрованными. То же правило применяется к ответам загрузки резюме/транскриптов. Режим фиксируется при приеме, применяется только при отправке и сбрасывается на полный при завершении. Поздние события субагента используют режим на момент публикации: полный во время простоя или режим активного запроса, когда выполняется последующий запрос. Они не наследуют режим запроса, который изначально запустил субагента. Это применяется ко всем подписчикам сеанса; отфильтрованные детали нельзя позже извлечь из кольца. /acp session/prompt демона принимает то же необязательное расширение верхнего уровня с той же областью действия (недопустимые значения вызывают ошибку JSON-RPC -32602); это не стандартное поле ACP и не пересылается дочернему процессу.

Передать запрос агенту. Вызывающие стороны с несколькими запросами используют FIFO-очередь для каждой сессии (ACP гарантирует один активный запрос на сессию).

Запрос:

{ "prompt": [{ "type": "text", "text": "What does src/main.ts do?" }], "delivery": { "kind": "channel", "target": { "channelName": "dingtalk", "type": "user", "id": "platform-user-id" } } }

delivery является необязательным и требует возможности channel_delivery. Демон все равно возвращает 202 {promptId,lastEventId} при приеме запроса. После успешного end_turn сеанс отправляет видимый финальный текст уже запущенному Channel Worker точного рабочего пространства. Полезная нагрузка содержит только последний блок ответа ассистента без вызова инструментов; преамбулы вызовов инструментов, повествование между инструментами, замененные повторные попытки и более ранние блоки автоматического продолжения исключаются. Пустой или состоящий только из пробелов финальный результат все равно порождает коррелированный channel_delivery_result с status: "skipped" после потребления авторизации, но не связывается с воркером. Успех или ошибка доставки приходит позже через то же воспроизводимое событие и никогда не изменяет turn_complete на turn_error. Отмена, сбой агента Agent и прекращение по лимиту токенов не отправляют и не публикуют результат доставки.

Валидация: prompt должен быть непустым массивом объектов. Другие ошибки возвращают 400 до достижения моста.

idempotencyKey — это необязательная непустая строка длиной не более 200 символов (недопустимые значения возвращают 400 invalid_idempotency_key). Она идентифицирует EVENT вызывающей стороны, а не HTTP-запрос: первый прием запоминает ключ для сеанса, а повторный POST с тем же ключом возвращает исходный прием — 202 { promptId, lastEventId, eventEpoch, deduplicated: true } с promptId ПЕРВОГО запроса и новым курсором/эпохой — без постановки второго тура в очередь. Это позволяет релею, который надежно отмечает принятие после 202 (или теряет ответ), безопасно выполнять повторную попытку. Дедупликация выполняется по принципу «наилучших усилий» в течение жизни демона: кольцо находится в памяти и ограничено (128 ключей на сеанс, 1024 сеанса, вытеснение по FIFO на обоих уровнях). Любой лимит может вытеснить ключ, пока запись очереди его события еще активна — лимит на сеанс в обычном случае одного долгоживущего сеанса, принимающего более 128 различных событий, или лимит сеансов, когда более 1024 сеансов хранят ключи — и повторная попытка после такого вытеснения приводит к повторной инъекции. Кольцо также умирает вместе с защищаемой им очередью запросов в памяти, поэтому перезапуск демона теряет оба компонента одновременно, и повторная инъекция становится правильным способом восстановления. Ключи имеют область действия в рамках сеанса; это поле является метаданными приема и никогда не пересылается агенту.

Канальные ретрансляторы используют три _meta ключа, которые маршрут удаляет перед пересылкой: qwen.channel.prompt (классификация канального хода), qwen.daemon.channelBoundRelay: true (намерение связанной доставки между рабочими пространствами) и qwen.daemon.channelPromptAuthorization (токен авторизации канального воркера; эталонный воркер отправляет его только при наличии учетных данных). qwen.channel.prompt требует зарегистрированного токена авторизации канального воркера — регистрации для целевого рабочего пространства или, для CROSS-связанной доставки между рабочими пространствами, любого зарегистрированного токена воркера плюс явный маркер qwen.daemon.channelBoundRelay: true и основной/доверенный адресат. Ретранслятор, отправляющий только два маркера без учетных данных, не получает классификации канала: он принимается как обычный интерактивный запрос (отклонение с обнаружением цикла, живое заполнение привязки PR на основе ретранслируемого текста). Обычный запрос с чужим токеном воркера также не получает классификации канала и проекции отображения, предоставленной вызывающим.

Ответ:

{ "promptId": "session-id########1", "lastEventId": 42 }

Ответ 202 подтверждает прием, а не завершение работы Agent. Наблюдайте за потоком SSE сессии после lastEventId и коррелируйте turn_complete или turn_error по promptId. Каждая достижимая проверка приема до постановки в очередь — включая зарезервированный контроль изолированной рабочей директории — выбрасывает исключение синхронно, поэтому маршрут отвечает ошибкой, отличной от 202, ДО какого-либо принятия: контроль возвращает 409 working_directory_missing (retryable: true, Retry-After: 5), когда управляемое связывание разговора сессии еще не освобождено, а канальная ретрансляция откладывает событие вместо потребления фантомного принятия. Если мост все же отклоняет отложенное событие до приема, маршрут сбрасывает idempotencyKey запроса, чтобы последующий повтор был принят заново, а не дедуплицирован в фантомный прием — это защита на нескольких уровнях для данного случая, текущая доставка которого уже потреблена вызывающим.

turn_complete.data.stopReason содержит ACP StopReason, возвращенный агентом — end_turn, max_tokens, max_turn_requests, refusal или cancelled. Демон также может выдать cancelled для запроса, прерванного без выполнения агентом, включая удаление из очереди, отключение клиента или остановку/закрытие; это значение не доказывает, что агент выполнил запрос. Воспринимайте поле как открытую строку: оно имеет тип string на уровне протокола, набор значений ACP может расширяться, и клиент, выполняющий исчерпывающий switch по этому полю, сломается при следующем добавлении значения.

Два результата на стороне демона не приходят в этом поле. Ход, завершившийся ошибкой внутри демона — истечение срока действия, сброс при закрытии, падение дочернего процесса — публикуется как событие turn_error, никогда как stopReason turn_complete. Его data всегда содержит message; code присутствует только тогда, когда демон классифицировал ошибку (истечение срока → prompt_deadline_exceeded, сброс при закрытии → channel_closed, session_closed, session_killed или daemon_shutdown), а кадр для запроса, отклоненного из-за гибели дочернего процесса ACP во время обработки, не содержит ни code, ни errorKind. Считайте оба поля необязательными и ветвите логику по message.

Ход, восстановленный из сохраненной истории после перезапуска, вообще не публикуется повторно в потоке; он появляется в теле ответа promptTerminals[] в методе POST /session/:id/load — как { terminal: "completed", stopReason: "reconstructed_from_transcript" }, если сохраненный хвост показывает завершение хода, или как { terminal: "interrupted", code: "daemon_lost" } с отсутствующим stopReason, если демон завершился посреди хода. Сопоставьте запись по promptId и ветвите логику по terminal; promptTerminals полностью опускается в ответе, если журнал не содержит свидетельств для данной сессии.

Если HTTP-клиент разрывает соединение посреди запроса, демон отправляет агенту уведомление ACP cancel, которое завершает выполнение запроса с помощью stopReason: "cancelled".

Когда поддерживается prompt_absolute_deadline, deadlineMs может сократить настроенный срок ожидания сервера. Истечение срока генерирует коррелированное turn_error с code: "prompt_deadline_exceeded". Срок освобождает вызывающего, не убивая агента; если агент позже завершит работу, опросы статуса хода для этого promptId вернут результат завершенного транскрипта вместо ошибки истечения срока.

POST /session/:id/cancel

Отмените текущий активный запрос в сессии. На стороне ACP это уведомление, а не запрос — агент подтверждает получение, разрешая активный prompt() со значением cancelled.

curl -X POST http://127.0.0.1:4170/session/$SID/cancel # → 204 No Content

Контракт для нескольких запросов: отмена влияет только на активный запрос. Запросы, которые тот же клиент ранее отправил через POST и которые все еще находятся в очереди за активным, продолжат выполнение. Очередь нескольких запросов — это поведение, введенное демоном (отсутствует в спецификации ACP); контракт для запросов в очереди гласит: «они продолжают выполняться, пока вы не отмените каждый из них или не завершите сессию через выход из канала».

Если запросы в очереди неожиданны в многоклиентском развертывании, сначала убедитесь, разделяют ли вызывающие сессию sessionScope: "single" по умолчанию. Для независимых диалогов в отдельных потоках создавайте сессии с sessionScope: "thread", чтобы запросы сериализовались только внутри этого потока.

DELETE /session/:id

Явно закройте живую сессию. Принудительно закрывает даже при подключении других клиентов — отменяет любой активный запрос, разрешает ожидающие разрешения как отмененные, публикует событие session_closed, закрывает EventBus и удаляет сессию из карт демона. Сохраненные на диске сессии НЕ удаляются — их можно перезагрузить через POST /session/:id/load. Предварительная проверка caps.features.session_close.

curl -X DELETE http://127.0.0.1:4170/session/$SID # → 204 No Content

Идемпотентно: возвращает 404 для неизвестных сессий. Конверт ошибки использует code: "session_not_found"; одновременное закрытие может вернуть code: "session_closing", который клиенты могут рассматривать как то же самое успешное терминальное состояние для этого маршрута.

Событие session_closed. Подписчики SSE получают терминальное событие session_closed с { sessionId, reason: 'client_close', closedBy?: '<clientId>' } перед завершением потока. Редукторы SDK обрабатывают его идентично session_died (устанавливают alive: false, очищают pendingPermissions).

PATCH /session/:id/metadata

Обновление изменяемых метаданных сессии. Предварительная проверка caps.features.session_metadata. Группировка и закрепление намеренно не входят в этот маршрут; используйте PATCH /session/:id/organization под session_organization.

Запрос:

{ "displayName": "My Investigation Session", "pr": { "number": 123, "url": "https://github.com/QwenLM/qwen-code/pull/123", "state": "open" } }
ПолеОбязательноеПримечания
displayNameнетСтрока. Значения длиннее 256 кодовых единиц UTF-16 обрезаются, причем обрезка не учитывает пары суррогатов, поэтому имя, оканчивающееся символом вне BMP, может потерять половину одиночного суррогата. Пустое значение или значение, состоящее только из пробелов, отвергается с ошибкой 400 invalid_metadata; пропустите поле, чтобы оставить имя неизменным.
prнетПривязка одного pull request. Требуется положительное целое число number, HTTP(S) url длиной не более 2048 символов без управляющих символов и необязательные state: open, merged или closed.

Ответ:

{ "sessionId": "<uuid>", "displayName": "My Investigation Session", "prs": [ { "number": 123, "url": "https://github.com/QwenLM/qwen-code/pull/123", "state": "open" } ] }

prs представляет собой эффективную ограниченную историю привязок и может включать обновленные ссылки на задачи. Публикует событие session_metadata_updated в потоке SSE сессии, содержащее только изменившуюся группу полей: переименование выдает displayName (плюс titleSource, если имя задано) и оставляет prs отсутствующим, тогда как изменение привязки PR выдает prs и эхо текущего displayName, если он задан. Воспринимайте отсутствие поля в событии как неизменность, а не очистку, и перечитывайте тело 200 или список сессий, когда вам нужны полные метаданные.

PATCH /session/:id/organization and PATCH /workspaces/:workspace/session/:id/organization

Обновите локальное состояние организации сессии через существующий шлюз мутаций. Предварительная проверка caps.features.includes('session_organization'); множественный маршрут дополнительно требует workspace_qualified_rest_core. На множественном маршруте :workspace сначала разрешается как точный зарегистрированный идентификатор рабочего пространства, а затем как закодированный в URL канонический абсолютный путь текущей рабочей директории (cwd). Выбранная среда выполнения должна быть доверенной. Проверка существования сессии и ненулевого значения groupId ограничена активным сохраненным, архивным сохраненным и живым состоянием сессий данного рабочего пространства и групповым хранилищем, без отката к основному или другому рабочему пространству. Устаревший маршрут остается доступным только для основного рабочего пространства.

Запрос:

{ "isPinned": true, "groupId": "018f..." }
ПолеОбязательноеПримечания
isPinnedнетЛогическое значение. true устанавливает pinnedAt, если оно еще не было закреплено; false снимает закрепление pinnedAt.
groupIdнетПользовательский идентификатор группы или null для отсутствия группировки. Неизвестные идентификаторы групп возвращают ошибку 404 { code: "group_not_found" }.
colorнетПоддерживаемый токен цвета сессии или null для очистки цвета сессии.

Ответ:

{ "sessionId": "<uuid>", "groupId": "018f...", "color": "blue", "isPinned": true, "pinnedAt": "2026-07-04T12:00:00.000Z", "updatedAt": "2026-07-04T12:00:00.000Z" }

Это состояние хранится в sidecar-файле организации сессий на уровне проекта, расположенном в каталоге хранилища среды выполнения демона. Это не содержимое транскрипта, не обновляет mtime транскрипта, не экспортируется вместе с транскриптами и сохраняется при архивировании/разархивировании.

POST /session/:id/heartbeat

Обновите учет записи «последний раз просмотрено» для этой сессии в демоне. Долгоживущие адаптеры (TUI/IDE/web) периодически отправляют этот запрос, чтобы будущая политика отзыва (Wave 5 PR 24) могла отличить мертвые клиенты от тихих.

Заголовки:

ЗаголовокОбязательныйПримечания
X-Qwen-Client-IdнетПовторяет идентификатор, выданный демоном из POST /session. Идентифицированные клиенты также обновляют свой временной штамп по клиенту; анонимные сигналы сердечного ритма обновляют только водяной знак по сессии. Должен соответствовать той же форме [A-Za-z0-9._:-]{1,128}, что и в других местах.

Тело запроса пустое ({} допустимо — сегодня поля не читаются).

Ответ:

{ "sessionId": "<sid>", "clientId": "<cid>", "lastSeenAt": 1700000000123 }

clientId повторяется только если был предоставлен доверенный X-Qwen-Client-Id. lastSeenAt — это эпоха Date.now() на стороне демона (в мс), сохраненная мостом.

Ошибки:

  • 400 — { code: 'invalid_client_id' }, если заголовок имеет неверную форму (правило формы заголовка) или содержит clientId, который не зарегистрирован для этой сессии (мост выбрасывает InvalidClientIdError перед обновлением любого временного штампа).
  • 404 — неизвестная сессия.

Проверка возможностей: предварительная проверка caps.features.client_heartbeat. Старые демоны возвращают 404 для этого пути.

POST /session/:id/model

Переключает активную модель внутри текущей привязанной службы моделей сессии. Сериализуется через очередь смены модели для каждой сессии.

(Для переключения самой службы — Alibaba ModelStudio vs OpenRouter и т. д. — передайте modelServiceId в POST /session для новой сессии. На этапе 1 нет маршрута живого переключения служб.)

Запрос:

{ "modelId": "qwen-staging" }

Ответ: результат переключения модели ACP-агента, передаваемый дословно — демон не изменяет его структуру, поэтому верхний уровень не содержит modelId. Детали переключения читайте в _meta.qwenModelSwitch.

При успехе публикует model_switched в поток SSE. При ошибке публикует model_switch_failed (чтобы пассивные подписчики видели ошибку, а не только вызывающий агент). Конкурирует с выходом канала агента, чтобы зависший дочерний процесс не мог заблокировать HTTP-обработчик. Успешное переключение также записывает модель сессии в JSONL сессии по мере возможности; когда запись выполнена, загрузка/возобновление демоном пытается восстановить модель этой сессии перед аутентификацией. Если записанная модель больше не может быть применена (модель удалена, учетные данные недоступны), восстановление использует маршрут реестра с тем же ID, если он существует — для записи снимка среды выполнения это может быть другой конечный пункт, чем записанная привязка — и продолжает работу с значением по умолчанию settings.model.name только если маршрут не разрешается. settings.model.name все еще обновляется как значение по умолчанию для новых сессий.

GET /session/:id/message-groups

Тег возможности: session_message_deletion. Мост → extMethod ACP qwen/status/session/message_groups. Это чтение владельца живой сессии: оно определяет среду выполнения, владеющую сессией, и никогда не возвращается к основному рабочему пространству. Возможность всего демона не подразумевает поддержку всех сессий; рабочие пространства SSH и авторитетное хранилище Managed отказывают в удалении.

Перечисляет группы подсказок/ответов или отображения команд, которые можно безопасно атрибутировать, в порядке активной истории. Он не получает аренду писателя для обычного устаревшего регистратора. Чтение ожидает более ранних изменений истории; возвращаемый снимок может устареть по мере изменения сессии. Само удаление требует простойной сессии.

Ответ (200):

{ "snapshot": "<opaque selection token>", "groups": [ { "id": "<group-id>", "promptId": "<prompt-id>", "recordIds": ["<user-record-id>", "<reply-record-id>"] } ] }

promptId и promptIds — необязательные идентификаторы подсказок; recordIds идентифицирует сохраненные записи группы. Пустая история возвращает groups: []. Относитесь к идентификаторам групп и снимку как к непрозрачным данным; передавайте возвращаемый снимок без изменений при удалении.

Ошибки включают 404 для неизвестной сессии, 400 unsupported_operation для рабочего пространства SSH и 422 session_message_deletion_unsupported для неподдерживаемого хранилища или недоступной записи. Существующие ошибки целостности писателя также могут возвращать 409 session_writer_conflict, 409 session_writer_lost, 409 session_transcript_changed или 503 session_writer_unavailable.

POST /session/:id/messages/delete

Тег возможности: session_message_deletion. Мост → extMethod ACP qwen/control/session/messages_delete. Строгий гейт мутации; ограничен владельцем живой сессии без возврата к основному рабочему пространству. Принимает необязательный X-Qwen-Client-Id для атрибуции событий.

Запрос:

{ "snapshot": "<token from message-groups>", "groupIds": ["<group-id>"] }

Оба поля обязательны. snapshot должен быть непустым; groupIds должен содержать различные непустые строки. Выбранные группы должны все еще существовать в этом снимке. Сервер отвергает занятые сессии вместо того, чтобы ставить удаление в очередь после хода модели.

Ответ (200):

{ "deletedGroupIds": ["<group-id>"], "deletedPromptIds": ["<daemon-prompt-id>"] }

Выбранные оригинальные записи и их принадлежащий им сырой контекст сжатия физически удаляются из JSONL. Существующий текст резюме, эффекты команд и файлы проекта сохраняются. Удаление всех групп оставляет сессию доступной для последующих подсказок.

Ошибки:

  • 400 invalid_message_groups — неверный, неизвестный или дублирующийся выбор группы.
  • 400 invalid_client_id — неверная форма заголовка атрибуции.
  • 404 — сессия неизвестна.
  • 409 session_busy — активна мутация подсказки или истории; повторите попытку после ее завершения.
  • 409 worktree_reset_active — эта сессия заменяется сбросом worktree; переподключитесь к заменяющей сессии.
  • 409 message_groups_conflict — снимок выбора устарел; повторно получите группы и потребуйте новый выбор перед повторной попыткой.
  • 409 message_history_invalid — историю нельзя безопасно атрибутировать; изменение только токена выбора не может исправить это.
  • 400 unsupported_operation / 422 session_message_deletion_unsupported — хранилище сессии/рабочего пространства не поддерживает эту операцию; обновление и повтор запроса не включат ее.
  • 409 session_writer_conflict, 409 session_writer_lost, 409 session_transcript_changed или 503 session_writer_unavailable — эксклюзивное владение транскриптом не могло быть установлено или сохранено.
  • 503 message_deletion_context_unavailable — удаление сохранено, но живой контекст не удалось установить. Тело JSON включает persisted: true, deletedGroupIds и deletedPromptIds. Синхронизируйте UI с авторитетной историей и повторно откройте сессию перед отправкой следующей подсказки. Не повторяйте удаление так, будто ничего не было удалено; последующие подсказки остаются запрещенными до успешного восстановления контекста.

Последующие мутации, зависящие от истории, и запросы к модели во время восстановления контекста возвращают 503 session_history_context_unavailable без идентификаторов persisted или удаления. Загрузка или возобновление сессии повторяет установку авторитетного JSONL-контекста; только успешная установка повторно открывает допуск к модели. Закрытие жизненного цикла остается доступным.

После сохранения сессия публикует session_messages_deleted с данными {sessionId, deletedGroupIds, deletedPromptIds} и необязательным originatorClientId в конверте событий. Внутренние API-идентификаторы истории не публикуются в этом канале. Это событие также происходит для пути persisted-503. Пиринги должны инвалидировать кэшированные транскрипты, состояние навигации, поиска и выбора, а затем перезагрузить авторитетную историю. Инициатор должен согласовывать как успешные HTTP-ответы, так и ошибки persisted: true; он не должен полагаться на обработку собственного эха SSE.

POST /session/:id/recap

Тег возможности: session_recap. Мост → extMethod ACP qwen/control/session/recap.

Создайте однострочное резюме «на чем я остановился» для сессии. Оборачивает core generateSessionRecap (packages/core/src/services/sessionRecap.ts), который выполняет побочный запрос к быстрой модели с отключенными инструментами, maxOutputTokens: 300 и строгим форматом вывода <recap>...</recap>. Побочный запрос читает существующую историю чата GeminiClient сессии и не добавляет в нее.

Тело запроса игнорируется (отправьте {} или пустое). Нестрогий гейт мутаций — поведение зеркалит /session/:id/prompt (вызов расходует токены, но не изменяет состояние). Событие SSE не публикуется.

Ответ (200):

{ "sessionId": "sess:42", "recap": "Debugging the auth retry race. Next: add deterministic timing to the integration test." }

recap является null (обычный 200, не ошибка), когда:

  • в сессии еще меньше двух диалоговых ходов,
  • побочный запрос вернул извлекаемую нагрузку <recap>...</recap>,
  • или произошла любая внутренняя ошибка модели (вспомогательная функция core работает по принципу best-effort и никогда не выбрасывает исключений).

Ошибки:

  • 400 {code: 'invalid_client_id'} — некорректный заголовок X-Qwen-Client-Id.
  • 404 — неизвестная сессия.

Отмена: отсутствует в v1. Маршрут не слушает разрыв соединения HTTP-клиента, AbortSignal не передается в мост, и дочерний процесс ACP выполняет побочный запрос до завершения независимо от того, отключился ли вызывающий. Единственные ограничения — это резервный таймаут моста 60 секунд (SESSION_RECAP_TIMEOUT_MS) и гонка закрытия транспорта против смерти канала ACP. Это приемлемо, поскольку резюме короткое (одна попытка, maxOutputTokens: 300, типично ~1–5 с); метод ext-method отмены на основе request-id может обеспечить полную сквозную отмену в будущем релизе, если затраты на пропускную способность когда-либо оправдают это.

POST /session/:id/generate

Тег возможности: session_generation.

Выполнение генерации текста в рамках запроса на основе промпта, предоставленного вызывающим. Запрос не читает и не изменяет историю разговора и не предоставляет инструментов. Он предпочитает настроенную быструю модель, переходя на основную модель сессии, если быстрая модель отсутствует или не может быть разрешена. Конечная точка не зависит от задачи; перевод — лишь один из возможных промптов, определяемых вызывающим.

Запрос:

{ "prompt": "Translate into Chinese: Hello" }

Ответ — text/event-stream. Сервер немедленно записывает начальный комментарий SSE, за которым следуют started, необязательное событие прогресса thinking, ноль или более событий delta и done. Событие thinking не содержит содержимого рассуждений. Сбой модели после начала потоковой передачи приводит к событию error; повторная попытка с другой моделью не выполняется. Промты ограничены 32 КиБ текста в кодировке UTF-8. Отключение HTTP-клиента отменяет запрос на генерацию.

POST /session/:id/btw/stream

Тег возможности: session_btw_stream. Область: владелец живой сессии.

Задайте побочный вопрос без использования инструментов, используя контекст разговора активной сессии, не добавляя его в транскрипт и не прерывая основную задачу. Существующий JSON API POST /session/:id/btw остается доступным с исходным поведением.

Запрос:

{ "question": "Why?", "requestId": "unique-request-id", "resume": false }

question — непустая строка с удаленными пробелами по краям, ограниченная 4096 символами до удаления пробелов. requestId содержит от 1 до 64 символов ASCII: буквы, цифры, подчеркивания или дефисы. resume необязателен и по умолчанию равен false. X-Qwen-Client-Id, если указан, должен называть клиента, зарегистрированного в активной сессии. Каждое начало и возобновление использует разрешенную среду выполнения владельца и ее шлюзы доверия и мутаций.

Ответом является text/event-stream, с v: 1 в каждом событии:

СобытиеНагрузка
answer{ "type": "answer", "requestId": "...", "answer": "accumulated text" }
done{ "type": "done", "requestId": "...", "answer": "trimmed complete text" }
error{ "type": "error", "requestId": "...", "code": "...", "message": "..." }

Каждый answer заменяет предыдущий текст; это не дельта для добавления. Частичные снимки сохраняют пробелы; done удаляет ведущие и завершающие пробелы, что соответствует существующему результату JSON BTW. Начальный снимок может быть пустым. done и error завершают поток. Комментарии heartbeat отправляются каждые 15 секунд. Ответ ограничен 1 МиБ текста в кодировке UTF-8; превышение лимита приводит к btw_failed. Демон завершает генерацию через пять минут с помощью btw_timeout; сроки очистки дочерних процессов и мостов составляют еще пять и десять секунд соответственно. Явная отмена приводит к btw_cancelled; другие сбои со стороны источника приводят к btw_failed.

Разрыв HTTP-соединения только отключает подписчика. Переподключитесь с тем же requestId, вопросом и идентификатором клиента и используйте resume: true, чтобы получить последний снимок без запуска нового вызова модели. Повторные начальные отправки с тем же идентификатором также повторно используют сохраненную работу. Курсор событий или Last-Event-ID не требуются. Запрос resume: true, для которого отсутствующая сохраненная запись, возвращает 410 и никогда не запускает новую работу. Запросы ограничены идентичностью моста, сессией, клиентом и ID запроса; они не могут возобновиться после перезапуска демона или замены среды выполнения владельца.

Существует максимум 32 сохраненных запроса на одну среду выполнения владельца. Терминальные квитанции и маркеры отмены истекают через 60 секунд и могут быть удалены раньше, начиная с самых старых, чтобы принять новый запрос. Активные запросы не удаляются. Генерация не удерживает блокировку допуска общего архива сессии во время ожидания модели или подписчика; активность моста все еще защищает ее от очистки из-за простоя. Явное закрытие сессии и остановка демона сохраняют обычное поведение отмены.

Ошибки до начала SSE:

  • 400 invalid_btw_request — недопустимый вопрос, ID запроса или флаг возобновления.
  • 400 invalid_client_id — неверно сформированный или незарегистрированный идентификатор клиента.
  • 403 untrusted_workspace — разрешённый владелец, не являющийся основным, не является доверенным.
  • 404 session_not_found — живой владелец не существует.
  • 409 btw_request_conflict — сохранённый ID принадлежит другому вопросу.
  • 410 btw_expired — для возобновления нет сохранённого запроса.
  • 429 btw_limit — все 32 сохранённых запроса активны.
  • 501 btw_stream_not_supported — мост владельца не может транслировать BTW.

DELETE /session/:id/btw/stream/:requestId

Тег возможности: session_btw_stream. Область действия: живой владелец сессии.

Отмените соответствующий запрос BTW с той же идентичностью сессии и клиента, которая использовалась для его запуска. Возвращает пустой 204; отмена идемпотентна для сохраненного запроса. Точный кэшированный идентификатор может выполнить отмену после отключения своего клиента, тогда как неизвестные ID требуют все еще зарегистрированного клиента. Отмена неизвестного ID записывает кратковременный маркер отмены, чтобы отложенный начальный POST не мог запустить работу.

Маршрут использует то же разрешение владельца, шлюзы доверия и мутаций, что и потоковый POST. Недействительные ID возвращают 400 invalid_btw_request; недействительные идентификаторы клиентов возвращают 400 invalid_client_id. Неизвестная отмена возвращает 429 btw_limit, когда все 32 сохраненных запроса активны. Известный активный запрос всегда можно отменить, даже если среда выполнения достигла емкости. Отмена не отменяет основной промт или BTW другой сессии.

Изменения: режим одобрения, инструменты, навыки, init, перезапуск MCP

Демон предоставляет пять маршрутов управления изменениями, позволяющих удалённым клиентам изменять конфигурацию среды выполнения без взаимодействия с CLI хоста демона. Управление режимом одобрения сохраняет свой нестрогий совместимый шлюз. Переключение инструмента, переключение навыка, инициализация рабочего пространства и перезапуск MCP используют строгий шлюз изменений: проходят запросы основного доверенного loopback, аутентифицированные bearer-токеном и спаренные запросы Local Control. Основной запрос без токена, достигающий шлюза без полномочий доверенного loopback, получает 401 {code: 'token_required'}; отсутствующие или недопустимые настроенные учётные данные и несопоставленные учётные данные Local Control отвергаются ранее простым 401 Unauthorized. Все пять:

  • Принимайте и ставьте штамп заголовку X-Qwen-Client-Id (цепочка аудита PR 7). Когда заголовок содержит доверенный id, демон испускает originatorClientId в соответствующем событии SSE, чтобы межклиентские UI могли подавлять эхо своих собственных изменений.
  • Предварительно проверяйте каждую возможность по тегу перед раскрытием функции. Демон, у которого отсутствует маршрут, возвращает 404. Для маршрутов настроек Skill отсутствие тега вместо этого может означать, что те же пути обслуживают устаревший контракт с проверкой каталога: маршрут с одной целью может вернуть HTTP 404 skill_not_found или 409 skill_not_toggleable, тогда как пакетный маршрут возвращает HTTP 200 с ошибками, производными от каталога, в errors[]. Не используйте зондирование маршрутов как проверку версии.

Маршруты переключения инструмента, переключения навыка, init и перезапуска MCP испускают события с областью действия рабочего пространства: каждая активная шина SSE сессии получает событие, независимо от того, какая сессия была подключена при запуске изменения. approval-mode испускает событие с областью действия сессии, потому что изменение локально для Config одной сессии.

POST /session/:id/approval-mode

Тег возможности: session_approval_mode_control. Мост → ACP extMethod qwen/control/session/approval_mode.

Измените режим одобрения живой сессии. Новый режим попадает внутрь Config дочернего процесса ACP для каждой сессии немедленно и восстанавливается из транскрипта этой сессии, когда запись чата доступна. Выбранный режим исполнения Plan восстанавливается вместе с ним. Настройки рабочего пространства НЕ записываются по умолчанию — передайте persist: true, чтобы также записать tools.approvalMode в настройки рабочего пространства. Если запись отключена или деградирована, режим остаётся только для живой сессии.

Запрос:

{ "mode": "auto-edit", "persist": false }

mode должен быть одним из 'plan' | 'default' | 'auto-edit' | 'auto' | 'yolo' (зеркало enum ApprovalMode ядра; SDK экспортирует DAEMON_APPROVAL_MODES для валидации во время выполнения). persist по умолчанию равен false.

Ответ (200):

{ "sessionId": "sess:42", "mode": "auto-edit", "previous": "default", "persisted": false }

Ошибки:

  • 400 {code: 'invalid_approval_mode', allowed: [...]} — неизвестный литерал режима.
  • 400 {code: 'invalid_persist_flag'} — persist не является булевым значением.
  • 403 {code: 'trust_gate', errorKind: 'auth_env_error'} — запрошенный режим требует доверенную папку (привилегированные режимы в недоверенных рабочих пространствах отвергаются Config.setApprovalMode ядра).
  • 404 — сессия неизвестна.

Событие SSE (область действия сессии): approval_mode_changed с {sessionId, previous, next, persisted, originatorClientId?}.

POST /workspace/tools/:name/enable

Тег возможности: workspace_tool_toggle. Чистый файловый ввод-вывод — нет roundtrip ACP.

Переключите имя инструмента в списке настроек tools.disabled рабочего пространства. Инструменты, перечисленные там, вообще не регистрируются (в отличие от permissions.deny, который оставляет инструмент зарегистрированным, но отвергает вызов). И встроенные инструменты, и инструменты, обнаруженные через MCP, проходят через ToolRegistry.registerTool, который сверяется с набором отключённых.

⚠️ Имена должны точно совпадать с открытым идентификатором реестра. Разрешение алиасов не происходит — маршрут сохраняет любую строку из параметра пути в tools.disabled, и следующий дочерний процесс ACP сравнивает её с tool.name во время регистрации. Встроенные модули используют своё каноническое имя реестра (форма глагола snake_case): run_shell_command, read_file, write_file, list_directory, glob, grep_search, web_fetch и т. д. — НЕ отображаемые метки (Shell, Read, Write), которые показывает CLI. Инструменты, обнаруженные через MCP, используют квалифицированную форму mcp__<server>__<name> (которая также является формой, которую рассылают события tool_toggled и которую перечисляет GET /workspace/mcp). Отключение Bash НЕ помешает run_shell_command зарегистрироваться в следующей сессии.

Живые дочерние процессы ACP сохраняют уже зарегистрированные инструменты — переключение вступает в силу при следующем запуске дочернего процесса ACP. Комбинируйте с POST /workspace/mcp/:server/restart (для инструментов из источника MCP) или созданием новой сессии, чтобы изменение вступило в силу в текущем демоне.

Неизвестные имена инструментов допускаются: предварительное отключение еще не установленного инструмента MCP является законным вариантом использования.

Запрос:

{ "enabled": false }

Ответ (200):

{ "toolName": "run_shell_command", "enabled": false }

Ошибки:

  • 400 {code: 'invalid_tool_name'} — пустой параметр пути или параметр пути превышает лимит в 256 символов.
  • 400 {code: 'invalid_enabled_flag'} — enabled отсутствует или имеет небулевый тип.

Событие SSE (в области видимости рабочего пространства): tool_toggled с {toolName, enabled, originatorClientId?}.

Разделение конфигурации Skills и маршрутов выполнения

Тег возможности: workspace_skills_config_runtime.

GET /workspace/config/skills читает владельца глобальной конфигурации, локальной для демона. GET /workspaces/:workspace/config/skills читает точную зарегистрированную конфигурацию рабочего пространства и не требует активного или доверенного выполнения. Оба возвращают стандартную структуру статуса Skills без обязательного runtimeEpoch:

{ "v": 1, "workspaceCwd": "/work/project", "initialized": true, "skills": [] }

GET /workspace/runtime/skills и GET /workspaces/:workspace/runtime/skills читают выбранное доверенное выполнение, не запуская его. Активный каталог включает поколение выполнения, которое его создало:

{ "v": 1, "workspaceCwd": "/work/project", "initialized": true, "runtimeEpoch": 4, "skills": [] }

Глобальная установка и удаление используют POST /workspace/config/skills/install и DELETE /workspace/config/skills/:name?scope=global. Установка, удаление и включение в рабочем пространстве используют POST /workspaces/:workspace/config/skills/install, DELETE /workspaces/:workspace/config/skills/:name?scope=workspace и POST /workspaces/:workspace/config/skills/:name/enable. Записи конфигурации фиксируют долговременное состояние перед планированием согласования выполнения. Их activation равно deferred, когда текущее выполнение не может быть обновлено, и reconciling, когда обновление поставлено в очередь; переключатель без изменений сохраняет активацию фасада вместо утверждения о том, что произошло обновление выполнения.

Удаление конфигурации возвращает 503 skills_config_unavailable вместо окончательного ответа «не найдено», если инвентарь Skill, локальный для демона, не может быть перечислен. Удаление из выполнения (DELETE /workspace/skills/:name и его форма с указанием рабочего пространства) требует свежего перечисления каталога: оно возвращает то же 503 skills_config_unavailable, когда ни каталог выполнения, ни перечисление на стороне демона не дают инициализированного каталога, даже если существует сохраненное наблюдение. 404 skill_not_found означает, что каталог был перечислен и имя отсутствует в нем.

Владелец единичной мутации отвергает область рабочего пространства с помощью 400 workspace_scope_requires_qualified_workspace; квалифицированные мутации отвергают глобальную область с помощью 400 global_scope_requires_singular_owner. Квалифицированные записи требуют доверенного рабочего пространства. Устаревший внедренный мост возвращает 501 workspace_runtime_not_supported для квалифицированных записей конфигурации, требующих координации выполнения. Неизвестные, удаленные, переходящие и опустошаемые рабочие пространства следуют стандартным ошибкам квалифицированного рабочего пространства и никогда не возвращаются к основному.

Установка архивов нативных Skill и квитанции

skill_archive_install и skill_zip_file_source объявляют поддержку входных данных в формате ZIP и соответствующих запросов квитанций в области владельца. Оба ограничены современной поверхностью конфигурации. Их limits.maxSkillArchiveBytes, maxSkillArchiveExpandedBytes, maxSkillArchiveEntries и maxSkillArchiveManifestBytes применяются только к этим двум источникам: 50 МиБ в сжатом виде, 200 МиБ в распакованном виде, 4096 записей ZIP и 32 МиБ для эффективного корневого SKILL.md. Явные директории и игнорируемые метаданные учитываются как записи до фильтрации; неявные родительские элементы не учитываются. Игнорируемые метаданные не раздуваются. Ограничения устаревших base64, директорий и GitHub, включая парсер JSON размером 10 МиБ, остаются неизменными.

  • POST /workspace/config/skills/install-archive?name=...&attemptId=... устанавливает глобально; /workspaces/:workspace/config/skills/install-archive устанавливает в выбранное рабочее пространство. Отправляйте application/octet-stream без кодирования содержимого HTTP. Маршрут фиксирует область.
  • Существующие современные маршруты установки JSON также принимают { "name": "example", "scope": "global", "attemptId": "<UUID>", "source": { "type": "zip-file", "path": "/absolute/daemon/path.zip", "sha256": "<optional 64 hex digits>" } }. Квалифицированные маршруты требуют scope: "workspace". Демон копирует открытый обычный файл в приватное хранилище и никогда не удаляет и не переименовывает файл вызывающей стороны. Сохраняйте источник стабильным во время копирования; проверки метаданных сами по себе не являются атомарным снимком.
  • GET /workspace/config/skills/install-attempts/:attemptId и GET /workspaces/:workspace/config/skills/install-attempts/:attemptId запрашивают исходного владельца. Ответы содержат running (с фазой), committed, not_committed или recovery_required. Отсутствие или недоступность квитанций означают, что результат не может быть подтвержден. Запросы не запускают выполнение и не получают координатор мутаций.

Клиент предоставляет UUID перед отправкой; SDK может сгенерировать его. Дубликат ID под тем же владельцем возвращает 409 без повторения установки. На одном демоне могут быть активны две операции, включая ожидание блокировки и очистку; дальнейшее принятие возвращает 429 с Retry-After. Существует один бюджет в 120 секунд, охватывающий прием квитанций до принятия публикации. Отмена до публикации останавливает операцию; после принятия фиксация или откат завершаются, даже если HTTP-соединение исчезнет.

Успех сохраняет результат мутации и активацию, добавляя attemptId и archiveSha256. Сбой активации после фиксации является activationWarning, а не неудачной фиксацией. Квитанции представляют собой исторические результаты, локальные для процесса, сохраняемые в течение десяти минут после завершения работы, с емкостью 256 и без раннего вытеснения. Они не доказывают, что более поздняя операция не заменила установленный контент. Перезапуск, истечение срока действия или потеря доступа к владельцу делают результат неизвестным. Клиенты должны выполнять запрос после неоднозначных сбоев и не должны автоматически повторно передавать данные.

POST /workspace/skills/refresh

Тег возможности: workspace_session_refresh. Форма с указанием рабочего пространства — POST /workspaces/:workspace/skills/refresh. Единичный маршрут владеет основным выполнением; квалифицированный маршрут разрешает точное выбранное доверенное выполнение и никогда не возвращается к основному. Оба используют строгий шлюз мутаций и существующие проверки идентичности клиента.

Явно перезагрузите настройки и контент Skill для всех активных сессий или только для предоставленных ID сессий:

{ "sessionIds": ["session-1", "session-2"] }

Отсутствие sessionIds обновляет все активные сессии. ID должны быть непустыми строками и дедуплицируются в порядке запроса; [] возвращает нулевые счетчики без обновления сессий или кэшей. Неверные формы запроса возвращают HTTP 400. Неизвестные ID и ID, принадлежащие другому рабочему пространству, возвращают HTTP 404 до начала любого обновления. Выбранные сессии направляются только в свои каналы движка выполнения; несвязанные каналы не получают команду обновления.

Ответ (200):

{ "sessionsRefreshed": 2, "sessionsFailed": 0, "configsRefreshed": 3, "configsFailed": 0, "reason": "all" }

Счетчики описывают выбранные сессии и общие/настройки сессий, обновленные для их каналов владельцев. Выбор управляет немедленным обновлением сессий и уведомлениями команд; общие кэши каналов могут изменяться, а события рабочего пространства остаются общими для всего рабочего пространства. Явный запрос не обновляет ревизию или готовность координатора Skill. Включение/выключение, установка/удаление Skill и автоматическое согласование сохраняют свое существующее поведение полного обновления. Этот маршрут не добавляет refreshRuntime или sessionIds в тела переключателей Skill.

POST /workspace/skills/:name/enable

Тег возможности: workspace_skill_settings_toggle. Форма с указанием рабочего пространства — POST /workspaces/:workspace/skills/:name/enable.

Обновляет настройки Skill для указанного имени в рабочей области. При включении демон сначала разрешает имена, созданные расширениями, используя свежее наблюдение каталога, обходя сохраненные данные и ограничение частоты чтения, а также ожидая ACP и локального резервного механизма демона при необходимости; если оба свежих источника недоступны, перед использованием эвристики по имени применяется сохраненное наблюдение. Имена не проверяются на наличие в каталоге. Отключение не читает каталог. Обрезанное имя из запроса передается в хранилище и возвращается в ответе. Включение любого имени фиксирует opt-in уровня skills.enabled в рабочей области, даже до установки; отключение удаляет этот opt-in и добавляет запись skills.disabled в рабочей области. Существующие записи для Skills, которые больше не загружаются, сохраняются, а дубликаты или варианты с разным регистром для целевого элемента объединяются. Жесткая запись skills.disabled, унаследованная из более высокого уровня, остается определяющей для фактической доступности, но не мешает уровню рабочей области фиксировать или удалять собственное объявление. В остальных случаях объявления рабочей области участвуют в обычном разрешении skills.disabled > skills.enabled > skills.defaultDisabled и могут переопределять записи skills.defaultDisabled или skills.enabled более высоких уровней.

Это отличается от управляемой операции Skill через ACP qwen/skills/setEnabled и поля frontmatter disable-model-invocation. Для активного родительского Extension фактическая доступность Skill определяется следующим порядком: skills.disabled > skills.enabled > skills.defaultDisabled > внутреннее переопределение рабочей области > манифест skillStates > enabled. Как жесткое, так и отключение по умолчанию удаляют skill из доступности для slash-команд/модели и отклоняют последующее выполнение skill. disable-model-invocation: true сохраняет возможность прямого вызова пользователем, но скрывает skill только от вызова моделью.

Запрос:

{ "enabled": false }

Ответ (200):

{ "skillName": "review", "enabled": false, "changed": true, "activation": "applied", "sessionsRefreshed": 2, "sessionsFailed": 0 }

activation отражает активность дочернего процесса и любые необходимые обновления независимо от changed. Он равен applied, когда дочерний процесс ACP активен и все необходимые обновления выполнены успешно, deferred, когда ни один дочерний процесс не был активен во время проверки активности или измененный запрос теряет свой дочерний процесс/сессию во время необходимого обновления, и partial, когда хотя бы одно другое необходимое обновление завершается ошибкой. Таким образом, операция без изменений может быть applied или deferred с changed: false; когда changed истинно, а активация равна deferred, сохраненное объявление используется при запуске дочернего процесса. Занятые сессии включаются в необходимое обновление. Демон перезагружает настройки рабочей области для дочернего процесса ACP и каждой активной сессии, уведомляет потребителей SkillManager и отправляет available_commands_update. Запрос, уже отправленный модели, не переписывается; последующая валидация, снимки команд и контексты модели используют новое состояние. Если сохранение завершается ошибкой, обновление или событие не генерируются. Если обновление сессии завершается ошибкой, зафиксированная настройка сохраняется. Когда дочерний процесс возвращает результаты по сессиям, количество сессий точно. Если сам механизм обновления завершается ошибкой до возврата этих результатов, sessionsFailed: 1 является консервативной нижней границей, указывающей на то, что запрос на обновление завершился неудачей.

Ошибки:

  • 400 {code: 'invalid_skill_name'} — пустой параметр пути или длина более 256 символов.
  • 400 {code: 'invalid_enabled_flag'} — отсутствует поле enabled или оно имеет небулевый тип.
  • 403 {code: 'untrusted_workspace'} — выбранная рабочая область не является доверенной.

Мутация повторно использует событие settings_changed уровня рабочей области для каждого измененного ключа (skills.disabled и/или skills.enabled); она не добавляет новый тип события. Каждое из этих событий включает тот же объект mutation: { id, kind: 'skill_toggle', skills: [{ name, enabled }], activation, sessionsRefreshed, sessionsFailed }. Поле id связывает все события настроек, порожденные одним запросом на переключение. Поле skills перечисляет запрошенные имена и запрошенные значения enabled, чьи объявления в настройках рабочей области действительно изменились; настройка более высокого уровня может оставить фактическую доступность неизменной. Ячейки статуса skill в рабочей области включают необязательные поля disabledReason: 'hard' | 'default' | 'inactive_extension' и lockedScope: 'system' | 'user' | 'systemDefaults'.

POST /workspace/skills/enable

Тег возможности: workspace_skill_settings_batch_toggle. Форма с указанием рабочей области — POST /workspaces/:workspace/skills/enable.

Обновляет настройки Skill в рабочей области для максимум 100 имен в одном запросе; лимит подсчитывает исходные записи skillNames до дедупликации. Имена обрезаются и дедуплицируются без учета регистра, сохраняя порядок первого появления и регистр. При включении демон сначала разрешает имена, созданные расширениями, используя свежее наблюдение каталога, обходя сохраненные данные и ограничение частоты чтения, а также ожидая ACP и локального резервного механизма демона при необходимости; если оба свежих источника недоступны, перед использованием эвристики по имени применяется сохраненное наблюдение. Имена не проверяются на наличие в каталоге. Отключение не читает каталог. Все resulting изменения объявлений применяются в одной блокированной записи настроек, и, если что-то изменилось, активные сессии обновляются один раз. Включение всегда фиксирует явный opt-in skills.enabled уровня рабочей области, включая еще не установленные имена, чтобы переопределить внутреннее отключение Extension. Повторение идентичного объявления остается операцией без изменений. Неожиданные ошибки сохранения или генерации среды выполнения приводят к отказу всего запроса.

Запрос:

{ "skillNames": ["review", "deploy", "missing"], "enabled": false }

Ответ (200):

{ "enabled": false, "activation": "applied", "sessionsRefreshed": 2, "sessionsFailed": 0, "results": [ { "skillName": "review", "enabled": false, "changed": true }, { "skillName": "deploy", "enabled": false, "changed": true }, { "skillName": "missing", "enabled": false, "changed": true } ], "errors": [] }

Некорректные запросы возвращают HTTP 400 с кодом invalid_skill_names, invalid_skill_name или invalid_enabled_flag. Аутентификация, доверие к рабочей области, идентификация клиента, неожиданные ошибки сохранения и ошибки генерации среды выполнения приводят к отказу всего запроса через стандартные шлюзы маршрутизации. Поле errors остается в ответе для совместимости протокола и пусто для структурно корректных имен. Пакетные поля activation, sessionsRefreshed и sessionsFailed описывают активность дочернего процесса и единственное обновление живых сессий, общее для всех измененных результатов. Пакет, в котором ни одна цель не изменилась, может вернуть applied, если дочерний процесс активен, или deferred, если его нет, что соответствует ответу на операцию без изменений для одного Skill, поэтому определите фактические изменения по флагу changed в каждом результате. Когда хотя бы одна цель изменяется, демон отправляет те же метаданные мутации settings_changed, что и маршрут для одного Skill; каждое событие skills.disabled / skills.enabled из этого запроса разделяет один идентификатор mutation.id.

PUT /skills/activation

Возможность: user_skill_activation (объявляется, когда доступно сохранение пользовательских настроек). Эта глобальная для процесса мутация сохраняет предпочтения User Skill без требования доверенной рабочей области или сканирования установленных Skills. Она использует обычные аутентифицированные шлюзы мутаций; необязательный параметр X-Qwen-Client-Id должен принадлежать все еще управляемой среде выполнения, включая находящуюся в процессе завершения работы.

{ "skillNames": ["rust:pdf", "go:review"], "state": "enabled" }

Укажите от 1 до 100 имен (максимум 256 символов каждое). Имена обрезаются и дедуплицируются без учета регистра. Поле state принимает значение enabled или disabled. Только disabled принимает mode: hard (по умолчанию) или default. Имена не обязательно должны быть установлены. Некорректные пакеты отклоняются до любой записи.

Qwen считывает последние настройки пользователя под блокировкой записи настроек, добавляет каждое явно указанное имя в skills.enabled, skills.disabled или skills.defaultDisabled и удаляет это же имя из двух других списков при одном сохранении. Другие имена, написания, области действия и настройки сохраняются. В частности, включение rust:pdf не удаляет ограничение pdf. Явные простые имена сохраняют существующие правила сопоставления. Системные ограничения по-прежнему применяются.

{ "scope": "user", "state": "enabled", "results": [ { "skillName": "rust:pdf", "changed": true }, { "skillName": "go:review", "changed": true } ], "propagation": "queued" }

Отключенные ответы также включают нормализованный mode. propagation описывает планирование, а не измеренную доступность: queued означает, что хотя бы одна активная среда выполнения поставила обновление в очередь; deferred означает, что сохраненные настройки ожидают работы жизненного цикла среды выполнения; not_required означает, что этот запрос ничего не изменил. Пустая операция (no-op) ни записывает данные, ни отправляет события и не подтверждает завершение предыдущих обновлений. Для получения эффективного состояния читайте существующие конечные точки конфигурации рабочего пространства и статуса Skill среды выполнения.

Успешные изменения инвалидируют кэши конфигурации и рассылает обычные события settings_changed с scope: "user". Обновления атомарно перечитывают настройки пользователя и рабочего пространства; неудачные обновления конфигурации или сеанса сохраняют отложенную работу для восстановления жизненного цикла и ограниченных повторных попыток. Существующее поведение обновления только для рабочего пространства осталось неизменным.

Некорректный ввод возвращает HTTP 400 (invalid_skill_names, invalid_skill_name, invalid_skill_activation_state или invalid_skill_activation_mode). Если основная генерация среды выполнения закрыта до фиксации, возвращается HTTP 503 с Retry-After: 1, как на маршрутах настроек. SDK предоставляет setUserSkillsActivation(skillNames, activation, { clientId? }) и отправляет один PUT без предварительного чтения конфигурации.

POST /workspace/init

Тег возможности: workspace_init. Чистый файловый ввод-вывод — без обмена ACP, без вызова LLM.

Создает пустой QWEN.md (или переопределение настроек рабочего пространства context.fileName) в корневом каталоге основного рабочего пространства демона. Только механическое действие — для заполнения содержимого с помощью ИИ используйте последующий вызов POST /session/:id/prompt.

По умолчанию отказывается перезаписывать файл, если он существует и содержит непустые символы (кроме пробелов). Файлы, содержащие только пробелы, считаются отсутствующими (соответствует локальной слэш-команде /init).

Запрос:

{ "force": false }

Ответ (200):

{ "path": "/work/bound/QWEN.md", "action": "created" }

action равно 'created' для новых созданий, 'noop', когда существующий файл, содержащий только пробелы, остался нетронутым (запись не выполнялась), и 'overwrote', когда force: true заменил непустое содержимое. SSE-событие workspace_initialized отражает действие из ответа — наблюдатели могут фильтровать по action !== 'noop', чтобы реагировать только на фактические изменения на диске.

Ошибки:

  • 400 {code: 'invalid_force_flag'} — force не является булевым значением.
  • 409 {code: 'workspace_init_conflict', path, existingSize} — файл существует с непустым содержимым (кроме пробелов), и force опущен или равен false. Тело содержит абсолютный путь и размер (в байтах), чтобы клиенты SDK могли отобразить запрос «Перезаписать N байт?» без повторного статирования файла.

SSE-событие (с областью видимости рабочего пространства): workspace_initialized с {path, action, originatorClientId?}.

POST /workspace/mcp/reload

Перезагрузите сохраненные настройки MCP в конфигурацию обнаружения рабочего пространства и каждую активную сессию. Форма с указанием рабочего пространства — POST /workspaces/:workspace/mcp/reload.

Тело запроса:

{ "forceReconnectAll": true }

forceReconnectAll необязателен и по умолчанию равен false, сохраняя инкрементальную сверку. Если true, демон переподключается ко всем подходящим настроенным серверам MCP после сверки настроек. Альтернативно, передайте forceReconnectWhich: ["server-a", "server-b"], чтобы переподключить только указанные серверы. Опции взаимоисключающие. Принудительное переподключение заставляет каждый транспорт читать учетные данные, которые другой локальный процесс Qwen Code мог записать в хранилище токенов; оно не запускает поток авторизации OAuth.

Маршрут возвращает 202 { "accepted": true }; опрашивайте GET /workspace/mcp для получения окончательного статуса подключения. Недействительные значения опций возвращают 400.

POST /workspace/mcp/:server/restart

Тег возможности: workspace_mcp_restart. Мост → extMethod ACP qwen/control/workspace/mcp/restart.

Перезапускает настроенный сервер MCP через McpClientManager.discoverMcpToolsForServer дочернего процесса ACP (отключение + переподключение + повторное обнаружение). Предварительно проверяет снимок живого бюджета из учета PR 14 v1, чтобы перезапуск в рабочем пространстве с исчерпанным бюджетом возвращал мягкий отказ вместо запуска каскада BudgetExhaustedError.

Тело запроса пусто ({}). Параметр пути — закодированное в URL имя сервера, как оно указано в конфигурации mcpServers.

Ответ (200) — дискриминированное объединение по restarted:

{ "serverName": "docs", "restarted": true, "durationMs": 1234 }
{ "serverName": "docs", "restarted": false, "skipped": true, "reason": "budget_would_exceed" }

Причины мягкого пропуска (все возвращают 200):

reasonЗначение
'in_flight'Другое обнаружение / перезапуск для этого сервера уже выполняется. Маршрут немедленно возвращается, не ожидая исходного промиса. Вызывающей стороне следует повторить попытку после короткой задержки.
'disabled'Сервер настроен, но указан в excludedMcpServers. Включите его перед перезапуском.
'budget_would_exceed'Демон находится в состоянии --mcp-budget-mode=enforce, целевой сервер в настоящее время отсутствует в reservedSlots, и общее количество достигло clientBudget. Вызывающей стороне следует сначала освободить слот.

Ошибки (не 2xx):

  • 400 {code: 'invalid_server_name'} — пустой параметр пути.
  • 404 — имя сервера отсутствует в конфигурации mcpServers или не существует живого канала ACP (перезапуск по своей природе требует живого экземпляра McpClientManager).
  • 500 — внутренняя ошибка (например, ToolRegistry не инициализирован).

SSE-события (с областью видимости рабочего пространства): mcp_server_restarted с {serverName, durationMs, originatorClientId?} при успехе; mcp_server_restart_refused с {serverName, reason, originatorClientId?} при мягком пропуске.

POST /workspaces/:workspace/language

Синхронизация языка пользовательского интерфейса рабочего пространства без сеанса, регистрируется, когда доступно сохранение настроек. Владение: выбранная среда выполнения. Разрешает идентификатор рабочего пространства или закодированный абсолютный текущий рабочий каталог, требует активной доверенной среды выполнения и проверяет владение клиентом относительно этой среды выполнения. Неактивные генерации возвращают 503 workspace_runtime_unavailable; неразрешенные селекторы никогда не используют резервное основное рабочее пространство.

Запрос: { "language": "zh" }. Язык использует те же поддерживаемые коды, что и POST /language. Сохраняет только general.language выбранного рабочего пространства, затем читает его эффективное объединенное настройку и синхронизирует уже живые каналы через qwen/control/user/language с scope: "workspace". Холодные каналы пропускаются без запуска процесса или создания разговора. Настройки выходного языка и модели остаются неизменными; syncOutputLanguage: true отклоняется.

Ответ имеет ту же форму, что и POST /language, с outputLanguage: null; language — это эффективная настроенная локаль (включая auto), а refresh учитывает только выбранную среду выполнения. При успехе генерируется событие рабочего пространства language_changed. SDK предоставляет client.workspaceByCwd(cwd).setUiLanguage(language) через REST независимо от транспорта сеанса.

POST /language

Тег возможности: user_language_sync (условный — публикуется только при доступности сохранения настроек, то же условие, что и для workspace_settings). Bridge → ACP extMethod qwen/control/user/language для каждого доверенного runtime с активным каналом.

Синхронизация языка на уровне пользователя без сессии (issue #10234), для хостов, которым нужно переключить язык до создания какой-либо сессии (например, на приветственной странице). Классификация владения: глобальная для процесса — изменяет только глобальное состояние пользователя (~/.qwen/settings.json, глобальный ~/.qwen/output-language.md) плюс best-effort обновление runtime, поэтому не требует селектора workspace или идентификатора сессии. Зарегистрировано за нестрогим гейтом mutate(), согласуясь с родственным POST /session/:id/language.

Процесс демона является единственным писателем: он сохраняет general.language (и, если syncOutputLanguage истинно, general.outputLanguage плюс глобальный output-language.md) перед рассылкой. Затем каждый доверенный runtime переключает свой собственный UI-язык процесса и перезагружает настройки пользовательского уровня с диска; когда syncOutputLanguage истинно, он также обновляет системную инструкцию каждой локальной сессии. Отличия от маршрута с привязкой к сессии:

  • Файлы выходного языка, привязанные к проекту, НЕ перезаписываются — сессия, чей workspace имеет собственный .qwen/output-language.md, сохраняет это переопределение (область проекта имеет приоритет при обновлении).
  • Ноль сессий и ноль активных каналов runtime считаются успехом, а не ошибкой; runtime без активного канала пропускается (не считается ошибкой) и читает сохраненные файлы при следующем запуске своего канала.
  • Недоверенные runtime’ы workspace пропускаются, что соответствует позиции 403 untrusted_workspace маршрута сессии.

Запрос:

{ "language": "zh", "syncOutputLanguage": true }

language должен быть одним из кодов, перечисленных в /capabilities supportedLanguages (плюс 'auto'); syncOutputLanguage по умолчанию равно false.

Ответ (200):

{ "language": "zh", "outputLanguage": "Chinese", "refresh": { "runtimes": 1, "sessions": 2, "failed": 0 } }

language — это локализованный демоном UI-язык, поэтому запрос 'auto' может вернуть конкретный язык; хостам следует читать сохраненную настройку general.language для состояния селектора. outputLanguage равно null, если syncOutputLanguage было ложным. refresh.runtimes подсчитывает количество runtime’ов, применивших переключение через активный канал; refresh.sessions суммирует обновления системных инструкций по сессиям; refresh.failed агрегирует сбои обновления по сессиям и неудачные runtime’ы (рассылка выполняется best-effort и никогда не приводит к ошибке запроса после успешного сохранения).

Ошибки:

  • 400 {code: 'invalid_language', allowed: [...]} — неизвестный языковой код.
  • 400 {code: 'invalid_sync_flag'} — syncOutputLanguage не является булевым значением.
  • 400 {code: 'invalid_client_id'} — X-Qwen-Client-Id не является клиентом, известным какому-либо runtime (заголовок опционален; опустите его, если сессия не существует).
  • 500 {code: 'persist_error'} — запись файла настроек пользователя или выходного языка не удалась; шаг, вызвавший ошибку, и все последующие шаги не выполнялись (ранее примененные шаги, такие как запись general.language, не откатываются).
  • 404 — демон старше данного маршрута или был собран без поддержки сохранения настроек (проверьте тег возможности).

SSE событие (привязанное к workspace, во всех шинах сессий runtime’ов): language_changed с {language, outputLanguage, userLevel: true, originatorClientId?}. Маркер userLevel: true отличает эту рассылку от события по сессиям в маршруте сессии, которое содержит вместо этого sessionId.

GET /session/:id/events (SSE)

Подписаться на поток событий сессии.

Заголовки:

Accept: text/event-stream Last-Event-ID: 42 ← optional, replays from after id 42 X-Qwen-Event-Epoch: ... ← optional, pairs the cursor with its bus epoch X-Qwen-Client-Id: ... ← optional client identity and diagnostic correlation

Параметры запроса:

ПараметрОбязательныйПримечания
maxQueuedнетОграничение живого накопления кадров для каждого подписчика. Диапазон [16, 2048], по умолчанию 256. Кадры воспроизведения, принудительно отправляемые при подписке, не учитываются в ограничениях по кадрам и байтам; фактическое потребление происходит при поступлении живых событий, пока подписчик еще обрабатывает большое Last-Event-ID: 0 воспроизведение. Увеличьте значение для холодных переподключений, чтобы хвост живых событий не вызывал предупреждение о медленном клиенте / вытеснение до того, как потребитель догонит поток. Ограничение по сериализованным байтам для живых данных фиксировано на стороне демона (по умолчанию 2 MiB) и не имеет параметра запроса. Значения вне диапазона / не десятичные / присутствующие, но пустые возвращают 400 invalid_max_queued до открытия SSE рукопожатия. Предварительная проверка caps.features.slow_client_warning — старые демоны молча игнорируют этот параметр.
connectReasonнетКлиентская диагностическая подсказка: initial, resume, prompt_restart, stream_end, transport_error, state_resync или unknown. Недействительные значения нормализуются к unknown и никогда не отклоняют рукопожатие. Демон не использует это поле для авторизации, воспроизведения, вытеснения, дедупликации или замены потока.
previousStreamIdнетUUID предыдущего принятого REST/SSE потока, сообщаемый клиентом. Недействительные значения игнорируются. Это lineage только best-effort и никогда не меняет поведение потока.

Успешное рукопожатие включает X-Qwen-SSE-Stream-Id: <uuid>. Браузерные шлюзы должны сохранять этот заголовок ответа и предоставлять его через Access-Control-Expose-Headers. Старые демоны или промежуточные узлы могут опустить его; клиенты должны продолжать работу нормально и считать lineage недоступным. Идентификатор определяет данное физическое REST/SSE соединение и коррелирует жизненный цикл демона, диагностику очереди и трассировку запроса.

Формат кадра. Строка data: представляет собой полную оболочку события, сериализованную в JSON в одну строку — {id?, v, type, data, originatorClientId?}. Специфичная для ACP полезная нагрузка (аргументы sessionUpdate, requestPermission и т.д.) находится под полем data оболочки; собственное type оболочки совпадает со строкой SSE event:.

id: 7 event: session_update data: {"id":7,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}} id: 8 event: permission_request data: {"id":8,"v":1,"type":"permission_request","data":{"requestId":"<uuid>","sessionId":"<sid>","toolCall":{...},"options":[...]}} : heartbeat ← every 15s, no payload event: client_evicted ← terminal frame, no id (synthetic) data: {"v":1,"type":"client_evicted","data":{"reason":"queue_overflow","droppedAfter":42,"queueSize":256,"maxQueued":256,"queuedBytes":1800000,"maxQueuedBytes":2097152}} event: client_evicted ← terminal frame for byte overflow, no id (synthetic) data: {"v":1,"type":"client_evicted","data":{"reason":"queue_bytes_overflow","droppedAfter":43,"queueSize":1,"maxQueued":256,"queuedBytes":1900000,"maxQueuedBytes":2097152,"eventBytes":300000}}

Строки SSE уровня id: / event: дублируют envelope.id / envelope.type для совместимости с EventSource. Потребители raw-fetch (parseSseStream SDK) читают все из JSON-оболочки и игнорируют строки преамбулы SSE.

Тип событияТриггер
session_updateЛюбое уведомление ACP sessionUpdate (чанки LLM, вызовы инструментов, использование)
permission_requestАгент запросил одобрение инструмента
permission_resolvedНекоторые клиент проголосовал по разрешению через POST /permission/:requestId
permission_partial_vote(только консенсус) Голос был записан, но кворум еще не достигнут. Содержит {requestId, sessionId, votesReceived, votesNeeded, quorum, optionTallies}. Предварительная проверка caps.features.permission_mediation.
permission_forbiddenГолос был отклонен активной политикой (несоответствие designated, local-only не loopback или голосующий consensus отсутствует в снимке). Содержит {requestId, sessionId, clientId?, reason}. Предварительная проверка caps.features.permission_mediation.
model_switchedPOST /session/:id/model выполнено успешно
model_switch_failedPOST /session/:id/model отклонено
session_diedДочерний процесс агента неожиданно завершился. Терминальное: SSE поток закрывается после этого кадра; сессия исчезает из byId. Подписчики должны переподключиться через POST /session, чтобы создать новую.
slow_client_warningЛокально для подписчика: накопление живых кадров или сериализованных байтов ≥ 75% от лимита. Не терминальное — поток продолжается; предупреждение служит сигналом перед вытеснением. Содержит {queueSize, maxQueued, lastEventId, queuedBytes?, maxQueuedBytes?, threshold?}, где threshold равно frames, bytes или frames_and_bytes. Срабатывает ОДИН раз за эпизод переполнения; повторно активируется после того, как оба показателя упадут ниже 37.5%. Без id (синтетическое). Предварительная проверка caps.features.slow_client_warning.
client_evictedЛокально для подписчика: переполнение очереди. reason равно queue_overflow для лимита живых кадров и queue_bytes_overflow для лимита сериализованных байтов. Терминальное: SSE-поток закрывается после этого кадра (без id — синтетическое). Другие подписчики в той же сессии продолжают работу.
stream_errorОшибка на стороне демона во время рассылки. Терминальное: SSE-поток закрывается после этого кадра (без id — синтетическое).

Семантика переподключения:

  • Отправьте Last-Event-ID: <n> для повторного воспроизведения событий с id > n из кольцевого буфера каждой сессии (глубина по умолчанию 8000, настраивается через qwen serve --event-ring-size <n>).
  • Обнаружение пропусков: в пределах одной эпохи, если первый сохраненный id кольца больше, чем <n> + 1, демон отправляет кадр state_resync_required без id с полем reason: 'ring_evicted' перед повторным воспроизведением сохранившегося хвоста. Другие причины: epoch_reset, seeded_replay_not_in_ring и replay_budget_exceeded; сигнал бюджета следует за доставленным префиксом повторного воспроизведения. См. причины ресинхронизации и порядок для их триггеров, полей курсора и правил восстановления.
  • SDK фиксирует awaitingResync; клиентам следует вызывать POST /session/:id/load и восстанавливать состояние из текущего ограниченного окна снимка повторного воспроизведения. Этот снимок может сам начинаться с history_truncated, если более старые записи повторного воспроизведения в памяти были удалены; этот маркер носит информационный характер и не должен запускать новый цикл ресинхронизации. Его необязательный якорь пагинации фиксируется при вытеснении окна повторного воспроизведения.
  • ID монотонно возрастают в пределах эпохи шины событий сессии, начиная с 1. Отклоненная публикация не должна потреблять id; см. инварианты последовательности событий.
  • Синтетические кадры (client_evicted, slow_client_warning, stream_error, state_resync_required, replay_complete) намеренно не содержат id, чтобы они не занимали слоты последовательности для других подписчиков. replay_complete отмечает конец повторного воспроизведения, включая пустое или неполное воспроизведение; это не означает завершение ресинхронизации.

Обратное давление:

  • Очередь каждого подписчика по умолчанию содержит maxQueued: 256 живых элементов плюс принадлежащий демону лимит в 2 МиБ на сериализованные байты. Кадры повторного воспроизведения при переподключении, slow_client_warning и client_evicted обходят оба лимита.
  • Переопределите только лимит кадров через ?maxQueued=N (диапазон [16, 2048]) в запросе SSE. Специально отсутствует ?maxQueuedBytes; клиенты не могут увеличить бюджет памяти демона.
  • Когда накопление живых кадров или живых байтов у подписчика превышает 75% от лимита, шина принудительно отправляет этому подписчику синтетический кадр slow_client_warning (один раз за эпизод переполнения; повторно активируется после падения обоих показателей ниже 37.5%). Поток остается открытым — предупреждение служит сигналом, позволяющим клиенту быстрее освободить очередь или корректно отсоединиться и переподключиться.
  • Если происходит переполнение лимита живых кадров, шина отправляет client_evicted с reason: "queue_overflow". Пустая живая очередь пропускает одно слишком большое событие без изменений. Перед отклонением переполнения по байтам в непустой очереди шина может удалить целое rawOutput.html до '' приложения MCP, когда его fallbackText непусто. Если эта копия помещается, доставка продолжается без вытеснения или сигнала ресинхронизации. В противном случае отправляется reason: "queue_bytes_overflow". При любом вытеснении терминальный кадр отправляется принудительно, а подписка закрывается.
  • Это необязательное деградирование приложения влияет только на доставку из очереди. Прямая доставка и кольцо повторного воспроизведения сохраняют оригинал; окно повторного воспроизведения компактизации применяет свой собственный бюджет. Деградированная копия имеет тот же id события, что и оригинальная запись в кольце. Следовательно, доставленный полезный груз с id может не совпадать с полезной нагрузкой кольца, и возобновление после этого id не восстановит его HTML. Клиентам, которым требуется документ, необходимо явно повторно запросить сохраненную стенограмму, которая также подвержена ограничениям размера страницы.

POST /session/:id/permission/:requestId

Отправьте тот же голос, документированный ниже, но маршрутизируйте его через среду выполнения, которой принадлежит указанная активная сессия. Новые многопространственные интеграции должны использовать эту форму вместо устаревшего глобального для процесса маршрута. Предварительная проверка caps.features.session_permission_vote.

Тело запроса, политики медиации, результаты и успешный ответ идентичны POST /permission/:requestId. Необязательный заголовок X-Qwen-Client-Id участвует в политике назначения и консенсуса. Сбои используют стабильные значения code, где указано; некорректный ввод и гонка при потере ожидающего запроса могут опустить code:

  • 400 — некорректное тело голоса (нет code) или недействительный идентификатор клиента (invalid_client_id), или invalid_option_id, когда выбранный вариант не был предложен. Вместо повторной попытки того же голоса перечитайте предложенные варианты.
  • 403 — permission_forbidden, если активная политика отклоняет голосующего, или untrusted_workspace, если неосновное рабочее пространство с правами владельца не является доверенным. Неосновной владелец основного рабочего пространства освобождается от проверки доверия, и его голос может быть принят.
  • 404 — session_not_found, если нет активного владельца, или code, если запрос не находится в состоянии ожидания.
  • 500 — cancel_sentinel_collision, если allowedOptionIds агента содержит зарезервированный маркер __cancelled__, или ambiguous_session_owner, если более одного рабочего пространства претендует на сеанс.
  • 501 — permission_policy_not_implemented для политики, которая не реализована в этой сборке.
  • 503 — workspace_runtime_unavailable, если среда выполнения владельца недоступна, или daemon_draining, если демон больше не принимает задачи.

Повторные попытки через основной мост никогда не выполняются. Метод TypeScript SDK: respondToSessionPermission().

POST /permission/:requestId

Голосование по ожидающему permission_request. Активная политика медиации определяет победителя:

ПолитикаПоведение
first-responder (по умолчанию)Любой проверенный голосующий побеждает; последующие голосующие получают 404. Базовая линия до F3.
designatedТолько инициатор запроса (originatorClientId) решает; неинициаторы получают 403 permission_forbidden / designated_mismatch. Откатывается к первому ответившему для анонимных запросов.
consensusN из M голосующих должны согласиться (по умолчанию N = floor(M/2) + 1, переопределяется через policy.consensusQuorum). Побеждает первый вариант, набравший N. Неразрешенные голосования получают SSE-фреймы 200 + permission_partial_vote.
local-onlyРешают только локальные (loopback) голосующие; удаленные вызывающие получают 403 permission_forbidden / remote_not_allowed.

Активная политика настраивается в settings.json под ключом policy.permissionStrategy и отображается в /capabilities по адресу body.policy.permission. Предварительно проверьте caps.features.permission_mediation (с modes: [...]) для набора, поддерживаемого сборкой.

F3 (#4175): координация разрешений между несколькими клиентами. F3 добавила четыре вышеуказанные политики. До F3 демоны жестко использовали стратегию «первый ответивший»; формат данных в сети остается бит-в-бит неизменным, когда настроена политика first-responder. Новые события (permission_partial_vote, permission_forbidden) являются аддитивными — старые SDK воспринимают их как unrecognized_known_event и корректно игнорируют.

Тайм-аут разрешения (по умолчанию отключен). permission_request остается в состоянии ожидания, пока: (a) какой-либо клиент не проголосует здесь, (b) не сработает POST /session/:id/cancel, (c) HTTP-клиент, управляющий запросом, не отключится (отмена во время запроса завершает незавершенные разрешения со статусом cancelled), (d) сеанс не будет завершен, (e) демон не остановится, или (f) не сработает настроенный тайм-аут. При срабатывании тайм-аута requestPermission агента завершается со статусом {outcome: 'cancelled'}, в кольцевом буфере аудита фиксируется запись permission.timeout, в stderr демона выводится однострочное сообщение-маркер, а шина SSE рассылает стандартный кадр отмены permission_resolved, чтобы подписчики могли выполнить очистку. Общий тайм-аут можно настроить через BridgeOptions.permissionResponseTimeoutMs или qwen serve --permission-response-timeout-ms. Его значение по умолчанию — 0, поэтому как обычные разрешения, так и ask_user_question бесконечно ожидают решения человека. Отмена голосующим, отмена сеанса, очистка при отключении и остановка демона по-прежнему завершают незавершенные взаимодействия со статусом «отменено».

Запрос:

{ "outcome": { "outcome": "selected", "optionId": "proceed_once" } }

Результаты:

  • { "outcome": "selected", "optionId": "<one-of-the-options>" } — принять / отклонить / продолжить один раз / и т.д., согласно вариантам, предложенным агентом
  • { "outcome": "cancelled" } — отбросить запрос (соответствует внутреннему поведению cancelSession / shutdown)

Ответ:

  • 200 {} — ваш голос принят (разрешен ИЛИ записан при достижении кворума консенсуса)
  • 400 — недопустимое тело голоса (отсутствует code), invalid_client_id или invalid_option_id, если выбранный вариант не был предложен; вместо повторной отправки того же голоса повторно прочитайте список предложенных вариантов
  • 403 { "code": "permission_forbidden", "reason": "designated_mismatch" | "remote_not_allowed", "requestId", "sessionId" } — F3: активная политика отклонила ваш голос
  • 404 { "error": "..." } — requestId неизвестен (уже разрешен, никогда не существовал или сессия была уничтожена)
  • 500 { "code": "cancel_sentinel_collision", ... } — F3: allowedOptionIds агента содержит зарезервированный служебный маркер '__cancelled__'; нарушение контракта между агентом и демоном
  • 501 { "code": "permission_policy_not_implemented", "policy": "<name>" } — обратная совместимость F3: литерал политики попал в схему, но ветка медиатора для него еще не реализована (в настоящее время недостижимо; зарезервировано для будущих политик)

После успешного голосования каждый подключенный клиент видит permission_resolved с тем же requestId и выбранным outcome. При политике consensus промежуточные голоса дополнительно рассылают permission_partial_vote до достижения кворума.

Маршруты авторизации через Device Flow (issue #4175 PR 21)

Демон выступает посредником для OAuth 2.0 Device Authorization Grant (RFC 8628), позволяя удаленному клиенту SDK инициировать вход, токены которого сохраняются в файловой системе демона, а не клиента. Демон сам опрашивает IdP; единственная задача клиента — отобразить URL верификации и пользовательский код и (опционально) подписаться на SSE для получения событий завершения.

Тег возможности: auth_device_flow (всегда объявляется). Поддерживаемые провайдеры в v1: qwen-oauth.

Note

Для новых интеграций используйте Qwen Account with Subscription. GET /auth/subscription?paywallScene=cli возвращает ссылку на покупку в поле plansUrl. Отдельно запустите вход в браузере через POST /auth/subscription/flows?paywallScene=cli, затем опрашивайте GET /auth/subscription/flows/:id, чтобы получить authorizationUrl.

Бесплатный тариф Qwen OAuth закрыт 15 апреля 2026 года. qwen-oauth — прежний идентификатор провайдера v1 для описанных ниже маршрутов.

Локальность выполнения. Демон никогда не запускает браузер — даже если может. Клиент решает, вызывать ли open(verificationUri) локально; на headless-подке (каноническое развертывание Mode B) пользователь открывает URL на любом устройстве, где у него есть браузер. См. docs/users/qwen-serve.md для рекомендуемого UX.

Отсутствие утечки токенов в событиях. auth_device_flow_started содержит только {deviceFlowId, providerId, expiresAt}. Пользовательский код и URL верификации возвращаются точечно в теле ответа POST 201 и через GET /workspace/auth/device-flow/:id; они никогда не транслируются по SSE.

Единственный экземпляр на провайдера. Второй вызов POST для того же провайдера, пока поток находится в состоянии ожидания, является идемпотентным перехватом — он возвращает существующую запись с attached: true, не начиная новый запрос к IdP.

POST /workspace/auth/device-flow

Строгий контроль мутаций: первичные запросы без токена от доверенных loopback-источников проходят. Первичный запрос без токена, достигший контроля без полномочий доверенного loopback, получает 401 token_required; отсутствующие или недействительные настроенные учетные данные и неподтвержденные учетные данные Local Control отвергаются ранее с простой ошибкой 401 Unauthorized.

Запрос:

{ "providerId": "qwen-oauth" }

Ответ (201 новый запуск, 200 идемпотентный перехват):

{ "deviceFlowId": "fa07c61b-…", "providerId": "qwen-oauth", "status": "pending", "userCode": "USER-1", "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device?entry=qwen_code&paywallScene=cli", "verificationUriComplete": "https://chat.qwen.ai/api/v1/oauth2/device?user_code=USER-1&entry=qwen_code&paywallScene=cli", "expiresAt": 1700000600000, "intervalMs": 5000, "attached": false }

Ошибки:

  • 400 unsupported_provider — неизвестный providerId (ответ включает supportedProviders)
  • 409 too_many_active_flows — достигнут лимит рабочих пространств (4); отмените один через DELETE
  • 401 token_required — строгий контроль отказал первичному запросу без токена, не имеющему полномочий доверенного loopback
  • 502 upstream_error — IdP вернул непредвиденную ошибку

GET /workspace/auth/device-flow/:id

Чтение текущего состояния. Ожидающие записи возвращают userCode/verificationUri/expiresAt/intervalMs; терминальные записи (5-минутная пауза) убирают их и показывают status + необязательный errorKind/hint.

Возвращает 404 device_flow_not_found для неизвестных id и записей, удаленных после периода ожидания.

DELETE /workspace/auth/device-flow/:id

Идемпотентная отмена:

  • ожидающая запись → 204 + генерация auth_device_flow_cancelled
  • терминальная запись → 204 no-op (без повторной генерации события)
  • неизвестный id → 404

GET /workspace/auth/status

Снимок ожидающих потоков + поддерживаемых провайдеров:

{ "v": 1, "workspaceCwd": "/work/bound", "providers": [], "pendingDeviceFlows": [ { "deviceFlowId": "fa07c61b-…", "providerId": "qwen-oauth", "expiresAt": 1700000600000 } ], "supportedDeviceFlowProviders": ["qwen-oauth"] }

SSE-события Device Flow

Пять типизированных событий (привязаны к рабочему пространству, рассылаются во все активные шины сессий):

  • auth_device_flow_started {deviceFlowId, providerId, expiresAt} — POST выполнен успешно; SDK должен подписаться (здесь нет userCode, при необходимости получите его через GET)
  • auth_device_flow_throttled {deviceFlowId, intervalMs} — демон учел upstream slow_down; клиенты, опрашивающие GET, должны увеличить интервал опроса в соответствии с этим значением
  • auth_device_flow_authorized {deviceFlowId, providerId, expiresAt?, accountAlias?} — учетные данные сохранены; accountAlias является меткой без PII (никогда не email/телефон)
  • auth_device_flow_failed {deviceFlowId, errorKind, hint?} — терминальное состояние; errorKind является одним из expired_token | access_denied | invalid_grant | upstream_error | persist_failed. persist_failed является внутренним для демона: обмен с IdP прошел успешно, но демон не смог надежно сохранить учетные данные (EACCES / EROFS / ENOSPC). Пользователь должен повторить попытку после устранения проблемы с диском.
  • auth_device_flow_cancelled {deviceFlowId} — DELETE выполнен успешно для ожидающей записи

Не совместимо с MCP. Спецификация авторизации MCP (2025-06-18) требует OAuth 2.1 + PKCE с кодом авторизации и обратным вызовом перенаправления, что не работает для демонов в headless-pod. Поверхность device-flow в режиме B является приватной для демона — клиенты, ориентированные на серверы, совместимые с MCP, должны использовать другой путь авторизации.

Загрузка частей присоединенных файлов к сессии

Возможность: session_attachment_chunk_upload. Все четыре маршрута ниже являются мутациями, принадлежащими runtime активной сессии. Они используют существующий bearer и необязательные заголовки X-Qwen-Client-Id. Загрузка принадлежит хранилищу создавшей её сессии и точному идентификатору клиента; отсутствующий идентификатор отличается от названного клиента.

Пусть U = /session/:id/attachment-uploads:

ЗапросТелоОтвет
POST UJSON { name, mimeType, size }, максимум 4 КиБ201 { uploadId }
POST U/:uploadId/chunks?offset=NНеобработанный application/octet-stream, максимум 512 КиБ200 { offset }, следующий смещение байта
POST U/:uploadId/completeТело не требуется; JSON тело анализируется и игнорируется200 существующая ссылка на присоединенный файл { type, attachmentId, mimeType, size }
DELETE U/:uploadIdПусто204 для принимаемых, неудачных или отсутствующих загрузок; 409 во время завершения или после завершения

Максимальный размер присоединенного файла по-прежнему составляет 8 МиБ. Метаданные следуют существующим правилам для имени файла/MIME; пустые файлы изображений недействительны. Отправляйте последовательные части по 512 КиБ, с точным остатком для последней части. Только непосредственно предшествующая часть может быть повторена с тем же смещением, длиной и байтами. Разрывы, перекрытия, измененные повторы или преждевременное завершение возвращают 409; некорректный ввод возвращает 400, превышение размера — 413, а неподдерживаемый тип медиа части или кодировка JSON возвращают 415 invalid_attachment_upload_content_type. Неподдерживаемые кодировки запроса при создании и отправке частей возвращают 415 invalid_attachment_upload_encoding. Неизвестные, истекшие или принадлежащие другим владельцам ID загрузки возвращают 404 на трех маршрутах POST; DELETE возвращает 204 для отсутствующих загрузок и 409 во время или после завершения. Запросы на создание и отправку частей не принимают сжатые тела. Маршрут завершения использует глобальный парсер JSON, поэтому корректное сжатое JSON тело может быть проигнорировано, тогда как некорректный JSON отклоняется до завершения.

Завершение проверяет принадлежность сессии/клиента/runtime до и после сохранения. Конкурентное или повторяющееся завершение возвращает тот же результат без дополнительной записи. Конкурентная копия сессии временно возвращает 503 attachment_upload_store_busy с retryable: true; подготовленные байты остаются доступными для завершения с тем же ID загрузки после окончания копирования. Фактические сбои хранения остаются терминальными для этого ID загрузки. Принимаемые загрузки истекают через пять минут после создания. Квитанции об успехе/неудаче истекают через пять минут и ограничены 1024 на процесс демона. Закрытие/удаление сессии и финальная регистрация клиента аннулируют незавершенные загрузки. Только завершенные присоединенные файлы могут быть прочитаны, на них можно ссылаться или их можно скопировать. DELETE не может откатить ужеfinalizeing или завершенный присоединенный файл; завершенный файл может остаться после потери ответа.

Подготовка ограничена 128 МиБ и 32 активными загрузками во всех runtime процесса, с максимум восемью активными загрузками на хранилище сессии. Исчерпание возвращает 429 attachment_upload_capacity_exceeded; существующие ограничения скорости запросов также применяются. Это ограничение буфера подготовки, а не общая память процесса. Временные ID не переживают перезапуск демона.

TypeScript SDK автоматически использует этот протокол для присоединенных файлов размером более 512 КиБ, если он объявлен, с двумя одновременными файлами на клиента и последовательными частями. Он использует аутентифицированный REST даже с транспортами ACP, кэширует корректное обнаружение на 60 секунд и сохраняет существующую необработанную конечную точку для меньших файлов и более старых демонов. Повторные попытки добавления/завершения выполняются не более двух раз после сетевых ошибок, таймаутов запросов или 502/503/504; повторные попытки при ограничении скорости учитывают Retry-After в пределах пятиминутного дедлайна операции. Неоднозначные ошибки создания и исчерпания емкости не повторяются. Для неудачных загрузок выполняется одна независимая попытка очистки через две секунды. DaemonAttachmentUploadError сохраняет причину и httpStatus; его status опускает внутренние для загрузки 404, чтобы потребители не приняли это за неподдерживаемую легаси маршрутизацию присоединенных файлов.

См. полный дизайн для деталей жизненного цикла и совместимости.

Перезагрузка каталога моделей

Возможность workspace_model_reload объявляет POST /workspaces/:workspace/models/reload. Эта аутентифицированная, доверенная, выбранная-runtime мутация повторно читает настройки пользовательского и рабочего пространства scope, обновляет overlay среды выбранного runtime и повторно применяет среду в его дочернем процессе ACP перед перестроением конфигурации поставщика моделей. Изменения среды поддерживают учетные данные поставщика и могут включать другие ключи окружения; они не сообщаются в ответе или событии настроек. Окружение процесса демона и включение workflow остаются неизменными. Возвращает { "status": "applied" | "deferred" | "failed" }; deferred означает, что живой дочерний процесс ACP не применил каталог. Он не загружает платформенный каталог, не пишет настройки, не переключает активную модель разговора и не рассылает событие настроек. После этого прочитайте /session/:id/context существующей сессии для её текущих выбираемых моделей; перед созданием сессии прочитайте поставщиков выбранного рабочего пространства. Неизвестные, недоверенные, draining или закрытые runtime не переходят на primary.

Формат данных потока

События выдаются как стандартные кадры EventSource. Демон записывает одну строку data: на кадр (JSON не содержит встроенных новых строк после JSON.stringify); парсер SDK в packages/sdk-typescript/src/daemon/sse.ts обрабатывает как эту форму, так и разрешенную спецификацией много-data: форму на стороне приема.

Кадры ошибок во время потоковой передачи

Если итератор bridge выбрасывает исключение при обслуживании подписчика SSE, демон выдает терминальный кадр stream_error (без id). Строка data: представляет собой полный конверт (такая же форма, как у любого другого кадра SSE в этом документе); фактическое сообщение об ошибке находится под envelope.data.error:

event: stream_error data: {"v":1,"type":"stream_error","data":{"error":"<message>"}}

Затем соединение закрывается.

Переменные окружения

ПеременнаяНазначение
QWEN_SERVER_TOKENBearer-токен. Обрезается по пробелам в начале и конце при запуске.

Структура исходного кода

ПутьНазначение
packages/cli/src/commands/serve.tsКоманда yargs + схема флагов
packages/cli/src/serve/run-qwen-serve.tsЖизненный цикл слушателя + обработка сигналов
packages/cli/src/serve/server.tsСборка приложения Express, порядок middleware и оставшиеся прямые маршруты
packages/cli/src/serve/routes/*.tsСфокусированные группы маршрутов Express, включая сессии, SSE, авторизацию рабочего пространства, статус рабочего пространства и файловые маршруты
packages/cli/src/serve/auth.tsbearer + allowlist хостов + запрет CORS
packages/cli/src/serve/acp-session-bridge.tsФасад совместимости моста для локального CLI, обеспечивающий spawn-or-attach, FIFO для каждой сессии и реестр разрешений
packages/acp-bridge/src/status.tsТипы данных wire для статуса демона (только чтение) + ServeErrorKind + BridgeTimeoutError + mapDomainErrorToErrorKind
packages/cli/src/serve/env-snapshot.tsЧистая вспомогательная функция, которая формирует полезные нагрузки /workspace/env из состояния process.*, включая удаление учетных данных
packages/acp-bridge/src/eventBus.tsОграниченная асинхронная очередь + кольцо воспроизведения
packages/sdk-typescript/src/daemon/DaemonClient.tsTS клиент
packages/sdk-typescript/src/daemon/sse.tsПарсер кадров EventSource
integration-tests/cli/qwen-serve-routes.test.ts18 тестов, без LLM
integration-tests/cli/qwen-serve-streaming.test.ts3 теста, реальный дочерний процесс qwen --acp, поддерживаемый локальным фейковым сервером OpenAI (только POSIX; пропускается на Windows)
Last updated on