Режим демона (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 Shell —
qwen 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); хелпер SDKclient.recapSession(sessionId).- Известное ограничение — усиление затрат токенов: маршрут является конечной точкой с чистыми затратами (каждый вызов — это побочный запрос LLM, без выгоды от состояния), а у демона нет ограничения скорости (rate limit) для каждого маршрута в v1. При стандартной loopback-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте
--token(и опционально--require-auth) на общих хостах для разработки перед открытием демона. - Безопасность параллельного получения резюме: два одновременных вызова
/recapдля одной сессии запускают два независимых побочных запроса.generateSessionRecapчитает снимок истории чата черезGeminiClient.getChat().getHistory()и передает его в отдельный вызовBaseLlmClient.generateText(черезrunSideQuery); он никогда не добавляет и не изменяетGeminiChatсессии. Безопасно вызывать из нескольких клиентов без координации.
- Известное ограничение — усиление затрат токенов: маршрут является конечной точкой с чистыми затратами (каждый вызов — это побочный запрос LLM, без выгоды от состояния), а у демона нет ограничения скорости (rate limit) для каждого маршрута в v1. При стандартной loopback-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте
Известные ограничения 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
| Flag | Default | Purpose |
|---|---|---|
--port <n> | 4170 | TCP-порт. 0 = эфемерный порт, назначенный ОС. |
--hostname <addr> | 127.0.0.1 | Интерфейс привязки. Всё, что выходит за пределы loopback, требует токен. |
--token <str> | — | Bearer-токен. Если не указан, используется переменная окружения QWEN_SERVER_TOKEN (с удалением начальных и конечных пробелов — удобно для $(cat token.txt)). |
--require-auth | false | Отказ от запуска без 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, которые должны создать новый дочерний процесс, возвращают 503 (с Retry-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-bridge | true | Режим этапа 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-web | true | Обслуживание собранного Web Shell SPA в корне демона (GET /, /assets/* и fallback для deep-link SPA). Статическая оболочка регистрируется до шлюза bearer-аутентификации — браузер не может прикрепить токен к субресурсу <script> или навигации в адресной строке, оболочка не содержит секретов, и каждый API-маршрут остается защищенным токеном независимо от этого. При привязке к не-loopback интерфейсам в stderr выводится предупреждение в одну строку о том, что UI доступен без аутентификации. Используйте --no-web для демона только с API. Не действует, если сборка не включает ассеты Web Shell (демон логирует breadcrumb и работает только с API). |
--open | false | После запуска слушателя открывает 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 браузера — возвращает
403JSON. Передайте--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-соединения могут накапливаться и в конечном итоге достичь потолка в 256server.maxConnections.Установите
--writer-idle-timeout-ms <n>(issue #4514 T2.9), чтобы закрыть этот пробел явным дедлайном простоя на уровне приложения: если ни одна запись не была успешно сброшена (flushed) заnмс, демон отправляет терминальный фреймclient_evictedсreason: 'writer_idle_timeout'и закрывает поток. Флаг выключен по умолчанию для сохранения legacy-контракта — операторам в сетях, которые проглатывают RST, следует выбрать значение значительно выше 15-секундного интервала хартбитов (например,60000–300000), чтобы легитимные неактивные соединения не отключались, в то время как реально зависшие писатели быстро удалялись. Выполните pre-flightcaps.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-секундного хартбита (например, 30000–300000), чтобы легитимные неактивные потоки не отключались; значения < 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-проектах:
loadSession/unstable_resumeSessionover HTTP — без этого ни одна интеграция не переживет падение дочернего процесса или перезапуск демона, и ни один оркестратор, координирующий демон, также не сможет восстановить состояние.- Постоянная идентификация клиента (парные токены + отзыв для каждого клиента) — на этапе 1 (Stage 1) используется один общий bearer-токен; утечка токена отзывает доступ у всех, а
originatorClientIdзадается самим клиентом, а не проставляется демоном на основе аутентифицированной идентичности.
Базовая надежность:
Heartbeat-путь, инициируемый клиентом— реализовано в #4175 PR 9.POST /session/:id/heartbeatзаписывает метки времени последнего обращения (last-seen timestamps) в демоне (тег возможностиclient_heartbeat); хелперы SDK:DaemonClient.heartbeat()/DaemonSessionClient.heartbeat().- Событие
permission_already_resolved, когда голосование проигрывает гонку первого ответа — в настоящее время UI вынуждены определять состояние по коду404. Увеличенное кольцо повтора (replay ring)— увеличено до 8000. Настраиваемое для каждой сессии кольцо остается открытым вопросом — для мобильных / многословных рабочих нагрузок могут потребоваться переопределения для каждой сессии.- Событие
slow_client_warningпередclient_evicted— мягкое обратное давление (soft backpressure), чтобы корректно работающие медленные клиенты могли самостоятельно ограничивать скорость (уменьшать глубину рендеринга, отбрасывать чанки) перед тем, как будут отключены.
Эргономика интеграции:
POST /session/:id/_metaдля контекста в стиле IM — пары ключ-значение для каждой сессии, прикрепляемые к последующим промптам (id чата, отправитель, id треда), заменяют импровизацию для каждого канала.- Реальные переговоры о возможностях через
/capabilities—protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }, чтобы клиенты могли обнаруживать расхождения (drift) вместо того, чтобы скатываться к “неизвестный фрейм, игнорировать”. - Первоклассная документация по долговечности (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/toolsallowlist /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 на любом устройстве с браузером.
Бесплатный уровень 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 → authorizedTypeScript 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. Сервер НЕ подключается. Вызывающая сторона должна сообщить пользователю о нехватке бюджета.
Ошибки:
| Статус | Код | Когда |
|---|---|---|
400 | invalid_server_name | Имя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-] |
400 | missing_required_field | config отсутствует или не является объектом non-null |
400 | invalid_client_id | Заголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства |
400 | invalid_config | Форма конфигурации отклонена валидатором транспорта MCP |
401 | token_required | Bearer-токен не настроен (строгий шлюз) |
409 | mcp_budget_would_exceed | Установлен --mcp-budget-mode=enforce и бюджет исчерпан |
502 | mcp_server_spawn_failed | Процесс сервера завершился или истек тайм-аут при подключении; тело содержит serverName, exitCode, stderr |
503 | acp_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"
}Возвращается, если имени не было в оверлее времени выполнения (оно может по-прежнему существовать в настройках — записи настроек не могут быть удалены через этот маршрут).
Ошибки:
| Статус | Код | Когда |
|---|---|---|
400 | invalid_server_name | Имя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-] |
400 | invalid_client_id | Заголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства |
401 | token_required | Bearer-токен не настроен (строгий шлюз) |
503 | acp_channel_unavailable | Нет активного дочернего процесса ACP |
Семантика затенения
Записи времени выполнения образуют временный оверлей поверх MCP-серверов, определенных в настройках:
- Добавление сервера времени выполнения с тем же именем, что и запись в настройках, затеняет её — конфигурация времени выполнения имеет приоритет. Исходная запись в настройках не изменяется.
- Удаление сервера времени выполнения, который затенял запись в настройках, снимает затенение — конфигурация, определенная в настройках, снова становится активной при следующем подключении.
- Перезапуск демона теряет все записи времени выполнения. Только серверы, определенные в настройках, переживают перезапуски. Серверы времени выполнения ограничены временем жизни сессии.
GET /workspace/mcpсообщает объединенное представление — как серверы, определенные в настройках, так и серверы времени выполнения, появляются в массивеservers[]. Сегодня на уровне wire нет различий между этими двумя источниками в снимке.
События
Оба маршрута генерируют SSE-события уровня рабочего пространства (их получают все активные шины сессий):
| Событие | Генерируется, когда | Поля payload |
|---|---|---|
mcp_server_added | POST успешен (не пропущен) | name, transport, replaced, shadowedSettings, toolCount, originatorClientId |
mcp_server_removed | DELETE успешен (не пропущен) | name, wasShadowingSettings, originatorClientId |
Пропущенные ответы (budget_warning_only, not_present) НЕ генерируют события. |
События, связанные с бюджетом, из существующей поверхности mcp_guardrail_events (mcp_budget_warning, mcp_child_refused_batch) также срабатывают, когда добавления во время выполнения превышают порог бюджета.
Что дальше
- Настраиваете долгоживущий демон? Локальные шаблоны запуска (systemd / launchd / nohup / tmux) для v0.16-alpha (только локально).
- Создаете клиент? См. краткое руководство по DaemonClient для TypeScript и справочник по протоколу HTTP.
- Читаете исходный код? Код моста находится в
packages/cli/src/serve/; SDK-клиент — вpackages/sdk-typescript/src/daemon/. - Отслеживаете дорожную карту? Прогресс на этапах Stage 1.5 / Stage 2 отслеживается в issue #3803 .