Справочник по HTTP-протоколу qwen serve
Этап 1 дизайна демона qwen-code . Все маршруты находятся по базовому URL демона (по умолчанию http://127.0.0.1:4170).
Аутентификация
Если демон был запущен с флагом --token или переменной окружения QWEN_SERVER_TOKEN, каждый маршрут, кроме /health при привязке к loopback-интерфейсу, должен содержать заголовок:
Authorization: Bearer <token>Без настроенного токена (стандартно для loopback в режиме разработки) заголовок необязателен. Сравнение токенов выполняется за константное время. Ответы с кодом 401 унифицированы для случаев missing header / wrong scheme / wrong token.
--open-with-auth. Этот режим CLI, отключённый по умолчанию, требует привязки к loopback и доступного Web Shell. Он повторно использует обычный выбор --token поверх QWEN_SERVER_TOKEN или генерирует 32 случайных байта в кодировке base64url до запуска демона, если этот выбор пуст. Браузер получает выбранный bearer через #token= и сохраняет его для каждой вкладки; протокол и middleware видят обычный настроенный токен. Обычный --open, прямые встраивающие вызывающие, привязки не к loopback и другие клиенты не получают автоматических учётных данных. В окружениях без поддержки браузера выводится URL с секретом в фрагменте для ручного открытия. Loopback /health и статические ресурсы Web Shell сохраняют исключения, описанные ниже; --require-auth всё равно защищает /health.
Исключение для /health (Bctum): при привязке к loopback-интерфейсу (127.0.0.1 / localhost / ::1 / [::1]) маршрут /health регистрируется ДО bearer middleware, поэтому liveness-пробам внутри пода не нужно передавать токен, даже если демон был запущен с флагом --token. При привязке к не-loopback интерфейсам (--hostname 0.0.0.0 и т.д.) /health защищается bearer-аутентификацией, как и любой другой маршрут — см. раздел GET /health для получения подробностей.
--require-auth (#4175 PR 15). Передайте этот флаг при запуске, чтобы распространить правило «обязательное наличие токена» и на loopback-интерфейс. Запуск завершится ошибкой без токена; исключение для /health отменяется (поэтому /health также требует Authorization: Bearer …).
Когда флаг включен, глобальный bearerAuth middleware защищает каждый маршрут, включая /capabilities. Поэтому неаутентифицированный клиент не может выполнить pre-flight запрос к caps.features, чтобы узнать, что требуется аутентификация: поверхностью обнаружения в этом случае является само тело ответа 401 (унифицированное для всех маршрутов согласно разделу Аутентификация). Тег возможности require_auth — это подтверждение пост-аутентификации: как только клиент успешно проходит аутентификацию и читает /capabilities, наличие тега подтверждает, что демон был запущен с флагом --require-auth (полезно для UI аудита/соответствия требованиям и для SDK-клиентов, чтобы отображать «это развертывание усилено» на панели настроек). Маршруты мутации, подключенные к строгому режиму для каждого маршрута (доработки Wave 4), отклоняют запрос с 401 { code: "token_required", error: "…" } при обращении к ним в конфигурации loopback по умолчанию без токена — но при включенном --require-auth глобальный bearer middleware прерывает запрос до проверки на уровне маршрута, поэтому неаутентифицированные вызывающие стороны фактически видят устаревшее тело Unauthorized.
--allow-origin <pattern> (T2.4 #4514 ). Браузерные веб-интерфейсы, обращающиеся к демону с другого origin, по умолчанию блокируются — любой запрос с заголовком Origin возвращает 403 {"error":"Request denied by CORS policy"}, поскольку CLI/SDK-клиенты никогда не отправляют Origin, и демон расценивает его наличие как признак того, что запрос поступил из браузерного контекста, который оператор не разрешил. Передайте --allow-origin <pattern> (флаг можно повторять) при запуске, чтобы установить список разрешенных origin вместо сплошной блокировки. Каждый паттерн может быть:
- Буквальное
*— разрешить любой origin. Опасно: запуск завершится ошибкой, если настроено*, но не задан bearer-токен (из любого источника:--token,QWEN_SERVER_TOKENили--require-auth, который требует токен при запуске). При наличии*в списке boot-процесс выводит предупреждение в stderr. Рекомендация: используйте в связке с--require-authпри привязке к loopback, чтобы/healthтакже защищался bearer-аутентификацией — по умолчанию он регистрируется до bearer middleware на loopback (чтобы k8s/Compose-пробы могли обращаться к/healthбез токена), а allowlist*делает его доступным из любого cross-origin браузера.--require-authвсё равно оставляет статические ассеты Web Shell (/,/assets/*и навигации по документам/session/:id) pre-auth на loopback по дизайну — они монтируются до bearer middleware — поэтому при allowlist*они остаются доступными для чтения из любого cross-origin браузера;--no-webубирает эту поверхность. При привязке к не-loopback интерфейсам bearer уже обязателен при запуске и/healthрегистрируется за ним, поэтому единственная поверхность, которую*открывает без токена — это статические ассеты Web Shell (/,/assets/*и навигации по документам/session/:id— их JS всё равно обращается к маршрутам, защищённым токеном).--no-webубирает и это; фактическая поверхность API защищена в любом случае. - Канонический URL origin —
<scheme>://<host>[:<port>]. Без завершающего слэша, без пути, без userinfo, без query. Запуск завершится ошибкойInvalidAllowOriginPatternError, если запись не проходит проверкуnew URL(pattern).origin === pattern; в сообщении об ошибке указывается некорректный паттерн и каноническая форма. Строгость заложена намеренно: тихая нормализация (например, удаление завершающего/) позволила бы опечаткам проскользнуть и принимать неоднозначный ввод.
Сопоставленные origin получают стандартные заголовки ответа CORS в каждом запросе:
Access-Control-Allow-Origin: <echoed origin>
Vary: Origin
Access-Control-Allow-Methods: GET, POST, 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-IdAccess-Control-Allow-Origin дословно повторяет origin запроса (в нижнем/верхнем регистре, как отправил браузер), а не буквальное *, даже при использовании паттерна * — кэши браузера ключируют ответы по нему в паре с Vary: Origin, а повторение оставляет возможность добавить Access-Control-Allow-Credentials в будущих релизах без изменения схемы. Открытые заголовки позволяют браузерным веб-интерфейсам учитывать подсказки о повторных попытках, сохранять эпоху SSE и коррелировать принятые физические потоки. Access-Control-Allow-Credentials на данный момент НЕ отправляется: демон аутентифицируется через bearer в Authorization, что работает cross-origin без credentials: 'include'.
OPTIONS preflight-запросы (OPTIONS с Access-Control-Request-Method или Access-Control-Request-Headers) прерываются с кодом 204 No Content и заголовками, указанными выше. Это стандартный паттерн CORS, и он безопасен — preflight только подтверждает, какие методы/заголовки демон будет принимать; фактический последующий запрос всё равно проходит полную цепочку (allowlist хостов → bearer-аутентификация → маршруты), поэтому защита от DNS-rebinding и проверка bearer срабатывают до чтения или изменения любого состояния. Обычные OPTIONS-запросы из сопоставленных origin продолжают передаваться дальше с прикрепленными заголовками CORS.
Origin, не совпадающие с allowlist, всё равно получают 403 {"error":"Request denied by CORS policy"} — ту же оболочку, что и при сплошной блокировке по умолчанию, поэтому клиентам, которые уже парсят ответ блокировки, не нужно обрабатывать особый случай для демонов с развернутым allowlist. Путь отклонения не отправляет никаких заголовков Access-Control-* (браузер всё равно бы их проигнорировал, а их отправка косвенно раскрыла бы размер allowlist через наличие заголовков).
Список настроенных паттернов намеренно НЕ повторяется в /capabilities — браузерный веб-интерфейс и так знает свой собственный origin (в конце концов, он обратился к демону), а вывод списка позволил бы неаутентифицированному читателю /capabilities перечислить все доверенные origin (полезная разведка для неправильно настроенного развертывания). SDK-клиенты ориентируются на тег caps.features.allow_origin для понимания «этот демон принимает cross-origin запросы из браузера», не зная конкретных origin.
Loopback self-origin запросы (например, когда Web Shell обращается к демону на том же 127.0.0.1:port) обрабатываются отдельным shim-слоем для удаления Origin, который работает ДО CORS middleware и удаляет заголовок Origin для 127.0.0.1:port / localhost:port / [::1]:port / host.docker.internal:port. Поэтому они проходят независимо от конфигурации --allow-origin — операторам не нужно указывать собственный порт демона, чтобы заставить работать демо-страницу.
Общий формат ошибок
Ответы с кодом 5xx содержат code и data исходной ошибки, если они присутствуют (в стиле JSON-RPC — ACP SDK пересылает {code, message, data} от агента):
{
"error": "Internal error",
"code": -32000,
"data": { "reason": "model quota exceeded" }
}Некорректный JSON в теле запроса возвращает:
{ "error": "Invalid JSON in request body" }со статусом 400.
SessionNotFoundError для неизвестного id сессии возвращает:
{
"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"
}Используйте это для pre-flight обнаружения несовпадения: прочитайте 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 отклоняет новую сессию, возвращается та же форма ответа с "scope": "total".
Подключения к существующим сессиям НЕ учитываются в лимите, поэтому переподключения к неактивному демону продолжают работать даже при достижении лимита.
RestoreInProgressError — возвращается POST /session/:id/load, POST /session/:id/resume или POST /session с id, предоставленным вызывающей стороной, когда другую регистрацию уже владеет этим 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"
}Возникает, когда session/load вызывается для id, по которому уже выполняется session/resume (или наоборот), или когда спавн с id, предоставленным вызывающей стороной, конкурирует с любым направлением восстановления. Подождите хотя бы Retry-After секунд и повторите попытку. Гонки с одинаковыми действиями (load против load, resume против resume) объединяются вместо выдачи ошибки, пока восстановление активно.
reason различает два забора, которые используют этот код, и заголовок Retry-After отслеживает его:
reason | Значение | Retry-After |
|---|---|---|
restore_in_progress | Выполняется обычное восстановление. | 5 (аналогично session_limit_exceeded) |
awaiting_abandoned_cleanup | Публичный вызывающий уже получил 504, и не отменяемый запрос ACP плюс его очистка ещё не завершились. | эффективный бюджет восстановления в секундах, ограниченный 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_id_override', 'session_scope_override',
'session_load', 'session_resume', 'session_transcript',
'unstable_session_resume',
'session_list', 'session_info', 'session_prompt', 'session_mid_turn_message_mutation',
'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_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', 'session_monitor_tool_correlation', 'session_stats',
'session_lsp', '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',
'session_approval_mode_control', 'workspace_tool_toggle', 'workspace_skill_toggle',
'workspace_skill_batch_toggle',
'extension_batch_activation_v2',
'workspace_settings', 'workspace_init', 'workspace_mcp_restart',
'session_recap', 'session_generation', 'session_btw', 'session_shell_command',
'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', 'session_rewind',
'workspace_hooks', 'session_hooks', 'workspace_extensions',
'session_branch', 'rate_limit', 'workspace_reload', 'channel_delivery',
'multi_workspace_sessions', 'multi_workspace_session_rewind',
'multi_workspace_session_shell', 'persistent_workspace_registration',
'workspace_display_name',
'workspace_qualified_rest_core', 'workspace_qualified_voice',
'workspace_qualified_memory', 'extension_management_v2', 'extension_git_credentials',
'workspace_persisted_transcript',
'workspace_session_export', 'workspace_archived_session_export',
'workspace_session_live_state',
'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_scope_override — это дескриптор согласования для поля sessionScope в каждом запросе к POST /session (см. ниже). Старые демоны молча игнорируют это поле, поэтому SDK-клиенты должны предварительно проверять caps.features на наличие этого тега перед его отправкой.
session_id_override — это дескриптор согласования для опционального 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. Записи возможностей рабочих пространств добавляют опциональный removable; только строки с removable: true могут быть удалены. Удаление также забывает все псевдонимы постоянной регистрации для рантайма, но никогда не удаляет файлы, настройки, транскрипты или архивы.
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 WebUI даёт 15 секунд; клиенты, обращающиеся к старому демону, должны использовать 70 секунд и 75 секунд соответственно.
session_transcript анонсирует GET /session/:id/transcript — постраничное представление транскрипта только для чтения поверх сохранённого JSONL активной сессии. Он отделён от /load: не подключает клиент, не заполняет живую шину событий, не создаёт живую сессию и не изменяет окно живого воспроизведения. Клиенты должны использовать его, когда им нужен полный транскрипт на диске для длинной сессии, и продолжать использовать /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: выпущенные демоны могут анонсировать более широкую возможность workspace REST без реализации этого маршрута, поэтому клиенты должны предварительно проверять этот тег напрямую. Тег безусловный, поскольку доверенное основное рабочее пространство с одной рабочей областью может использовать маршрут по id или cwd; проверки доверия для каждого рабочего пространства по-прежнему применяются при каждом запросе, и маршрут не расширяет разрешительную политику чтения сохранённого каталога для недоверенных вторичных рабочих пространств на состояние живого бриджа. Тег означает, что эндпоинт существует; он не обещает, что каждый живой элемент содержит опциональный watermark активности updatedAt, который зависит от жизненного цикла.
slow_client_warning описывает поведение backpressure для SSE: (a) демон отправляет синтетический фрейм потока событий slow_client_warning, когда очередь живых фреймов или очередь живых сериализованных байт подписчика превышает 75% заполнения, один раз за эпизод переполнения (сбрасывается после того, как оба показателя падают ниже 37,5%); (b) GET /session/:id/events принимает query-параметр ?maxQueued=N (диапазон [16, 2048]) для предварительного задания размера очереди фреймов на каждого подписчика при холодных переподключениях к большому кольцу повтора. Лимит сериализованных байт контролируется демоном (по умолчанию 2 MiB на подписчика), работает только для живых соединений и намеренно не имеет query-параметра. Размер кольца для всего демона управляется флагом --event-ring-size (по умолчанию 8000, согласно #3803 §02). Старые демоны молча не поддерживают поведение предупреждений/query-параметров — предварительно проверяйте этот тег перед его использованием.
typed_event_schema анонсирует, что полезные нагрузки событий демона соответствуют схеме SDK KnownDaemonEvent. Старые демоны могут по-прежнему транслировать совместимые фреймы, но 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 должны предварительно проверять эти теги перед отображением UI группировки или закрепления.
session_archive анонсирует API архивации состояния каталогов v1: POST /sessions/archive, POST /sessions/unarchive и GET /workspace/:id/sessions?archiveState=active|archived. Архивированные сессии не могут быть загружены или возобновлены до тех пор, пока не будут разархивированы.
workspace_qualified_rest_core анонсирует маршруты REST с множественным числом под /workspaces/:workspace/.... Селектор разрешается сначала как точный id рабочего пространства, затем как URL-кодированный абсолютный cwd после канонизации. Новые демоны с одним рабочим пространством включают основной рантайм в workspaces[] даже при отсутствии multi_workspace_sessions, позволяя клиентам обнаруживать id, необходимый для маршрутов с указанием рабочего пространства; клиенты должны использовать фолбэк на capabilities.workspaceCwd для старых демонов, не включающих массив. Статус доверия и маршруты запросов доверия доступны для зарегистрированных недоверенных рабочих пространств; маршруты чтения файлов следуют существующей политике чтения файловой системы. Зарегистрированные недоверенные вторичные рабочие пространства также предоставляют каталоги сессий и групп сессий только из сохранённых данных: эти чтения не подключаются к сессии, не запускают ACP и не объединяют состояние живого бриджа. Запись файлов, мутации каталогов и другие маршруты с множественным числом требуют доверенного рабочего пространства, если отдельная возможность явно не определяет более узкую политику только для чтения, такую как workspace_persisted_transcript. Недоверенное основное рабочее пространство по-прежнему получает 403 { code: "untrusted_workspace" } от маршрутов каталогов с множественным числом и транскриптов; устаревшие маршруты с единственным числом для основного рабочего пространства сохраняют своё существующее поведение совместимости. Этот тег охватывает основные поверхности файлов, статуса, настроек, разрешений, доверия, жизненного цикла, управления MCP, переключения инструментов и навыков, памяти, CRUD агентов рабочего пространства и хранилища сессий. Он не охватывает аутентификацию, голос, расширения, транспорт ACP/WebSocket, маршрутизацию channel-воркеров или экспорт сессий с указанием рабочего пространства; предварительно проверяйте workspace_session_export или workspace_archived_session_export отдельно. Доверие рабочего пространства — это не ACL: клиент, владеющий токеном демона, может читать все зарегистрированные поверхности рабочего пространства, разрешённые этой политикой.
workspace_qualified_voice анонсирует маршруты Voice, выбираемые доверенным рантаймом рабочего пространства: GET и POST /workspaces/:workspace/voice, POST /workspaces/:workspace/voice/transcribe и WS /workspaces/:workspace/voice/stream. Анонсируется только когда включены рантаймы нескольких рабочих пространств и общий слушатель WebSocket ACP/Voice. Селектор следует тем же правилам id-или-URL-кодированного-абсолютного-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 для всех квалифицированных модальностей 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 читает их обратно. Анонсируется только когда включены ACP HTTP и рантаймы нескольких рабочих пространств. Селектор следует тем же правилам id-или-URL-кодированного-абсолютного-cwd, что и другие маршруты с множественным числом. Каждое зарегистрированное рабочее пространство получает свою полосу задач; квалифицированная полоса основного рабочего пространства — это тот же экземпляр, что и поверхность с единственным числом /workspace/memory, поэтому задача, поставленная в очередь на одном, читается на другом. Разрешение строго по выбранному рантайму без фолбэка на основной: неизвестный селектор возвращает 400 { code: "workspace_mismatch" }, недоверенный селектор возвращает 403 { code: "untrusted_workspace" }, а неактивный или дренируемый рантайм возвращает 503 { code: "workspace_runtime_unavailable" }. Чтения никогда не выделяют полосу, поэтому опрос рабочего пространства без задач возвращает 404 { code: "<kind>_task_not_found" }. Id задач привязаны к своей полосе и не переживают реконфигурацию рабочего пространства или замену рантайма; устаревший id возвращает 404, а не условие потери данных. Когда ACP HTTP отключен, тег не анонсируется, и неквалифицированный запрос не к основному рабочему пространству возвращает не повторяемый 501 { code: "workspace_memory_unavailable" }, тогда как квалифицированный маршрут основного рабочего пространства продолжает работать через локальную полосу.
session_lsp анонсирует GET /session/:id/lsp — снимок структурированного статуса LSP только для чтения для клиентов демона. Старые демоны возвращают 404; предварительно проверяйте этот тег перед отображением удаленного статуса LSP.
session_status анонсирует GET /session/:id/status — сводку live-моста для одной сессии по её id. Помимо clientCount и hasActivePrompt, живые сессии предоставляют isWaitingForPermission, isWaitingForUserQuestion, pendingInteractionCount и сохранённый turnError после неудачного хода. Ошибка очищается при фактическом запуске следующего промпта. Живая сессия, завершившая ход running в текущем бридже, также несёт updatedAt — тот же watermark активности, задокументированный в маршруте live-state; поскольку этот маршрут возвращает сводку бриджа напрямую, значение не сливается с 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_toggle, workspace_skill_batch_toggle, extension_batch_activation_v2, workspace_init и workspace_mcp_restart анонсируют маршруты управления мутациями, задокументированные ниже. Они строго защищены mutation gate (демон, настроенный без bearer-токена, отклоняет их с 401 token_required). Старые демоны возвращают 404; предварительно проверяйте каждый тег перед предоставлением соответствующей функции.
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 для текста/списка/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 — это строгие маршруты мутации
и требуют настроенного bearer-токена даже на loopback.
workspace_file_upload охватывает POST /file/upload — маршрут загрузки бинарных данных:
тело application/octet-stream ограничено MAX_UPLOAD_BYTES (50 МиБ) и
записывается в рабочее пространство без перезаписи — занятое имя
автоматически нумеруется (name (1).ext, name (2).ext, …). Это также строгий
маршрут мутации.
Когда анонсирован 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.
Тот же тег также предоставляет 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. Клиенты должны предварительно проверять этот тег перед отправкой URL userinfo или credentialPersistence; старые демоны отклоняют URL-учётные данные. Тег описывает поддержку протокола на стороне бэкенда, а не доступность хранилища ключей: режим stored сообщает выбранный бэкенд в терминальном результате операции.
extension_batch_activation_v2 добавляет PUT /extensions/activation и PUT /workspaces/:workspace/extensions/activation. Оба принимают от 1 до 100 имён в extensionNames, дедуплицируют их без учёта регистра с сохранением порядка первого появления, сохраняют изменённые цели в одной генерации и возвращают один operation handle 202. Цель не обязана быть установлена при установке enabled или disabled: её имя создаёт декларацию желаемого состояния, которая сохраняется при установке расширения с таким именем. Глобальный маршрут принимает state: "enabled" | "disabled", записывает V2 defaultActivation и согласует каждый зарегистрированный рантайм. Маршрут рабочего пространства также принимает "inherit", применяет или очищает точные переопределения для выбранного доверенного рантайма и согласует только этот рантайм. inherit не декларирует неизвестное имя; полная очистка неизвестных сообщает updated: false и пропускает согласование. Единичные маршруты активации остаются только для установленных и адресуются по id.
Контракт провода Extension Management V2
Все маршруты используют правила bearer-аутентификации демона, описанные выше. X-Qwen-Client-Id необязателен для маршрутов мутации V2; при указании он должен идентифицировать клиента, зарегистрированного в одном из целевых рантаймов рабочего пространства мутации. :extensionId — это 64-символьный шестнадцатеричный идентификатор расширения в нижнем регистре. :workspace разрешается сначала как точный id рабочего пространства, а иначе как URL-кодированный абсолютный cwd после канонизации.
| Метод и путь | Успех |
|---|---|
GET /extensions | 200 глобальный каталог артефактов |
PUT /extensions/activation | 202 пакетная операция глобальной активации по умолчанию |
PUT /extensions/:extensionId/activation | 202 операция глобальной активации по умолчанию |
POST /extensions/install | 202 операция установки |
POST /extensions/check-updates | 202 операция проверки обновлений |
POST /extensions/:extensionId/update | 202 операция обновления |
DELETE /extensions/:extensionId | 202 операция удаления или идемпотентный 204, если расширение отсутствует |
GET /extensions/operations/:operationId | 200 снимок операции |
GET /workspaces/:workspace/extensions | 200 проекция активации рабочего пространства |
PUT /workspaces/:workspace/extensions/activation | 202 пакетная операция точной активации рабочего пространства |
PUT /workspaces/:workspace/extensions/:extensionId/activation | 202 операция точной активации рабочего пространства |
DELETE /workspaces/:workspace/extensions/:extensionId/activation | 202 операция очистки переопределения |
POST /workspaces/:workspace/extensions/refresh | 202 операция обновления рантайма |
Ответ глобального каталога:
{
"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. ref не применяется к npm, а 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, если не указан. Режим stored сохраняет учётные данные через гибридное хранилище секретов демона и сохраняет только чистый URL репозитория в метаданных установки, поэтому расширение остаётся обновляемым. Режим one-time не сохраняет ни URL репозитория, ни учётные данные и создаёт не обновляемый snapshot; autoUpdate: true отклоняется для этого режима. Передача поля без URL-учётных данных, передача недействительных учётных данных или использование учётных данных с npm, архивом, локальным, SSH или не-Git источниками возвращает 400.
Ответы и операции установки с учётными данными предоставляют credentialPersistence и могут предоставлять credentialStorage как keychain или encrypted_file. Операции one-time опускают source; операции stored могут возвращать чистый source. Записи каталога/статуса snapshot опускают source, устанавливают credentialPersistence в one_time и сообщают not updatable. Обновление завершается ошибкой extension_not_updatable; недоступный сохранённый секрет завершается ошибкой до сетевого доступа с extension_credential_unavailable.
Глобальные PUT-запросы активации и активации рабочего пространства используют одинаковое тело:
{ "state": "enabled" }state принимает значение enabled или disabled. Запросы обновления, удаления, проверки обновлений, очистки активации и обновления не имеют обязательного тела.
Пакетные запросы активации используют имена расширений:
{
"extensionNames": ["formatter", "review-tools"],
"state": "disabled"
}Пакетный запрос рабочего пространства также принимает "state": "inherit". Терминальные глобальные результаты содержат name и defaultActivation; результаты рабочего пространства содержат name, workspaceActivation (null для inherit) и 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. История операций является локальной для процесса, хранит только ограниченное количество терминальных записей и теряется при перезапуске демона; клиенты должны повторно читать каталог или проекцию рабочего пространства и сравнивать генерации, когда id операции исчезает.
Снимок операции имеет следующий вид:
{
"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, ключированные по имени расширения, со значениями, такими как checking for updates, update available, up to date, not updatable или error.
Долговременный коммит с неполной очисткой или согласованием рантайма не сообщается как неудачная мутация. Он возвращает succeeded_with_warnings и сохраняет зафиксированный результат:
{
"v": 1,
"operationId": "<operation-id>",
"operation": "activation",
"status": "succeeded_with_warnings",
"createdAt": 1750000000000,
"updatedAt": 1750000000200,
"result": {
"status": "disabled",
"name": "demo",
"refreshed": 1,
"failed": 1
},
"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 для неверных опций source/ref/registry, отсутствующего согласия или отсутствующей/неверной начальной активации. Мутация, завершившаяся ошибкой после 202, представляется, пока она хранится в истории операций, с status: "failed", error и необязательным стабильным code; распространённые коды включают extension_prepare_timeout и extension_conflict. HTTP 404 для операции не подразумевает откат, поскольку история операций не является долговременной.
daemon_status анонсирует GET /daemon/status — консолидированный диагностический снимок
только для чтения для оператора, задокументированный ниже.
Условные теги. Эти теги возможностей анонсируются только при включении соответствующего переключателя развертывания, подключении рантайма или выполнении условия доступности. Наличие тега означает, что документированное поведение доступно; отсутствие означает либо более старый демон до появления этого тега, либо текущий демон, где это условие не выполнено. В настоящее время:
| Тег | Анонсируется, когда … |
|---|---|
require_auth | демон был запущен с флагом --require-auth (или requireAuth: true через встроенный API). Bearer-токен обязателен для каждого маршрута, включая /health на loopback-биндах. |
mcp_workspace_pool | общий пул транспортов MCP активен. Пропускается, когда QWEN_SERVE_NO_MCP_POOL=1 отключает пул. |
mcp_pool_restart | общий пул транспортов MCP активен; ответы на перезапуск могут включать многоэлементные формы с учетом пула. |
external_tool_guard | qwen serve завершил стартовое рукопожатие для --external-tool-guard-mode=required; каждый созданный канал ACP должен подтвердить установленный callback до создания сессии, и каждый поддерживаемый управляемый инструмент ACP верхнего уровня, достигающий границы выполнения, должен получить одно внешнее разрешение перед выполнением. Более ранние отказы разрешений/хуков не делают запрос к провайдеру. Вложенное выполнение AgentCore находится за пределами v1 и отклоняется. |
allow_origin | T2.4 (#4514 ). Демон был запущен хотя бы с одним --allow-origin <pattern> (или allowOrigins: [...] через встроенный API). Кросс-доменные запросы с совпадающих источников получают правильные заголовки ответов CORS; несовпадающие источники по-прежнему получают стандартный 403. Настроенный список паттернов намеренно НЕ возвращается в /capabilities, чтобы не раскрывать набор доверенных источников неаутентифицированным читателям — браузерный webui уже знает свой собственный источник. |
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 | демон был создан с доступным сохранением настроек. |
workspace_voice | сохранение настроек доступно, поэтому устаревшие маршруты Voice основного рабочего пространства активны. |
workspace_voice_transcription | основное рабочее пространство имеет настроенную модель транскрипции Voice. |
session_shell_command | выполнение shell-команд в сессии явно включено. |
session_artifacts_persistence | сохранение артефактов сессии подключено для рантайма. |
session_generation | доступны помощники генерации сессии. |
workspace_generation | доступны помощники генерации уровня рабочего пространства. |
rate_limit | --rate-limit / QWEN_SERVE_RATE_LIMIT=1 / ServeOptions.rateLimit включен. |
workspace_reload | поддержка перезагрузки рабочего пространства доступна в конфигурации встроенных маршрутов. |
workspace_trust_hot_reload | мониторинг политики доверия рабочего пространства и согласование поколения рантайма подключены, поэтому изменения доверия вступают в силу без перезапуска демона, а отчёты о статусе доверия v2 отражают сходимость. |
channel_reload | менеджер channel-воркеров, управляемых демоном, включен и может перезагружать свой текущий выбор. |
channel_control | подключено управление рантаймом channel-воркеров, управляемых демоном. |
channel_management | подключены конфигурация Channel, жизненный цикл и управление привязками в масштабе рабочего пространства. |
multi_workspace_sessions | зарегистрировано более одного рантайма рабочего пространства, поэтому создание сессии может выбрать доверенный рантайм по cwd. |
multi_workspace_session_rewind | зарегистрировано более одного рантайма рабочего пространства; маршруты перемотки живой сессии с единственным числом разрешают владеющий рантайм. |
multi_workspace_session_shell | зарегистрировано более одного рантайма рабочего пространства и выполнение shell-команд в сессии явно включено; REST shell с единственным числом разрешает владеющий рантайм. |
dynamic_workspace_registration | фабрика рантаймов рабочих пространств подключена к демону, поэтому существующий доверенный каталог может быть зарегистрирован как вторичный рантайм во время выполнения. |
persistent_workspace_registration | хранилище регистрации рабочих пространств подключено к демону. Продакшен-путь runQwenServe поставляет хранилище уровня пользователя автоматически; прямые встраивания createServeApp должны внедрить его явно и самостоятельно управлять восстановлением при запуске своего реестра рабочих пространств. |
scratch_workspace_registration | доступно создание управляемого scratch-рабочего пространства — подключены фабрика рантаймов, проверенный управляемый scratch-корень и утилизация рантайма, и каждый управляемый рантайм соблюдает границу scratch-корня. |
workspace_runtime_removal | удаляемые динамические или восстановленные из постоянного хранилища вторичные рантаймы могут быть дренированы и удалены через маршрут управления. |
workspace_qualified_acp | ACP HTTP и рантаймы нескольких рабочих пространств активны, поэтому эндпоинт ACP с множественным числом может выбрать вторичный рантайм. |
workspace_qualified_voice | рантаймы нескольких рабочих пространств и общий слушатель WebSocket ACP/Voice активны, поэтому все модальности Voice с указанием рабочего пространства доступны для вторичного рантайма. |
workspace_qualified_memory | ACP HTTP и рантаймы нескольких рабочих пространств активны, поэтому маршруты управляемой памяти с указанием рабочего пространства могут выбрать полосу задач для каждого рабочего пространства для операций remember, forget и dream. |
client_mcp_over_ws | демон принимает клиентские MCP-серверы через WebSocket ACP. Это явное opt-in, не требуется для пути CDP-туннеля. |
cdp_tunnel_over_ws | демон предоставляет обратный WebSocket-туннель /cdp — либо через явное opt-in, либо потому что origin расширения Chrome разрешен. Это означает только существование туннеля; это не означает, что инструменты Chrome DevTools MCP зарегистрированы. |
browser_automation_mcp | ACP HTTP включен, cdp_tunnel_over_ws активен, bearer-токен не блокирует /cdp, а QWEN_CDP_MCP_COMMAND указывает на внешний stdio MCP-адаптер. Основной пакет CLI не включает адаптер автоматизации браузера; без этого тега чат на боковой панели расширения Chrome может работать, но инструменты console/network/screenshot/click не регистрируются по умолчанию. |
voice_transcribe | конечная точка WebSocket Voice смонтирована; для успешной транскрипции все равно требуется настроенная модель Voice. |
realtime_voice | демон macOS WebShell включил Live Voice и нативную интеграцию Host. /live/status сообщает о готовности, но возможность отзывается до включения функции. |
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 для адаптеров, которым нужен простой UI лага.
Маршруты
Клиенты могут обнаруживать session_turn_status через проверку возможностей и опрашивать GET /session/:id/turns/current или GET /session/:id/turns/:promptId. Эти маршруты требуют живой владеющей сессии и никогда не загружают и не сканируют другое рабочее пространство. Завершённые результаты — это транскрипты best-effort, читаемые из активной ветки с ограниченным сканированием; prompt_not_found означает, что результат не найден в живой очереди, терминальном оверлее на 64 записи или ограниченном активном окне. resultText — это сырой окончательный ответ родительской модели после последней границы инструмента, до опционального переписывания сообщения, и может отсутствовать. Результаты более 32 768 кодовых единиц UTF-16 включают resultTruncated: true и resultCode: "RESULT_TEXT_TRUNCATED".
GET /health
Liveness-проба. Стандартная форма возвращает 200 {"status":"ok"}, если слушатель работает — дешевая операция, без обращения к bridge, подходит для высокочастотных liveness-проб в k8s/Compose.
Передайте ?deep=1 (также принимается ?deep=true или просто ?deep) для пробы по всему демону, которая агрегирует счетчики bridge через каждый управляемый рантайм рабочего пространства, включая рабочее пространство, которое всё ещё дренируется (только в информационных целях, не является настоящей проверкой жизнеспособности):
{
"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 истинен, когда любой рантайм имеет принятый, но не урегулированный промпт (включая промпт, ожидающий в FIFO), запущенного фонового агента, уведомление терминала агента в очереди/в процессе или фоновую работу с оболочками, управляемую сессией. Работа с оболочками остаётся активной, пока реестр оболочек сообщает о запущенной записи и пока уведомление терминала находится в очереди или управляет продолжением родителя; любое количество оболочек вносит один ограниченный агрегированный удержатель. Monitor-ы, рабочие процессы, cron-задачи, предложения продолжения и внешние процессы, которые реестр оболочек больше не может отслеживать, остаются за пределами поля. Он ограничен сессией: работа на уровне канала без привязанной сессии — запуск в процессе, ожидающее восстановление, обнаружение или аутентификация MCP — не учитывается, поэтому activeWork может показывать false, пока демон отказывается возвращать этот канал. Не следует читать это поле как «демон можно вернуть»; оно описывает только работу, принадлежащую сессии. activeWorkReporting говорит, какая часть этого булева значения фактически подтверждена: full, когда каждая живая сессия покрыта свежим отчётом от потомка, который сообщает все необходимые категории, none, когда ни одна сессия не согласовала отчётность, partial для всего, что между ними — включая устаревший снимок или согласованного потомка, который пропускает необходимую категорию. Снимок старше трёх интервалов отчётов перестаёт считаться покрытием: это не отчёт о простое сессии, поэтому сессия возвращается к чтению как сохранённому, точно так же, как если бы потомок никогда не сообщал. Обычная автоматическая очистка также отключена для согласованного, но неполного потомка; потомок, который не понимает shell, не может безопасно авторизовать условное закрытие согласно полному текущему предикату. Полностью неподдерживаемые исторические потомки сохраняют устаревшее поведение очистки, а явное закрытие, убийство, завершение работы и выход канала остаются силовыми операциями. activeWorkStaleMs — это возраст самого старого снимка, на котором основано булево значение среди покрытых сессий, и равен 0, когда ни одна сессия не покрыта; он диагностический, поскольку свежесть уже учтена в activeWorkReporting демоном (только демон знает согласованный каденс каждого канала). Оценка вычисляется один раз по каждому управляемому рантайму, а не для каждого рантайма, а затем объединяется — рантайм без сессий считается условно полным, и рассмотрение этого как доказательства позволило бы пустому рабочему пространству поручиться за несообщённые сессии другого рабочего пространства. lastActivityAt — это последнее ненулевое время активности рабочего пространства, а idleSinceMs вычисляется из того же снимка. channelAlive означает, что хотя бы один управляемый канал рабочего пространства активен; это не означает, что каждое рабочее пространство здорово. connectedClients и опциональный rateLimitHits остаются счетчиками уровня демона, а не суммами по рабочим пространствам.
Контроллеры перезапуска должны считать демон занятым, когда:
const busy =
health.activePrompts > 0 ||
health.activeWork ||
health.activeWorkReporting !== 'full';Отбрасывание третьего члена делает activeWork === false неотличимым от «ни один потомок ничего не сообщил», что является единственным случаем, когда действовать небезопасно. Неизвестные ответы и неудачные пробы также должны предотвращать перезапуск. activePrompts остаётся независимым сигналом совместимости.
Эти поля являются кэшем наблюдения, а не арендой перезапуска: даже свежий, полностью оценённый, пустой ответ описывает момент выборки, и работа может начаться сразу после этого. Правило выше существенно снижает риск неправильного перезапуска, но не устраняет его — строгая безопасность требует prepare-restart fence, который останавливает приём новой работы, подтверждает дренирование и только затем завершает работу.
⚠️ Глубокая проба носит информационный характер, а не является реальной проверкой жизнеспособности или атомарной арендой возврата. Согласованные дочерние процессы 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 (127.0.0.1, ::1, [::1]) /health регистрируется до bearer middleware, поэтому liveness-пробы k8s/Compose внутри пода не нужно передавать токен. При привязке не к loopback (--hostname 0.0.0.0 и т.д.) маршрут регистрируется после bearer middleware и возвращает 401 без валидного токена — в противном случае неаутентифицированный вызывающий абонент мог бы опрашивать произвольные адреса, чтобы подтвердить существование qwen serve, что представляет собой утечку информации с низким уровнем серьезности, которая плохо сочетается со сканированием портов. CORS deny + Host allowlist по-прежнему применяются к исключению для loopback.
GET /daemon/status
Диагностика оператора только для чтения. В отличие от /health, это обычный API демона:
он регистрируется после bearer-аутентификации и rate limiting, включая привязки
к loopback. Query-параметр:
detail=summary(по умолчанию) считывает только состояние демона в памяти.detail=fullтакже включает диагностику активных сессий, диагностику подключений ACP, счетчики auth device-flow и разделы статуса рабочего пространства.- любой другой
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": {
"maxSessions": 32,
"maxTotalSessions": null,
"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 необязателен. Если присутствует, он сообщает только о задержке
event loop процесса демона, выборках ожидания в FIFO-очереди промптов и счетчиках байтов
pipe между демоном и дочерним процессом; задержка event loop дочернего процесса ACP
не включается в /daemon/status.
status принимает значение error, если какая-либо проблема имеет уровень серьезности error, warning, если какая-либо проблема имеет
уровень серьезности 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 и workspace_status_unavailable. В течение
короткого окна после готовности слушателя, но до монтирования полного runtime,
/daemon/status может сообщать daemon_runtime_starting; если асинхронное
монтирование runtime завершается ошибкой, он сообщает daemon_runtime_failed, в то время как
маршруты runtime, не связанные со статусом, возвращают 503.
runtime.activity сообщает об активности промптов во всем демоне. activePrompts считает сессии с промптом в процессе выполнения. pendingPrompts считает все принятые промпты, которые еще не завершены, включая выполняющийся промпт и промпты, ожидающие в FIFO. queuedPrompts считает промпты, ожидающие в FIFO, которые были приняты, но еще не отправлены в обработку. lastActivityAt — это временная метка ISO 8601 последнего запуска/завершения промпта или создания сессии; null, если демон никогда не обрабатывал активность с момента загрузки. idleSinceMs вычисляется на основе lastActivityAt на момент генерации ответа.
limits.memory является аддитивным и сообщает разрешённые значения памяти демона: обязательное enforced: false, объект childHeap (mode; 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-потомки с живым, не умирающим каналом — включает переходные или заблокированные записи, но исключает рабочее пространство, чьё завершение началось, даже если потомок ещё не завершился; не channel-воркеры, не MCP-потомки и не непривязанные резервирования запуска), childRssCoverage (active_children — каждый ACP-потомок с живым каналом, то есть множество, которое считает activeAcpChildren; старые демоны отправляют primary_only), объект children, описанный ниже, и объект modeled, содержащий recommendedShareAtRegisteredMb (null, если ни одно рабочее пространство не зарегистрировано) и recommendedShareAtActiveMb (null, если ни один потомок не активен). Каждая доля ограничена потолком устаревшего потомка и имеет нижнюю границу только при минимальной куче потомка, если потолок позволяет — на маленьком хосте потолок находится ниже нижней границы, поэтому доля × количество может превышать пул потомков. Читайте долю как рекомендательную, а не как раздел пула. Всё это наблюдение: ни один аргумент запуска потомка не выводится из этих значений, и ни один запрос не отклоняется на их основе. На обычном пути runQwenServe бюджет разрешается до создания приложения загрузки, поэтому limits.memory уже заполнен во время окна загрузки. Он равен 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-потомки и каждый channel-воркер отсутствуют. Это не память дерева демона. Поле необязательно в зеркале SDK, потому что демоны, сообщающие primary_only, никогда его не отправляют.
runtime.memory.children.heap является аддитивным внутри этого блока и сообщает пожизненные high-water mark старого поколения V8 каждого ACP-потомка, агрегированные как максимум, а не сумма: peakOldGenerationBytes, peakLiveSetBytes, peakTotalHeapBytes, majorGcCount, majorGcMs, unclassifiedSpaceNames и reported. Потолок кучи применяется для каждого потомка, и пики были достигнуты в разное время, поэтому сумма не отвечала бы ни на какой вопрос; каждое поле является независимым максимумом по отчетным потомкам, а не портретом одного потомка, и потолок для каждого потомка оценивается по каждой оси отдельно. reported считает, сколько из sampled внесли данные, и является меньшим, когда некоторые потомки предшествуют этим полям. Каждое байтовое значение охватывает старое поколение — то, что на самом деле ограничивает --max-old-space-size — а не только old_space, потому что потомок может исчерпать свой потолок при old_space в несколько мегабайт, пока large_object_space содержит всё остальное. peakOldGenerationBytes — это committed bytes и растёт с потолком, который был дан потомку, поэтому читайте его как верхнюю границу того, что нужно рабочей нагрузке, а не как её требование; peakLiveSetBytes — это то, что выживает после major GC и не движется с потолком, что делает его значением, способным сказать, что потомок не помещается; читайте его как верхнюю границу, а не как точный живой набор, потому что события GC поступают асинхронно, и всё, что выделено между сборкой и чтением, учитывается. peakLiveSetBytes равен 0, пока не наблюдается major GC, что является отсутствием, а не измерением. 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.maxTotalSessions является аддитивным. null означает, что эффективный лимит свежих сессий для всего демона отключен. Когда присутствует несколько рабочих пространств при запуске/восстановлении, --max-total-sessions не указан, а maxSessionsPerWorkspace конечен, демон вычисляет эффективный общий лимит один раз как maxSessionsPerWorkspace * startupWorkspaceCount; последующая динамическая регистрация не пересчитывает его. Когда установлен, он ограничивает создание свежих сессий по всему демону и сообщает о сбоях общего лимита с существующей формой ошибки session_limit_exceeded плюс scope: "total".
runtime.channel.live сообщает о канале ACP bridge внутри демона. Это
не worker адаптера канала. Каналы, управляемые демоном, используют
runtime.channelWorker, чей state может быть disabled, starting,
running, exited, failed или stopped. Когда worker переходит в состояние running
и затем завершает работу, /daemon/status оставляет демон в сети и сообщает код проблемы
channel_worker_exited с уровнем warning.
Запуск worker’а канала, управляемого демоном, по-прежнему работает по принципу fail-fast: если qwen serve --channel ... не может запустить worker, который достигнет состояния ready, запуск serve завершается ошибкой.
После того как worker достиг состояния ready, неожиданные завершения перезапускаются
супервизором serve в рамках ограниченной политики: до 3 попыток перезапуска в 5-минутном
окне, с задержкой 1 с, 5 с, затем 15 с. Worker отправляет IPC-хартбиты каждые
15 с; если хартбит не наблюдается в течение 45 с, супервизор считает worker
устаревшим, убивает его, записывает 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; работающий worker с
restartCount > 0 считается здоровым, если не применима другая проблема. Работающий worker,
чьи 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 channel-сервиса на протяжении всей транзакции и не освобождает её, пока не будет подтверждён выход каждого релевантного дочернего процесса.
Стабильные ошибки управления:
400 invalid_channel_selection,channel_workspace_mismatchилиambiguous_channel_workspace403 untrusted_workspace409 channel_service_conflictилиchannel_worker_not_enabled500 channel_worker_stop_failed502 channel_worker_start_failedсrolledBackи необязательнымrollbackErrorс маскированными учётными данными503 daemon_draining
Строгие записи к демону без настроенного токена возвращают 401 token_required до запуска кода управления. После начала запроса отключение HTTP-клиента не отменяет транзакцию жизненного цикла; клиенты могут безопасно повторить тот же PUT.
Для 502 channel_worker_start_failed ответ также может включать startupFailures[] и startupFailuresTruncated. Каждая ошибка добавляет доверенный workspaceCwdattempted воркера. Эти поля описывают неудачную транзакцию, тогда как state описывает текущее состояние после отката; последующий GET не сохраняет неудачную попытку. Частично подключенный воркер вместо этого возвращает успех и раскрывает свои ошибки в снимке воркера. Ошибки во время запуска по-прежнему прерывают qwen serve до появления доступного для запросов демона.
qwen channel status без --daemon-url продолжает читать метаданные pidfile; с --daemon-url он читает GET /workspace/channel. Во время окна перезапуска pidfile, принадлежащий serve, остаётся зарезервированным, но workerPid опускается, чтобы клиенты не отображали устаревший процесс воркера. На демоне с несколькими рабочими пространствами pidfile также несёт аддитивный массив workers[] (для каждого рабочего пространства workspaceId / workspaceCwd / channels / живой workerPid), тогда как верхнеуровневые channels (объединение) и workerPid (основное) остаются заполненными для старых клиентов; демоны с одним рабочим пространством сохраняют исходную форму с одним воркером. stdout/stderr воркера перенаправляются в лог демона с маскированием bearer-токенов, конфиденциальных значений окружения воркера и учётных данных URL прокси.
Управление Channel рабочего пространства
Возможность channel_management анонсирует конфигурацию Channel в масштабе рабочего пространства и управление рантаймом. Маршруты /workspace в единственном числе нацелены на основной рантайм. /workspaces/:workspace разрешает точный зарегистрированный доверенный рантайм и никогда не переключается на основной рантайм.
Обнаружение только для чтения использует:
GET /workspace/channel-typesGET /workspace/channelsGET /workspaces/:workspace/channel-typesGET /workspaces/:workspace/channels
Каталог помечает типы, поддерживаемые этим API управления, флагом manageable: true. Снимки экземпляров включают ревизию, маскированные метаданные наличия секретов, состояние запуска и состояние рантайма; буквальные секреты никогда не возвращаются. Снимки Channel используют Cache-Control: no-store.
Дескрипторы полей могут предоставлять метаданные вложенных объектов через properties. Числовые дескрипторы могут использовать exclusiveMinimum для открытых нижних границ. Клиенты, которые не отображают рекламируемый тип поля, должны сохранять существующее значение конфигурации вместо его приведения или удаления. Поля объектов не могут быть обязательными, а вложенные свойства не могут быть секретами или полями, разрешаемыми из окружения; эти протоколы управления остаются только на верхнем уровне. Вложенное свойство required применяется только пока его родительский объект присутствует в записи; пропуск родительского объекта оставляет его вложенные требования непроверенными. Записи заменяют сохранённое значение каждого поля целиком, поэтому сохранение объекта означает повторную отправку сохранённого объекта; демон не объединяет частичные объекты.
Записи конфигурации используют оптимистичную параллельность и строгий bearer-токен шлюз:
PUT /workspace/channels/:nameDELETE /workspace/channels/:namePUT /workspace/channels/:name/startup- эквивалентные маршруты
/workspaces/:workspace/...
Каждая мутация настроек включает expectedRevision. Запросы upsert содержат объект config и могут содержать явные операции с секретами: preserve, replace или clear. Конфигурация Channel не может выбрать рабочий каталог вне разрешенного рабочего пространства.
Действия рантайма — это POST-запросы с строгим шлюзом к .../channels/:name/start, stop или restart. Они работают только с воркером, принадлежащим разрешенному рабочему пространству.
Управление привязками доступно только для экземпляров, настроенных с политикой отправителя pairing или групповой политикой:
GET .../channels/:name/pairing-requestsPOST .../channels/:name/pairing-requests/approveс{ "code": "..." }GET .../channels/:name/pairing-approvalsDELETE .../channels/:name/pairing-approvalsс{ "senderId": "..." }или{ "groupId": "..." }
Все маршруты привязок требуют bearer-токен и используют Cache-Control: no-store. Запросы, одобрения и отзыва привязаны к выбранному экземпляру Channel и рабочему пространству. Ожидающие запросы включают типизированный субъект пользователя или группы; групповые запросы также сохраняют отправителя, инициировавшего запрос. Снимки одобрений содержат senderIds и groupIds, поскольку allowlist не сохраняет отображаемые имена. Отзыв неизвестного пользователя или группы возвращает 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 означает, что Promise отправки Channel разрешился; это не доказывает принятие провайдером, получение пользователем или уведомление о прочтении. Валидация ответа провайдера и согласованная семантика причин ошибок между 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."
}
}Пустой успешный финал промпта опускает поля ошибок:
{
"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 означает, что допустимый ход завершился успешно, но его последний блок ответа ассистента без вызовов инструментов был пустым или содержал только пробелы. Демон потребляет авторизацию доставки и публикует событие без разрешения Channel Worker. Запланированная корреляция использует taskId и firedAt. Событие никогда не содержит ID целей, текст сообщения, учётные данные или секреты вебхуков.
Безопасность: ответ никогда не включает bearer-токены, id клиентов, полные id соединений ACP, пользовательские коды device-flow или 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": {
"maxPendingPromptsPerSession": 5,
"maxSessionsPerWorkspace": 32,
"maxTotalSessions": 64,
"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, необходимый для маршрутов с указанием рабочего пространства; старые демоны могут опускать массив. Каждая запись имеет вид{ id, cwd, displayName?, primary, trusted, removable? }.displayNameпредназначен только для отображения и опускается, если не задан. Первое/основное рабочее пространство по-прежнему отражается вworkspaceCwd; новые клиенты выбирают неосновной рантайм, передаваяcwdэтой записи вPOST /session. Недоверенные рабочие пространства анонсируются для диагностики, но отклоняют создание новых сессий с403 untrusted_workspaceдо изменения доверия.removableприсутствует на демонах, поддерживающих удаление рантаймов, и равен true только для процессно-динамических или восстановленных из постоянного хранилища вторичных рантаймов.
Теги возможностей рабочего пространства и workspaces[] динамичны. Клиенты, добавляющие рабочее пространство, должны снова получить /capabilities после завершения мутации; демон не транслирует изменения возможностей клиентам, кэшировавшим более ранний ответ. Забывание постоянного хранилища не выгружает активный рантайм, поэтому этот рантайм продолжает анонсироваться до перезапуска.
POST /workspaces
Регистрация дополнительного рантайма рабочего пространства. Путь должен быть существующим, доступным, абсолютным каталогом, который не дублирует и не вкладывается в другое зарегистрированное рабочее пространство. Регистрация привязана к процессу, если клиент не отправит persist: true; клиенты должны предварительно проверить persistent_workspace_registration перед запросом сохранения. Когда анонсирован workspace_display_name, запрос также может включать опциональный displayName.
{
"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, 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? }. Метаданные рантайма обновляются всегда. Если рантайм имеет совпадающие идентификаторы постоянной регистрации, все псевдонимы обновляются атомарно через существующее хранилище регистрации schema-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, задачи памяти или channel-воркеры рабочего пространства. Отправка { "force": true } запрашивает завершение этих ресурсов. Удаление постоянного хранилища — это точка фиксации: последующая очистка ограничена и выполняется в режиме best-effort, сбои очистки логируются, а логическое удаление всё равно сходится вместо восстановления рантайма. Успешный ответ:
{
"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
}
}Немедленно занятое не-принудительное запрос возвращает быстрый снимок активности до дренирования. После начала дренирования ответ busy или success содержит окончательный снимок, сделанный после закрытия шлюзов допуска и дренирования 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, пока её рантайм дренируется, потому что рантайм всё ещё владеет живыми ресурсами до завершения удаления.
Записи включают опциональный 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. Как и другие маршруты мутации, этот эндпоинт требует аутентификации мутации, когда включена аутентификация демона.
Маршруты статуса runtime только для чтения
Эти маршруты сообщают о снимках runtime на стороне демона. Это аддитивные маршруты v1,
они не мутируют состояние и не изменяют версию протокола serve. Маршруты статуса рабочего пространства
намеренно не запускают дочерний процесс ACP только потому, что
клиент опрашивает GET-маршрут: если демон простаивает, они возвращают
initialized: false с пустым снимком. Маршруты статуса сессии требуют
активной сессии и используют стандартную форму 404 { code: "session_not_found", ... } для неизвестных
id.
Теги возможностей:
workspace_mcp→GET /workspace/mcpworkspace_skills→GET /workspace/skillsworkspace_providers→GET /workspace/providersworkspace_acp_status→GET /workspace/acp/statusworkspace_env→GET /workspace/envworkspace_preflight→GET /workspace/preflightsession_context→GET /session/:id/contextsession_supported_commands→GET /session/:id/supported-commandssession_tasks→GET /session/:id/taskssession_monitor_tool_correlation→ записи монитора изGET /session/:id/tasksвключаютtoolUseIdдля корреляции транскрипта с задачамиsession_status→GET /session/:id/statussession_info→GET /workspace/:id/session-infoиGET /workspaces/:workspace/session-infosession_transcript→GET /session/:id/transcriptworkspace_persisted_transcript→GET /workspaces/:workspace/session/:id/transcriptworkspace_session_export→GET /workspaces/:workspace/session/:id/exportworkspace_archived_session_export→GET /workspaces/:workspace/session/:id/archive/exportworkspace_session_live_state→GET /workspaces/:workspace/sessions/live-stateworkspace_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 основного рабочего пространства в режиме best-effort. 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 — это закрытое перечисление (enum), общее для /workspace/preflight,
/workspace/env и (в будущем) MCP guardrails, чтобы SDK-клиенты могли отображать инструкции по устранению ошибок для каждой категории вместо парсинга произвольных сообщений. Исходные семь литералов статуса пришли из #4175; restore_timeout был добавлен отдельно для запросов восстановления сессии. blocked_egress остаётся зарезервированным до тех пор, пока не будет добавлен egress-зонд.
Полезные нагрузки (payloads) статуса никогда не раскрывают значения переменных окружения MCP, заголовки, детали OAuth/сервисных аккаунтов, API-ключи провайдеров, baseUrl / envKey провайдеров, тело skill, файловые пути skill, определения хуков или значения секретных переменных окружения. /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 опускается, если обнаружение (discovery) прошло успешно.
MCP client guardrails (issue #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'. Потребители ДОЛЖНЫ корректно обрабатывать дополнительные нераспознанные значения scope.
disabledReason в ячейках отдельных серверов позволяет отличить отключение оператором ('config' — список конфигурации disabledMcpServers) от отказа из-за лимита ('budget' — обнаружен, но никогда не подключался из-за режима enforce). Отказы детерминированы порядком объявления в Object.entries(mcpServers). Поля status: 'error', errorKind: 'budget_exhausted' на уровне сервера перекрывают сырой mcpStatus: 'disconnected' (что верно, но не отражает критичность для оператора).
Применение лимитов основано на возможностях. С mcp_workspace_pool сессии внутри одного рантайма рабочего пространства разделяют транспорты и один WorkspaceMcpBudget; разные рантаймы рабочих пространств никогда не разделяют пул или бюджет. Без этого тега McpClientManager каждой ACP-сессии применяет свою собственную копию лимита, и снимок представляет это устаревшее представление на уровне сессии.
Обнаружение нехватки лимита (budget pressure). Два интерфейса, оба заполняются после 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
{
"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]"
}
]
}level принимает одно из значений: project, user, extension или bundled.
userInvocable (boolean, опционально) опускается для обычных навыков (что означает
true) и присутствует только как false, когда навык не может быть вызван вручную
или переключен через API навыков. modelInvocable независим: false
означает, что навык остаётся доступным вручную, но скрыт от вызова моделью.
installedPath — это существующий абсолютный путь к SKILL.md навыка; демон
возвращает его как есть без отдельного разрешения символических ссылок или
канонизации. Текущие демоны выдают его для каждого навыка, тогда как клиенты должны
допускать его отсутствие от старых демонов v1. Тела навыков, хуки, skillRoot
и другая конфигурация навыков по-прежнему исключены. errors опускается, если
обнаружение прошло успешно.
Повторные чтения обслуживаются из последнего зафиксированного снимка рабочего пространства,
периодически проверяемого относительно кэша в памяти дочернего процесса. Чтение никогда не
сканирует каталоги навыков и не разбирает файлы SKILL.md повторно. Дочерний процесс проверяет,
что его источники расширений не изменились — один readdir каталога расширений
плюс stat для каждой записи, файла включения и состояния активации хранилища — и
обновляет только при их изменении, поэтому расширение, установленное или переключённое вне демона,
всё равно будет обнаружено при следующем чтении. Безопасный и bare режим пропускают проверку,
соответствуя их исключению расширений.
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; предварительная проверка окружения (environment preflight) находится в /workspace/preflight и /workspace/env (ниже). Поле errors опускается, если создание снимка прошло успешно.
GET /workspace/env
Сообщает о runtime, платформе, песочнице (sandbox), прокси процесса демона и наличии секретных переменных окружения из белого списка. Всегда отвечает на основе состояния 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;
}Политика сокрытия данных (Redaction policy). Ячейки 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.
Ответ в режиме ожидания (нет дочернего процесса ACP):
{
"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не существует или не является директорией; ошибка парсинга skill, указывающая на отсутствующий или нечитаемый файл.parse_error— ошибка парсингаSKILL.md, некорректный JSON конфигурации.auth_env_error—validateAuthMethodвернул непустую строку ошибки, или подклассModelConfigErrorбыл проброшен из разрешения провайдера.init_timeout— отклонениеwithTimeoutв bridge (реальный таймаут при ожидании ACP roundtrip). Определяется через типизированный классBridgeTimeoutError. Примечание: временная ячейкаwarningдляmcp_discoveryсconnecting > 0НЕ содержит этот тип — это нормальное состояние выполнения рукопожатия, отличное от реального таймаута.restore_timeout— восстановление (load или resume) сессии превысило выделенный бюджет восстановления. REST-ответ —504, повторяемый; отличается от инициализации дочернего процесса и от ограничений окна воспроизведения.protocol_error— ACPextMethodотклонен, так как канал закрылся в середине запроса, или потому что реестр инструментов неожиданно отсутствовал.blocked_egress— зарезервировано для PR 14 (#4175). PR 13 оставляет ячейкуegressсо статусомstatus: 'not_started'.
Если bridge не может достичь дочернего процесса ACP при обработке preflight-запроса
(например, из-за закрытия канала в середине запроса), массив errors в envelope
содержит одну ячейку ServeStatusCell, описывающую сбой, а остальные ячейки
откатываются к ACP-заглушкам со статусом not_started. Ячейки уровня daemon
по-прежнему возвращаются.
Маршруты файлов рабочего пространства
Все пути к файлам разрешаются через основное рабочее пространство демона. Ответы используют пути относительно рабочего пространства и никогда не возвращают абсолютные пути файловой системы для обычных успешных случаев. Успешные ответы с файлами включают:
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
Читает текстовый файл. Query-параметры: 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=… → следующая страница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
Читает сырые байты из файла без декодирования. Query-параметры: 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:..."
}POST /file/write
Создает или заменяет текстовый файл. Это строгий маршрут мутации: при запросе через loopback
без настроенного токена он возвращает 401 { "code": "token_required" }.
При использовании --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(), а затем переименовывает файл на целевое имя.
Это предотвращает чтение частично записанного файла и сериализует операции записи
от демона в один и тот же файл, но это не межпроцессный атомарный compare-and-swap
на уровне ядра: внешний редактор все еще может попасть в крошечное окно между
финальной проверкой хэша и переименованием.
{
"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
}GET /session/:id/context
{
"v": 1,
"sessionId": "<sid>",
"workspaceCwd": "/canonical/path",
"state": {
"models": {},
"modes": {},
"configOptions": []
}
}state повторяет те же структуры ACP model/mode/config-option, которые используются в
POST /session, POST /session/:id/load и POST /session/:id/resume.
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
}
]
}Этот маршрут представляет собой снимок только для чтения (out-of-band). Он намеренно не является промптом и может быть запрошен во время стриминга сессии. Ответ содержит только метаданные из белого списка из реестров задач agent, shell и monitor; контроллеры, таймеры, смещения, ожидающие сообщения и сырые объекты реестра никогда не раскрываются.
Задачи agent, порожденные другим субагентом (вложенные субагенты, ограниченные
maxSubagentDepth), содержат три опциональных поля происхождения: parentAgentId
(id породившей задачи agent), parentName (subagentType породившего agent,
сохраняемый при регистрации, чтобы пережить удаление родителя из реестра) и depth
(глубина запуска, начиная с 0; 0 = порождено сессией верхнего уровня). У agent,
запущенных сессией верхнего уровня, поля parentAgentId и parentName отсутствуют;
клиенты должны рассматривать все три поля как опциональные и возвращаться к плоскому
списку, если они отсутствуют.
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
(включая bare mode) возвращает HTTP 200 с enabled: false, нулевыми счетчиками и
servers: []. Включенный LSP без настроенных серверов возвращает enabled: true,
configuredServers: 0 и servers: []. Если инициализация завершилась ошибкой до
создания клиента, ответ может включать initializationError; если активный клиент
не может предоставить снимок, ответ включает statusUnavailable: true.
Этот маршрут раскрывает только стабильные клиентские поля. Он намеренно опускает отладочные внутренние данные, такие как ID процессов, аргументы запуска, хвосты stderr, корневые URI и пути к папкам рабочего пространства.
POST /session
Запускает новый agent или подключается к существующему (при sessionScope: 'single', по умолчанию).
Запрос:
{
"cwd": "/absolute/path/to/workspace",
"modelServiceId": "qwen-prod",
"sessionId": "550e8400-...",
"sessionScope": "thread"
}| Поле | Обязательно | Примечания |
|---|---|---|
cwd | нет | Абсолютный путь, соответствующий одному зарегистрированному рабочему пространству. Если не указан, маршрут использует основное рабочее пространство (его можно прочитать из /capabilities.workspaceCwd). Когда features содержит multi_workspace_sessions, клиенты могут передать любой доверенный workspaces[].cwd; в противном случае принимается только основное рабочее пространство. Несоответствующий непустой cwd возвращает 400 workspace_mismatch. Пути рабочего пространства канонизируются через realpathSync.native (с фолбэком на resolve-only для несуществующих путей), чтобы файловые системы без учета регистра не отклоняли сессии из-за разного написания. |
modelServiceId | нет | Выбирает, какой настроенный сервис моделей будет использовать agent (бэкенд-провайдер — Alibaba ModelStudio, OpenRouter и т.д.). Если не указан, agent использует свой сервис по умолчанию. Если в рабочем пространстве уже есть сессия, это вызывает setSessionModel для существующей сессии и транслирует model_switched. Отличается от modelId в POST /session/:id/model, который выбирает модель внутри уже привязанного сервиса. Массив modelServices в /capabilities зарезервирован для рекламы настроенных сервисов; на Этапе 1 он всегда равен [] (используется сервис agent’а по умолчанию, и он не перечисляется по HTTP). |
sessionId | нет | UUID варианта RFC v1-v5, выбранный вызывающей стороной. Демон нормализует его в нижний регистр и всегда создаёт новую сессию-треда; он никогда не рассматривает это поле как идемпотентное подключение. Подтвердите, что caps.features содержит session_id_override, перед отправкой, поскольку старые демоны могут игнорировать неизвестные поля. null эквивалентно отсутствию. |
sessionScope | нет | Переопределение для каждого запроса при совместном использовании сессии. 'single' (дефолтный для daemon) заставляет повторный POST /session для того же рабочего пространства переиспользовать существующую сессию (attached: true); 'thread' принудительно создает новую уникальную сессию при каждом вызове. Не указывайте, чтобы унаследовать дефолтное значение для daemon. Значения вне перечисления возвращают 400 { code: 'invalid_session_scope' }. Старые daemon (до PR 5 из #4175) молча игнорируют это поле — проверяйте caps.features.session_scope_override в pre-flight перед отправкой. Дефолтное значение для daemon сегодня жестко задано как 'single' в продакшене; #4175 может добавить CLI-флаг --sessionScope в последующих обновлениях. |
Ответ:
{
"sessionId": "<uuid>",
"workspaceCwd": "/canonical/path",
"attached": false
}attached: true означает, что сессия для этого рабочего пространства уже существовала, и теперь вы используете её совместно.
ID, предоставленные вызывающей стороной, уникальны across всех зарегистрированных runtime рабочих пространств и каждого всё ещё живого поколения bridge, включая дренируемые замены. Живой, ожидающий, активный, архивированный или worktree-дубликат возвращает 409 session_id_conflict. Некорректные значения возвращают 400 invalid_session_id; недоступная проверка живого владельца или сохранённого состояния возвращает повторяемый 503 session_id_admission_unavailable. Повторяйте попытку с ограниченным backoff после изменений здоровья bridge или хранилища; 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во время запуска, даже если подписка происходит через несколько мс после ответа на создание).
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 в нижнем регистре. Основное и workspace-квалифицированное 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
Восстановление сохраненной ACP-сессии по id и воспроизведение её истории через SSE. Id в пути является авторитетным; любое поле sessionId в теле запроса игнорируется. Предварительная проверка caps.features.session_load — старые демоны возвращают 404 для этого маршрута.
Запрос:
{
"cwd": "/absolute/path/to/workspace"
}| Поле | Обязательно | Примечания |
|---|---|---|
cwd | нет | Те же правила каноникализации и workspace_mismatch, что и для POST /session. Пропустите, чтобы наследовать /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, что и исходный вызов load — демон кэширует его при входе; мутации во время выполнения (например, model_switched) доставляются через SSE-поток, а не в последующих ответах на подключение.
attached: true означает, что сессия уже была активна (либо из-за предыдущего session/load/session/resume, либо потому, что объединенный параллельный вызов опередил его).
Воспроизведение истории через SSE. Пока loadSession выполняется на стороне агента, агент может отправлять уведомления session_update для сохранённых ходов или возвращать массовые обновления воспроизведения в метаданных ответа. Демон заполняет этими событиями ограниченное окно снимка воспроизведения сессии до возврата ответа маршрута. Для живых сессий POST /session/:id/load обещает только это ограниченное окно (compactedReplay, liveJournal, lastEventId), а не полный транскрипт. Окно ограничено по байтам через --compacted-replay-max-bytes (по умолчанию 4 МиБ, максимум 256 МиБ); если старые записи воспроизведения были отброшены, compactedReplay[0] — это маркер history_truncated без id. Незавершённый 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"; уже активные сессии остаются доступными, пока канал дренируется.
Ошибки:
404— сохраненный id сессии не существует (SessionNotFoundError).400—workspace_mismatch(та же структура, что и вPOST /session).403—untrusted_workspace, когдаcwdуказывает на недоверенное неосновное рабочее пространство.503—session_limit_exceeded(учитывается в лимите--max-sessions; выполняющиеся в данный момент восстановления также учитываются).504—session_restore_timeout; повторяемый, сRetry-After, полученным из бюджета восстановления (ограниченным 5-120 с), потому что тот же id сессии остаётся под забором, пока поздняя очистка не завершится.503—acp_channel_unavailable, когда канал рабочего пространства закрыт для новой работы сессии.reasonуказывает причину:restore_cleanup_failed, когда заброшенное восстановление не удалось очистить окончательно, илиrestore_settlement_overdue, когда заброшенное восстановление всё ещё не завершило один полный бюджет восстановления после своего дедлайна. В обоих случаях существующие сессии остаются доступными, а новая работа сессии может быть повторена после дренирования канала рабочего пространства — тело несётretryAfterSeconds, а заголовок — соответствующийRetry-Afterна основе бюджета, потому что карантин переживает забор и свежий id никогда не увидит 409, который нёс бы подсказку.409—restore_in_progress(session/resumeдля того же id уже выполняется, или свежий спавн предоставил 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 для этого маршрута.
Query-параметры:
| Поле | Обязательно | Примечания |
|---|---|---|
cursor | нет | Непрозрачный курсор base64url, возвращённый предыдущей страницей. Пропустите для первой страницы. Курсор выпущен демоном и проверен на подделку; его изменение возвращает 400 invalid_transcript_cursor. Он привязан к идентичности файла транскрипта и замороженному размеру байтов первой страницы; удаление, обрезка, замена или архивация файла инвалидирует его и возвращает 409. |
limit | нет | Количество активных ChatRecord для включения на страницу. По умолчанию 100, максимум 500. Одна запись может дать несколько фреймов воспроизведения, поэтому events.length может быть больше limit. Некорректные значения возвращают 400 invalid_transcript_limit. |
Ответ:
{
"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 }. Они не несут id EventBus, и ответ никогда не включает lastEventId. Вызов этого маршрута не вызывает /load, не подключает клиент, не заполняет живую шину событий, не создаёт живую сессию и не изменяет текущее окно живого воспроизведения. Живые и неактивные активные сессии реконструируются методом статуса только для чтения на стороне дочернего процесса, поэтому воспроизведение использует те же настройки рабочего пространства, выходной каталог рантайма, эмиттеры и семантику истории /load без изменения состояния сессии демона.
Первая страница замораживает текущий размер снимка JSONL. Последующие страницы читают только этот байтовый префикс, поэтому добавления после страницы 1 не изменяют набор результатов. Если файл исчезает, обрезается ниже замороженного размера, заменяется другим inode или перемещается в архив, следующая страница возвращает 409, и клиент должен начать со страницы 1 или попросить пользователя повторно открыть транскрипт.
Для защиты памяти и задержки демона снимки выше лимита индексации транскриптов завершаются ошибкой до сканирования JSONL демоном. Клиенты получают 413 transcript_too_large и должны переключиться на экспорт/офлайн-обработку или попросить пользователя сократить/архивировать более старую историю.
partial: true и replayError могут появиться, если конвертация воспроизведения завершается ошибкой после создания некоторых фреймов. Частичные ответы никогда не включают nextCursor, поэтому клиенты не могут молча пагинировать мимо записей, которые не были конвертированы.
Ошибки:
400— некорректная формаlimit,cursorили id сессии.404— активный сохранённый id сессии не существует при запросе первой страницы.409—session_archived,session_archivingилиsession_conflictиз тех же проверок загружаемости, что и/load.409— снимок транскрипта недоступен, потому что файл был удалён, обрезан, заменён или архивирован после выдачи курсора; это также применяется, когда preflight больше не может найти активный файл для запроса курсора.413—transcript_too_large, когда замороженный снимок транскрипта превышает лимит индексации демона.413—transcript_page_too_large, когда одна агрегатная запись превышает бюджет страницы с указанием рабочего пространства или сериализованная страница превышает свой бюджет ответа.
GET /workspaces/:workspace/session/:id/transcript
Возвращает ту же проекцию DaemonSessionTranscriptPage, что и маршрут с единственным числом, из активного сохранённого JSONL выбранного зарегистрированного рабочего пространства. Предварительная проверка workspace_persisted_transcript; эта возможность независима от multi_workspace_sessions и работает для доверенного основного рабочего пространства с одним рабочим пространством, выбранного по id или cwd.
Селектор и query-параметры следуют существующим правилам рабочих пространств с множественным числом и транскриптов. Доверенные основные и вторичные рантаймы и недоверенные вторичные рантаймы могут читать. Недоверенное основное рабочее пространство возвращает 403 untrusted_workspace. Архивное содержимое не возвращается.
Для этого маршрута с указанием рабочего пространства 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 до проверки сессии или формата.
Опциональный query 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.
Опциональный query 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 остается устаревшим алиасом для обратной совместимости со старыми клиентами.
Та же форма запроса, что и у /load. Та же форма ответа — 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, продолжали работать.
GET /workspace/:id/session-info и GET /workspaces/:workspace/session-info
Возвращает агрегированные счётчики сохранённых сессий для выбранного рабочего пространства без изменения пути пагинированного списка сессий:
{
"active": 450,
"archived": 30,
"total": 480,
"live": 2,
"expensive": true,
"cost": "disk_scan"
}active, archived и total считают локальные сессии JSONL. live — это соответствующий счётчик моста в памяти и опускается для зарегистрированного недоверенного вторичного рабочего пространства, потому что это чтение только из сохранённых данных не должно запрашивать живое состояние. expensive всегда true и cost всегда "disk_scan"; клиенты должны вызывать этот эндпоинт нечасто, а не опрашивать его. Если сканирование достигает своего лимита безопасности или не может классифицировать каждый файл-кандидат, ответ добавляет "truncated": true, и сохранённые счётчики являются нижними границами. Отсутствующее хранилище возвращает нулевые сохранённые счётчики. Маршрут с множественным числом использует тот же селектор рабочего пространства и политику доверия, что и каталог сессий с множественным числом; недоверенное основное рабочее пространство по-прежнему возвращает 403 untrusted_workspace.
TypeScript daemon SDK предоставляет маршрут с множественным числом через workspaceById(...) или workspaceByCwd(...), затем getWorkspaceSessionInfo().
GET /workspace/:id/sessions и GET /workspaces/:workspace/sessions
Вывод сессий, чьё каноническое рабочее пространство совпадает с :id или :workspace. Параметр пути сначала разрешается как точный id рабочего пространства, затем как 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 или пользовательский id группы. Неизвестные id групп возвращают 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 транскрипта и watermark живой активности. Живой watermark существует только в памяти, поэтому ключ сессии может регрессировать к своему mtime, когда живая запись удаляется между двумя выборками страниц. Курсор компенсирует это: он несёт идентификаторы, уже выданные при живом ключе — сохраняя их, пока строка отсутствует из коллекции страницы и пока переключение закрепления может их повторно допустить — и исключает их до конца прохода, поэтому перемещение живого ключа возвращает сессию не более одного раза за проход. Гарантия ограничена переносом: она ограничена 64 идентификаторами (избыточные идентификаторы в одном проходе деградируют до дубликата не более одного раза, а не до ошибки), и строка только из сохранённых данных, выданная до изменения её состояния закрепления, никогда не переносится, поэтому отмена закрепления между выборками может вернуть эту строку второй раз, как и до появления этого поля. Вызывающие стороны, накапливающие страницы, поэтому всегда должны ключировать строки по sessionId, а не только в случае более 64. Строки всё ещё могут перемещаться или пропускаться при одновременной активности, как и раньше; вызывающей стороне, которой нужен согласованный вид, следует перезагрузиться с первой страницы после изменения активности.
Дополнительные поля могут появляться в каждой сессии при view=organized:
{
"isPinned": true,
"pinnedAt": "2026-07-04T12:00:00.000Z",
"groupId": "018f..."
}Доверенные активные списки включают поля live-оверлея демона, такие как clientCount и hasActivePrompt. Списки недоверенных вторичных и архивные списки предназначены только для хранилища: поля live-оверлея остаются отсутствующими или ложными, а архивные записи устанавливают isArchived в true. Возвращается пустой массив (а не 404), если сессий не существует — UI выбора сессии не должен выдавать ошибку только из-за того, что рабочее пространство неактивно.
GET /workspaces/:workspace/sessions/live-state
Возвращает снимок живых сессий выбранного рантайма рабочего пространства только из памяти плюс версию каталога в памяти, чтобы клиенты могли прекратить опрашивать сохранённый каталог по маршруту GET /workspaces/:workspace/sessions для изменчивого состояния, такого как hasActivePrompt, флаги ожидания и clientCount. Предварительно проверяйте workspace_session_live_state; тег независим от workspace_qualified_rest_core, поэтому старые демоны, анонсирующие более широкую возможность workspace REST, не реализуют этот маршрут. Селектор разрешается сначала как точный id рабочего пространства, затем как URL-кодированный абсолютный cwd после канонизации, соответствуя другим маршрутам сессий с множественным числом. Маршрут доступен только доверенным клиентам как для основных, так и для вторичных рантаймов: он никогда не переключается на основной рантайм и не использует разрешительную политику сохранённого каталога, которая предоставляет недоверенным вторичным рабочим пространствам ограниченное чтение каталога. Эндпоинт не имеет query-параметров и не выполняет обращений к хранилищу сессий, настройкам, внешним командам или ACP, поэтому его стоимость не зависит от количества сохранённых сессий и размера JSONL; стандартный лимит живых сессий ограничивает ответ, а при отключённом лимите стоимость остаётся пропорциональной только количеству живых сессий.
Ответ:
{
"v": 1,
"catalogVersion": {
"generation": "7eca3164-bce1-4f50-94d8-c842c480f213",
"revision": 17
},
"sessions": [
{
"sessionId": "session-123",
"clientCount": 1,
"hasActivePrompt": true,
"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. Статические поля каталога, такие как отображаемое имя, временные метки, организация и метаданные источника, намеренно исключены и остаются во владении полного каталога. Отсутствующая строка live-state очищает только изменчивые поля известной строки каталога; она никогда не удаляет строку сохранённого каталога.
updatedAt — это опциональный watermark активности, наблюдаемый демоном, присутствующий, когда промпт, достигший состояния running, опубликовал формальный терминал в текущем бридже. Он продвигается ровно один раз для каждого такого терминала — успех, ошибка, отмена и дедлайн alike — записывается перед публикацией события терминала и строго возрастает для каждой живой сессии, даже если два терминала попадают в одну миллисекунду настенных часов или настенные часы идут назад; поэтому скачок часов вперёд сохраняется до тех пор, пока настенное время не догонит. Он никогда не раньше createdAt сессии: первый переход ограничивается временем создания, поэтому откат настенных часов между созданием и первым терминалом не может поместить строку раньше createdAt, по которому она уже была в списке. Приём промпта, ожидание в очереди, потоковые обновления, отмена только из очереди, heartbeat-и ожидание взаимодействия никогда не продвигают его. Клиенты используют его для обновления актуальности строки каталога, которую они уже держат, вместо перезагрузки полного каталога после завершённого хода. Это не подтверждение сохранением: рекордер записывает результаты ходов асинхронно, поэтому значение доказывает только то, что демон наблюдал завершение попытки running. Он отсутствует до первого терминала running в поколении бриджа — в том числе для сессии, восстановленной с диска — поэтому отсутствие не является зондом поддержки, и он исчезает, когда перезапуск демона или замена рантайма рабочего пространства устанавливает новый бридж. Когда для одной сессии существуют и живая, и сохранённая сводка, ответы полного каталога сообщают более позднюю допустимую временную метку, поэтому GET /session/:id/status, который возвращает сводку бриджа напрямую без этого слияния, может сообщать более раннее значение, чем ответ списка.
catalogVersion — токен равенства для изменений каталога, наблюдаемых демоном. generation — случайный UUID, создаваемый с каждым экземпляром бриджа и изменяющийся при перезапуске демона или замене рантайма рабочего пространства; revision начинается с нуля и монотонно увеличивается внутри поколения. Единственная поддерживаемая операция — проверка равенства для всей пары: одинаковые generation и revision означают отсутствие наблюдаемых демоном изменений каталога, а любое различие означает необходимость перезагрузки полного каталога. Клиенты не должны выполнять арифметику с revision или сравнивать revision между поколениями; допустимы консервативные дополнительные инкременты. Версия покрывает членство в каталоге и изменения статических метаданных, наблюдаемые демоном; обычная активность ходов, жизненный цикл промптов, подключение/отключение и переходы состояния ожидания не продвигают её, поскольку живой снимок уже несёт соответствующие изменчивые поля. Изменённый updatedAt при неизменной версии поэтому допустим и ожидаем и не инвалидирует кэши сохранённого списка демона. Два значения изменчивого оверлея намеренно исключены из обоих сигналов: состояние ошибки хода (hasTurnError/turnError) и количество/содержимое ожидающего взаимодействия (pendingInteractionCount/pendingInteractions) не продвигают версию и не появляются в снимке, поэтому клиент, которому они нужны, должен продолжать читать поток событий сессии или полный каталог, а не полагаться на этот маршрут; любое из этих полей может быть добавлено wire-аддитивно, когда конкретный потребитель требует его. Мутации, записанные напрямую другим демоном, TUI или внешним процессом, не наблюдаются, поэтому, как только клиент прекращает периодический опрос полного каталога, эти записи не имеют ограниченного времени обнаружения и проявляются только после явной полной перезагрузки, другой наблюдаемой мутации каталога, переподключения или замены демона/рантайма.
Клиенты согласуют пакет каталога двухчитательским рукопожатием: читают live-state A, загружают полный список сессий (плюс GET /workspaces/:workspace/session-groups, когда клиент потребляет session_organization), затем читают live-state B. Одинаковые версии A и B принимают пакет; различающиеся версии помечают каталог как устаревший и объединяют не более одной завершающей перезагрузки вместо входа в плотный цикл повторных попыток. Каждый принятый запрос каталога должен быть инициирован после A — запрос или дедуплицированное обещание, начатое до A, не может удовлетворить согласование. Перезагрузки, управляемые версиями, выполняются в режиме single-flight для каждого рабочего пространства и подчиняются ненулевому фоновому минимальному интервалу, поэтому устойчивая турбулентность каталога не может вызвать одно полное сканирование каталога на каждый опрос live-state; явные локальные мутации могут по-прежнему запрашивать немедленное обновление через ту же операцию single-flight.
Ошибки:
400— существующее поведение проверки селектора илиworkspace_mismatchдля неизвестного, некорректного, вложенного или незарегистрированного селектора; маршрут никогда не разрешает неизвестный селектор в основной рантайм.403—untrusted_workspaceдля любого недоверенного рантайма, включая недоверенное основное рабочее пространство.503—workspace_runtime_unavailableсRetry-Afterдля рантайма на этапе бутстрапа, перехода, дренирования, блокировки или удаления, или поколения рантайма, закрывающегося во время запроса.500— неожиданные локальные ошибки используют существующее маппирование ошибок бриджа.
GET /workspace/:id/session-groups
Список пользовательских групп сессий для рабочего пространства. Селектор GET с единственным числом принимает любой зарегистрированный id рабочего пространства или URL-кодированный канонический cwd. Псевдоним GET с множественным числом также доступен недоверенному вторичному рабочему пространству и читает только сайдкар организации. Мутации групп с множественным числом остаются под контролем доверия, тогда как мутации групп с единственным числом сохраняют своё поведение совместимости только для основного рабочего пространства. Предварительная проверка 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-файлов сессий. Демон сначала по мере возможности (best-effort) закрывает активные сессии, а затем удаляет активный или архивный JSONL. Если для одного и того же id существуют как активная, так и архивная копии, удаляются обе. Сайдкары (sidecars) worktree с обеих сторон очищаются; история файлов, транскрипты субагентов и runtime-сайдкары намеренно сохраняются.
Запрос:
{ "sessionIds": ["<uuid>"] }Ответ:
{
"removed": ["<uuid>"],
"notFound": [],
"errors": []
}POST /sessions/archive
Архивирование одной или нескольких сессий. Архивирование — это переход состояния, а не удаление: JSONL перемещается из chats/<id>.jsonl в chats/archive/<id>.jsonl. История файлов, транскрипты субагентов и runtime-сайдкары остаются на месте. Если сессия активна, демон сначала выполняет строгое закрытие и требует, чтобы обработчик закрытия ACP-агента сбросил (flush) запись чата; если закрытие или сброс не выполняются, JSONL не перемещается. Pre-flight caps.features.session_archive.
Запрос:
{ "sessionIds": ["<uuid>"] }sessionIds должен быть непустым массивом строк, содержащим не более 100 id. Дубликаты схлопываются.
Ответ:
{
"archived": ["<uuid>"],
"alreadyArchived": [],
"notFound": [],
"errors": []
}Записи в errors имеют формат { "sessionId": "<uuid>", "error": "message" }. Активные и архивные файлы с одинаковым id рассматриваются как конфликт и сообщаются в errors; ни один файл не перезаписывается.
POST /sessions/unarchive
Восстановление архивных сессий в активную директорию. Само по себе это не возобновляет сессию; это лишь перемещает chats/archive/<id>.jsonl обратно в chats/<id>.jsonl. После успешного разархивирования клиенты могут вызвать POST /session/:id/load или POST /session/:id/resume.
Запрос:
{ "sessionIds": ["<uuid>"] }Ответ:
{
"unarchived": ["<uuid>"],
"alreadyActive": [],
"notFound": [],
"errors": []
}Если для данного id уже существует активный JSONL, разархивирование сообщает о конфликте в errors и не перезаписывает его. Выполняющееся в данный момент архивирование или разархивирование для того же id возвращает 409 session_archiving до начала пакетной обработки.
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/mid-turn-message, GET /session/:id/mid-turn-messages, 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. Демон маршрутизирует каждый запрос к доверенному рантайму, владеющему живой сессией. Недоверенный неосновной владелец возвращает 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>" } и передаёт владение демону: сообщение дренируется в активный ход или promovается в обычную FIFO-очередь промптов, когда сессия становится неактивной. Клиенты с session_mid_turn_message_query отправляют стабильный messageId; повторение идемпотентно, пока сообщение находится в очереди, ожидает или в ограниченных кольцах примирения. Полная очередь отклоняет новый запрос без принятия владения. Новые клиенты, подключённые к старому демону, обнаруживают отсутствующую возможность и сохраняют локальный фолбэк.
GET /session/:id/mid-turn-messages возвращает очередь на уровне сессии, принадлежащую демону, а также ограниченные кольца settledMessageIds и promotedMessageIds. Settled id были внедрены или явно удалены; promoted id вошли в обычную FIFO-очередь промптов. Id в любом кольце не должен быть отправлен повторно.
Когда сообщение из очереди дренируется в активный ход, демон публикует mid_turn_message_injected с выровненными массивами messages и messageIds (и promptId текущего хода, когда известен). Это переходный сигнал дедупликации, а не элемент транскрипта: клиенты завершают колбэки завершения, зарегистрированные под этими message ids, и удаляют все локальные ожидающие строки для них. Старые демоны дополнительно содержат originatorClientId в полезной нагрузке. Пропущенное эхо восстанавливается из кольца settled через запрос выше.
Когда анонсирован session_mid_turn_message_mutation, клиент подключённой сессии может вызвать DELETE /session/:id/mid-turn-messages/:messageId. Это удаляет сообщение либо из очереди mid-turn, либо из его promoted состояния ожидающего промпта; удаление promoted сообщения, которое уже выполняется, прерывает этот ход, соответствуя обычному удалению ожидающего промпта. Добавления и удаления очереди, принадлежащей демону, публикуют существующие события сессии pending_prompt_added и pending_prompt_completed, чтобы подключённые клиенты обновляли оба авторитетных снимка очереди. { "removed": false } означает, что сообщение уже было внедрено, завершено или не найдено.
POST /session/:id/prompt
Пересылка промпта агенту. Вызывающие стороны с несколькими промптами ставят их в 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. Отмена, сбой агента и завершение по лимиту токенов не отправляют и не публикуют результат доставки.
Валидация: prompt должен быть непустым массивом объектов. При других ошибках возвращается 400 до достижения bridge.
Ответ:
{ "promptId": "session-id########1", "lastEventId": 42 }Ответ 202 подтверждает допуск, а не завершение агента. Наблюдайте за SSE-потоком сессии после lastEventId и коррелируйте turn_complete или turn_error по promptId. turn_complete.data.stopReason может быть end_turn, cancelled, max_tokens, error или length.
Если HTTP-клиент отключается во время выполнения промпта, демон отправляет агенту ACP-уведомление cancel, которое завершает промпт с stopReason: "cancelled".
Когда анонсирован prompt_absolute_deadline, deadlineMs может сократить настроенный серверный дедлайн. Истечение срока выдаёт коррелированный turn_error с errorKind: "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 и удаляет сессию из мап (maps) демона. Сохраненные на диске сессии НЕ удаляются — их можно перезагрузить через POST /session/:id/load. Pre-flight 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
Обновление изменяемых метаданных сессии. В настоящее время поддерживается только displayName. Pre-flight caps.features.session_metadata. Группировка и закрепление намеренно не включены в этот маршрут; используйте PATCH /session/:id/organization в рамках session_organization.
Запрос:
{ "displayName": "My Investigation Session" }| Поле | Обязательно | Примечания |
|---|---|---|
displayName | нет | Строка, макс. 256 символов. Пустая строка очищает имя. Пропустите, чтобы оставить как есть. |
Ответ:
{ "sessionId": "<uuid>", "displayName": "My Investigation Session" }Публикует событие session_metadata_updated в SSE-стриме сессии с { sessionId, displayName }.
PATCH /session/:id/organization и PATCH /workspaces/:workspace/session/:id/organization
Обновление локального состояния организации сессии через существующий шлюз мутации. Предварительная проверка caps.features.includes('session_organization'); маршрут с множественным числом дополнительно требует workspace_qualified_rest_core. На маршруте с множественным числом :workspace разрешается сначала как точный зарегистрированный id рабочего пространства, затем как URL-кодированный канонический абсолютный cwd. Выбранный рантайм должен быть доверенным. Проверка существования сессии и ненулевого groupId привязана к активному сохранённому, архивному сохранённому и живому состоянию сессии и хранилищу групп этого рантайма, без фолбэка на основное или другое рабочее пространство. Устаревший маршрут остаётся только для основного рабочего пространства.
Запрос:
{ "isPinned": true, "groupId": "018f..." }| Поле | Обязательно | Примечания |
|---|---|---|
isPinned | нет | Boolean. true устанавливает pinnedAt, если элемент еще не был закреплен; false очищает pinnedAt. |
groupId | нет | Пользовательский id группы или null для отсутствия группы. Неизвестные id групп возвращают 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) организации сессий на уровне проекта в директории runtime-хранилища демона. Это не контент транскрипта, оно не обновляет mtime транскрипта, не экспортируется вместе с транскриптами и сохраняется при архивировании/разархивировании.
POST /session/:id/heartbeat
Обновление учетной записи last-seen (последнее посещение) для этой сессии в демоне. Долгоживущие адаптеры (TUI/IDE/web) пингуют этот эндпоинт с определенным интервалом, чтобы будущая политика отзыва (Wave 5 PR 24) могла отличать мертвых клиентов от просто молчащих.
Заголовки:
| Заголовок | Обязательно | Примечания |
|---|---|---|
X-Qwen-Client-Id | нет | Возвращает (echoes) id, выданный демоном из POST /session. Идентифицированные клиенты также обновляют свой клиентский таймстамп; анонимные heartbeat-запросы обновляют только вотермарк (watermark) сессии. Должен соответствовать тому же формату [A-Za-z0-9._:-]{1,128}, что и в других местах. |
Тело запроса пустое (подойдет {} — на сегодняшний день поля не читаются).
Ответ:
{
"sessionId": "<sid>",
"clientId": "<cid>",
"lastSeenAt": 1700000000123
}clientId возвращается только в том случае, если был передан доверенный X-Qwen-Client-Id. lastSeenAt — это сохраненная bridge серверная эпоха Date.now() (в мс) демона.
Ошибки:
400—{ code: 'invalid_client_id' }, если заголовок имеет неверный формат (правило формы заголовка) или если в нем переданclientId, который не зарегистрирован для этой сессии (bridge выбрасываетInvalidClientIdErrorдо обновления любого таймстампа).404— неизвестная сессия.
Гейтинг возможностей (Capability gating): pre-flight caps.features.client_heartbeat. Более старые демоны возвращают 404 для этого пути.
POST /session/:id/model
Переключение активной модели внутри текущего привязанного сервиса моделей сессии. Сериализуется через очередь смены модели для каждой сессии.
(Для переключения самого сервиса — Alibaba ModelStudio, OpenRouter и т.д. — передавайте modelServiceId в POST /session для новой сессии. В Stage 1 нет маршрута для переключения сервиса в реальном времени.)
Запрос:
{ "modelId": "qwen-staging" }Ответ:
{ "modelId": "qwen-staging" }При успехе публикует model_switched в SSE-стрим. При ошибке публикует model_switch_failed (чтобы пассивные подписчики видели ошибку, а не только вызывающая сторона). Запускается параллельно с выходом из канала агента, чтобы зависший дочерний процесс не мог заблокировать HTTP-обработчик. Успешное переключение также записывает модель сессии в JSONL сессии в режиме best-effort; когда запись создана, загрузка/возобновление демона пытается восстановить модель этой сессии перед аутентификацией. Если записанная модель больше не может быть применена (модель удалена, учётные данные недоступны), восстановление использует одноимённый маршрут реестра, если он существует — для записи runtime-snapshot это может быть другой эндпоинт, чем записанная привязка — и переходит на значение по умолчанию settings.model.name только когда ни один маршрут не разрешён. settings.model.name по-прежнему обновляется как значение по умолчанию для новых сессий.
POST /session/:id/recap
Тег возможности (Capability tag): session_recap. Bridge → ACP extMethod qwen/control/session/recap.
Генерация краткого резюме сессии из одного предложения в формате “на чем я остановился”. Обертка над generateSessionRecap из core (packages/core/src/services/sessionRecap.ts), которая выполняет side-query к быстрой модели с отключенными инструментами, maxOutputTokens: 300 и строгим форматом вывода <recap>...</recap>. Side-query читает существующую историю чата GeminiClient сессии и не добавляет в нее ничего.
Тело запроса игнорируется (отправьте {} или оставьте пустым). Нестрогий мутационный гейт — поведение зеркально /session/:id/prompt (вызов стоит токенов, но не мутирует состояние). Событие SSE не публикуется.
Ответ (200):
{
"sessionId": "sess:42",
"recap": "Отладка гонки при повторной попытке авторизации. Далее: добавить детерминированный тайминг в интеграционный тест."
}recap равен null (обычный 200, не ошибка), когда:
- в сессии еще не было двух диалоговых ходов,
- side-query не вернул извлекаемую нагрузку
<recap>...</recap>, - или произошла любая базовая ошибка модели (хелпер core работает в режиме best-effort и никогда не выбрасывает исключения).
Ошибки:
400 {code: 'invalid_client_id'}— неверный формат заголовкаX-Qwen-Client-Id.404— неизвестная сессия.
Отмена: отсутствует в v1. Маршрут не слушает отключение HTTP-клиента, AbortSignal не передается в bridge, и дочерний процесс ACP выполняет side-query до завершения независимо от того, отключился ли вызывающий клиент. Единственными ограничениями являются 60-секундный резервный таймаут bridge (SESSION_RECAP_TIMEOUT_MS) и гонка transport-closed с гибелью канала ACP. Это приемлемо, поскольку recap выполняется быстро (одна попытка, maxOutputTokens: 300, обычно ~1–5 с); ext-метод отмены на основе 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-клиента отменяет запрос генерации.
Мутация: approval, tools, skills, init, перезапуск MCP
Демон предоставляет пять маршрутов управления изменениями, которые позволяют удалённым клиентам изменять состояние среды выполнения без использования CLI хоста демона. Все пять:
- Защищены строгим шлюзом изменений из PR 15. Демон, настроенный без bearer-токена, отклоняет их с ошибкой
401 {code: 'token_required'}. Настройте--token(илиQWEN_SERVER_TOKEN) перед включением. - Принимают и помечают заголовок
X-Qwen-Client-Id(цепочка аудита из PR 7). Если заголовок содержит доверенный id, демон добавляетoriginatorClientIdв соответствующее событие SSE, чтобы интерфейсы других клиентов могли подавлять эхо собственных изменений. - Выполняют pre-flight проверку каждой возможности для каждого тега перед предоставлением доступа. Более старые версии демона возвращают
404для этого маршрута.
Маршруты переключения инструментов, переключения навыков, init и перезапуска MCP генерируют события в масштабе рабочего пространства: каждое активное событие SSE-шины сессии получает это событие, независимо от того, какая сессия была подключена в момент инициирования изменения. approval-mode генерирует событие в масштабе сессии, так как изменение является локальным для Config только одной сессии.
POST /session/:id/approval-mode
Тег возможности: session_approval_mode_control. Bridge → ACP extMethod qwen/control/session/approval_mode.
Изменяет режим одобрения для активной сессии. Новый режим немедленно применяется в Config дочернего процесса ACP для конкретной сессии. По умолчанию настройки НЕ записываются на диск — передайте 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. Чистый файловый ввод-вывод — без обращения к ACP.
Переключает имя инструмента в списке настроек tools.disabled рабочего пространства. Инструменты, указанные там, не регистрируются вообще (в отличие от permissions.deny, который оставляет инструмент зарегистрированным, но отклоняет его вызов). Как встроенные инструменты, так и инструменты, обнаруженные через MCP, проходят через ToolRegistry.registerTool, который проверяет набор отключенных инструментов.
⚠️ Имена должны точно совпадать с открытым идентификатором в реестре. Разрешение псевдонимов не выполняется — маршрут сохраняет любую строку из path-параметра в
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'}— пустой path-параметр или его длина превышает лимит в 256 символов.400 {code: 'invalid_enabled_flag'}—enabledотсутствует или не является булевым значением.
Событие SSE (в масштабе рабочего пространства): tool_toggled с {toolName, enabled, originatorClientId?}.
POST /workspace/skills/:name/enable
Тег возможности: workspace_skill_toggle. Форма с указанием рабочего пространства — POST /workspaces/:workspace/skills/:name/enable.
Переключение загруженного, вызываемого пользователем навыка через настройки навыков рабочего пространства, аналогично поведению клавиши Space в панели /skills CLI. Поиск выполняется без учёта регистра, тогда как сохранение и ответ используют каноническое имя навыка. Включение навыка skills.defaultDisabled добавляет opt-in skills.enabled рабочего пространства; отключение удаляет этот opt-in и добавляет запись skills.disabled рабочего пространства. Существующие записи для навыков, которые больше не загружены, сохраняются, а дублирующиеся записи или записи с другим регистром для целевого навыка сворачиваются. Запись жёсткого отключения, унаследованная от системных настроек по умолчанию, пользователя или системной области, блокирует навык: область рабочего пространства не может её переопределить.
Это отличается от операции управляемого навыка ACP qwen/skills/setEnabled и поля frontmatter disable-model-invocation. Эффективная доступность навыка следует порядку skills.disabled > skills.enabled > skills.defaultDisabled. Как жёсткое, так и стандартное отключение удаляют навык из доступности slash-команд/модели и отклоняют последующее выполнение навыка. disable-model-invocation: true сохраняет прямой вызов пользователем доступным и только скрывает навык от вызова моделью.
Запрос:
{ "enabled": false }Ответ (200):
{
"skillName": "review",
"enabled": false,
"changed": true,
"activation": "applied",
"sessionsRefreshed": 2,
"sessionsFailed": 0
}activation — это applied, когда каждая активная сессия обновилась, deferred, когда дочерний процесс ACP отсутствует (сохранённая настройка используется при его запуске), и partial, когда хотя бы одна активная сессия не смогла обновиться. Занятые сессии включены. Демон перезагружает настройки рабочего пространства для дочернего процесса ACP и каждой активной сессии, уведомляет потребителей SkillManager и отправляет available_commands_update. Запрос, уже отправленный модели, не переписывается; последующая валидация, снимки команд и контексты модели используют новое состояние. Если сохранение не удаётся, обновление или событие не выдаются. Если обновление сессии не удаётся, зафиксированная настройка сохраняется. Когда дочерний процесс возвращает результаты для каждой сессии, счётчики сессий точны. Если сам контроль обновления завершается ошибкой до возврата этих результатов, sessionsFailed: 1 является консервативной нижней границей, указывающей на сбой запроса обновления.
Ошибки:
400 {code: 'invalid_skill_name'}— пустой path-параметр или более 256 символов.400 {code: 'invalid_enabled_flag'}—enabledотсутствует или не является булевым значением.403 {code: 'untrusted_workspace'}— выбранное рабочее пространство не является доверенным.404 {code: 'skill_not_found'}— ни один загруженный навык не соответствует имени.409 {code: 'skill_not_toggleable', reason: 'not_user_invocable' | 'inactive_extension' | 'locked', lockedScope?: 'system' | 'user' | 'systemDefaults'}— панель CLI не позволила бы переключить целевой навык.lockedScopeприсутствует только когдаreason—locked.
Мутация повторно использует событие settings_changed в масштабе рабочего пространства для каждого изменённого ключа (skills.disabled и/или skills.enabled); она не добавляет новый тип события. Каждое из этих событий включает тот же объект mutation: { id, kind: 'skill_toggle', skills: [{ name, enabled }], activation, sessionsRefreshed, sessionsFailed }. id коррелирует каждое событие настроек, созданное одним запросом переключения. skills перечисляет канонические имена и результирующие состояния включённости навыков, которые фактически изменились. Ячейки статуса навыков рабочего пространства включают опциональные поля disabledReason: 'hard' | 'default' | 'inactive_extension' и lockedScope: 'system' | 'user' | 'systemDefaults'.
POST /workspace/skills/enable
Тег возможности: workspace_skill_batch_toggle. Форма с указанием рабочего пространства — POST /workspaces/:workspace/skills/enable.
Переключает до 100 загруженных навыков в одном запросе; лимит считает сырые записи skillNames до дедупликации. Имена обрезаются и дедуплицируются без учёта регистра с сохранением порядка первого вхождения. Демон проверяет по одному снимку статуса навыка, сохраняет все корректные изменения в одной блокированной записи настроек и обновляет активные сессии один раз. Обработка работает по мере возможности (best-effort) для ожидаемых ошибок цели: неизвестная, скрытая, неактивное расширение или заблокированная цель записывается в errors без предотвращения применения других корректных целей. Неожиданные сбои сохранения или генерации runtime всё равно проваливают весь запрос.
Запрос:
{
"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
}
],
"errors": [
{
"skillName": "missing",
"code": "skill_not_found",
"error": "Skill not found: missing"
}
]
}Ошибки цели используют skill_not_found, skill_not_toggleable или skill_inactive_extension. Неправильно сформированные запросы возвращают HTTP 400 с invalid_skill_names, invalid_skill_name или invalid_enabled_flag. Аутентификация, доверие рабочего пространства, идентификация клиента, неожиданные сбои сохранения и сбои генерации runtime проваливают весь запрос через стандартные шлюзы маршрута. Batch-level activation, sessionsRefreshed и sessionsFailed описывают единое обновление живой сессии, общее для всех изменённых результатов. activation сообщает о попытке обновления, а не о результате: пакет, в котором ни одна цель не изменилась (например, каждая цель завершилась ошибкой), всё равно отвечает applied, когда сессия активна, соответствуя ответу no-op для одного навыка, поэтому выводите фактические изменения из флага changed каждого результата и массива errors. Когда хотя бы одна цель изменяется, демон выдаёт те же метаданные мутации settings_changed, что и маршрут одиночного навыка; каждое событие skills.disabled / skills.enabled из этого запроса разделяет один mutation.id.
POST /workspace/init
Тег возможности: workspace_init. Чистый файловый ввод-вывод — без обращения к ACP, без вызова LLM.
Создает пустой QWEN.md (или то, что возвращает getCurrentGeminiMdFilename() при переопределении --memory-file-name) в корневой папке рабочего пространства, привязанного к демону. Только механическое действие — для заполнения содержимого с помощью ИИ, выполните POST /session/:id/prompt.
По умолчанию отказывается перезаписывать файл, если целевой файл существует и содержит непробельные символы. Файлы, содержащие только пробелы, считаются отсутствующими (аналогично локальной slash-команде /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 байт?» без повторного вызова stat.
Событие 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. Bridge → ACP extMethod qwen/control/workspace/mcp/restart.
Перезапускает настроенный MCP-сервер через McpClientManager.discoverMcpToolsForServer дочернего процесса ACP (отключение + повторное подключение + повторное обнаружение). Предварительно проверяет актуальный снимок бюджета из системы учета PR 14 v1, чтобы перезапуск в рабочем пространстве с исчерпанным бюджетом возвращал мягкий отказ, а не запускал каскад ошибок BudgetExhaustedError.
Тело запроса пустое ({}). Path-параметр — это 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'}— пустой path-параметр.404— имя сервера отсутствует в конфигурацииmcpServersили не существует активного ACP-канала (перезапуск по своей сути требует активного экземпляраMcpClientManager).500— внутренняя ошибка (например,ToolRegistryне инициализирован).
События SSE (в масштабе рабочего пространства): mcp_server_restarted с {serverName, durationMs, originatorClientId?} при успехе; mcp_server_restart_refused с {serverName, reason, originatorClientId?} при мягком пропуске.
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 correlationQuery-параметры:
| Параметр | Обязательный | Примечания |
|---|---|---|
maxQueued | нет | Лимит очереди живых фреймов для каждого подписчика. Диапазон [16, 2048], по умолчанию 256. Фреймы повторной передачи, принудительно отправляемые при подписке, не подпадают под ограничения по количеству фреймов и байт; фактически они потребляются живыми событиями, которые поступают, пока подписчик все еще обрабатывает большую повторную передачу Last-Event-ID: 0. Увеличьте значение для холодных переподключений, чтобы живой хвост не вызывал предупреждение о медленном клиенте / исключение до того, как потребитель догонит. Лимит живых сериализованных байт жестко задан на стороне демона (по умолчанию 2 МиБ) и не имеет query-параметра. Значения вне диапазона / не в десятичном формате / присутствующие, но пустые возвращают 400 invalid_max_queued до открытия SSE-соединения. Pre-flight 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 недоступным. Id идентифицирует это физическое 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}}Строки id: / event: на уровне SSE дублируют envelope.id / envelope.type для совместимости с EventSource. Потребители, использующие чистый fetch (например, parseSseStream из SDK), читают все данные из JSON-конверта и игнорируют строки преамбулы SSE.
| Тип события | Триггер |
|---|---|
session_update | Любое ACP-уведомление sessionUpdate (чанки LLM, вызовы инструментов, использование) |
permission_request | Агент запросил подтверждение инструмента |
permission_resolved | Какой-либо клиент проголосовал за разрешение через POST /permission/:requestId |
permission_partial_vote | (только для consensus) Голос учтен, но кворум еще не достигнут. Содержит {requestId, sessionId, votesReceived, votesNeeded, quorum, optionTallies}. Pre-flight caps.features.permission_mediation. |
permission_forbidden | Голос отклонен активной политикой (несоответствие designated, local-only не для loopback или голосующий в consensus отсутствует в снапшоте). Содержит {requestId, sessionId, clientId?, reason}. Pre-flight caps.features.permission_mediation. |
model_switched | POST /session/:id/model выполнен успешно |
model_switch_failed | POST /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 (синтетическое). Pre-flight 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>). - Обнаружение пропусков: если
<n>старше самого старого события, всё ещё находящегося в кольце, демон выдаёт фреймstate_resync_requiredбез id перед воспроизведением сохранившегося хвоста. SDK устанавливаетawaitingResync; клиенты должны вызватьPOST /session/:id/loadи перестроить из текущего ограниченного окна снимка воспроизведения. Этот снимок может сам начинаться сhistory_truncated, когда старые записи воспроизведения в памяти были отброшены; этот маркер информационный и не должен запускать другой цикл ресинхронизации. - ID монотонны в рамках сессии, начинаются с 1
- Синтетические фреймы (
client_evicted,slow_client_warning,stream_error) намеренно не содержатid, чтобы не занимать слот в последовательности для других подписчиков
Противодавление:
- Очередь для каждого подписчика по умолчанию имеет лимит
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". При переполнении лимита живых байтов отправляетсяreason: "queue_bytes_overflow". В обоих случаях терминальный фрейм отправляется принудительно, и подписка закрывается.
POST /permission/:requestId
Проголосуйте по ожидающему обработки permission_request. Активная политика медиации определяет, чей голос победит:
| Политика | Поведение |
|---|---|
first-responder (по умолчанию) | Побеждает любой валидированный голосующий; последующие голосующие получают 404. Базовая версия до F3. |
designated | Решает только инициатор промпта (originatorClientId); не-инициаторы получают 403 permission_forbidden / designated_mismatch. Для анонимных промптов используется fallback на first-responder. |
consensus | N из M голосующих должны согласиться (по умолчанию N = floor(M/2) + 1, переопределяется через policy.consensusQuorum). Побеждает первый вариант, набравший N голосов. Нерешающие голоса получают 200 + SSE-фреймы 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; форма провода остаётся бит-в-бит неизменной, когда настроенная политика —
first-responder. Новые события (permission_partial_vote,permission_forbidden) являются аддитивными — старые SDK видят их какunrecognized_known_eventи корректно игнорируют.
Таймаут разрешения (по умолчанию 5 минут).
permission_requestостаётся ожидающим до тех пор, пока: (a) какой-либо клиент не проголосует здесь, (b) не сработаетPOST /session/:id/cancel, (c) HTTP-клиент, управляющий промптом, не отключится (отмена в середине промпта разрешает outstanding разрешения какcancelled), (d) сессия не будет убита, (e) демон не завершит работу, или (f) не сработает таймаут разрешения на сессию (DEFAULT_PERMISSION_TIMEOUT_MS, 5 минут). При срабатывании таймаута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 {}— ваш голос принят (разрешён ИЛИ записан при кворуме consensus)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.
Бесплатный уровень Qwen OAuth был прекращён 15.04.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
Строгий шлюз мутации: требует bearer-токен даже при конфигурации loopback без токена (401 token_required).
Запрос:
{ "providerId": "qwen-oauth" }Ответ (201 свежий запуск, 200 идемпотентный перехват):
{
"deviceFlowId": "fa07c61b-…",
"providerId": "qwen-oauth",
"status": "pending",
"userCode": "USER-1",
"verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device",
"verificationUriComplete": "https://chat.qwen.ai/api/v1/oauth2/device?user_code=USER-1",
"expiresAt": 1700000600000,
"intervalMs": 5000,
"attached": false
}Ошибки:
400 unsupported_provider— неизвестныйproviderId(ответ включаетsupportedProviders)409 too_many_active_flows— достигнут лимит рабочего пространства (4); отмените один черезDELETE401 token_required— строгий шлюз отклонил запрос без токена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 - терминальная запись →
204no-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}— демон соблю upstreamslow_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 auth-code с редирект-callback, что не работает для демонов на headless-подах. Поверхность device-flow Mode B является частной для демона — клиенты, нацеленные на MCP-совместимые серверы, должны использовать другой путь аутентификации.
Потоковый формат провода
События выдаются в виде стандартных фреймов 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_TOKEN | Bearer-токен. Обрезается от ведущих/замыкающих пробелов при запуске. |
Структура исходного кода
| Путь | Назначение |
|---|---|
packages/cli/src/commands/serve.ts | Команда yargs + схема флагов |
packages/cli/src/serve/run-qwen-serve.ts | Жизненный цикл listener + обработка сигналов |
packages/cli/src/serve/server.ts | Сборка Express-приложения, порядок middleware и оставшиеся прямые маршруты |
packages/cli/src/serve/routes/*.ts | Сфокусированные группы маршрутов Express, включая сессию, SSE, аутентификацию рабочего пространства, статус рабочего пространства и файлы |
packages/cli/src/serve/auth.ts | bearer + allowlist хостов + CORS deny |
packages/cli/src/serve/acp-session-bridge.ts | Совместимый фасад бриджа CLI для spawn-or-attach, per-session 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.ts | TS-клиент |
packages/sdk-typescript/src/daemon/sse.ts | Парсер фреймов EventSource |
integration-tests/cli/qwen-serve-routes.test.ts | 18 тестов, без LLM |
integration-tests/cli/qwen-serve-streaming.test.ts | 3 теста, реальный дочерний процесс qwen --acp с локальным фейковым сервером OpenAI (только POSIX; пропускается на Windows) |