Skip to Content
Руководство для пользователейРежим демона (qwen serve)

Режим демона (qwen serve)

Запускайте Qwen Code как локальный HTTP-демон, чтобы несколько клиентов (плагины для IDE, веб-интерфейсы, скрипты CI, пользовательские CLI) использовали одну сессию агента через HTTP + Server-Sent Events вместо того, чтобы каждый из них запускал свой собственный подпроцесс.

🚧 v0.16-alpha: qwen serve впервые публикуется в npm в версии v0.16-alpha как текстовый чат / кодинг с только локальным развертыванием. Поддержка вложений изображений / файлов в пути промпта, контейнеризированное развертывание (Docker / k8s / nginx reverse-proxy) и защита удаленного / мультидемонного режима будут добавлены в последующем патче, когда будет подтвержден пилотный проект для предприятий. Полный список отложенных функций см. в разделе Известные ограничения v0.16-alpha.

Статус: Этап 1 (экспериментальный). Протокольная поверхность зафиксирована в таблице маршрутов §04 из issue #3803 . Этап 1.5 (флаг qwen --serve — TUI размещает тот же HTTP-сервер) и Этап 2 (рефакторинг внутри процесса + полировка mDNS/OpenAPI/WebSocket/Prometheus) следуют непосредственно за ним.

Честность в отношении охвата: Этап 1 рассчитан на разработчиков, создающих прототипы клиентов для протокольной поверхности, и на локальное сотрудничество одного пользователя / небольшой команды. Рабочие нагрузки производственного уровня с множеством клиентов / длительным выполнением / нестабильной сетью (мобильные приложения, IM-боты с 1000+ чатами) требуют гарантий Этапа 1.5+, которых нет в этом релизе. Полный список пробелов см. в разделе Гарантии времени выполнения Этапа 1.5+, а дорожную карту сближения — в #3803.

