Режим демона (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 из задачи #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, чтобы автоматически открыть его в браузере. Он обслуживается на том же источнике, что и API, поэтому второй порт или обратный прокси не требуется. Используйте--no-webдля демона только с API. - Один процесс агента, множество клиентов — при настройке по умолчанию
sessionScope: 'single'каждый клиент, подключающийся к демону, использует один сеанс ACP. Совместная работа в реальном времени между клиентами над одним разговором, одними файловыми diff’ами, одними запросами разрешений. - Безопасное восстановление при потоковой передаче — SSE с
Last-Event-IDпозволяет клиенту прерваться и продолжить ровно с того места, где он остановился (в пределах окна воспроизведения кольцевого буфера). - Разрешения первого ответившего — когда агент запрашивает разрешение на запуск инструмента, все подключенные клиенты видят запрос; ответивший первым получает разрешение.
- Один демон, одна рабочая область — каждый процесс
qwen serveпривязывается ровно к одной рабочей области при запуске (согласно #3803 §02). Развертывания с несколькими рабочими областями запускают по одному демону на область на отдельных портах (или за оркестратором). - Управление удаленным выполнением (#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, без пользы для состояния), и демон не имеет ограничения частоты на маршрут в v1. При настройке по умолчанию без токенов циклической связи ошибочный или вредоносный локальный клиент может спамить этим для сжигания токенов. Настройте
--token(и, возможно,--require-auth) на общих хостах разработчиков перед открытием демона. - Безопасность одновременных кратких описаний: два одновременных вызова
/recapдля одного сеанса запускают два независимых побочных запроса.generateSessionRecapчитает снимок истории чата черезGeminiClient.getChat().getHistory()и передаёт его в отдельный вызовBaseLlmClient.generateText(черезrunSideQuery); он никогда не добавляет и не изменяетGeminiChatсеанса. Безопасно вызывать из нескольких клиентов без координации.
- Известное ограничение — увеличение стоимости токенов: маршрут является чисто затратным (каждый вызов — побочный запрос LLM, без пользы для состояния), и демон не имеет ограничения частоты на маршрут в v1. При настройке по умолчанию без токенов циклической связи ошибочный или вредоносный локальный клиент может спамить этим для сжигания токенов. Настройте
Известные ограничения v0.16-alpha
Первый npm-релиз qwen serve (v0.16-alpha) намеренно узок — текстовый чат / кодинг для разработчиков, запускающих демон на своей машине. Список ниже делает отложенную функциональность явной, чтобы пользователи могли планировать; всё перечисленное находится на карте исправлений v0.16.x или в ближайшем последующем релизе.
Поверхность продукта — только текст:
-
✅ Текстовые запросы и текстовые ответы (чат, кодинг, вызовы инструментов, интеграция с MCP)
-
❌ Прикрепление изображений / файлов в цепочке запросов —
MessageEmitterсейчас обрабатывает только текст; мультимодальный эхо-ответ появится, когда будет зафиксирована альфа-цель с потребностью в изображениях (#4175 chiga0 #27 P0 element) -
❌ Потоковые загрузки — то же ограничение, что и для мультимодальности Поверхность развёртывания — только локально:
-
✅ 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 workspace × N сессий. Межхостовые федерации, ключевание токенов по пути инстанса и очистка устаревших токенов откладываются до v0.16.x. -
❌ Автоматически генерируемые токены демона — в альфа-версии используется собственный токен (один вызов
openssl rand -hex 32). Автогенерация и инфраструктура хранилища токенов откладываются до v0.16.x.
Усиление защиты — минимально необходимое для локального однопользовательского режима:
- ✅ Шлюз безопасности при запуске (отказывает в привязке к не-loopback адресу без токена, PR 15 / #4236 )
- ✅ Шлюз аутентификации для мутирующих маршрутов, маршрутизация разрешений в разрезе сессий (PR волны 4)
- ✅ Ограждения MCP + координация разрешений для нескольких клиентов (F2 / F3)
- ✅ Абсолютный дедлайн промпта + таймаут простоя SSE-писателя — включается опционально через
--prompt-deadline-msи--writer-idle-timeout-ms; рекламируется черезprompt_absolute_deadlineиwriter_idle_timeout, когда включены. - ✅ Ограничение HTTP-запросов (rate limiting) — включается опционально через
--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 слушает на http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
# → qwen serve: bearer-аутентификация отключена (стандартный loopback). Установите QWEN_SERVER_TOKEN, чтобы включить.Привязка по умолчанию — 127.0.0.1:4170. Bearer-аутентификация выключена на loopback, чтобы локальная разработка «работала из коробки». Демон привязывается к текущему рабочему каталогу; используйте --workspace /path/to/dir для переопределения.
Откройте веб-интерфейс терминала. Перейдите на http://127.0.0.1:4170/ (или запустите демон с qwen serve --open, чтобы он открылся автоматически) для полноценного браузерного терминала — чат, диффы, вызовы инструментов и запросы разрешений. Интерфейс подаётся в корне демона на том же источнике, что и 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 означает, что лимит отключён.
Демон также предоставляет снимки состояния только для чтения для UI клиентов и
операторов: GET /daemon/status, GET /workspace/mcp,
GET /workspace/skills, GET /workspace/providers, GET /workspace/env,
GET /workspace/preflight,
GET /session/:id/context, GET /session/:id/supported-commands,
GET /session/:id/tasks и GET /session/:id/lsp.
GET /session/:id/lsp возвращает структурированное состояние LSP для каждой сессии. Запустите
демон с флагом --experimental-lsp, чтобы включить LSP в создаваемых сессиях агентов;
в противном случае маршрут возвращает enabled: false без серверов.
GET /daemon/status — консолидированный снимок для поиска неисправностей. По умолчанию
detail=summary считывает только состояние демона в памяти (сессии, разрешения,
количество подключений SSE/ACP, отклонённые из-за лимита частоты, память процесса, установленные лимиты)
и не запускает дочерний процесс ACP. Используйте GET /daemon/status?detail=full для
диагностики по сессиям, деталей подключений ACP, счётчиков device-flow аутентификации и
разделов состояния рабочей области, когда вы активно ищете проблему.
GET /workspace/mcp, GET /workspace/skills и GET /workspace/providers
сообщают о состоянии работающего 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 никогда не перезаписывает. Явная запись в игнорируемые пути
разрешена, но аудируется. Запись бинарных файлов, удаление/перемещение/создание каталогов
и рекурсивное создание родительских каталогов не входят в этот интерфейс.
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="<из шага 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-конверта, чтобы клиенты, использующие raw-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":"Что делает src/main.ts?"}]}'
# → {"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
# → запуск отказывается без QWEN_SERVER_TOKENКлиенты затем отправляют Authorization: Bearer $QWEN_SERVER_TOKEN в каждом запросе. /health освобождается от этого только на loopback-привязках, чтобы пробы liveness от 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, … все требуют 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 (или любой другой индекс — не null после аутентификации означает, что тег присутствует)curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
# → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
# Неверный токен → 401Сравнение токенов выполняется за постоянное время (SHA-256 + crypto.timingSafeEqual); ответы 401 единообразны для «отсутствующего заголовка», «неверной схемы» и «неверного токена», чтобы побочный канал не мог их различить.
Параметры командной строки
| Флаг | По умолчанию | Описание |
|---|---|---|
--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 для разработчиков на общих хостах / CI-раннерах / многопользовательских рабочих станциях, где любой локальный пользователь может обратиться к слушателю. Запускается только с --token или установленным QWEN_SERVER_TOKEN; также закрывает /health за bearer. |
--max-sessions <n> | 20 | Предел одновременных активных сессий. Новые запросы POST /session, которые привели бы к созданию свежего дочернего процесса, возвращают 503 (с Retry-After: 5) при достижении лимита; подключения к существующим сессиям не учитываются. Установите 0 для отключения. Рассчитан на одного пользователя / небольшую команду; увеличьте, если ваше развёртывание имеет достаточный запас ОЗУ/файловых дескрипторов (~30–50 МБ на сессию). |
--max-pending-prompts-per-session <n> | 5 | Ограничение на сессию для подсказок (prompts), принятых через POST /session/:id/prompt, но ещё не завершённых, включая поставленные в очередь и активную подсказку. Мост синхронно отклоняет переполнение кодом 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 на каждую рабочую область на отдельных портах. |
--max-connections <n> | 256 | Ограничение TCP-соединений на уровне слушателя (server.maxConnections). Ограничивает количество сырых сокетов независимо от количества сессий — медленные/фантомные SSE-клиенты отклоняются на этапе принятия, когда лимит заполнен. Увеличьте вместе с --max-sessions, если ваше развёртывание ожидает много подписчиков SSE на сессию. |
--event-ring-size <n> | 8000 | Глубина кольцевого буфера SSE сессии для повторного воспроизведения (§02 в #3803 target). Задаёт размер отставания, доступного для GET /session/:id/events с Last-Event-ID: N. Больше = больше запаса для переподключения ценой нескольких сотен КБ дополнительной ОЗУ на сессию. SDK-клиенты могут дополнительно запросить больший лимит отставания для конкретной подписки через ?maxQueued=N (диапазон [16, 2048], по умолчанию 256). Демоны также отправляют нетерминальный SSE-фрейм slow_client_warning при заполнении очереди на 75%, чтобы клиенты могли выполнить сброс / переподключиться до вытеснения. Предварительная проверка: caps.features.slow_client_warning. |
--mcp-client-budget <n> | — | Положительное целое ограничение на количество живых MCP-клиентов на ACP-сессию (#4175 PR 14 v1; PR 23 переводит это на уровень рабочей области через общий пул MCP). Комбинируется с --mcp-budget-mode. Если не задано, учёт не применяется принудительно (но GET /workspace/mcp всё равно сообщает clientCount). Отличается от MCP_SERVER_CONNECTION_BATCH_SIZE в claude-code, который ограничивает параллелизм запуска, а не общее количество клиентов. Предварительная проверка: 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(). Нативный встроенный режим этапа 2 станет доступен позже. |
--allow-origin <pat> | — | T2.4 (#4514 ). Список разрешённых источников (cross-origin) для браузерных webui-клиентов. Можно повторять. Каждое значение — * (любой источник — запуск откажется, если не настроен bearer-токен; рекомендуется --require-auth на loopback, чтобы /health и /demo также были закрыты bearer, так как по умолчанию они доступны без аутентификации на loopback) или канонический URL источника (<scheme>://<host>[:<port>], без завершающего слеша / пути / userinfo / query). Подстановочные знаки субдоменов (https://*.example.com) намеренно не поддерживаются — перечислите каждый субдомен явно или используйте * с настроенным токеном (и --require-auth для полного усиления). Для совпавших источников возвращаются CORS-заголовки ответа (Access-Control-Allow-Origin, Vary: Origin, методы, заголовки, max-age и открытый заголовок Retry-After); для несовпавших источников по-прежнему возвращается 403 с той же обёрткой, что и сегодняшняя стена. Origin: null (песочница iframe, file:// документы) всегда отклоняется, даже при *. Предварительная проверка: caps.features.allow_origin. На запросы с самого loopback это не влияет. |
--web / --no-web | true | Обслуживать встроенный SPA веб-оболочки в корне демона (GET /, /assets/* и fallback для глубоких ссылок SPA). Статическая оболочка регистрируется до шлюза bearer-аутентификации — браузер не может прикрепить токен к подресурсу <script> или навигации по адресной строке, оболочка не несёт секретов, а каждый API-маршрут остаётся под защитой токена независимо. При привязке к не-loopback адресу выводится однострочное предупреждение в stderr о том, что UI доступен без аутентификации. Используйте --no-web для демона только с API. Не действует, если сборка не включает ассеты веб-оболочки (демон записывает breadcrumb и работает только с API). |
--open | false | После запуска слушателя открыть веб-оболочку в браузере по умолчанию по URL демона (с добавленным фрагментом #token=, если настроен токен — фрагмент никогда не отправляется на сервер, что защищает токен от попадания в журналы доступа и заголовки Referer). Не работает с --no-web, а также в headless/CI/SSH-средах, где нет доступного браузера. |
Настройка ручек нагрузки.
--max-sessions— это потолок новых дочерних сессий. Есть ещё три слоя, которые тоже ограничивают нагрузку — при настройке для высококонкурентного развёртывания их нужно выставлять совместно:
- на уровне слушателя:
--max-connections/server.maxConnections=256ограничивает количество сырых TCP-соединений (обратное давление от медленных клиентов).- на подписчиков на сессию: 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). Если рабочее пространство объявляет 30 MCP-серверов в
mcpServers, то без явного ограничения будет запущено 30 клиентов.--mcp-client-budget=Nограничивает количество активных MCP-клиентов;--mcp-budget-mode={enforce,warn,off}выбирает поведение. По умолчанию —warn, если бюджет задан (снимок показывает предупреждение, но ни один клиент не отклоняется — полезно для измерения реального количества разветвлений перед включением принудительного режима). Отклонённые серверы в режимеenforceполучаютdisabledReason: 'budget'в своей ячейке, а ячейкаbudgets[0]показываетstatus: 'error'+errorKind: 'budget_exhausted'. Резервирование слотов осуществляется по имени сервера и переживает переподключения / таймауты обнаружения — отклонённый сервер не может забрать слот у здорового.⚠️ Область применения v1: на сессию, а не на рабочее пространство. Каждая ACP-сессия внутри демона имеет собственный
Config/McpClientManager(создаётся черезnewSessionConfigдля каждой сессии). Бюджет ограничивает активные MCP-клиенты на сессию, а не суммарно по всем сессиям рабочего пространства. Снимок вGET /workspace/mcpотражает представление начальной сессии (ячейка для честности содержитscope: 'session'). Если вы запустите 5 одновременных ACP-сессий с--mcp-client-budget=10, то в сумме по демону может быть до 50 активных MCP-клиентов — ограничение действует на каждую сессию. Волна 5 PR 23 (общий пул MCP) вводит менеджер с областью видимости рабочего пространства и переводит это в полноценное ограничение на рабочее пространство.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 для правила без токена.- Белый список заголовка Host — при привязке к loopback демон проверяет, что
Host:совпадает сlocalhost:port/127.0.0.1:port/[::1]:port/host.docker.internal:port(без учёта регистра, согласно RFC 7230 §5.4) для защиты от DNS-ребандинга. Привязки не к loopback (--hostname 0.0.0.0) намеренно обходят белый список Host — оператор сам выбрал поверхность атаки, поэтому единственным уровнем аутентификации является проверка bearer-токена; обратные прокси / SNI / привязка клиентских сертификатов — ответственность оператора, а не демона. Если вам нужна изоляция на основе Host на не-loopback привязке, завершайте TLS и проверяйте Host на фронтальном прокси. - CORS по умолчанию запрещает любой браузерный Origin — возвращает JSON с
403. Передайте--allow-origin <шаблон>(повторяемый, T2.4 #4514), чтобы разрешить определённые браузерные источники. Каждое значение — либо литерал*(любой источник — загрузка отклоняется, если не настроен bearer-токен; для полного усиления рекомендуется--require-authна loopback, так как/healthи/demoпо умолчанию остаются без аутентификации на loopback), либо канонический URL-происхождение (<схема>://<хост>[:<порт>], без завершающего слэша / пути / userinfo). Для совпавших источников возвращаются правильные заголовки CORS-ответа (Access-Control-Allow-Origin: <эхо>,Vary: Origin, плюс стандартные методы / заголовки / max-age и открытыйRetry-After); несовпавшие источники по-прежнему получают 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по-прежнему возвращаются к тем же Этапу 1 вариантам, что и раньше: упаковка в нативное приложение (Electron/Tauri), чтобы не отправлялся заголовок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 сначала отправляются дочерним агентам, затем закрывается слушатель (таймаут 10с на каждого агента).
⚠️ Известная проблема Stage 1 — разрешения глобальны для демона, а не для конкретной сессии (BUy4H).
pendingPermissionsсуществует на уровне демона; любой клиент, владеющий bearer-токеном, может голосовать за любойrequestIdдля любой доступной ему сессии (и SSE-событияpermission_requestнесут requestId в своей полезной нагрузке). Это приемлемо в модели доверия для одного пользователя / небольшой команды, где каждый аутентифицированный клиент — это один и тот же человек или доверенные коллеги. В Stage 1.5 будет реализованPOST /session/:id/permission/:requestIdс картой незавершённых запросов в рамках сессии и идентификацией каждого клиента (обязательное требование #3 из последующего обзора); до тех пор не запускайтеqwen serveс bearer-токеном, доступным ненадёжным сторонам.⚠️ Известная проблема Stage 1 — тело запроса
POST /session/:id/promptограничено 10 МБ (BUy4L). Мультимодальные запросы, содержащие изображения / PDF / аудио и превышающие 10 МБ, будут отклонены на этапе разбора тела до выполнения логики маршрута (без стриминга, без прерывания во время загрузки). Обходной путь: сжать контент на стороне клиента или передать ссылку на путь и позволить агенту прочитать файл черезreadTextFile. В Stage 1.5 будет приниматьсяmultipart/form-dataили чанкованное кодирование на/prompt, чтобы большие запросы не упирались в предел.⚠️ Известная проблема Stage 1 — фантомные SSE-соединения за NAT. Демон обнаруживает «мёртвых» клиентов по TCP back-pressure на heartbeats (интервал 15 с). Клиент, который исчез БЕЗ TCP RST (например, NAT-коробка молча сбрасывает неактивные потоки), остаётся в ядре как “живой” сокет до тайм-аута keepalive-проб Node — обычно около 2 часов по умолчанию в Linux. В развёртываниях с
--hostname 0.0.0.0за такими NAT могут накапливаться фантомные SSE-соединения, и в итоге будет достигнут лимитserver.maxConnections(256).Установите
--writer-idle-timeout-ms <n>(issue #4514 T2.9), чтобы закрыть пробел с явным тайм-аутом бездействия на уровне приложения: если ни одна запись не была успешно отправлена в течениеnмс, демон отправляет терминальный фреймclient_evictedсreason: 'writer_idle_timeout'и закрывает поток. Флаг по умолчанию выключен для сохранения обратной совместимости — операторы в сетях, которые «проглатывают» RST, должны выбрать значение значительно выше 15-секундного интервала heartbeat (например,60000–300000), чтобы законно неактивные соединения не были вытеснены, а действительно зависшие писатели были принудительно завершены. Предварительно проверяйтеcaps.features.includes('writer_idle_timeout')из вашего SDK, чтобы убедиться, что демон поддерживает эту возможность.
Тайм-ауты и бездействие записывающего потока
Issue #4514 T2.9 содержит два опциональных флага, которые закрывают пробелы для длительных / удалённых развёртываний, которые не покрывают 15-секундный heartbeat + 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-соединения. Если ни одна запись не была УСПЕШНО отправлена в течение n мс (ни реальное событие, ни 15-секундный heartbeat), демон отправляет терминальный фрейм client_evicted с data.reason = 'writer_idle_timeout' (также дублируется в data.errorKind) и закрывает поток. Выберите значение комфортно выше 15-секундного heartbeat (например, 30000–300000), чтобы законно неактивные потоки не были вытеснены; значения < 15000 БУДУТ вытеснять иначе здоровые неактивные соединения до того, как сработает первый heartbeat (преднамеренно только для тестов / кратковременных сессий разработки). Тег возможности (условно): writer_idle_timeout. |
Оба флага принимают положительное целое число в миллисекундах; значения 0, NaN, нецелые или отрицательные отклоняются при запуске с чётким сообщением об ошибке. Флаг командной строки имеет приоритет над переменной окружения; явное поле ServeOptions (у встроенных вызывающих) имеет приоритет над переменной окружения. Потребителям SDK следует предварительно проверять соответствующий тег возможности, прежде чем полагаться на любое из этих поведений — демоны, созданные до этого 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при подписке, чтобы воспроизвести самое старое доступное событие из кольцевого буфера.
Для обработки нескольких пользователей (каждый со своей квотой, журналом аудита, песочницей) или для масштабирования за пределы возможностей одного процесса (бюджет холодного запуска, количество файловых дескрипторов, RSS) порождайте по одному демону на рабочее пространство на пользователя за внешним оркестратором. Этот оркестратор (мультитенантность / OIDC / квоты / аудит / k8s) выходит за рамки проекта qwen-code — см. задачу #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),
// затем живые события.
}Перед вызовом проверяйте caps.features.session_load / caps.features.session_resume — старые демоны возвращают 404. unstable_session_resume всё ещё рекламируется как устаревший псевдоним для совместимости. Одновременные запросы одного действия для одного идентификатора объединяются; пересекающиеся действия (конкуренция load и resume) получают 409 restore_in_progress с Retry-After: 5. См. протокол для полной структуры ошибки.
Примечание: воспроизведение истории ограничено кольцевым буфером SSE (по умолчанию 8000 фреймов). Длинные истории с болтливыми шагами могут превысить этот лимит — самые ранние фреймы молча отбрасываются. Для очень длинных сессий предпочитайте resume и полагайтесь на локально сохранённый UI клиента.
Модель надежности
Сессии в Stage 1 остаются эфемерными при перезапуске демона, но сохраненные на диске сессии можно перезагрузить:
- При сбое дочернего процесса публикуется
session_diedи активная сессия удаляется из карт демона. Сохраненную на диске сессию можно перезагрузить черезPOST /session/:id/load, если доступен новый дочерний агент. - Перезапуск демона приводит к потере всех активных сессий. Сохраненные на диске сессии остаются и могут быть загружены в новый процесс демона с соблюдением тех же правил привязки рабочего пространства.
- Длительные отключения клиента (>5 мин в оживленном обмене) могут превысить размер кольцевого буфера SSE (по умолчанию 8000 фреймов) — повторное подключение через
Last-Event-IDвозможно, но состояние может быть несогласованным. Для мобильных клиентов или клиентов с нестабильной сетью рекомендуется переоткрывать SSE при длительных разрывах или вызыватьPOST /session/:id/loadдля воспроизведения с диска. - Файловые операции (
writeTextFile) атомарны при сбоях (сначала запись, затем переименование); они не являются атомарными при перезапуске демона в смысле воспроизведения — файловая запись либо выполнена, либо нет.
Если вашей интеграции требуется межперезапускная надежность на стороне сервера, выходящая за рамки session/load (например, управляемые сервером очереди повторных попыток), вам все равно потребуется восстановление состояния на уровне приложения. Не храните долгоживущее, чувствительное к перезапускам состояние внутри сессии демона.
Гарантии времени выполнения Stage 1.5+
Контракт Stage 1 рассчитан на прототипирование. Согласно #3889 обзору downstream-потребителя chiga0 , следующее не входит в Stage 1 — для производственных интеграций требуется Stage 1.5+ прежде чем полагаться на них:
Препятствия для серьезного downstream-использования:
loadSession/unstable_resumeSessionчерез HTTP — без этого ни одна интеграция не может пережить сбой дочернего процесса или перезапуск демона, и ни один оркестратор, управляющий демоном, не сможет восстановить состояние.- Постоянная идентификация клиента (парные токены + отзыв на клиента) — в Stage 1 используется один общий bearer; утечка токена отзывает доступ для всех, а
originatorClientIdобъявляется самим клиентом, а не устанавливается демоном на основе аутентифицированной личности.
Базовый уровень надежности:
Инициируемый клиентом путь heartbeat— реализован в PR #4175 (#4175).POST /session/:id/heartbeatзаписывает временные метки последнего обращения на демоне (тег возможностиclient_heartbeat); хелперы SDK —DaemonClient.heartbeat()/DaemonSessionClient.heartbeat().- Событие
permission_already_resolved, когда голосование проигрывает гонку первого ответчика — в настоящее время интерфейсы вынуждены выводить состояние из404. Увеличенный кольцевой буфер— увеличен до 8000. Настраиваемый на сессию кольцевой буфер все еще открыт — мобильным нагрузкам или нагрузкам с оживленным обменом могут потребоваться переопределения на сессию.- Событие
slow_client_warningпередclient_evicted— мягкое обратное давление, чтобы хорошо ведущие себя медленные клиенты могли самоограничиться (уменьшить глубину рендеринга, отбрасывать чанки) до завершения.
Удобство интеграции:
POST /session/:id/_metaдля контекста в стиле IM — привязанная к сессии пара ключ-значение, добавляемая к последующим подсказкам (идентификатор чата, отправитель, идентификатор потока) заменяет импровизацию на канал./capabilitiesфактическое согласование функций —protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }, чтобы клиенты могли обнаруживать расхождения вместо того, чтобы попадать в “неизвестный фрейм, игнорировать”.- Перворазрядная документация по надежности (этот раздел) — уже опубликована выше.
Полный план конвергенции отслеживается в #3803 .
Границы области Stage 1 — что мы не будем исправлять в Stage 1.5
Два структурных выбора являются явными нецелевыми для основной дорожной карты Stage 1 / 1.5 / 2. Если ваш сценарий использования зависит от них, планируйте обходные пути, а не ждите нас.
Состояние сессии — только локальные мутации (согласно обзору LaZzyMan #4270256721 )
План Stage 1.5 описывает TUI как подписчика EventBus в процессе. На практике UI TUI строго больше, чем проводной протокол:
- Локальный только интерфейс — около 15 компонентов диалогов Ink (
ModelDialog,MemoryDialog,PermissionsDialog,SessionPicker,WelcomeBackDialog,FolderTrustDialog, …) и локальные jsx-команды (/ide,/auth,/init,/resume,/rename,/delete,/language,/arena, …) рендерят терминал-специфичный Ink JSX. Удаленные клиенты по HTTP/SSE не могут эквивалентно рендерить Ink, и эти потоки не генерируют проводных событий. - Мутации состояния сессии без проводных событий —
/approval-mode,/memory add,/mcp add-server,/agents,/tools enable/disable,/auth,/init(записьCLAUDE.md) — все изменяют поведение агента, но только/modelв настоящее время публикует событие (model_switched).
Выбор Stage 1 — вариант (A) из обзора: не продвигать эти мутации до проводных событий. Два режима развертывания имеют разные последствия.
Режим 1 — безголовый qwen serve (этот PR)
Никакая оболочка TUI не выполняется внутри демона. Перечисленные выше слэш-команды не существуют в этом режиме — нет терминального интерфейса для их выдачи. Таким образом, состояние сессии:
- Boot-time-frozen для
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).
Следствие: удаленные клиенты в режиме без TUI видят полное состояние сессии. Никакой TUI не скрывает дополнительного состояния; дрейф невозможен. Если вы хотите изменить approval-mode, перезапустите демон с новыми настройками. MCP-серверы теперь можно добавлять/удалять во время выполнения через маршруты мутации (POST /workspace/mcp/servers, DELETE /workspace/mcp/servers/:name) — см. Управление MCP-серверами во время выполнения.
Режим 2 — Стадия 1.5 qwen --serve совместно расположенная TUI (не в этом PR)
Когда выйдет Стадия 1.5 с qwen --serve (процесс TUI совместно размещает тот же HTTP-сервер), TUI существует вместе с удаленными клиентами. Локальный оператор, вводящий /approval-mode yolo или /mcp add-server, изменяет состояние сессии, и удаленные клиенты по HTTP не имеют события, чтобы наблюдать это изменение.
В этом режиме TUI является «суперклиентом» — он наблюдает тот же диалог агента, что и удаленные клиенты, И может изменять состояние сессии, которое удаленным клиентам недоступно. Асимметрия такова:
- ✅ И TUI, и удаленные клиенты видят одни и те же сообщения агента, вызовы инструментов, различия в файлах, запросы разрешений.
- ❌ Только TUI видит/изменяет approval-mode / memory / список MCP-серверов / agents / tools allowlist / auth state.
Следствие в режиме 2: если интерфейс удаленного клиента пытается зеркалировать настройки сессии, он может дрейфовать после любой команды TUI со слешем. Удаленные клиенты должны повторно получать состояние при подключении / переподключении (используйте Last-Event-ID: 0, чтобы воспроизвести самое старое событие кольцевого буфера для таких вещей, как model_switched); им НЕ следует полагаться на инкрементальные события для мутаций со стороны TUI.
Почему (A), а не (B) (продвижение мутаций в семейство событий session_state_changed)
(B) — более амбициозный ответ, но он привязывает Стадию 1.5 к существенно большей поверхности проводов, которая также должна чисто проходить через запланированный рефакторинг внутри процесса. Мы предпочитаем честно пройти меньший объем. Работа по таксономии событий состояния сессии — перечисление того, какие потоки TUI локальны по дизайну, а какие потенциально могут быть переведены на провода в рамках будущего расширения по типу opt-in (B), — переносится в #3803 , а не в код Стадии 1.5.
N параллельных сессий используют один дочерний процесс qwen --acp
Несколько сессий в одной рабочей области используют один дочерний процесс qwen --acp благодаря встроенной поддержке мультисессионности агента (packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>). Мост вызывает connection.newSession({cwd, mcpServers}) для каждой сессии — агент хранит их в своей карте sessions и демультиплексирует по sessionId при каждом вызове.
Конкретная стоимость при N=5 сессий в одной рабочей области:
| Ресурс | На сессию | При N=5 |
|---|---|---|
| Процесс Node демона | один | 30–50 МБ (один демон) |
Дочерний процесс qwen --acp | общий | 60–100 МБ (один дочерний) |
| Дочерние процессы MCP-серверов | на сессию | 3×N, если конфиги отличаются |
FileReadCache (в куче дочернего) | общий | анализируется один раз |
Парсинг CLAUDE.md / иерархической памяти | общий | анализируется один раз |
| Состояние OAuth refresh-токена | общий | один путь обновления |
| Автоматически запомненные факты | общий | одна база знаний на дочерний |
| Холодный старт | только первый | <200 мс после первой сессии |
Мост держит один канал на демон (один демон на рабочую область, согласно §02). Канал остается активным, пока жива хотя бы одна сессия; последний killSession (или сбой на уровне канала) убивает дочерний процесс.
Дочерние процессы MCP-серверов пока создаются на каждую сессию — каждая сессия может иметь свой конфиг с разными серверами, поэтому они запускаются независимо. План на дальнейшую Стадию 1.5: подсчет ссылок на дочерние процессы MCP-серверов по (workspace, config-hash), чтобы одинаковые конфиги разделялись. Не входит в рамки этого PR.
Пирнговые агенты (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) все используют одно-процессную мультисессионность. qwen-code повторяет их на уровне агента; мост Стадии 1 в этом PR делает ту же архитектуру видимой через HTTP.
Вход в удаленный демон (issue #4175 PR 21)
Когда демон запущен на удаленном поде (без общего дисплея с вами), клиент может инициировать OAuth device flow через HTTP. Демон опрашивает IdP сам; ваша задача — просто открыть URL на любом устройстве, где есть браузер.
Бесплатный уровень Qwen OAuth был отключен 15 апреля 2026 года. Примеры с qwen-oauth
ниже описывают протокол device-flow и устаревший идентификатор провайдера;
новые настройки должны использовать текущий поддерживаемый провайдер аутентификации.
# 1. Start a flow. The daemon contacts the IdP, returns a code + 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. Visit the URL on your phone / laptop, enter the user code.
# 3. Poll for completion (or subscribe to SSE for the auth_device_flow_authorized event):
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
-H "Authorization: Bearer $TOKEN"
# → status transitions: 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(`Open ${flow.verificationUri}\nCode: ${flow.userCode}`);
const result = await flow.awaitCompletion({ signal: abortCtrl.signal });
// result.status === 'authorized'Демон никогда не открывает браузер от вашего имени. Даже при локальном запуске демон остаётся пассивным — он возвращает URL и позволяет SDK/пользователю решать, где его открыть. Это сделано намеренно: демон на headless-поде, вызвавший xdg-open, молча завершится ошибкой, скрывая реальную поверхность аутентификации. Реализуйте в своём клиенте UX, аналогичный gh auth login («Нажмите Enter, чтобы открыть браузер»).
--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. Если ваш интерфейс различает «я начал этот процесс» и «чужой поток, к которому я присоединился», используйте поле attached, возвращаемое методом start().
Файл журнала демона
qwen serve записывает диагностический журнал каждого процесса в:
${QWEN_RUNTIME_DIR или ~/.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-серверы на ходу без перезапуска демона. Записи во время выполнения живут в эфемерном слое, который затеняет серверы с тем же именем из настроек; лежащий в основе settings.json / конфиг mcpServers никогда не изменяется.
Проверка перед вызовом: проверьте наличие mcp_server_runtime_mutation в caps.features перед вызовом любого из маршрутов. Демоны старой версии без этого тега возвращают 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 отсутствует или не является ненулевым объектом |
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[]. В текущей версии снимка на уровне протокола нет различия между двумя источниками.
События
Оба маршрута генерируют внутри рабочей области события SSE (все активные сессионные шины их получают):
| Событие | Генерируется когда | Поля полезной нагрузки |
|---|---|---|
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/. - Следите за дорожной картой? Ход выполнения этапов 1.5 / 2 отслеживается в задаче #3803 .