デーモンモード(qwen serve)
Qwen Code をローカル HTTP デーモンとして起動し、複数のクライアント(IDE プラグイン、Web UI、CI スクリプト、カスタム CLI)が HTTP + Server-Sent Events 経由で、それぞれサブプロセスを起動する代わりに一つのエージェントセッションを共有できます。
🚧 v0.16-alpha:
qwen serveは v0.16-alpha として npm に初リリースされます。この段階ではテキストのみのチャット/コーディングとローカル専用デプロイが対象です。プロンプトパスでの画像/ファイル添付、コンテナ化デプロイ(Docker / k8s / nginx リバースプロキシ)、リモート/マルチデーモンの堅牢化は、エンタープライズパイロットが確定した後続パッチで追加されます。保留機能の全一覧は v0.16-alpha の既知の制限 を参照してください。
ステータス: Stage 1(実験的)。プロトコルサーフェスは issue #3803 の §04 ルートテーブルでロック済みです。Stage 1.5(
qwen --serveフラグ — TUI が同一 HTTP サーバーを共同ホスト)および Stage 2(インプロセスリファクタ +mDNS/ OpenAPI / WebSocket / Prometheus の整備)は直後にリリース予定です。スコープの説明: Stage 1 はプロトコルサーフェスに対してクライアントをプロトタイプする開発者およびローカル単一ユーザー/小規模チームのコラボレーションを対象として設計されています。モバイルコンパニオンや 1000 件以上のチャットを処理する IM ボットなど、本番グレードのマルチクライアント/長時間稼働/ネットワーク不安定なワークロードには、このリリースに含まれない Stage 1.5+ の保証が必要です。ギャップの全一覧は Stage 1.5+ の実行時保証 を、コンバージェンスロードマップは #3803 を参照してください。
提供される機能
- 組み込みの Web Shell UI —
qwen serveはデーモンルート(http://127.0.0.1:4170/)でブラウザベースの Web Shell をすぐに提供します。qwen serve --openを使用するとブラウザで自動的に開きます。API と同じオリジンで配信されるため、追加ポートやリバースプロキシは不要です。API のみのデーモンにするには--no-webを指定してください。 - 一つのエージェントプロセスで複数クライアント — デフォルトの
sessionScope: 'single'では、デーモンに接続するすべてのクライアントが一つの ACP セッションを共有します。同一の会話、同一のファイル差分、同一の権限プロンプトをリアルタイムにクロスクライアントでコラボレーションできます。 - 再接続に安全なストリーミング —
Last-Event-IDによる再接続対応の SSE で、クライアントが切断しても(リングのリプレイウィンドウ内で)正確に中断した箇所から再開できます。 - 最初に応答した者が権限を持つ — エージェントがツールの実行許可を求めると、接続中のすべてのクライアントがリクエストを受信し、最初に応答したクライアントの回答が採用されます。
- 一つのデーモン、一つのワークスペース — 各
qwen serveプロセスは起動時にちょうど一つのワークスペースにバインドされます(#3803 §02 準拠)。マルチワークスペースデプロイでは、別々のポートで(またはオーケストレーターの背後で)ワークスペースごとに一つのデーモンを起動してください。 - リモートランタイム制御(#4175 PR 17)— セッションの承認モードを変更(
POST /session/:id/approval-mode)、ワークスペース単位でツールのトグル(POST /workspace/tools/:name/enable)、空のQWEN.mdのスキャフォールド(POST /workspace/init、機械的な操作のみ — モデルを呼び出しません。AI で内容を埋めるには続けてPOST /session/:id/promptを使用してください)、予算チェック付きで単一の MCP サーバーを再起動(POST /workspace/mcp/:server/restart)、またはデーモンを再起動せずに実行時に MCP サーバーを追加・削除(POST /workspace/mcp/servers、DELETE /workspace/mcp/servers/:name)できます。すべて strict-gated — 事前に--tokenを設定してください。 - セッション要約(#4175 フォローアップ)— アクティブなセッションの「どこで中断したか」を一文で取得(
POST /session/:id/recap)。コアのgenerateSessionRecapを高速モデルへのサイドクエリとしてラップします。メインのチャット履歴や SSE ストリームには影響しません。Non-strict gate(/promptと同じ姿勢)。SDK ヘルパーclient.recapSession(sessionId)。- 既知の制限 — トークンコスト増幅: このルートは純粋なコストエンドポイントです(各呼び出しが LLM サイドクエリで、状態の利益はありません)。v1 ではデーモンにルート単位のレート制限がありません。no-token ループバックのデフォルトでは、バグのある、または悪意あるローカルクライアントがスパムしてトークンを消費する可能性があります。共有開発ホストでデーモンを公開する前に
--token(および必要に応じて--require-auth)を設定してください。 - 同時実行時の recap の安全性: 同一セッションで同時に
/recapを 2 回呼び出すと、それぞれ独立したサイドクエリが実行されます。generateSessionRecapはGeminiClient.getChat().getHistory()経由でチャット履歴のスナップショットを読み取り、別のBaseLlmClient.generateText呼び出し(runSideQuery経由)に渡します。セッションのGeminiChatへの追記や変更は行いません。複数のクライアントから調整なしで安全に呼び出せます。
- 既知の制限 — トークンコスト増幅: このルートは純粋なコストエンドポイントです(各呼び出しが LLM サイドクエリで、状態の利益はありません)。v1 ではデーモンにルート単位のレート制限がありません。no-token ループバックのデフォルトでは、バグのある、または悪意あるローカルクライアントがスパムしてトークンを消費する可能性があります。共有開発ホストでデーモンを公開する前に
v0.16-alpha の既知の制限
qwen serve の最初の npm リリース(v0.16-alpha)は意図的に限定的なスコープになっています — 開発者が自分のマシンでデーモンを実行するためのテキストのみのチャット/コーディングです。以下のリストは保留されたサーフェスを明示し、採用者が計画を立てられるようにしています。ここに記載されたすべての項目は v0.16.x のパッチロードマップまたは近い将来のフォローアップリリースに含まれています。
製品サーフェス — テキストのみ:
- ✅ テキストプロンプトとテキストレスポンス(チャット、コーディング、ツール呼び出し、MCP 連携)
- ❌ プロンプトパスでの画像/ファイル添付 —
MessageEmitterは現在テキストのみをレンダリングします。マルチモーダルエコーは画像ニーズのあるアルファターゲットが確定したときに追加されます(#4175 chiga0 #27 P0 アイテム) - ❌ ストリーミングアップロード — マルチモーダルと同じゲーティング
デプロイサーフェス — ローカル専用:
- ✅ ループバック(
127.0.0.1、デフォルト)— 認証不要、開発用ワークステーションに適しています - ✅
systemd/launchd/nohup &/tmux経由のローカル起動 — ローカル起動テンプレート を参照 - ✅
QWEN_SERVER_TOKEN環境変数によるベアラートークンの持ち込み(設定方法は 認証) - ❌ コンテナ化デプロイ — Docker / Compose / Kubernetes / TLS 終端の nginx リバースプロキシは v0.16-alpha 非対応。エンタープライズパイロットが確定後に v0.16.x で追加予定(検証者がいない状態では品質低下が避けられないため)。
- ❌ 単一ホストでのマルチデーモン調整 —
1 daemon = 1 workspace × N sessionsが強制されます。クロスホストフェデレーション、インスタンスパストークンキーイング、古いトークンのクリーンアップは v0.16.x で対応予定。 - ❌ デーモントークンの自動生成 — アルファは BYO トークン(
openssl rand -hex 32一発で生成)。自動生成 + トークンストアのインフラは v0.16.x で対応予定。
堅牢化 — ローカル単一ユーザーの最低限の実行可能性:
- ✅ 起動時のセキュリティゲート(トークンなしでのループバック以外のバインドを拒否、PR 15 / #4236 )
- ✅ ミューテーションルートの認証ゲート、セッションスコープの権限ルーティング(Wave 4 PR)
- ✅ MCP ガードレール + マルチクライアント権限調整(F2 / F3)
- ✅ プロンプト絶対デッドライン + SSE ライターアイドルタイムアウト —
--prompt-deadline-msと--writer-idle-timeout-msでオプトイン。有効時はprompt_absolute_deadlineとwriter_idle_timeoutでアドバタイズされます。 - ✅ HTTP レート制限 —
--rate-limitとティア別しきい値でオプトイン。有効時はrate_limitでアドバタイズされます。 - ⏸️ Prometheus メトリクス + ロードテストハーネス — 30〜50 のアクティブセッションが実際のターゲットになる v0.17 F4 Phase-1 スケールインストルメンテーションで対応予定。
- ⏸️
--max-body-sizeCLI フラグ — デーモンはデフォルトでexpress.json({ limit: '10mb' })を適用しており、テキストのみのプロンプト(モデルコンテキストウィンドウは 10 MiB の文字数を大幅に下回ります)には十分です。v0.16.x でフラグによる調整が可能になります。
Stage 1 で修正しないことの詳細な説明(単一ホストのセッション状態ミューテーションモデル + 1 つの ACP 子プロセスを共有する N 並列セッション)については、以下の Stage 1 のスコープ境界 を参照してください。
クイックスタート
1. デーモンを起動する(ループバック、認証なし)
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 です。ローカル開発がすぐに動作するよう、ループバックではベアラー認証は無効です。デーモンはカレントワーキングディレクトリにバインドします。オーバーライドするには --workspace /path/to/dir を使用してください。
Web Shell UI を開く。 http://127.0.0.1:4170/ をブラウズするか(または qwen serve --open でデーモン起動時に自動的に開く)、チャット、差分、ツール呼び出し、権限プロンプトを含むフルブラウザターミナルをご利用ください。UI はデーモンルートで API と同じオリジンから提供されます。このガイドの残りの部分では、API に対して直接スクリプトを実行できるよう生の HTTP を使用します。
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 フィールドはバインドされたワークスペースを示すため、クライアントはプリフライトチェックができ、POST /session で cwd を省略できます。
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 ステータスを構造化して返します。スポーンされたエージェントセッションで LSP を有効にするには --experimental-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
はライブ ACP ランタイムをレポートし、アイドル時には ACP 子プロセスを起動しません。アイドル中のデーモンは空のスナップショットで initialized: false を返します。セッションがアクティブになると initialized: true に切り替わり、実際の状態を表示します。
GET /workspace/env と GET /workspace/preflight は ACP の状態に関わらず常に initialized: true で応答します。env は ACP を参照しません(デーモンプロセスの情報のみ)。preflight は process.* からデーモンレベルのセルを返し、子プロセスがアイドル中の ACP レベルセルには status: 'not_started' プレースホルダーを返します。
GET /workspace/env はデーモンプロセスのランタイム、プラットフォーム、サンドボックス、プロキシ、および OPENAI_API_KEY などのホワイトリスト済みシークレット環境変数の存在(値は返しません)をレポートします。プロキシ URL は認証情報が除去され、host:port 形式に変換されてから出力されます。このルートは常にデーモンプロセスから直接応答し、ACP 子プロセスをスポーンしません。
GET /workspace/preflight は準備状況チェックのリストを返します。デーモンレベルのセル(Node バージョン、CLI エントリ、ワークスペースディレクトリ、ripgrep、git、npm)は常にレンダリングされます。ACP レベルのセル(認証、MCP ディスカバリー、スキル、プロバイダー、ツールレジストリ、egress)にはライブの ACP 子プロセスが必要です — デーモンがアイドル中の場合、ACP を起動する代わりに status: 'not_started' プレースホルダーを返します。失敗は閉じられた errorKind 列挙型(missing_binary、auth_env_error、init_timeout、protocol_error、missing_file、parse_error、blocked_egress)にマッピングされるため、クライアント UI は構造化された修復手順をレンダリングできます。
デーモンはワークスペースファイルヘルパーも公開しています:
GET /fileはテキストファイルを読み取り、生バイトのsha256:<hex>ハッシュを返します。GET /file/bytesは境界付きの生バイトウィンドウを読み取り、base64 コンテンツを返します。POST /file/writeはテキストファイルを作成または置き換えます。POST /file/editは一つの正確なテキスト置換を適用します。
Write / edit は strict ミューテーションルート: ループバックでも設定済みのベアラートークンが必要で、ない場合は token_required を返します。置換と編集には GET /file(またはフルウィンドウの GET /file/bytes)からの最新の expectedHash が必要です。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 を POST すると 400 workspace_mismatch が返ります(デーモンは正確に一つのワークスペースにバインドされています。別のワークスペースには別のデーモンを起動してください)。
2 番目のクライアントが /session に POST すると(一致する 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 の下に入ります。SSE レベルの id: / event: 行は 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"}ステップ 4 の curl -N がフレームを受信次第表示します。
認証
ループバック以外では、ベアラートークンを必ず渡す必要があります:
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 はポッド内でデーモンが 127.0.0.1 でリッスンしている場合(k8s/Compose のライブネスプローブが認証情報を必要としないよう)、ループバックバインドのみ免除されます。非ループバックバインド(--hostname 0.0.0.0 など)では、/health も他のすべてのルートと同様にトークンが必要です — そうしないと攻撃者がデーモンの存在を確認するために任意のアドレスを探索できてしまいます。トークンがエンドツーエンドで正しいか確認するには /capabilities を使用してください(常に認証が必要です):
ループバックの堅牢化(
--require-auth)。 デフォルトのループバックでトークンなしの動作は、単一ユーザーのノートパソコンには問題ありませんが、ローカルユーザーがcurl 127.0.0.1:4170を実行できる共有開発ホスト、CI ランナー、マルチテナントワークステーションでは安全ではありません。--require-authを指定すると、127.0.0.1にバインドしていても、/healthや/capabilitiesを含むすべてのルートでベアラートークンが必須になります。トークンなしでは起動に失敗します。このフラグが有効の場合、未認証のクライアントは/capabilitiesで認証が必要であることを発見できません。発見サーフェスは 401 レスポンスボディ自体です。認証後はcaps.features.require_authタグがデプロイが堅牢化されていることの事後確認になります(監査/コンプライアンス UI に有用):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 レスポンスは「ヘッダーなし」「スキームが違う」「トークンが違う」を区別しない統一された形式で、サイドチャネルによる識別を防ぎます。
CLI フラグ
| フラグ | デフォルト | 目的 |
|---|---|---|
--port <n> | 4170 | TCP ポート。0 = OS が割り当てるエフェメラルポート。 |
--hostname <addr> | 127.0.0.1 | バインドインターフェース。ループバック以外ではトークンが必要。 |
--token <str> | — | ベアラートークン。QWEN_SERVER_TOKEN 環境変数にフォールバック(先頭と末尾の空白は除去されます — $(cat token.txt) に便利)。 |
--require-auth | false | ループバックでも、ベアラートークンなしでは起動を拒否します。ローカルユーザーがリスナーにアクセスできる共有開発ホスト / CI ランナー / マルチテナントワークステーション向けに 127.0.0.1 のデベロッパーデフォルトを堅牢化します。--token または QWEN_SERVER_TOKEN が設定されている場合のみ起動し、/health もベアラーで保護されます。 |
--max-sessions <n> | 20 | 同時アクティブセッションの上限。上限に達すると、新しい子プロセスをスポーンする POST /session リクエストは 503(Retry-After: 5 付き)を返します。既存セッションへのアタッチはカウントされません。0 で無効化。単一ユーザー/小規模チームの使用を想定したサイズ; デプロイに RAM/FD のヘッドルームがある場合は引き上げてください(セッションあたり約 30〜50 MB)。 |
--max-pending-prompts-per-session <n> | 5 | POST /session/:id/prompt で受け付けたが未完了のプロンプト(キューに入ったプロンプトとアクティブなプロンプトを含む)のセッション単位の上限。ブリッジは promptId を返す前にオーバーフローを 503、Retry-After: 5、code: "prompt_queue_full" で同期的に拒否します。0 で無効化。branchSession は同じ FIFO でシリアライズされますが、このプロンプト上限にはカウントされません。 |
--workspace <path> | process.cwd() | このデーモンがバインドする絶対ワークスペースパス(#3803 §02 準拠 — 1 daemon = 1 workspace)。cwd が一致しない POST /session リクエストは 400 workspace_mismatch を返します。マルチワークスペースデプロイでは、ワークスペースごとに別々のポートで一つの qwen serve を実行してください。 |
--max-connections <n> | 256 | リスナーレベルの TCP 接続上限(server.maxConnections)。セッション数に関わらず生ソケット数を制限します — 上限に達すると遅い/ファントム SSE クライアントは accept 時に拒否されます。セッションあたり多くの SSE 購読者を想定するデプロイでは --max-sessions と一緒に引き上げてください。 |
--event-ring-size <n> | 8000 | セッション単位の SSE リプレイリング深度(#3803 §02 ターゲット)。GET /session/:id/events で Last-Event-ID: N を使った再接続に利用できるバックログを設定します。大きいほど再接続のヘッドルームが増えますが、セッションあたり数百 KB の追加 RAM が必要です。SDK クライアントは特定のサブスクリプションで ?maxQueued=N(範囲 [16, 2048]、デフォルト 256)を使って購読者単位の追加バックログ上限を指定できます。デーモンはキュー使用率 75% で non-terminal slow_client_warning SSE フレームを送信し、クライアントが evict される前に排水/再接続できます。caps.features.slow_client_warning でプリフライト確認できます。 |
--mcp-client-budget <n> | — | ACP セッションあたりのライブ MCP クライアント数の正の整数上限(issue #4175 PR 14 v1; PR 23 でこれを共有 MCP プール経由のワークスペース単位に昇格)。--mcp-budget-mode と組み合わせて使用。未設定の場合、アカウンティング駆動の強制はありません(ただし GET /workspace/mcp は引き続き clientCount をレポートします)。MCP_SERVER_CONNECTION_BATCH_SIZE(起動同時実行数をゲートするもの)とは異なり、こちらはクライアント総数をゲートします。caps.features.mcp_guardrails でプリフライト確認できます。 |
--mcp-budget-mode <m> | warn / off | --mcp-client-budget の強制方法。warn(予算設定時のデフォルト): 拒否なし、スナップショットの budgets[0].status が予算の ≥75% で warning に変わります。enforce: 上限を超える接続は拒否され、サーバー単位のセルに disabledReason: 'budget' が表示され、mcpServers 宣言順で決定論的。off(予算未設定時のデフォルト): 純粋な観測可能性。予算なしで enforce を指定すると起動に失敗します。 |
--http-bridge | true | Stage 1 モード: デーモンあたり一つの qwen --acp 子プロセス(起動時に一つのワークスペースにバインド、#3803 §02 準拠)。N セッションが ACP newSession() 経由でその子プロセスに多重化されます。Stage 2 のネイティブインプロセスは後で利用可能になります。 |
--allow-origin <pat> | — | T2.4 (#4514 )。ブラウザ webui クライアント向けのクロスオリジン許可リスト。繰り返し指定可能。各値は *(任意のオリジン — ベアラートークンが設定されていない場合は起動を拒否。ループバックでは /health と /demo がデフォルトで認証前なので、完全な堅牢化には --require-auth が推奨)、またはカノニカル URL オリジン(<scheme>://<host>[:<port>]、末尾スラッシュ / パス / ユーザー情報 / クエリなし)。サブドメインワイルドカード(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 でプリフライト確認できます。ループバックのセルフオリジンは影響を受けません。 |
--web / --no-web | true | ビルドされた Web Shell SPA をデーモンルート(GET /、/assets/*、SPA ディープリンクフォールバック)で提供します。静的シェルはベアラー認証ゲートの前に登録されます — ブラウザは <script> サブリソースやアドレスバーナビゲーションにトークンを付けられず、シェルにシークレットはなく、すべての API ルートは引き続きトークンで保護されます。非ループバックバインドでは、UI が認証なしで到達可能である旨の一行の stderr 警告が表示されます。API のみのデーモンには --no-web を使用してください。ビルドに Web Shell アセットが含まれていない場合は効果なし(デーモンはパンくずをログに記録し API のみで実行)。 |
--open | false | リスナーが起動した後、デーモン URL でデフォルトブラウザに Web Shell を開きます(トークンが設定されている場合は URL フラグメントとして #token= を追加 — フラグメントはサーバーに送信されないため、アクセスログや Referer ヘッダーにトークンが含まれません)。--no-web の場合、またはブラウザが使用できないヘッドレス / CI / SSH 環境では無効。 |
負荷制御ノブのサイジング。
--max-sessionsは新しい子プロセスの上限です。 負荷を制限するレイヤーは他にも 3 つあります — 高同時実行デプロイのサイジングでは、これらをまとめて調整してください:
- リスナーレベル:
--max-connections/server.maxConnections=256で生 TCP 接続を制限(遅いクライアントのバックプレッシャー)。- セッション単位の購読者: EventBus はデフォルトでセッション単位の SSE 購読者を 64 に制限。65 番目のクライアントは terminal
stream_errorを受け取り閉じられます。- セッション単位のプロンプト受付:
--max-pending-prompts-per-session=5で一つのセッションに受け付けた キューに入った + アクティブなプロンプトを制限。オーバーフローは503(Retry-After: 5)。- 購読者単位のバックログ: SSE クライアントあたり 256 フレームのキュー。 容量超過のクライアントは terminal
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(スナップショットに警告が表示されますがクライアントは拒否されません — 強制を有効にする前に実際のファンアウトを測定するのに有用)です。enforceモードで拒否されたサーバーはサーバー単位のセルにdisabledReason: 'budget'が表示され、budgets[0]セルにstatus: 'error'+errorKind: 'budget_exhausted'が表示されます。スロット予約はサーバー名でされ、再接続/ディスカバリータイムアウトをまたいで維持されます — 拒否されたサーバーが正常なサーバーのスロットを奪うことはできません。⚠️ v1 のスコープ: ワークスペース単位ではなくセッション単位。 デーモン内の各 ACP セッションは独自の
Config/McpClientManagerを持ちます(セッションごとにnewSessionConfigで作成)。予算はセッション内のライブ MCP クライアントをセッション単位で制限し、ワークスペース内の全セッションにわたる集計ではありません。GET /workspace/mcpのスナップショットはブートストラップセッションのビューを反映します(セルは正直さのためにscope: 'session'を持ちます)。--mcp-client-budget=10で 5 つの同時 ACP セッションを実行すると、デーモン全体で最大 50 のライブ MCP クライアントが存在する可能性があります — 上限はセッション単位で保持されます。**Wave 5 PR 23(共有 MCP プール)**でワークスペーススコープのマネージャーが導入され、真のワークスペース単位の強制に昇格します。qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn # later, after telemetry shows your real-world distribution: qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforceこれは claude-code の
MCP_SERVER_CONNECTION_BATCH_SIZE(起動同時実行数をゲートするもの)と同じではありません。それらは直交しています。PR 23 では実際の共有 MCP プール(budgets[]にセッション単位セルと並んでscope: 'workspace'セルを追加)が追加されます。PR 14 v1 はインプロセスカウンター + 既存のセッション単位マネージャーへのソフト強制です。プッシュイベント(issue #4175 PR 14b)。
GET /session/:id/eventsを購読している SDK クライアントは、予算しきい値を超えたときにタイプ付きフレームを受信します —mcp_budget_warning(合成、上方向の 75% クロスで一度発火、37.5% でヒステリシス再アーム、mcp_guardrail_eventsでアドバタイズ)とmcp_child_refused_batch(enforceモード下のディスカバリーパスごとに一度合体。readResourceレイジースポーン拒否からの length-1)。GET /workspace/mcpのスナップショットは再接続後の状態の信頼できる情報源です。イベントは変更エッジです。ポーリングなしのリアルタイムダッシュボーディングに有用です。
デフォルトのデプロイ脅威モデル
- 127.0.0.1 のみ — ループバックバインド、認証不要。
--hostname 0.0.0.0はトークンが必要 — ないと起動を拒否。LOOPBACK_BINDSには IPv6 が含まれます —::1と[::1]は no-token ルールのループバックとしてカウントされます。- Host ヘッダー許可リスト — ループバックバインドでは、デーモンは DNS リバインディングを防ぐために
Host:がlocalhost:port/127.0.0.1:port/[::1]:port/host.docker.internal:port(RFC 7230 §5.4 に従い大文字小文字を区別しない)と一致するか確認します。非ループバックバインド(--hostname 0.0.0.0)は Host 許可リストを意図的にバイパスします — オペレーターはサーフェスエリアを選択しているため、ベアラートークンゲートが唯一の認証レイヤーです。リバースプロキシ / SNI / クライアント証明書ピニングはオペレーターの責任です。非ループバックバインドで Host ベースの分離が必要な場合は、フロントプロキシで TLS を終端し Host を確認してください。 - CORS はデフォルトで任意のブラウザ Origin を拒否 —
403JSON を返します。特定のブラウザオリジンを通過させるには--allow-origin <pattern>(繰り返し可能、T2.4 #4514)を指定してください。各値はリテラル*(任意のオリジン — ベアラートークンが設定されていない場合は起動を拒否。ループバックでは/healthと/demoがデフォルトで認証前なので、完全な堅牢化には--require-authが推奨)、またはカノニカル URL オリジン(<scheme>://<host>[:<port>]、末尾スラッシュ / パス / ユーザー情報なし)。一致するオリジンには適切な CORS レスポンスヘッダー(Access-Control-Allow-Origin: <echoed>、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。ループバックのセルフオリジン(例:/demoページ)は影響を受けません —--allow-originに関わらず別の Origin ストリップシムが処理します。--allow-originが設定されていないブラウザ webui は引き続き Stage 1 と同じオプションにフォールバックします:Originヘッダーが送信されないようネイティブシェル(Electron/Tauri)としてパッケージするか、同一オリジンのリバースプロキシでデーモンをフロントに置く。 - スポーンされた
qwen --acp子プロセスはデーモンの環境を継承します — ただし一つの明示的なスクラブがあります:QWEN_SERVER_TOKENは子プロセス起動前に除去されます(デーモン自身のベアラー。エージェントは不要)。それ以外 —OPENAI_API_KEY/ANTHROPIC_API_KEY/QWEN_*/DASHSCOPE_API_KEY/ カスタムのmodelProviders[].envKeyなど — はすべて引き継がれます。エージェントが LLM の認証のためにこれらを必要とするためです。これは意図的な設計であり、サンドボックスではありません。 エージェントはシェルツールアクセスを持つ同じ UID で実行されるため、~/.bashrc/~/.aws/credentials/~/.npmrc内のすべてのものはプロンプトインジェクションで到達可能です。環境変数のパススルーはセキュリティ境界ではありません。ユーザーが信頼のルートです。エージェントを信頼したくない環境変数に認証情報を持つ ID でqwen serveを実行しないでください。 - 購読者単位の有界 SSE キュー — キューがオーバーフローした遅いクライアントは
client_evictedターミナルフレームを受け取り閉じられます。一つの遅いコンシューマーがデーモンを固定できません。 - セッション単位のプロンプト受付上限 — デフォルトはセッションあたり 5 つの受け付け済み未完了プロンプト。バグのあるクライアントは一つのセッションに対して無制限のプロンプト Promise や一時的な SSE 待機をエンキューできません。
- グレースフルシャットダウン — SIGINT/SIGTERM でリスナーを閉じる前にエージェント子プロセスをドレインします(子プロセスあたり 10 秒のデッドライン)。
⚠️ Stage 1 既知のギャップ — 権限はセッション単位ではなくデーモングローバル(BUy4H)。
pendingPermissionsはデーモンスコープに存在します。ベアラートークンを保持する任意のクライアントが、見えるセッションの任意のrequestIdに投票できます(SSEpermission_requestイベントはペイロードに requestId を含みます)。これは、すべての認証済みクライアントが同じ人間または信頼できるコラボレーターである単一ユーザー/小規模チームの信頼モデルでは許容されます。Stage 1.5 ではPOST /session/:id/permission/:requestId+ セッションスコープの pending マップ + クライアント単位のアイデンティティに移行します(下流レビューの必須事項 #3)。それまでは、信頼できない当事者と共有するベアラーの背後でqwen serveを実行しないでください。⚠️ Stage 1 既知のギャップ —
POST /session/:id/promptのボディは 10 MB に制限(BUy4L)。 10 MB を超える画像 / PDF / オーディオを含むマルチモーダルプロンプトは、ルートロジックが実行される前にボディパース時に失敗します(ストリーミングなし、アップロード中断なし)。回避策: クライアント側でコンテンツを縮小するか、パス参照を渡してエージェントがreadTextFile経由でファイルを読み取るようにしてください。Stage 1.5 では/promptでmultipart/form-dataまたはチャンクエンコーディングを受け付け、大きなプロンプトが崖に達しないようにします。⚠️ Stage 1 既知のギャップ — NAT の背後のファントム SSE 接続。 デーモンはハートビート(15 秒間隔)への TCP バックプレッシャーで 死んだクライアントを検出します。TCP RST なしに消えたクライアント (例: アイドルフローを無音でドロップする NAT ボックス)は、Node の keepalive プローブがタイムアウトするまで — 通常 Linux のデフォルトで 約 2 時間 — カーネルレベルのソケットを「アライブ」状態に保ちます。 このような NAT の背後にある
--hostname 0.0.0.0デプロイでは、ファントム SSE 接続が蓄積し、最終的にserver.maxConnectionsの 256 の上限に達する 可能性があります。
--writer-idle-timeout-ms <n>(issue #4514 T2.9) を設定して、明示的なアプリケーションレベルのアイドルデッドラインでこのギャップを 埋めてください:nms の間、書き込みが正常にフラッシュされない場合(15 秒の ハートビートも含む)、デーモンはreason: 'writer_idle_timeout'の terminalclient_evictedフレームを送信し(data.errorKindにもミラー)ストリームを 閉じます。このフラグはレガシー契約を維持するためデフォルトで無効です — RST を飲み込むネットワークのオペレーターは 15 秒のハートビート間隔を 大幅に上回る値(例:60000〜300000)を選択し、正当なアイドル接続が evict されず、真に詰まったライターが速やかに回収されるようにしてください。 SDK からcaps.features.includes('writer_idle_timeout')でプリフライト確認できます。
デッドラインとライターアイドルタイムアウト
Issue #4514 T2.9 では、15 秒ハートビート + AbortSignal ではカバーできない長時間稼働/リモートデプロイのギャップを埋める 2 つのオプトインフラグが追加されました。どちらもデフォルトで無効です — 単一ユーザーのループバックワークフローはビットレベルで変更なしです。
| フラグ | 環境変数 | デフォルト | 機能 |
|---|---|---|---|
--prompt-deadline-ms <n> | QWEN_SERVE_PROMPT_DEADLINE_MS | 未設定 | 単一の POST /session/:id/prompt に対するサーバーサイドのウォールクロック上限。期限切れになるとデーモンはプロンプトの AbortController を中断し、{code:"prompt_deadline_exceeded", errorKind:"prompt_deadline_exceeded", deadlineMs:n} とともに HTTP 504 を返します。プロンプトリクエストボディフィールド deadlineMs はフラグを下回る有効なデッドラインを短縮できますが、延長はできません。機能タグ(条件付き): prompt_absolute_deadline。 |
--writer-idle-timeout-ms <n> | QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | 未設定 | SSE 接続単位のアイドルデッドライン。n ms の間、実際のイベントでも 15 秒ハートビートでも書き込みが正常にフラッシュされない場合、デーモンは data.reason = 'writer_idle_timeout'(data.errorKind にもミラー)の terminal client_evicted フレームを送信しストリームを閉じます。15 秒のハートビートを大幅に上回る値を選択してください(例: 30000〜300000)。正当なアイドルストリームが evict されないよう。< 15000 の値は最初のハートビートが発火する前に正常なアイドル接続を evict します(テスト / 短命の開発セッション専用の意図的な設定)。機能タグ(条件付き): writer_idle_timeout。 |
どちらのフラグもミリ秒の正の整数を受け付けます。0、NaN、非整数、負の値は起動時に明確なエラーメッセージで拒否されます。CLI フラグは環境変数より優先。明示的な ServeOptions フィールド(埋め込み呼び出し元)は env より優先されます。SDK コンシューマーはいずれかの動作に依存する前に対応する機能タグをプリフライト確認してください — この PR 以前のデーモンはどちらのタグも省略しており、リクエストの deadlineMs フィールドは無音でドロップされます。
マルチセッション & マルチワークスペースデプロイ
#3803 §02 に従い、各 qwen serve プロセスは起動時に一つのワークスペースにバインドされます。そのワークスペース内では、エージェントのネイティブセッションマップ経由で N セッションを単一の qwen --acp 子プロセスに多重化します — セッションは子プロセス / OAuth 状態 / ファイル読み取りキャッシュ / 階層メモリパースを共有します。
複数のワークスペースをホストするには(一人のユーザーで複数のリポジトリ、または同一ホスト上の複数のユーザー)、複数のデーモンプロセスを実行してください — ワークスペースごとに一つ、それぞれ独自のポートで、systemd / docker-compose / k8s / qwen-coordinator リファレンスオーケストレーターで管理します。このトレードオフは意図的です: ワークスペースごとに一つの子プロセスを使用することで、loadSettings(cwd) / OAuth / MCP サーバースコープがバインドされたディレクトリと整合し、リクエスト間でドリフトしません。
アタッチ時に
modelServiceIdを POST する前に購読してください。 クライアントがmodelServiceIdを指定してPOST /sessionし、ワークスペースに異なるモデルで実行中のセッションが既にある場合、デーモンは内部的にsetSessionModel呼び出しを発行します — 失敗は HTTP エラーとして伝播されません(セッションは現在のモデルで動作し続けます)。可視の失敗シグナルはセッションの SSE ストリームのmodel_switch_failedイベントです。POST /sessionを呼び出してからのみGET /session/:id/eventsを開くと、失敗イベントを見逃し、間違ったモデルと静かに通信し続けることになります。最初に SSE ストリームを開くか、購読時にLast-Event-ID: 0を渡してリングの最古のイベントをリプレイしてください。
複数のユーザー(それぞれ独自のクォータ、監査ログ、サンドボックスを持つ)を処理するため、またはプロセスの範囲を超えてスケールするため(コールドスタートバジェット、FD 数、RSS)、外部オーケストレーターの背後でユーザーごとにワークスペースごとに一つのデーモンをスポーンしてください。そのオーケストレーター(マルチテナンシー / OIDC / クォータ / 監査 / k8s)は qwen-code プロジェクトのスコープ外 です — デザインポインターについては issue #3803 の「External Reference Architecture」を参照してください。
永続化されたセッションのロードと再開
デーモンは ACP の session/load と再開フローを HTTP 経由で 2 つのルートで公開しています:
| ルート | 使用するケース |
|---|---|
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' });
// Cold reconnect — daemon will replay history through SSE.
const session = await DaemonSessionClient.load(client, 'persisted-id');
// Or, if your UI already has the history, skip the replay:
// const session = await DaemonSessionClient.resume(client, 'persisted-id');
for await (const event of session.events()) {
// First the replayed `session_update` frames (load only),
// then live events.
}呼び出す前に 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を発行し、デーモンのマップからライブセッションを削除します。新しいエージェント子プロセスがスポーンできれば、ディスク上の永続化されたセッションをPOST /session/:id/load経由でリロードできます。 - デーモンの再起動はすべてのインフライトのライブセッションを失います。永続化されたセッションはディスク上に残り、同じワークスペースバインディングルールのもとで新しいデーモンプロセスに対してロードできます。
- 長いクライアント切断(おしゃべりなターンで 5 分以上)は SSE リプレイリング(デフォルト 8000 フレーム)を追い越す可能性があります —
Last-Event-IDでの再接続は成功しますが、状態が不整合になる可能性があります。モバイル / フラッキーネットワーククライアントでは、長い切断時に SSE を再開するかPOST /session/:id/loadを呼び出してディスクからリプレイする計画を立ててください。 - ファイル操作(
writeTextFile)はクラッシュをまたいでアトミックです(書き込み後リネーム)。デーモン再起動をまたいでアトミックではない — ファイル書き込みが完了したかどうかだけです。
統合が session/load がカバーする以上のサーバーサイドのクロス再起動耐久性(例: サーバー管理のリトライキュー)を必要とする場合、アプリケーションレベルの状態回復が依然として必要です。デーモンのセッション内に長時間稼働の再起動センシティブな状態を保持しないでください。
Stage 1.5+ の実行時保証
Stage 1 の契約はプロトタイピング用のサイズです。#3889 chiga0 下流コンシューマーレビュー に従い、以下は Stage 1 に含まれません — 本番グレードの統合がこれらに依存するには Stage 1.5+ が必要です:
深刻な下流利用のブロッカー:
- HTTP 経由の
loadSession/unstable_resumeSession— これがないと、どの統合も子プロセスのクラッシュやデーモンの再起動を生き残れず、デーモンを調整するオーケストレーターも状態を回復できません。 - 永続的なクライアントアイデンティティ(ペアトークン + クライアント単位の失効) — Stage 1 は共有ベアラーを 1 つ使用します。トークンが漏洩すると全員が失効し、
originatorClientIdはデーモンがスタンプした認証済みアイデンティティではなく、クライアントの自己申告です。
信頼性のベースライン:
クライアント開始のハートビートパス— #4175 PR 9 で配信済み。POST /session/:id/heartbeatはデーモンに最終確認タイムスタンプを記録します(機能タグclient_heartbeat)。SDK ヘルパーはDaemonClient.heartbeat()/DaemonSessionClient.heartbeat()。- 投票が最初の応答者レースに負けたときの
permission_already_resolvedイベント — 現在 UI は404から状態を推測する必要があります。 より大きなリプレイリング— 8000 に増加。セッション単位で設定可能なリング は未対応 — モバイル / おしゃべりなターンのワークロードにはセッション単位のオーバーライドが必要な場合があります。client_evicted前のslow_client_warningイベント — 行儀の良い遅いクライアントが終了前に自己スロットルできるソフトバックプレッシャー(レンダー深度を削減、チャンクをドロップ)。
統合の人間工学:
- IM スタイルのコンテキスト用
POST /session/:id/_meta— 後続プロンプトに添付されるセッション単位のキーバリュー(チャット id、送信者、スレッド id)が、チャネルごとの即興を置き換えます。 /capabilitiesの実際の機能ネゴシエーション —protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }でクライアントが「不明なフレーム、無視」にフォールスルーする代わりにドリフトを検出できます。- ファーストクラスの耐久性ドキュメント(このセクション)— 上で既に配信済み。
完全なコンバージェンスロードマップは #3803 で追跡されています。
Stage 1 のスコープ境界 — Stage 1.5 で修正しないこと
2 つの構造的な選択は Stage 1 / 1.5 / 2 のメインラインロードマップの明示的な非目標です。ユースケースがどちらかに依存する場合は、待機するのではなく回避策を計画してください。
セッション状態はローカルミューテーションのみ(LaZzyMan review #4270256721 に従い)
Stage 1.5 の計画は TUI をインプロセスの EventBus 購読者として説明しています。実際には TUI UI はワイヤープロトコルより厳密に大きい です:
- ローカルのみの UI — 約 15 の Ink ダイアログコンポーネント(
ModelDialog、MemoryDialog、PermissionsDialog、SessionPicker、WelcomeBackDialog、FolderTrustDialogなど)とlocal-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): これらのミューテーションをワイヤーイベントに昇格させない。2 つのデプロイモードで結果が異なります。
モード 1 — ヘッドレス qwen serve(この PR)
デーモン内に TUI シェルは実行されません。上記のスラッシュコマンドはこのモードには存在しません — 発行するターミナル UI がないためです。セッション状態は:
approval-mode/memory/agents/tools許可リスト /authについては起動時に固定 — デーモンのqwen --acp子プロセスが起動するときに設定 + ディスクからすべて読み込まれ、セッションのライフタイム中は不変です。設定定義の MCP サーバーも同様に起動時に固定されますが、実行時に追加されたサーバー(POST /workspace/mcp/servers経由)は再起動なしで追加または削除できます。POST /session/:id/model(model_switchedを発行)、POST /workspace/mcp/servers/DELETE /workspace/mcp/servers/:name(mcp_server_added/mcp_server_removedを発行)、および権限投票(POST /permission/:requestId)経由で HTTP 経由でミューテーション可能。
結果: ヘッドレスモードのリモートクライアントは完全なセッション状態を見ます。追加状態を隠す TUI はなく、ドリフトは不可能です。approval-mode を変更したい場合は、新しい設定でデーモンを再起動してください。MCP サーバーはミューテーションルート(POST /workspace/mcp/servers、DELETE /workspace/mcp/servers/:name)経由で実行時に追加/削除できます — ランタイム MCP サーバー管理 を参照してください。
モード 2 — Stage 1.5 の qwen --serve 共同ホスト TUI(この PR には含まれない)
Stage 1.5 で qwen --serve(TUI プロセスが同一 HTTP サーバーを共同ホスト)が追加されると、リモートクライアントと並んで TUI が存在します。ローカルオペレーターが /approval-mode yolo や /mcp add-server を入力するとセッション状態が変更され、HTTP 上のリモートクライアントはその変更を観察するイベントを持ちません。
このモードでは、TUI は**「スーパークライアント」** — リモートクライアントが見るのと同じエージェント会話を観察し、かつリモートクライアントができないセッション状態をミューテーションできます。非対称性は:
- ✅ TUI とリモートクライアントは同じエージェントメッセージ、ツール呼び出し、ファイル差分、権限プロンプトを見ます。
- ❌ TUI のみが承認モード / メモリ / MCP サーバーリスト / エージェント / ツール許可リスト / 認証状態を見る / ミューテーションできます。
モード 2 での結果: リモートクライアント UI がセッション設定をミラーしようとすると、TUI スラッシュコマンドの後にドリフトする可能性があります。リモートクライアントはアタッチ / 再接続時に状態を再フェッチするべきです(Last-Event-ID: 0 を使用してリングの最古のイベントをリプレイし、model_switched などを取得してください)。TUI サイドのミューテーションには増分イベントに依存するべきではありません。
なぜ (A) で (B)(ミューテーションを session_state_changed イベントファミリーに昇格)ではないのか
(B) はより野心的な答えですが、Stage 1.5 を計画されているインプロセスリファクタを通じてクリーンに通過しなければならない実質的に大きなワイヤーサーフェスに縛り付けます。より小さなスコープを正直に進む方が良いと考えています。セッション状態イベントの分類作業 — どの TUI フローが設計上ローカルのみで、どれが将来のオプトイン (B) フレーバー拡張でワイヤーに昇格できる可能性があるかの列挙 — は Stage 1.5 のコードではなく #3803 に移ります。
N 並列セッションは一つの qwen --acp 子プロセスを共有する
同一ワークスペースの複数のセッションは、エージェントのネイティブマルチセッションサポート(packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>)経由で一つの qwen --acp 子プロセスを共有します。ブリッジは各セッションで connection.newSession({cwd, mcpServers}) を呼び出します — エージェントはそれらをセッションマップに保存し、呼び出しごとの sessionId で逆多重化します。
N=5 セッションが同一ワークスペースにある場合の具体的なコスト:
| リソース | セッション単位 | N=5 での合計 |
|---|---|---|
| デーモン Node プロセス | 一つ | 30〜50 MB(一つのデーモン) |
qwen --acp 子プロセス | 共有 | 60〜100 MB(一つの子プロセス) |
| MCP サーバー子プロセス | セッション単位 | 設定が異なる場合 3×N |
FileReadCache(子プロセスのヒープ) | 共有 | 一度だけパース |
CLAUDE.md / 階層メモリパース | 共有 | 一度だけパース |
| OAuth リフレッシュトークン状態 | 共有 | 一つのリフレッシュパス |
| 自動メモリ学習済みファクト | 共有 | 子プロセスごとに一つのナレッジベース |
| コールドスタート | 最初のみ | 最初のセッション後 <200 ms |
ブリッジはデーモンあたり一つのチャネルを保持します(§02 に従いデーモンあたり一つのワークスペース)。チャネルは少なくとも一つのセッションがアクティブな間は維持されます。最後の killSession(またはチャネルレベルのクラッシュ)が子プロセスを終了させます。
MCP サーバー子プロセス は現在もセッション単位です — 各セッションの設定は異なるサーバーを指定できるため、独立してスポーンされます。Stage 1.5 のフォローアップ: (workspace, config-hash) で MCP サーバー子プロセスをリファレンスカウントし、同一の設定が共有されるようにします。この PR のスコープ外です。
ピアエージェント(Cursor / Continue / Claude Code / OpenCode / Gemini CLI)はすべて単一プロセスマルチセッションを採用しています。 qwen-code はエージェントレイヤーでそれらと一致します。この PR の Stage 1 ブリッジは同じアーキテクチャを HTTP 上で可視化します。
リモートデーモンへのログイン(issue #4175 PR 21)
デーモンがリモートポッド(共有ディスプレイなし)で実行されているとき、クライアントは HTTP 経由で OAuth デバイスフローをトリガーできます。デーモン自身が IdP をポーリングします。 ブラウザのあるデバイスで URL を開くだけです。
Qwen OAuth の無料ティアは 2026-04-15 に廃止されました。以下の qwen-oauth
の例はデバイスフローのプロトコル形状とレガシープロバイダー識別子を文書化しています。
新しいセットアップでは現在サポートされている認証プロバイダーを使用してください。
# 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 / ユーザーがどこで開くかを選択します。これは意図的です: ヘッドレスポッドのデーモンが xdg-open を呼び出すと、実際の認証サーフェスをマスクしながら静かに失敗します。クライアントで gh auth login の「Press Enter to open browser」UX をミラーしてください。
--require-auth と開発の利便性。 デバイスフローのルートはストリクトミューテーションゲート(PR 15)を使用します。つまり、トークンなしのループバックデフォルトは 401 token_required を返します。開発中のローカルでの最も簡単な回避策は qwen serve --token=dev-token です。ループバックデフォルトを堅牢化する場合でなければ --require-auth は必要ありません。
クロスデーモン制限。 oauth_creds.json はデーモン共有(~/.qwen/oauth_creds.json)なので、デーモン A でのログイン成功はデーモン B の次のトークンリフレッシュで自動的に取得されます — ただしデーモン B の SDK クライアントは auth_device_flow_authorized イベントを受信しません(イベントはデーモン単位)。
クロスクライアントのテイクオーバー。 同じデーモン上の 2 つの SDK クライアントが同じプロバイダーに対して POST /workspace/auth/device-flow を送信すると、プロバイダー単位のシングルトンを取得します: 最初の呼び出しは新しい IdP リクエストを開始し attached: false を返します。2 番目の呼び出しは attached: true で既存のインフライトエントリを返します。テイクオーバーは監査証跡に記録されます(2 番目のクライアントの X-Qwen-Client-Id の下)が、個別のイベントは発行しません — ユーザーが IdP ページを完了すると両方のクライアントが同じ auth_device_flow_authorized を受信します。UI で「自分が開始した」と「他の誰かのフローに参加した」を区別する場合は、start() が返す attached フィールドで分岐してください。
デーモンログファイル
qwen serve はプロセス単位の診断ログを以下に書き込みます:
${QWEN_RUNTIME_DIR or ~/.qwen}/debug/daemon/serve-<pid>-<workspaceHash>.log同じディレクトリの latest シンリンクは常に現在のプロセスのログを指しているため、tail -f ~/.qwen/debug/daemon/latest で実行中のデーモンをフォローできます。
ログはライフサイクルメッセージ、ルートエラー(route= と sessionId= コンテキスト付き)、ACP 子プロセスの stderr、および 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 設定は書き込まれません。
プリフライト: いずれかのルートを呼び出す前に caps.features で mcp_server_runtime_mutation を確認してください。このタグのない古いデーモンは 404 を返します。
POST /workspace/mcp/servers — ランタイム MCP サーバーの追加
Strict-gated(ベアラートークン必須)。ライブの McpClientManager 経由でサーバーに即座に接続し、ツールを発見します。
リクエスト:
{
"name": "my-server",
"config": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"]
}
}name は英数字と _、- のみ使用可能(最大 256 文字)。config は settings.json の mcpServers エントリと同じ MCP サーバー設定オブジェクトです(トランスポート依存フィールド: stdio の場合 command/args、SSE/HTTP の場合 url)。セキュリティセンシティブなフィールド(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 がない、または null 以外のオブジェクトでない |
400 | invalid_client_id | X-Qwen-Client-Id ヘッダーが存在するがこのワークスペースに登録されていない |
400 | invalid_config | MCP トランスポートバリデーターによって設定形状が拒否された |
401 | token_required | ベアラートークンが設定されていない(strict gate) |
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 サーバーの削除
Strict-gated。サーバーを切断し、ランタイムオーバーレイから削除します。冪等 — 追加されていない名前の削除はスキップレスポンスを返します(エラーではありません)。
: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 | ベアラートークンが設定されていない(strict gate) |
503 | acp_channel_unavailable | ライブ ACP 子プロセスなし |
シャドウセマンティクス
ランタイムエントリは設定定義の MCP サーバーの上にエフェメラルオーバーレイを形成します:
- 追加: 設定エントリと同名のランタイムサーバーを追加するとシャドウされます — ランタイム設定が優先されます。元の設定エントリは変更されません。
- 削除: 設定エントリをシャドウしていたランタイムサーバーを削除するとシャドウが外れます — 次の接続で設定定義の設定がアクティブになります。
- デーモン再起動: すべてのランタイムエントリが失われます。再起動をまたいで生き残るのは設定定義のサーバーのみです。ランタイムサーバーはセッションのライフタイムスコープです。
GET /workspace/mcp: マージされたビューをレポートします — 設定定義とランタイムの両方のサーバーがservers[]配列に表示されます。現在スナップショットでは 2 つのオリジン間のワイヤーレベルの区別はありません。
イベント
両ルートはワークスペーススコープの 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/に。 - ロードマップを追跡する? Stage 1.5 / Stage 2 の進捗は issue #3803 で追跡されています。