Что это дает

  • Встроенный веб-интерфейс Web Shellqwen serve «из коробки» обслуживает браузерный Web Shell в своем корне (http://127.0.0.1:4170/); запустите qwen serve --open, чтобы автоматически открыть его в браузере. Он обслуживается на том же источнике (origin), что и API, поэтому второй порт или обратный прокси не нужны. Передайте --no-web для демона только с API.
  • Один процесс агента, много клиентов — при стандартном sessionScope: 'single' каждый клиент, подключающийся к демону, использует одну сессию ACP. Живое кросс-клиентское сотрудничество в одном разговоре, с одними и теми же diff-ами файлов и одними и теми же запросами разрешений.
  • Потоковая передача с безопасным переподключением — SSE с переподключением через Last-Event-ID позволяет клиенту отключиться и возобновить работу ровно с того места, где он остановился (в пределах окна воспроизведения ring buffer).
  • Разрешения для первого ответившего — когда агент запрашивает разрешение на запуск инструмента, запрос видят все подключенные клиенты; побеждает тот клиент, который ответит первым.
  • Один демон, одно рабочее пространство — каждый процесс qwen serve при запуске привязывается ровно к одному рабочему пространству (согласно #3803  §02). Для развертывания с несколькими рабочими пространствами запускайте по одному демону на каждое рабочее пространство на отдельных портах (или за оркестратором).
  • Экспериментальные каналы под управлением демонаqwen serve --channel <name> запускает воркер канала, жизненным циклом которого управляет демон. Воркер — это отдельный процесс, который подключается обратно к демону через SDK и сообщает о своем состоянии в GET /daemon/status.
  • Удаленное управление средой выполнения (#4175  PR 17) — измените режим одобрения сессии (POST /session/:id/approval-mode), включите/выключите инструмент для рабочего пространства (POST /workspace/tools/:name/enable), создайте пустой QWEN.md (POST /workspace/init, только механическое действие — НЕ вызывает модель; для заполнения ИИ выполните POST /session/:id/prompt), перезапустите один MCP-сервер с предварительной проверкой бюджета (POST /workspace/mcp/:server/restart) или добавьте/удалите MCP-серверы во время выполнения без перезапуска демона (POST /workspace/mcp/servers, DELETE /workspace/mcp/servers/:name). Все строго ограничено — сначала настройте --token.
  • Краткое содержание сессии (#4175  follow-up) — получите краткое резюме из одного предложения «на чем я остановился» для активной сессии (POST /session/:id/recap). Обертывает generateSessionRecap из ядра как побочный запрос к быстрой модели; не засоряет ни основную историю чата, ни поток SSE. Не строгий шлюз (та же политика, что и для /prompt); хелпер SDK client.recapSession(sessionId).
    • Известное ограничение — усиление затрат токенов: маршрут является конечной точкой с чистыми затратами (каждый вызов — это побочный запрос LLM, без выгоды от состояния), а у демона нет ограничения скорости (rate limit) для каждого маршрута в v1. При стандартной loopback-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте --token (и опционально --require-auth) на общих хостах для разработки перед открытием демона.
    • Безопасность параллельного получения резюме: два одновременных вызова /recap для одной сессии запускают два независимых побочных запроса. generateSessionRecap читает снимок истории чата через GeminiClient.getChat().getHistory() и передает его в отдельный вызов BaseLlmClient.generateText (через runSideQuery); он никогда не добавляет и не изменяет GeminiChat сессии. Безопасно вызывать из нескольких клиентов без координации.

Известные ограничения v0.16-alpha

Первый релиз qwen serve в npm (v0.16-alpha) намеренно ограничен — это текстовый чат / кодинг для разработчиков, запускающих демон на своих машинах. Список ниже явно описывает отложенную функциональность, чтобы внедряющие могли это учесть; всё здесь входит в дорожную карту патчей v0.16.x или в ближайший последующий релиз.

Поверхность продукта — только текст:

  • ✅ Текстовые промпты и текстовые ответы (чат, кодинг, вызовы инструментов, интеграция MCP)
  • Вложения изображений / файлов в пути промптаMessageEmitter в настоящее время отображает только текст; мультимодальное эхо появится, когда будет подтверждена альфа-цель с потребностями в изображениях (#4175 chiga0 #27 P0 item)
  • Потоковые загрузки — те же ограничения, что и для мультимодальности

Поверхность развертывания — только локально:

  • ✅ Loopback (127.0.0.1, по умолчанию) — аутентификация не требуется, подходит для рабочих станций разработчиков
  • ✅ Локальный запуск через systemd / launchd / nohup & / tmux — см. Шаблоны локального запуска
  • ✅ Использование своего bearer-токена через переменную окружения QWEN_SERVER_TOKEN (настройка в разделе Аутентификация)
  • Контейнеризированное развертывание — Docker / Compose / Kubernetes / nginx reverse-proxy с терминацией TLS НЕ входят в v0.16-alpha. Отложено до v0.16.x после подтверждения пилотного проекта для предприятий (иначе устареет из-за отсутствия валидации).
  • Координация нескольких демонов на одном хосте — принудительно применяется 1 демон = 1 рабочее пространство × N сессий. Кросс-хостовая федерация, привязка токенов к путям экземпляров и очистка устаревших токенов отложены до v0.16.x.
  • Автоматически генерируемые токены демона — в альфе используется свой токен (всего в одном openssl rand -hex 32). Инфраструктура автогенерации и хранилища токенов отложена до v0.16.x.

Защита — минимум для локального использования одним пользователем:

  • ✅ Проверка безопасности при запуске (отказывает в привязке не к loopback без токена, PR 15 / #4236 )
  • ✅ Шлюз аутентификации для маршрутов мутации, маршрутизация разрешений с областью действия сессии (PR из Wave 4)
  • ✅ Ограничения MCP + координация разрешений для нескольких клиентов (F2 / F3)
  • Абсолютный дедлайн промпта + таймаут простоя SSE-писателя — включается через --prompt-deadline-ms и --writer-idle-timeout-ms; анонсируется через prompt_absolute_deadline и writer_idle_timeout при включении.
  • Ограничение скорости HTTP — включается через --rate-limit и пороги для каждого уровня; анонсируется через rate_limit при включении.
  • ⏸️ Метрики Prometheus + нагрузочное тестирование — отложено до v0.17 F4 Phase-1 масштабной инструментации, когда 30-50 активных сессий станут реальной целью.
  • ⏸️ CLI-флаг --max-body-size — демон по умолчанию применяет express.json({ limit: '10mb' }), чего с запасом хватает для текстовых промптов (окна контекста модели значительно меньше 10 МиБ символов). Настройка через флаг в v0.16.x.

Более подробный перечень «что мы не будем исправлять на Этапе 1» (модель мутации состояния сессии на одном хосте + N параллельных сессий, использующих один дочерний процесс ACP) см. в разделе Границы области Этапа 1 — что мы не будем исправлять на Этапе 1.5 ниже.

Быстрый старт

1. Запуск демона (loopback, без аутентификации)

cd your-project/ qwen serve # → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project) # → qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.

Привязка по умолчанию — 127.0.0.1:4170. Bearer-аутентификация отключена для loopback, чтобы локальная разработка «просто работала». Демон привязывается к текущему рабочему каталогу; используйте --workspace /path/to/dir, чтобы переопределить это.

Откройте веб-интерфейс Web Shell. Перейдите по адресу http://127.0.0.1:4170/ (или запустите демон с qwen serve --open, чтобы открыть его автоматически) для доступа к полноценному браузерному терминалу — чат, diff-ы, вызовы инструментов и запросы разрешений. Интерфейс обслуживается в корне демона на том же источнике, что и API. В остальной части этого руководства используется сырой HTTP, чтобы вы могли писать скрипты для работы с API напрямую.

2. Проверка работоспособности

curl http://127.0.0.1:4170/health # → {"status":"ok"} curl http://127.0.0.1:4170/capabilities # → {"v":1,"mode":"http-bridge","features":["health","daemon_status","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"} curl http://127.0.0.1:4170/daemon/status # → {"v":1,"detail":"summary","status":"ok","runtime":{...}}

Поле workspaceCwd показывает привязанное рабочее пространство, чтобы клиенты могли выполнить предварительную проверку и опустить cwd в POST /session. Поле limits.maxPendingPromptsPerSession анонсирует активный лимит на количество промптов в очереди для каждой сессии; null означает, что лимит отключен.

Запуск каналов из демона

# Start one configured channel under qwen serve qwen serve --channel telegram # Start several configured channels under one daemon-owned worker qwen serve --channel telegram --channel feishu # Start all configured channels qwen serve --channel all

Этот режим является экспериментальным и управляется демоном. Он не заменяет автономную команду qwen channel start: автономные каналы по-прежнему используют сервис AcpBridge на базе ACP. При использовании qwen serve --channel демон запускает один процесс воркера канала после готовности HTTP-среды выполнения. Если воркер завершает работу после запуска, демон продолжает работать, а GET /daemon/status сообщает о предупреждении channel_worker_exited. Автоматический перезапуск воркера отложен.

Демон привязан к одному рабочему пространству, поэтому cwd каждого выбранного канала должен разрешаться в рабочее пространство демона. --channel all нельзя комбинировать с именованными каналами.

Демон также предоставляет снимки среды выполнения только для чтения для клиентских интерфейсов и операторов: GET /daemon/status, GET /workspace/mcp, GET /workspace/skills, GET /workspace/providers, GET /workspace/env, GET /workspace/preflight, GET /session/:id/status, GET /session/:id/context, GET /session/:id/supported-commands, и GET /session/:id/tasks, и GET /session/:id/lsp.

GET /session/:id/status возвращает сводку live-моста для одной сессии: sessionId, workspaceCwd, createdAt, опциональный displayName, clientCount, и hasActivePrompt. Он возвращает 200 со сводкой, если демон хранит live-сессию с этим id, и 404 (тело { "error": …, "sessionId": … }) в противном случае. Используйте его для опроса, работает ли одна известная сессия (hasActivePrompt) или сколько клиентов подключено (clientCount) без получения и сканирования всего постраничного списка сессий:

curl http://127.0.0.1:4170/session/$SESSION_ID/status # → {"sessionId":"…","workspaceCwd":"…","createdAt":"…","clientCount":1,"hasActivePrompt":false}

Это необработанное представление live-сессии, поэтому clientCount и hasActivePrompt совпадают с соответствующей записью в GET /workspace/:id/sessions — но эти два маршрута не идентичны побайтово. Конечная точка списка обогащает каждый элемент сохраненными данными из хранилища сессий: его createdAt — это сохраненное время первого промпта, и он добавляет updatedAt и displayName, полученный из сохраненного заголовка или первого промпта. /status вместо этого сообщает собственный createdAt live-сессии, опускает updatedAt и возвращает displayName только если он установлен в live-сессии.

GET /session/:id/lsp возвращает структурированный статус LSP для каждой сессии. Запустите демон с --experimental-lsp, чтобы включить LSP в порожденных сессиях агента; иначе маршрут возвращает enabled: false без серверов.

GET /daemon/status — это консолидированный снимок для устранения неполадок. Стандартный detail=summary читает только состояние демона в памяти (сессии, разрешения, счетчики транспортов SSE/ACP, отклонения из-за ограничения скорости, память процесса, разрешенные лимиты) и не запускает дочерний процесс ACP. Используйте GET /daemon/status?detail=full для диагностики по каждой сессии, деталей подключения ACP, счетчиков потока устройств аутентификации и разделов статуса рабочего пространства, когда вы активно исследуете проблему.

GET /workspace/mcp, GET /workspace/skills и GET /workspace/providers сообщают о live-среде выполнения ACP и не запускают дочерний процесс ACP в режиме простоя; неактивный демон возвращает initialized: false с пустым снимком. Как только сессия становится активной, они переключаются на initialized: true и показывают реальное состояние.

GET /workspace/env и GET /workspace/preflight всегда отвечают initialized: true независимо от состояния ACP. env никогда не обращается к ACP (только информация о процессе демона); preflight отвечает ячейками уровня демона из process.* и выдает заполнители status: 'not_started' для ячеек уровня ACP, когда дочерний процесс неактивен.

GET /workspace/env сообщает о среде выполнения, платформе, песочнице, прокси и наличии (никогда о значении) переменных окружения с секретами из белого списка, таких как OPENAI_API_KEY. URL-адреса прокси очищаются от учетных данных и сводятся к host:port перед отправкой по сети. Маршрут всегда отвечает непосредственно от процесса демона и никогда не порождает дочерний процесс ACP.

GET /workspace/preflight возвращает список проверок готовности. Ячейки уровня демона (версия Node, точка входа CLI, каталог рабочего пространства, ripgrep, git, npm) отображаются всегда. Ячейки уровня ACP (аутентификация, обнаружение MCP, навыки, провайдеры, реестр инструментов, исходящий трафик) требуют активного дочернего процесса ACP — когда демон неактивен они выдают заполнители status: 'not_started' вместо запуска ACP только для их заполнения. Сбои сопоставляются с закрытым перечислением errorKind (missing_binary, auth_env_error, init_timeout, protocol_error, missing_file, parse_error, blocked_egress), чтобы клиентские интерфейсы могли отображать структурированные инструкции по устранению.

Демон также предоставляет хелперы для работы с файлами рабочего пространства:

  • GET /file читает текстовые файлы и возвращает хэш sha256:<hex> сырых байтов.
  • GET /file/bytes читает ограниченные окна сырых байтов и возвращает содержимое в base64.
  • POST /file/write создает или заменяет текстовые файлы.
  • POST /file/edit применяет одну точную текстовую замену.

Запись/редактирование — это строгие маршруты мутации: даже для loopback они требуют настроенного bearer-токена, иначе они возвращают token_required. Для замен и редактирования требуется последний expectedHash из GET /file (или полноразмерный GET /file/bytes). create никогда не перезаписывает. Явные записи в игнорируемые пути разрешены, но аудируются. Бинарная запись, удаление/перемещение/mkdir и рекурсивное создание родительских каталогов не входят в эту поверхность.

3. Открытие сессии

curl -X POST http://127.0.0.1:4170/session \ -H 'Content-Type: application/json' \ -d '{}' # → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}

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

Второй клиент, отправляющий запрос на /session (с любым совпадающим cwd или без него), получает "attached": true — теперь он использует общего агента.

4. Подписка на поток событий (сначала в другом терминале)

SESSION_ID="<from step 3>" curl -N http://127.0.0.1:4170/session/$SESSION_ID/events # → id: 1 # event: session_update # data: {"id":1,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}}

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

Откройте это до отправки промпта — буфер воспроизведения SSE хранит последние 8000 событий, чтобы запоздалый подписчик мог догнать через Last-Event-ID, но для простого случая «наблюдения за одним промптом» проще всего подписаться сначала и позволить ему передавать данные в реальном времени.

Поток отправляет session_update (фрагменты LLM, вызовы инструментов, использование), permission_request (инструменту требуется одобрение), permission_resolved (кто-то проголосовал), model_switched, model_switch_failed и терминальные фреймы session_died (дочерний процесс агента упал — SSE затем закрывается) и client_evicted (ваша очередь переполнена — SSE затем закрывается).

5. Отправка промпта (вернитесь в исходный терминал)

curl -X POST http://127.0.0.1:4170/session/$SESSION_ID/prompt \ -H 'Content-Type: application/json' \ -d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}' # → {"stopReason":"end_turn"}

curl -N из шага 4 будет печатать фреймы по мере их поступления.

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

Для всего, что выходит за рамки loopback, вы обязаны передать bearer-токен:

export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)" qwen serve --hostname 0.0.0.0 --port 4170 # → boot refuses without QWEN_SERVER_TOKEN

Затем клиенты отправляют Authorization: Bearer $QWEN_SERVER_TOKEN в каждом запросе. /health освобожден от этого только при привязке к loopback, чтобы проверки жизнеспособности k8s/Compose внутри пода (где демон слушает 127.0.0.1) не требовали учетных данных. При привязке не к loopback (--hostname 0.0.0.0 и т.д.) /health требует токен, как и любой другой маршрут — в противном случае злоумышленник может зондировать произвольные адреса, чтобы подтвердить существование демона. Используйте /capabilities, чтобы проверить правильность вашего токена сквозным способом (он всегда требует аутентификации):

Защищенный loopback (--require-auth). Стандартное поведение loopback без токена подходит для ноутбука одного пользователя, но небезопасно на общих хостах для разработки, CI-раннерах или многопользовательских рабочих станциях, где любой локальный пользователь может выполнить curl 127.0.0.1:4170. Передайте --require-auth, чтобы сделать bearer-токен обязательным для каждого маршрута — включая /health и /capabilities — даже при привязке к 127.0.0.1. Запуск завершится ошибкой без токена. При включенном флаге неаутентифицированный клиент не может прочитать /capabilities, чтобы узнать, что требуется аутентификация; поверхностью обнаружения является само тело ответа 401. После аутентификации тег caps.features.require_auth является постаутентификационным подтверждением того, что развертывание защищено (полезно для интерфейсов аудита / соответствия):

qwen serve --require-auth --token "$(openssl rand -hex 32)" # → /health, /capabilities, /session, … all require Authorization: Bearer … curl http://127.0.0.1:4170/health # → 401 curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4170/capabilities | jq '.features | index("require_auth")' # → 13 (or whatever index — non-null after authenticating means the tag is present)
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities # → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"} # Wrong token → 401

Сравнение токенов выполняется за константное время (SHA-256 + crypto.timingSafeEqual); ответы 401 одинаковы для «отсутствующий заголовок», «неверная схема» и «неверный токен», поэтому side-channel не может их различить.

HTTPS / TLS (для мобильного / кросс-девайс доступа)

По умолчанию демон обслуживает простой HTTP. Это нормально для localhost, но телефон или планшет, обращающийся к IP-адресу локальной сети (https://192.168.x.x:4170), не является безопасным контекстом  по http:// — поэтому браузеры блокируют getUserMedia (голосовой ввод), WebRTC и другие API, доступные только в безопасном контексте. Передайте --tls-cert + --tls-key, чтобы обслуживать Web Shell по HTTPS и разблокировать их:

# 1. Установите локальный CA и добавьте его в доверенные (однократно). Мобильное устройство # также должно доверять этому CA — mkcert выведет путь к корневому сертификату. mkcert -install # 2. Сгенерируйте сертификат для LAN IP вашей машины. Добавьте localhost / 127.0.0.1 в # SAN: при использовании `--open` демон переписывает URL браузера на # 127.0.0.1, поэтому сертификат, выпущенный только для LAN IP, будет отклонен с ошибкой # ERR_CERT_COMMON_NAME_INVALID. (mkcert именует выходные файлы по всем хостам.) mkcert 192.168.1.100 localhost 127.0.0.1 # 3. Запустите демон по HTTPS. Привязка к не-loopback интерфейсам по-прежнему требует токен, # а Origin браузера должен быть разрешен через CORS. qwen serve \ --hostname 0.0.0.0 \ --token "$(openssl rand -hex 32)" \ --tls-cert "./192.168.1.100+2.pem" \ --tls-key "./192.168.1.100+2-key.pem" \ --allow-origin "https://192.168.1.100:4170" # → qwen serve слушает на https://0.0.0.0:4170

Примечания:

  • Оба флага или ни одного — запуск завершится ошибкой, если указан только один из них (сертификат без ключа не может запустить HTTPS-слушатель).
  • TLS не зависит от аутентификации — HTTPS шифрует транспорт; bearer-токен по-прежнему защищает каждый API-маршрут. Привязка к не-loopback интерфейсам требует токен как с TLS, так и без него.
  • Только терминация TLS — без автогенерации, без ACME / Let’s Encrypt. Это удобство для LAN / разработки; для публичных развертываний терминацию TLS следует выполнять на reverse proxy (см. модель угроз ниже).

Флаги CLI

FlagDefaultPurpose
--port <n>4170TCP-порт. 0 = эфемерный порт, назначенный ОС.
--hostname <addr>127.0.0.1Интерфейс привязки. Всё, что выходит за пределы loopback, требует токен.
--token <str>Bearer-токен. Если не указан, используется переменная окружения QWEN_SERVER_TOKEN (с удалением начальных и конечных пробелов — удобно для $(cat token.txt)).
--require-authfalseОтказ от запуска без bearer-токена, даже на loopback. Усиливает стандартную настройку разработчика 127.0.0.1 для общих dev-хостов / CI-раннеров / многопользовательских рабочих станций, где любой локальный пользователь может обратиться к слушателю. Запускается только при наличии --token или QWEN_SERVER_TOKEN; также защищает /health bearer-токеном.
--tls-cert <path>Путь к файлу PEM-сертификата. Обслуживание по HTTPS вместо HTTP. Должен использоваться вместе с --tls-key (запуск завершится ошибкой, если указан только один). Открывает доступ к API браузера в безопасном контексте — голосовой ввод (getUserMedia), WebRTC — по LAN IP, которые в противном случае блокируются браузерами на обычном http://. Только терминация TLS; без автогенерации / ACME. См. HTTPS / TLS ниже.
--tls-key <path>Путь к файлу закрытого ключа PEM. Должен использоваться вместе с --tls-cert.
--max-sessions <n>20Лимит одновременных активных сессий. Новые запросы POST /session, которые должны создать новый дочерний процесс, возвращают 503Retry-After: 5) при достижении лимита; подключения к существующим сессиям НЕ учитываются. Установите 0 для отключения. Рассчитано на одного пользователя / небольшую команду; увеличьте значение, если ваше развертывание имеет запас по RAM/FD (~30–50 МБ на сессию).
--max-pending-prompts-per-session <n>5Лимит промптов на сессию, принятых POST /session/:id/prompt, но еще не завершенных, включая промпты в очереди и активный промпт. Bridge отклоняет превышение лимита синхронно с кодом 503, Retry-After: 5 и code: "prompt_queue_full" до возврата promptId. Установите 0 для отключения. branchSession сериализуется в той же FIFO-очереди, но не учитывается в этом лимите промптов.
--workspace <path>process.cwd()Абсолютный путь к рабочей области, к которой привязывается этот демон (согласно #3803  §02 — 1 демон = 1 рабочая область). Запросы POST /session с несовпадающим cwd возвращают 400 workspace_mismatch. Для развертываний с несколькими рабочими областями запускайте один qwen serve на каждую рабочую область на отдельных портах.
--channel <name|all>Экспериментальный воркер каналов, управляемый демоном. Повторите флаг, чтобы выбрать несколько настроенных каналов, или передайте all, чтобы запустить все настроенные каналы. all нельзя комбинировать с именованными каналами. Значения cwd выбранных каналов должны разрешаться в рабочую область демона. Воркер принадлежит qwen serve; остановите демон, чтобы остановить каналы, управляемые serve.
--max-connections <n>256Лимит TCP-соединений на уровне слушателя (server.maxConnections). Ограничивает количество сырых сокетов независимо от количества сессий — медленные / фантомные SSE-клиенты отклоняются на этапе accept при заполнении. Увеличьте вместе с --max-sessions, если ваше развертывание предполагает множество SSE-подписчиков на сессию.
--event-ring-size <n>8000Глубина кольцевого буфера воспроизведения SSE на сессию (цель #3803 §02). Устанавливает размер очереди, доступной для GET /session/:id/events с Last-Event-ID: N. Больше значение = больше запаса для переподключения за счет нескольких сотен КБ дополнительной RAM на сессию. SDK-клиенты могут дополнительно запросить больший лимит очереди на подписчика для конкретной подписки через ?maxQueued=N (диапазон [16, 2048], по умолчанию 256). Демоны также отправляют нетерминальный SSE-фрейм slow_client_warning при заполнении очереди на 75%, чтобы клиенты могли обработать данные / переподключиться до отключения. Pre-flight caps.features.slow_client_warning.
--mcp-client-budget <n>Лимит в виде положительного целого числа на количество активных MCP-клиентов на ACP-сессию (issue #4175  PR 14 v1; PR 23 переводит это на уровень рабочей области через общий пул MCP). Комбинируется с --mcp-budget-mode. Если не задано, принудительное ограничение на основе учета не применяется (но GET /workspace/mcp по-прежнему сообщает clientCount). Отличается от MCP_SERVER_CONNECTION_BATCH_SIZE в claude-code, который ограничивает параллелизм при запуске, а не общее количество клиентов. Pre-flight caps.features.mcp_guardrails.
--mcp-budget-mode <m>warn / offКак применяется --mcp-client-budget. warn (по умолчанию, если бюджет задан): без отказов, budgets[0].status в снапшоте переключается на warning при ≥75% бюджета. enforce: подключения сверх лимита отклоняются, ячейка на сервер показывает disabledReason: 'budget', детерминировано по порядку объявления mcpServers. off (по умолчанию, если бюджет не задан): только наблюдаемость. Запуск отклоняет enforce без бюджета.
--http-bridgetrueРежим этапа 1: один дочерний процесс qwen --acp на демон (привязан к одной рабочей области при запуске, согласно #3803  §02); N сессий мультиплексируются в этот дочерний процесс через ACP newSession(). Нативный in-process режим этапа 2 станет доступен позже.
--allow-origin <pat>T2.4 (#4514 ). Allowlist cross-origin для браузерных webui-клиентов. Флаг можно повторять. Каждое значение — это * (любой origin — запуск отклоняется, если bearer-токен не настроен; рекомендуется --require-auth на loopback, чтобы /health и /demo также защищались bearer-токеном, так как по умолчанию они доступны без аутентификации на loopback) или канонический URL origin (<scheme>://<host>[:<port>], без завершающего слэша / пути / userinfo / query). Поддоменные wildcards (https://*.example.com) намеренно не поддерживаются — перечислите каждый поддомен явно или используйте * с настроенным токеном (и --require-auth для полного усиления). Совпавшие origins получают CORS-заголовки ответа (Access-Control-Allow-Origin, Vary: Origin, methods, headers, max-age и exposed Retry-After); несовпавшие origins по-прежнему получают 403 с той же оберткой, что и сегодня. Origin: null (песочные iframes, документы file://) всегда отклоняется, даже при *. Pre-flight через caps.features.allow_origin. Попадания self-origin на loopback не затрагиваются.
--web / --no-webtrueОбслуживание собранного Web Shell SPA в корне демона (GET /, /assets/* и fallback для deep-link SPA). Статическая оболочка регистрируется до шлюза bearer-аутентификации — браузер не может прикрепить токен к субресурсу <script> или навигации в адресной строке, оболочка не содержит секретов, и каждый API-маршрут остается защищенным токеном независимо от этого. При привязке к не-loopback интерфейсам в stderr выводится предупреждение в одну строку о том, что UI доступен без аутентификации. Используйте --no-web для демона только с API. Не действует, если сборка не включает ассеты Web Shell (демон логирует breadcrumb и работает только с API).
--openfalseПосле запуска слушателя открывает Web Shell в браузере по умолчанию по URL демона (с добавлением #token= в качестве фрагмента URL, если токен настроен — фрагмент никогда не отправляется на сервер, что защищает токен от попадания в логи доступа и заголовки Referer). Не действует с --no-web или в headless / CI / SSH окружениях, где браузер недоступен.

Настройка лимитов нагрузки. --max-sessions — это лимит на новые дочерние процессы (new-child). Еще три уровня также ограничивают нагрузку — при настройке для высоконагруженного деплоя с высокой конкурентностью настраивайте их совместно:

  • уровень listener: --max-connections / server.maxConnections=256 ограничивает количество сырых TCP-соединений (back-pressure для медленных клиентов).
  • подписчики на сессию: EventBus по умолчанию ограничивает количество SSE-подписчиков до 64 на сессию; 65-й клиент получает терминальный stream_error и отключается.
  • прием промптов на сессию: --max-pending-prompts-per-session=5 ограничивает количество промптов в очереди + активных промптов, принимаемых для одной сессии. При переполнении возвращается 503 с Retry-After: 5.
  • бэклог на подписчика: очередь из 256 фреймов на SSE-клиент; клиент, превысивший емкость, получает терминальный фрейм client_evicted и отключается (один медленный потребитель не может “повесить” демон).

Эти лимиты взаимосвязаны: --max-sessions × 64 подписчика × 256 фреймов — это худший случай использования памяти для находящихся в обработке данных на уровне EventBus, а --max-sessions × --max-pending-prompts-per-session ограничивает принятую работу по промптам на уровне приема. Размер по умолчанию рассчитан на одного пользователя / небольшую команду; увеличивайте лимиты постепенно (и следите за RSS) для мультитенантных деплоев.

Ограничения для MCP-клиентов (issue #4175  PR 14). Если в воркспейсе в mcpServers объявлено 30 MCP-серверов, будет запущено 30 клиентов без какого-либо верхнего лимита, если вы его не зададите. --mcp-client-budget=N ограничивает количество активных MCP-клиентов; --mcp-budget-mode={enforce,warn,off} выбирает поведение. По умолчанию используется warn, если задан бюджет (снапшот выводит предупреждение, но ни один клиент не отклоняется — полезно для измерения реального fanout перед включением принудительного режима). Отклоненные серверы в режиме enforce получают disabledReason: 'budget' в своей ячейке на сервер, а ячейка budgets[0] показывает status: 'error' + errorKind: 'budget_exhausted'. Резервирование слота происходит по имени сервера и сохраняется при переподключениях / таймаутах обнаружения — отклоненный сервер не может занять слот у работающего.

⚠️ Область действия v1: на сессию, а не на воркспейс. Каждая ACP-сессия внутри демона имеет свой собственный Config/McpClientManager (создается через newSessionConfig для каждой сессии). Бюджет ограничивает количество активных MCP-клиентов на сессию, а не суммарно по всем сессиям в воркспейсе. Снапшот по GET /workspace/mcp отражает представление bootstrap-сессии (ячейка содержит scope: 'session' для прозрачности). Если вы запустите 5 параллельных ACP-сессий с --mcp-client-budget=10, у вас может быть до 50 активных MCP-клиентов по всему демону — лимит действует на каждую сессию. Wave 5 PR 23 (shared MCP pool) внедряет менеджер на уровне воркспейса и переводит это на настоящее принудительное ограничение на уровне воркспейса.

qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn # позже, после того как телеметрия покажет ваше реальное распределение: qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforce

Это не то же самое, что MCP_SERVER_CONNECTION_BATCH_SIZE в claude-code (который ограничивает конкурентность при запуске); они ортогональны. PR 23 добавит настоящий общий пул MCP (ячейку scope: 'workspace' в budgets[] рядом с ячейкой на сессию); PR 14 v1 — это внутрипроцессный счетчик + мягкое принуждение для существующего менеджера на сессию.

Push-события (issue #4175  PR 14b). SDK-клиенты, подписанные на GET /session/:id/events, получают типизированные фреймы при пересечении порогов бюджета — mcp_budget_warning (синтетический, срабатывает один раз при пересечении 75% в сторону увеличения с повторным взведением гистерезиса на 37.5%, анонсируется через mcp_guardrail_events) и mcp_child_refused_batch (объединяется один раз за проход обнаружения в режиме enforce; длина 1 при отказе в ленивом создании из readResource). Снапшот по GET /workspace/mcp по-прежнему является источником истины для состояния после переподключения; события — это фронты изменений. Полезно для построения дашбордов в реальном времени без опроса.

Модель угроз для деплоя по умолчанию

  • Только 127.0.0.1 — привязка к loopback, аутентификация не требуется.
  • --hostname 0.0.0.0 требует токен — запуск будет отклонен без него.
  • LOOPBACK_BINDS включает IPv6::1 и [::1] считаются loopback для правила без токена.
  • Allowlist заголовка Host — при привязке к loopback демон проверяет, что Host: совпадает с localhost:port / 127.0.0.1:port / [::1]:port / host.docker.internal:port (без учета регистра согласно RFC 7230 §5.4) для защиты от DNS rebinding. Привязки не к loopback (--hostname 0.0.0.0) намеренно обходят allowlist Host — оператор сам выбрал поверхность атаки, поэтому проверка bearer-токена является единственным уровнем аутентификации; обратные прокси / SNI / привязка клиентских сертификатов — это ответственность оператора, а не демона. Если вам нужна изоляция на основе Host при привязке не к loopback, завершайте TLS + проверяйте Host на фронтальном прокси.
  • CORS по умолчанию отклоняет любой Origin браузера — возвращает 403 JSON. Передайте --allow-origin <pattern> (можно повторять, T2.4 #4514), чтобы пропустить определенные Origins браузеров. Каждое значение — это либо литерал * (любой origin — запуск отклоняется, если не настроен bearer-токен; для полного усиления защиты рекомендуется --require-auth на loopback, так как /health и /demo по умолчанию остаются pre-auth на loopback), либо канонический URL origin (<scheme>://<host>[:<port>], без завершающего слэша / пути / userinfo). Совпавшие origins получают правильные заголовки ответа CORS (Access-Control-Allow-Origin: <echoed>, Vary: Origin, а также стандартные методы / заголовки / max-age и выставленный Retry-After); несовпавшие origins по-прежнему получают 403 с тем же конвертом, что и при стандартной блокировке. caps.features.allow_origin анонсируется условно, чтобы SDK / webui клиенты могли предварительно проверить, поддерживает ли демон кросс-доменные запросы, перед их выполнением. Пример: qwen serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173. Запросы с loopback на себя (например, страница /demo) не затрагиваются — отдельная заглушка для удаления Origin обрабатывает их независимо от --allow-origin. Браузерные webui без настроенного --allow-origin по-прежнему возвращаются к тем же опциям Stage 1, что и раньше: упаковывайте как нативную оболочку (Electron/Tauri), чтобы заголовок Origin не отправлялся, или ставьте перед демоном обратный прокси с тем же origin.
  • Запущенный дочерний процесс qwen --acp наследует окружение демона с одной явной очисткой: QWEN_SERVER_TOKEN удаляется перед запуском дочернего процесса (собственный bearer демона; агенту он не нужен). Все остальное — OPENAI_API_KEY / ANTHROPIC_API_KEY / QWEN_* / DASHSCOPE_API_KEY / ваши кастомные modelProviders[].envKey / и т.д. — передается дальше, потому что агенту законно нужны эти данные для аутентификации в LLM. Это сделано намеренно, а не является песочницей. Агент запускается с тем же UID и имеет доступ к shell-инструментам, поэтому что угодно в ~/.bashrc / ~/.aws/credentials / ~/.npmrc все равно будет доступно через инъекцию промпта. Передача окружения не является границей безопасности; граница — это пользователь как корень доверия. Не запускайте qwen serve от имени пользователя, у которого в окружении есть учетные данные, которые вы бы не доверили агенту.
  • Ограниченные SSE-очереди на подписчика — медленный клиент, переполнивший свою очередь, получает терминальный фрейм client_evicted и отключается; один зависший потребитель не может “повесить” демон.
  • Лимит приема промптов на сессию — по умолчанию 5 принятых, но еще не завершенных промптов на сессию. Сбойный клиент не может поставить в очередь неограниченное количество промптов или временных ожиданий SSE для одной сессии.
  • Корректное завершение работы — SIGINT/SIGTERM ожидают завершения дочерних процессов агента перед закрытием listener (10 секунд на каждый дочерний процесс).

⚠️ Известный пробел Stage 1 — разрешения глобальны для демона, а не для каждой сессии (BUy4H). pendingPermissions находится в области действия демона; любой клиент, владеющий bearer-токеном, может голосовать за любой requestId для любой видимой им сессии (и SSE-события permission_request содержат requestId в своих данных). Это приемлемо в модели доверия для одного пользователя / небольшой команды, где каждый аутентифицированный клиент — это один и тот же человек или коллеги, которым он доверяет. В Stage 1.5 будет осуществлен переход на POST /session/:id/permission/:requestId + карту ожиданий в области действия сессии + идентификацию на клиента (must-have #3 из ревью downstream); до этого не запускайте qwen serve с bearer-токеном, которым пользуются ненадежные стороны.

⚠️ Известный пробел Stage 1 — тело POST /session/:id/prompt ограничено 10 МБ (BUy4L). Мультимодальные промпты, содержащие изображения / PDF / аудио, превышающие 10 МБ, завершатся ошибкой на этапе разбора тела до запуска логики маршрута (без потоковой передачи, без прерывания в процессе загрузки). Обходной путь: уменьшите размер контента на стороне клиента или передайте ссылку на путь и позвольте агенту прочитать файл через readTextFile. В Stage 1.5 будет поддерживаться multipart/form-data или chunked encoding на /prompt, чтобы большие промпты не упирались в лимит.

⚠️ Известный пробел Stage 1 — фантомные SSE-соединения за NAT. Демон обнаруживает мертвых клиентов через TCP back-pressure на хартбитах (интервал 15 с). Клиент, который исчезает БЕЗ TCP RST (например, NAT-коробка, тихо отбрасывающая неактивные потоки), оставляет сокет на уровне ядра “живым”, пока не истечет время ожидания keepalive-зондов Node — обычно около 2 часов при настройках Linux по умолчанию. В деплоях с --hostname 0.0.0.0 за такими NAT, фантомные SSE-соединения могут накапливаться и в конечном итоге достичь потолка в 256 server.maxConnections.

Установите --writer-idle-timeout-ms <n> (issue #4514  T2.9), чтобы закрыть этот пробел явным дедлайном простоя на уровне приложения: если ни одна запись не была успешно сброшена (flushed) за n мс, демон отправляет терминальный фрейм client_evicted с reason: 'writer_idle_timeout' и закрывает поток. Флаг выключен по умолчанию для сохранения legacy-контракта — операторам в сетях, которые проглатывают RST, следует выбрать значение значительно выше 15-секундного интервала хартбитов (например, 60000300000), чтобы легитимные неактивные соединения не отключались, в то время как реально зависшие писатели быстро удалялись. Выполните pre-flight caps.features.includes('writer_idle_timeout') из вашего SDK, чтобы убедиться, что демон это поддерживает.

Дедлайны и таймаут простоя writer

Issue #4514  T2.9 добавляет два opt-in флага, которые закрывают пробелы для долго выполняющихся / удаленных деплоев, не покрываемые 15-секундным хартбитом + AbortSignal. Оба выключены по умолчанию — сценарии использования loopback одним пользователем остаются бит-в-бит неизменными.

ФлагПеременная окруженияПо умолчаниюЧто делает
--prompt-deadline-ms <n>QWEN_SERVE_PROMPT_DEADLINE_MSне заданоСерверное ограничение по настенным часам (wallclock) для одного POST /session/:id/prompt. По истечении срока демон прерывает AbortController промпта и возвращает HTTP 504 с {code:"prompt_deadline_exceeded", errorKind:"prompt_deadline_exceeded", deadlineMs:n}. Поле deadlineMs в теле запроса для каждого промпта может СОКРАТИТЬ эффективный дедлайн ниже значения флага, но никогда не продлить его. Тег возможности (условный): prompt_absolute_deadline.
--writer-idle-timeout-ms <n>QWEN_SERVE_WRITER_IDLE_TIMEOUT_MSне заданоДедлайн простоя для каждого SSE-соединения. Если ни одна запись не была УСПЕШНО сброшена (flushed) за n мс — ни реальное событие, ни 15-секундный хартбит — демон отправляет терминальный фрейм client_evicted с data.reason = 'writer_idle_timeout' (дублируется в data.errorKind) и закрывает поток. Выбирайте значение с запасом выше 15-секундного хартбита (например, 30000300000), чтобы легитимные неактивные потоки не отключались; значения < 15000 БУДУТ отключать иначе здоровые неактивные соединения до срабатывания первого хартбита (предназначено только для тестов / коротких dev-сессий). Тег возможности (условный): writer_idle_timeout.

Оба флага принимают положительное целое число в миллисекундах; 0, NaN, нецелые или отрицательные значения отклоняются при запуске с понятным сообщением об ошибке. CLI-флаг имеет приоритет над переменной окружения; явное поле ServeOptions (для встраиваемых вызовов) имеет приоритет над переменной окружения. Потребители SDK должны выполнять pre-flight соответствующего тега возможности перед использованием любого из этих поведений — демоны, выпущенные до этого PR, опускают оба тега, а поле deadlineMs в запросе молча отбрасывается.

Деплой с несколькими сессиями и воркспейсами

Согласно #3803  §02, каждый процесс qwen serve при запуске привязывается к одному воркспейсу. Внутри этого воркспейса он мультиплексирует N сессий на один дочерний процесс qwen --acp через нативную карту сессий агента — сессии совместно используют процесс дочернего элемента / состояние OAuth / кэш чтения файлов / парсинг иерархической памяти.

Для хостинга нескольких воркспейсов (один пользователь, несколько репозиториев; или несколько пользователей на одном хосте) запустите несколько процессов демона — по одному на воркспейс, каждый на своем порту, под управлением systemd / docker-compose / k8s / эталонного оркестратора qwen-coordinator. Компромисс сделан намеренно: один воркспейс на дочерний процесс означает, что loadSettings(cwd) / OAuth / область действия MCP-сервера остаются согласованными с привязанным каталогом и не дрейфуют между запросами.

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

Для обработки нескольких пользователей (каждый со своей квотой, журналом аудита, песочницей) или для масштабирования за пределы возможностей одного процесса (бюджет холодного старта, количество FD, RSS) запускайте по одному демону на воркспейс на пользователя за внешним оркестратором. Этот оркестратор (мультитенантность / OIDC / квоты / аудит / k8s) выходит за рамки проекта qwen-code — см. issue #3803  “External Reference Architecture” для указателей по архитектуре.

Загрузка и возобновление сохраненной сессии

Демон предоставляет поток session/load и возобновления ACP по HTTP через два маршрута:

МаршрутКогда использовать
POST /session/:id/loadУ клиента нет отрендеренной истории (холодное переподключение, выбор и затем открытие). Демон воспроизводит каждый сохраненный ход через SSE, чтобы подписчики видели полную историю. Тег возможности: session_load.
POST /session/:id/resumeУ клиента уже есть ходы на экране, и ему нужен только дескриптор на стороне демона. Контекст модели восстанавливается на стороне агента без воспроизведения UI — SSE-поток остается чистым. Тег возможности: session_resume (unstable_session_resume остается устаревшим псевдонимом для старых клиентов).

TypeScript SDK предоставляет оба метода как статические фабрики в DaemonSessionClient:

import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk'; const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' }); // Холодное переподключение — демон воспроизведет историю через SSE. const session = await DaemonSessionClient.load(client, 'persisted-id'); // Или, если в вашем UI уже есть история, пропустите воспроизведение: // const session = await DaemonSessionClient.resume(client, 'persisted-id'); for await (const event of session.events()) { // Сначала воспроизведенные фреймы `session_update` (только для load), // затем live-события. }

Выполняйте pre-flight caps.features.session_load / caps.features.session_resume перед вызовом — старые демоны возвращают 404. unstable_session_resume по-прежнему анонсируется как устаревший псевдоним для совместимости. Параллельные запросы с одинаковым действием для одного и того же id объединяются; при гонках с разными действиями (load конкурирует с resume) возвращается 409 restore_in_progress с Retry-After: 5. См. справочник по протоколу для полного конверта ошибки.

Примечание: воспроизведение истории ограничено SSE-кольцом (по умолчанию 8000 фреймов). Длинные истории с многословными ходами могут превысить этот лимит — самые ранние фреймы молча отбрасываются. Для очень длинных сессий предпочитайте resume и полагайтесь на локальный сохраненный UI клиента.

Модель долговечности

Сессии по-прежнему эфемерны в Stage 1 при перезапусках демона, но сохраненные на диск сессии можно перезагрузить:

  • При сбое дочернего процесса публикуется session_died, и live-сессия удаляется из карт демона. Сохраненную на диске сессию можно перезагрузить через POST /session/:id/load, если можно породить новый дочерний процесс агента.
  • При перезапуске демона теряется каждая live-сессия, находящаяся в процессе выполнения. Сохраненные сессии остаются на диске и могут быть загружены для нового процесса демона с учетом тех же правил привязки к воркспейсу.
  • Длительные отключения клиента (>5 мин при многословном ходе) могут обогнать кольцо воспроизведения SSE (по умолчанию 8000 фреймов) — переподключение с Last-Event-ID проходит успешно, но состояние может быть несвязным. Для мобильных клиентов / клиентов с нестабильной сетью планируйте повторное открытие SSE при длительных обрывах или вызывайте POST /session/:id/load для воспроизведения с диска.
  • Файловые операции (writeTextFile) атомарны при сбоях (запись, затем переименование); они не атомарны при перезапусках демона в смысле воспроизведения — запись файла либо произошла, либо нет.

Если вашей интеграции требуется долговечность на стороне сервера при перезапусках, выходящая за рамки того, что покрывает session/load (например, очереди повторных попыток, управляемые сервером), вам по-прежнему необходимо восстановление состояния на уровне приложения. Не храните долго выполняющееся, чувствительное к перезапуску состояние внутри сессии демона.

Гарантии времени выполнения в Stage 1.5+

Контракт Stage 1 рассчитан на прототипирование. Согласно #3889 chiga0 downstream-consumer review , следующее не входит в Stage 1 — интеграциям производственного уровня требуется Stage 1.5+ перед тем, как полагаться на них: Блокеры для полноценного использования в downstream-проектах:

  1. loadSession / unstable_resumeSession over HTTP — без этого ни одна интеграция не переживет падение дочернего процесса или перезапуск демона, и ни один оркестратор, координирующий демон, также не сможет восстановить состояние.
  2. Постоянная идентификация клиента (парные токены + отзыв для каждого клиента) — на этапе 1 (Stage 1) используется один общий bearer-токен; утечка токена отзывает доступ у всех, а originatorClientId задается самим клиентом, а не проставляется демоном на основе аутентифицированной идентичности.

Базовая надежность:

  1. Heartbeat-путь, инициируемый клиентом — реализовано в #4175  PR 9. POST /session/:id/heartbeat записывает метки времени последнего обращения (last-seen timestamps) в демоне (тег возможности client_heartbeat); хелперы SDK: DaemonClient.heartbeat() / DaemonSessionClient.heartbeat().
  2. Событие permission_already_resolved, когда голосование проигрывает гонку первого ответа — в настоящее время UI вынуждены определять состояние по коду 404.
  3. Увеличенное кольцо повтора (replay ring) — увеличено до 8000. Настраиваемое для каждой сессии кольцо остается открытым вопросом — для мобильных / многословных рабочих нагрузок могут потребоваться переопределения для каждой сессии.
  4. Событие slow_client_warning перед client_evicted — мягкое обратное давление (soft backpressure), чтобы корректно работающие медленные клиенты могли самостоятельно ограничивать скорость (уменьшать глубину рендеринга, отбрасывать чанки) перед тем, как будут отключены.

Эргономика интеграции:

  1. POST /session/:id/_meta для контекста в стиле IM — пары ключ-значение для каждой сессии, прикрепляемые к последующим промптам (id чата, отправитель, id треда), заменяют импровизацию для каждого канала.
  2. Реальные переговоры о возможностях через /capabilitiesprotocol_versions: { acp: '0.14.x', daemon_envelope: 1 }, чтобы клиенты могли обнаруживать расхождения (drift) вместо того, чтобы скатываться к “неизвестный фрейм, игнорировать”.
  3. Первоклассная документация по долговечности (durability) (этот раздел) — уже реализована выше.

Полный план сближения (convergence roadmap) отслеживается в #3803 .

Границы области Stage 1 — что мы не будем исправлять в Stage 1.5

Два структурных решения являются явными целями-исключениями (non-goals) для основного плана Stage 1 / 1.5 / 2. Если ваш сценарий использования зависит от любого из них, планируйте работу в обход них, а не ждите наших изменений.

Состояние сессии изменяется только локально (согласно LaZzyMan review #4270256721 )

План Stage 1.5 описывает TUI как подписчика на EventBus в рамках процесса. На практике UI TUI строго шире, чем wire-протокол:

  • Только локальный UI — около 15 диалоговых компонентов Ink (ModelDialog, MemoryDialog, PermissionsDialog, SessionPicker, WelcomeBackDialog, FolderTrustDialog, …) и slash-команды local-jsx (/ide, /auth, /init, /resume, /rename, /delete, /language, /arena, …) рендерят специфичный для терминала Ink JSX. Удаленные клиенты по HTTP/SSE не могут эквивалентно рендерить Ink, и эти потоки не генерируют wire-событий.
  • Изменения состояния сессии без wire-событий/approval-mode, /memory add, /mcp add-server, /agents, /tools enable/disable, /auth, /init (запись CLAUDE.md) — все они меняют поведение агента, но только /model в настоящее время публикует событие (model_switched).

Выбор для Stage 1 — вариант (A) из ревью: не повышать эти мутации до wire-событий. Два режима развертывания имеют разные последствия.

Режим 1 — headless qwen serve (этот PR)

Внутри демона не запускается оболочка TUI. Перечисленные выше slash-команды в этом режиме не существуют — нет терминального UI для их вызова. Следовательно, состояние сессии:

  • Замораживается при загрузке для approval-mode / memory / agents / tools allowlist / auth — все загружается из настроек и с диска при запуске дочернего процесса qwen --acp демона; неизменно в течение всего времени жизни сессии. Определенные в настройках MCP-серверы также замораживаются при загрузке, но серверы, добавленные во время выполнения (через POST /workspace/mcp/servers), могут быть добавлены или удалены без перезапуска.
  • Изменяется по HTTP через POST /session/:id/model (публикует model_switched), POST /workspace/mcp/servers / DELETE /workspace/mcp/servers/:name (публикует mcp_server_added / mcp_server_removed) и голоса разрешений (POST /permission/:requestId).

Последствие: удаленные клиенты в headless-режиме видят полное состояние сессии. Никакой TUI не скрывает дополнительное состояние; расхождений (drift) быть не может. Если вы хотите изменить approval-mode, перезапустите демон с новыми настройками. MCP-серверы теперь можно добавлять/удалять во время выполнения через маршруты мутации (POST /workspace/mcp/servers, DELETE /workspace/mcp/servers/:name) — см. Управление MCP-серверами во время выполнения.

Режим 2 — совместно размещенный TUI qwen --serve в Stage 1.5 (не в этом PR)

Когда в Stage 1.5 появится qwen --serve (процесс TUI совместно размещает тот же HTTP-сервер), TUI будет существовать наряду с удаленными клиентами. Локальный оператор, вводящий /approval-mode yolo или /mcp add-server, изменяет состояние сессии, и удаленные клиенты по HTTP не получают события для наблюдения за этим изменением.

В этом режиме TUI является “супер-клиентом” — он наблюдает за тем же разговором агента, что и удаленные клиенты, И может изменять состояние сессии, чего удаленные клиенты не могут. Асимметрия заключается в следующем:

  • ✅ И TUI, и удаленные клиенты видят одни и те же сообщения агента, вызовы инструментов, диффы файлов, запросы разрешений.
  • ❌ Только TUI видит / изменяет approval-mode / memory / список MCP-серверов / агентов / tools allowlist / состояние аутентификации.

Последствие в Режиме 2: если UI удаленного клиента пытается зеркалить настройки сессии, он может рассинхронизироваться (drift) после любой slash-команды TUI. Удаленные клиенты должны повторно загружать состояние при подключении / переподключении (используйте Last-Event-ID: 0, чтобы повторить самое старое событие в кольце для таких вещей, как model_switched); им НЕ следует полагаться на инкрементальные события для мутаций на стороне TUI.

Почему (A), а не (B) (повышение мутаций до семейства событий session_state_changed)

(B) — более амбициозный ответ, но он запирает Stage 1.5 на существенно большей wire-поверхности, которая также должна чисто пройти через запланированный in-process рефакторинг. Мы предпочли бы честно идти в рамках меньшей области. Работа по таксономии событий состояния сессии — перечисление того, какие потоки TUI являются локальными по дизайну, а какие могут правдоподобно перейти на wire-уровень в будущем расширении с явным включением (B)-варианта — переносится в #3803 , а не в код Stage 1.5.

N параллельных сессий используют один дочерний процесс qwen --acp

Несколько сессий в одном рабочем пространстве используют один дочерний процесс qwen --acp благодаря нативной поддержке мульти-сессий агентом (packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>). Мост вызывает connection.newSession({cwd, mcpServers}) для каждой сессии — агент сохраняет их в своей карте сессий и демультиплексирует sessionId для каждого вызова.

Конкретные затраты при N=5 сессиях в одном рабочем пространстве:

РесурсНа сессиюПри N=5
Node-процесс демонаодин30–50 МБ (один демон)
Дочерний процесс qwen --acpобщий60–100 МБ (один дочерний процесс)
Дочерние процессы MCP-серверовна сессию3×N, если конфигурации отличаются
FileReadCache (в куче дочернего процесса)общийпарсится один раз
Парсинг памяти CLAUDE.md / иерархииобщийпарсится один раз
Состояние OAuth refresh-токенаобщийодин путь обновления
Изученные факты Auto-memoryобщиеодна база знаний на дочерний процесс
Холодный старттолько первый<200 мс после первой сессии

Мост поддерживает один канал на демон (один демон на рабочее пространство, согласно §02). Канал остается активным, пока жива хотя бы одна сессия; последний killSession (или крах на уровне канала) убивает дочерний процесс.

Дочерние процессы MCP-серверов сегодня по-прежнему создаются для каждой сессии — конфигурация каждой сессии может указывать разные серверы, поэтому они запускаются независимо. Доработка для Stage 1.5: подсчет ссылок (refcount) для дочерних процессов MCP-серверов по (workspace, config-hash), чтобы идентичные конфигурации использовались совместно. Не входит в область данного PR.

Аналогичные агенты (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) все используют мульти-сессии в одном процессе. qwen-code соответствует им на уровне агента; мост Stage 1 в этом PR делает ту же архитектуру видимой по HTTP.

Вход в удаленный демон (issue #4175 PR 21)

Когда демон работает на удаленном поде (без общего с вами экрана), клиент может запустить OAuth device flow по HTTP. Демон сам опрашивает IdP; ваша задача — просто открыть URL на любом устройстве с браузером.

Note

Бесплатный уровень Qwen OAuth был отменен 15.04.2026. Приведенные ниже примеры qwen-oauth документируют форму протокола device-flow и устаревший идентификатор провайдера; для новых установок следует использовать текущий поддерживаемый провайдер аутентификации.

# 1. Запускаем flow. Демон обращается к IdP, возвращает код + URL. curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"providerId":"qwen-oauth"}' # → 201 { # "deviceFlowId": "fa07c61b-…", # "userCode": "USER-1", # "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device", # "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1", # "expiresAt": 1700000600000, # "intervalMs": 5000, # "attached": false # } # 2. Откройте URL на телефоне / ноутбуке, введите пользовательский код. # 3. Опрашивайте для завершения (или подпишитесь на SSE для события auth_device_flow_authorized): curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \ -H "Authorization: Bearer $TOKEN" # → переходы статуса: pending → authorized

TypeScript SDK оборачивает оба шага в один хелпер:

import { DaemonClient } from '@qwen-code/sdk'; const client = new DaemonClient({ baseUrl, token }); const flow = await client.auth.start({ providerId: 'qwen-oauth' }); console.log(`Откройте ${flow.verificationUri}\nКод: ${flow.userCode}`); const result = await flow.awaitCompletion({ signal: abortCtrl.signal }); // result.status === 'authorized'

Демон никогда не открывает браузер от вашего имени. Даже при локальном запуске демон остается пассивным — он возвращает URL и позволяет SDK / пользователю выбрать, где его открыть. Это сделано намеренно: демон на headless-поде, вызвавший xdg-open, молча завершится с ошибкой, скрывая фактическую поверхность аутентификации. Скопируйте UX gh auth login с “Press Enter to open browser” в своем клиенте.

--require-auth и удобство разработки. Маршруты device-flow используют строгий шлюз мутации (PR 15), что означает, что loopback по умолчанию без токена возвращает 401 token_required. Локально самый простой способ обойти это во время разработки — qwen serve --token=dev-token; --require-auth не нужен, если вы не ужесточаете loopback по умолчанию.

Ограничение между демонами. oauth_creds.json является общим для демонов (~/.qwen/oauth_creds.json), поэтому успешный вход в демоне A автоматически подхватывается при следующем обновлении токена демоном B — но SDK-клиенты демона B не получат событие auth_device_flow_authorized (события привязаны к демону).

Перехват между клиентами. Два SDK-клиента на одном демоне, оба вызывающие POST /workspace/auth/device-flow для одного и того же провайдера, получают синглтон для провайдера: первый вызов запускает новый запрос к IdP и возвращает attached: false; второй вызов возвращает СУЩЕСТВУЮЩУЮ выполняющуюся запись с attached: true. Перехват записывается в журнал аудита (под X-Qwen-Client-Id второго клиента), но НЕ генерирует отдельное событие — оба клиента в конечном итоге наблюдают ОДНО И ТО ЖЕ auth_device_flow_authorized, как только пользователь завершит работу на странице IdP. Если ваш UI различает “я начал это” и “flow кого-то еще, к которому я присоединился”, используйте ветвление по полю attached, возвращаемому start().

Файл лога демона

qwen serve записывает диагностический лог для каждого процесса в:

${QWEN_RUNTIME_DIR or ~/.qwen}/debug/daemon/serve-<pid>-<workspaceHash>.log

Символическая ссылка latest в том же каталоге всегда указывает на лог текущего процесса, поэтому tail -f ~/.qwen/debug/daemon/latest будет следить за любым работающим демоном.

Лог фиксирует сообщения жизненного цикла, ошибки маршрутов (с контекстом route= и sessionId=), stderr дочернего процесса ACP и — при установленном QWEN_SERVE_DEBUG=1 — дополнительные хлебные крошки моста. Строки, которые сегодня идут в stderr, по-прежнему идут в stderr; файловый лог является дополнительным, а не заменой.

Отключение

Установите QWEN_DAEMON_LOG_FILE=0 (или false/off/no), чтобы полностью пропустить логирование в файл. Вывод в stderr не затрагивается.

Связь с дебаг-логами сессий

Дебаг-логи уровня сессии (~/.qwen/debug/<sessionId>.txt и символическая ссылка ~/.qwen/debug/latest) независимы. Лог демона находится в соседнем подкаталоге daemon/; семантика дебаг-логов для каждой сессии не изменяется этой функцией.

Отсутствие ротации

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

Управление MCP-серверами во время выполнения (issue #4514 )

Добавляйте или удаляйте MCP-серверы во время выполнения без перезапуска демона. Записи времени выполнения находятся во временном оверлее, который затеняет (shadows) серверы с тем же именем, определенные в настройках; базовая конфигурация settings.json / mcpServers никогда не записывается.

Предварительная проверка: проверьте caps.features на наличие mcp_server_runtime_mutation перед вызовом любого из маршрутов. Более старые демоны без этого тега возвращают 404.

POST /workspace/mcp/servers — добавление MCP-сервера во время выполнения

Строгий шлюз (требуется bearer-токен). Немедленно подключает сервер через активный McpClientManager и обнаруживает его инструменты.

Запрос:

{ "name": "my-server", "config": { "command": "npx", "args": ["-y", "@my-org/mcp-server"] } }

name должен быть буквенно-цифровым, плюс _ и - (макс. 256 символов). config — это тот же объект конфигурации MCP-сервера, который используется в записях mcpServers файла settings.json (зависящие от транспорта поля: command/args для stdio, url для SSE/HTTP). Чувствительные к безопасности поля (trust, env, cwd, oauth, headers, authProviderType, includeTools, excludeTools, type) удаляются демоном и игнорируются.

Ответ (200) — успех:

{ "name": "my-server", "transport": "stdio", "replaced": false, "shadowedSettings": false, "toolCount": 3, "originatorClientId": "client-1" }
  • replaced: true — запись времени выполнения с тем же именем уже существовала, и отпечаток конфигурации отличается; старое соединение разорвано, установлено новое. Если отпечаток совпадает (идемпотентное повторное добавление), replaced равно false.
  • shadowedSettings: true — существует сервер с тем же именем, определенный в настройках; запись времени выполнения теперь затеняет его. Запись в настройках не затрагивается и снова вступит в силу, если запись времени выполнения будет позже удалена.
  • toolCount — количество инструментов, обнаруженных на только что подключенном сервере.

Ответ (200) — мягкий отказ (режим предупреждения о бюджете):

{ "name": "my-server", "skipped": true, "reason": "budget_warning_only" }

Возвращается, когда установлен --mcp-budget-mode=warn и добавление сервера превысит настроенный --mcp-client-budget. Сервер НЕ подключается. Вызывающая сторона должна сообщить пользователю о нехватке бюджета.

Ошибки:

СтатусКодКогда
400invalid_server_nameИмя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-]
400missing_required_fieldconfig отсутствует или не является объектом non-null
400invalid_client_idЗаголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства
400invalid_configФорма конфигурации отклонена валидатором транспорта MCP
401token_requiredBearer-токен не настроен (строгий шлюз)
409mcp_budget_would_exceedУстановлен --mcp-budget-mode=enforce и бюджет исчерпан
502mcp_server_spawn_failedПроцесс сервера завершился или истек тайм-аут при подключении; тело содержит serverName, exitCode, stderr
503acp_channel_unavailableНет активного дочернего процесса ACP (ни одна сессия еще не была создана)

DELETE /workspace/mcp/servers/:name — удаление MCP-сервера времени выполнения

Строгий шлюз. Отключает сервер и удаляет его из оверлея времени выполнения. Идмпотентно — удаление имени, которое никогда не добавлялось, возвращает ответ с пропуском (не ошибку).

Параметр пути :name — это URL-кодированное имя сервера.

Ответ (200) — успех:

{ "name": "my-server", "removed": true, "wasShadowingSettings": false, "originatorClientId": "client-1" }
  • wasShadowingSettings: true — удаленная запись времени выполнения затеняла сервер с тем же именем, определенный в настройках. Эта запись в настройках теперь не затеняется и будет использоваться при следующем обнаружении/перезапуске.

Ответ (200) — идемпотентный пропуск:

{ "name": "ghost", "skipped": true, "reason": "not_present" }

Возвращается, если имени не было в оверлее времени выполнения (оно может по-прежнему существовать в настройках — записи настроек не могут быть удалены через этот маршрут).

Ошибки:

СтатусКодКогда
400invalid_server_nameИмя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-]
400invalid_client_idЗаголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства
401token_requiredBearer-токен не настроен (строгий шлюз)
503acp_channel_unavailableНет активного дочернего процесса ACP

Семантика затенения

Записи времени выполнения образуют временный оверлей поверх MCP-серверов, определенных в настройках:

  • Добавление сервера времени выполнения с тем же именем, что и запись в настройках, затеняет её — конфигурация времени выполнения имеет приоритет. Исходная запись в настройках не изменяется.
  • Удаление сервера времени выполнения, который затенял запись в настройках, снимает затенение — конфигурация, определенная в настройках, снова становится активной при следующем подключении.
  • Перезапуск демона теряет все записи времени выполнения. Только серверы, определенные в настройках, переживают перезапуски. Серверы времени выполнения ограничены временем жизни сессии.
  • GET /workspace/mcp сообщает объединенное представление — как серверы, определенные в настройках, так и серверы времени выполнения, появляются в массиве servers[]. Сегодня на уровне wire нет различий между этими двумя источниками в снимке.

События

Оба маршрута генерируют SSE-события уровня рабочего пространства (их получают все активные шины сессий):

СобытиеГенерируется, когдаПоля payload
mcp_server_addedPOST успешен (не пропущен)name, transport, replaced, shadowedSettings, toolCount, originatorClientId
mcp_server_removedDELETE успешен (не пропущен)name, wasShadowingSettings, originatorClientId
Пропущенные ответы (budget_warning_only, not_present) НЕ генерируют события.

События, связанные с бюджетом, из существующей поверхности mcp_guardrail_events (mcp_budget_warning, mcp_child_refused_batch) также срабатывают, когда добавления во время выполнения превышают порог бюджета.

Что дальше

Last updated on