デーモンモード (qwen serve)
Qwen Code をローカル HTTP デーモンとして実行し、複数のクライアント(IDE プラグイン、Web UI、CI スクリプト、カスタム CLI)がそれぞれサブプロセスを生成するのではなく、HTTP + Server-Sent Events 経由で 1 つのエージェントセッションを共有できるようにします。
🚧 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は、ブラウザベースの Web Shell をルート(http://127.0.0.1:4170/)でそのまま提供します。qwen serve --openを実行すると、ブラウザで自動的に起動します。API と同じオリジンで提供されるため、2 つ目のポートやリバースプロキシは不要です。API のみのデーモンとして実行する場合は--no-webを渡します。 - プライマリ ACP 子プロセス 1 つ plus 信頼されたセカンダリごとにオンデマンド 1 つ、多数のクライアント — 本番環境ではプライマリブリッジの事前起動を試み、失敗時は初回使用時にリトライします。信頼されたセカンダリランタイムはオンデマンドで独自の子プロセスを起動し、信頼されていないセカンダリは子プロセスを起動しません。デフォルトの
sessionScope: 'single'では、同じワークスペースをターゲットにするクライアントが 1 つの ACP セッションを共有し、同じ会話、ファイルの差分、権限プロンプトでコラボレーションします。 - 再接続対応ストリーミング —
Last-Event-IDを使用した SSE 再接続により、クライアントは接続が切断されても、リングのリプレイウィンドウ内で、中断した場所から正確に再開できます。 - ページ化された永続トランスクリプト —
GET /session/:id/transcriptは、クライアントをアタッチしたりライブ SSE リプレイウィンドウを変更したりすることなく、完全なアクティブなディスク上のトランスクリプトをリプレイページとして返します。 - 初回応答者による権限管理 — エージェントがツールの実行権限を要求すると、接続されているすべてのクライアントにリクエストが表示され、最初に回答したクライアントの操作が採用されます。
- 1 つのデーモン、1 つ以上のワークスペース —
--workspaceを繰り返して、1 つのリスナーの下に隔離されたワークスペースランタイムを登録します。最初のワークスペースがプライマリであり、cwdを省略したリクエストのデフォルトとなります。 - 実験的なデーモン管理チャネル —
qwen serve --channel <name>で開始するか、チャネルなしで開始して後でqwen channel setで選択します。ワーカーはデーモンのライフサイクルに属する別のプロセスです。それらの選択は、デーモンを再起動せずにクエリ、置換、リロード、停止ができます。 - リモートランタイム制御 — セッションの承認モードの変更(
POST /session/:id/approval-mode)、ワークスペースごとのツールの切り替え(POST /workspace/tools/:name/enable)またはロードされたスキル(POST /workspace/skills/: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)。すべて厳格なゲートが適用されるため、まず--tokenを設定してください。 - セッションの要約(#4175 のフォローアップ)— アクティブなセッションの「どこまで進んだか」を示す 1 文の要約を取得します(
POST /session/:id/recap)。コアのgenerateSessionRecapをラップし、高速モデルに対するサイドクエリとして実行します。メインのチャット履歴も SSE ストリームも汚染しません。非厳格ゲート(/promptと同じ姿勢)。SDK ヘルパー:client.recapSession(sessionId)。- 既知の制限 — トークンコストの増幅: このルートは純粋なコストエンドポイントであり(呼び出しごとに LLM サイドクエリが実行され、状態のメリットはありません)、v1 にはルートごとのレート制限がありません。トークンなしのループバックデフォルトでは、バグのあるクライアントや悪意のあるローカルクライアントがスパムを送信してトークンを消費する可能性があります。デーモンを公開する前に、共有開発ホストで
--token(およびオプションで--require-auth)を設定してください。 - 同時要約の安全性: 同じセッションに対する 2 つの同時
/recap呼び出しは、2 つの独立したサイドクエリを実行します。generateSessionRecapはGeminiClient.getChat().getHistory()経由でチャット履歴のスナップショットを読み取り、それを別のBaseLlmClient.generateText呼び出し(runSideQuery経由)に渡します。セッションのGeminiChatに追加したり変更したりすることはありません。調整なしに複数のクライアントから安全に呼び出せます。
- 既知の制限 — トークンコストの増幅: このルートは純粋なコストエンドポイントであり(呼び出しごとに LLM サイドクエリが実行され、状態のメリットはありません)、v1 にはルートごとのレート制限がありません。トークンなしのループバックデフォルトでは、バグのあるクライアントや悪意のあるローカルクライアントがスパムを送信してトークンを消費する可能性があります。デーモンを公開する前に、共有開発ホストで
v0.16-alpha の既知の制限
qwen serve の最初の npm リリース(v0.16-alpha)は意図的に範囲を狭くしています。これは、自分のマシンでデーモンを実行する開発者向けのテキストのみのチャット / コーディングです。以下のリストは延期された機能を明示し、導入企業がそれを見越して計画できるようにするためのものです。ここにあるすべての機能は、v0.16.x パッチロードマップまたは近期内のフォローアップリリースに含まれています。
プロダクト表面 — テキストのみ:
- ✅ テキストプロンプトとテキストレスポンス(チャット、コーディング、ツール呼び出し、MCP 統合)
- ❌ プロンプトパスでの画像 / ファイル添付 —
MessageEmitterは現在テキストのみをレンダリングします。マルチモーダルエコーは、画像ニーズを持つ alpha ターゲットが確定した時点で実装されます(#4175 chiga0 #27 P0 アイテム) - ❌ ストリーミングアップロード — マルチモーダルと同じゲーティング
デプロイ表面 — ローカルのみ:
- ✅ ループバック(
127.0.0.1、デフォルト)— 認証不要、開発ワークステーションに適しています - ✅
systemd/launchd/nohup &/tmuxによるローカル起動 — ローカル起動テンプレートを参照 - ✅
QWEN_SERVER_TOKEN環境変数によるベアラートークンの持ち込み(設定については認証を参照) - ❌ コンテナ化されたデプロイ — TLS 終端を伴う Docker / Compose / Kubernetes / nginx リバースプロキシは v0.16-alpha には含まれていません。エンタープライズパイロットが確定次第、v0.16.x に延期されます(そうでなければ検証する人がいなくて腐ってしまうため)。
- ❌ 1 ホストでのマルチデーモン調整 — 1 つのデーモンが複数の明示的に登録されたワークスペースをホストできますが、デーモン同士は調整しません。クロスホストフェデレーション、インスタンスパストークンキーイング、および古いトークンのクリーンアップは v0.16.x に延期されます。
- ✅ 取り消し可能な Local Control ペアリングトークン —
--local-controlはデーモンが所有する別の LAN ペアリングトークンを生成します。一般的なデーモントークンストレージは引き続きトークン持ち込みです。
強化 — ローカルシングルユーザーの最小限の実用ライン:
- ✅ 起動時のセキュリティゲート(トークンなしの非ループバックバインドを拒否、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 と同じオリジンのデーモンルートで提供されます。このガイドの残りの部分では生の 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 フィールドはプライマリの互換性ワークスペースを表面化するため、クライアントは意図的に POST /session で cwd を省略できます。現在のクライアントは workspaces[] から信頼されたエントリを選択し、ランタイムを明示的にターゲットにする際にそのエントリの cwd を送信する必要があります。
limits.maxPendingPromptsPerSession フィールドは、アクティブなセッションごとのプロンプト受け入れ上限を通知します。null は上限が無効であることを意味します。limits.maxTotalSessions はオプションのデーモン全体の新規セッション上限を通知します。null は無制限を意味します。
デーモンからチャネルを実行する
# Start one configured channel under qwen serve
qwen serve --channel telegram
# Start several configured channels under daemon-owned workspace workers
qwen serve --channel telegram --channel feishu
# Start all configured channels
qwen serve --channel all
# Or start a token-protected daemon with no channel worker
QWEN_SERVER_TOKEN=secret qwen serve
# Enable or replace its runtime selection later
qwen channel set telegram --token secret
qwen channel set telegram feishu --token secret
qwen channel set all --token secret
# Inspect or stop daemon-managed channels
qwen channel status --daemon-url http://127.0.0.1:4170 --token secret
qwen channel stop --daemon-url http://127.0.0.1:4170 --token secretこのモードは実験的であり、デーモンによって管理されます。スタンドアロンの qwen channel start コマンドを置き換えるものではありません。--daemon-url なしでは、既存の qwen channel start、stop、status の動作はスタンドアロンのままです。qwen serve --channel では、デーモンはリッスン前にチャネルサービスのリースを予約し、初期ワーカーが準備できない場合は起動に失敗します。--channel なしでは、チャネルランタイムをロードせず、最初のランタイム PUT までチャネルサービスのリースを予約しません。準備完了後のワーカーがクラッシュした場合でもデーモンは実行を継続し、バウンドされたリスタートポリシーで再起動し、GET /daemon/status でその状態(channel_worker_exited 警告を含む)を報告します。
ランタイム制御は GET、PUT、DELETE /workspace/channel として公開されます。SDK ヘルパーは getChannelWorkerControl()、setChannelWorkerSelection()、stopChannelWorker() です。PUT/DELETE/リロードは厳格なミューテーションゲートを使用するため、デーモンにベアラートークンが設定されている必要があります。ランタイムの選択は意図的にエフェメラルです。PUT は設定やブートオプションを編集せず、再起動時は qwen serve --channel の選択に戻ります(そのフラグが省略されている場合は無効)。名前付き選択はトリミングされ、初出順に重複排除されます。順序は保持されます。最初のチャネルが共有モデルの選択に影響する可能性があるためです。
デーモンは各チャネルの設定(トークン、proxy、チャネルごとの model)をワーカーの起動時に読み取ります。選択を変更せずに設定を再読み取りするには、POST /workspace/channel/reload を呼び出します(SDK client.reloadChannelWorker()、または qwen channel reload)。リロードはワークスペースの所有権を再解決し、同じロールバック安全な reconcile パスを通じて選択されたワーカーを再起動します。channel_control ケーパビリティはランタイム制御が配線されているときに常に存在し、channel_reload はマネージャーが有効な間にのみ存在します。永続化されたスレッドはディスクから復元されます。
選択された各チャネルの cwd は登録されたワークスペースに解決される必要があり、チャネルは所有ワークスペースごとにグループ化されます。シングルワークスペースのデーモンは 1 つのワーカーを実行し(以前と変更なし)、マルチワークスペースのデーモン(--workspace を繰り返す)は選択されたチャネルを所有するワークスペースごとに 1 つのワーカーを実行し、それぞれがワークスペースの cwd、QWEN_DAEMON_WORKSPACE、および環境オーバーレイにバインドされます。プライマリ以外のワークスペースでチャネルをホストするには、そのワークスペース自身の .qwen/settings.json で定義するか(cwd は不要)、ワークスペースパスと等しい明示的な cwd を設定します。cwd なしでユーザースコープまたはシステムスコープのみで定義されたチャネルは、ワークスペース間で曖昧であり、ブートエラーを引き起こします。--channel all はプライマリのみです(プライマリワークスペースのチャネルをホストする)ため、名前付きチャネルと組み合わせることはできません。
選択を置換すると、何かを停止する前に設定、所有権、および信頼のプレフライトが行われます。順序付きの選択が変更されていないワークスペースワーカーは保持されます。変更されたワーカーが起動できない場合、デーモンは新しいワーカーを停止し、古い選択を復元します。デーモンが SIGKILL の後も古い子プロセスの終了を確認できない場合、PID リースを保持し、重複したワーカーの作成を拒否します。リクエストされたアダプターのうち少なくとも 1 つが接続した場合、ワーカーは準備完了とみなされます。PUT は partial: true を返し、/daemon/status は不足しているアダプターに対して channel_worker_partial_connect を報告します。
アダプターが connect() を拒否した場合、現在のワーカーのスナップショットに startupFailures エントリが含まれることがあります。チャネル、phase: "connect"、オプションのアダプターコード、および資格情報が削除されたメッセージが含まれます。qwen channel set、qwen channel reload、およびリモートの qwen channel status --daemon-url … はこれらの理由を出力します。動的な set または reload 中にすべてアダプターが失敗した場合、コマンドは 502 channel_worker_start_failed を受け取ります。レスポンスの理由は那次の試行とそのロールバック後の結果を記述します。失敗した試行は後の status リクエストでは保持されません。ワーカーの起動ごとに最大 64 の理由が保持され、アダプターコードは安定したカテゴリではなく診断として扱うべきです。初期の qwen serve --channel … 起動は、アダプターが接続しない場合でも終了します。
デーモンはクライアント UI とオペレーター向けに読み取り専用のランタイムスナップショットも公開します。GET /daemon/status、GET /workspace/mcp、GET /workspace/skills、GET /workspace/providers、GET /workspace/env、GET /workspace/preflight、GET /workspace/:id/session-info、GET /session/:id/status、GET /session/:id/context、GET /session/:id/supported-commands、GET /session/:id/tasks、GET /session/:id/lsp、および GET /session/:id/transcript です。
GET /workspace/:id/session-info(および複数形の GET /workspaces/:workspace/session-info ツイン)は、ワークスペースの集約セッション数を返します。永続化された active / archived / total と、ライブ状態が利用可能な場合の現在のインメモリの live カウントです。登録された信頼されていないセカンダリワークスペースは live を省略します。それらのカタログ読み取りはライブブリッジをクエリしないためです。ページ化された GET /workspace/:id/sessions リストは合計を含まないため、これが「セッションはいくつ存在するか?」専用の表面です。スケジュールされたタスクや定期タスクが大量のローカルストアを残す場合に有用です。
⚠️ ディスクスキャン — ポーリングしない。 このエンドポイントはワークスペースのチャットディレクトリ配下のローカルセッション JSONL ファイルを走査します。レスポンスには常に
expensive: trueとcost: "disk_scan"が含まれます。頻繁に呼び出さないでください(手動リフレッシュ、オペレーターツーリング、たまの UI ロード)。タイトなタイマーやサイドバーのレンダリングごとに呼び出さないでください。ページの閲覧にはGET /workspace/:id/sessionsを、ライブのインメモリセッションカウントにはGET /daemon/statusを優先してください。truncated: trueのレスポンスは、スキャンが安全制限に達したか、すべての候補ファイルを分類できなかったことを意味します。そのため、永続化されたカウントは下限です。
curl http://127.0.0.1:4170/workspace/$(python3 -c "import urllib.parse,os; print(urllib.parse.quote(os.getcwd(), safe=''))")/session-info
# → {"active":450,"archived":30,"total":480,"live":2,"expensive":true,"cost":"disk_scan"}GET /session/:id/status は、単一セッションのライブブリッジサマリーを返します。sessionId、workspaceCwd、createdAt、オプションの displayName、clientCount、および hasActivePrompt です。デーモンがその ID のライブセッションを保持している場合は 200 でサマリーを返し、それ以外の場合は 404(ボディ { "error": …, "sessionId": … })を返します。ページ化されたセッションリスト全体を取得してスキャンすることなく、既知の 1 つのセッションがまだ実行中かどうか(hasActivePrompt)または何人のクライアントが接続されているか(clientCount)をポーリングするために使用します。
curl http://127.0.0.1:4170/session/$SESSION_ID/status
# → {"sessionId":"…","workspaceCwd":"…","createdAt":"…","clientCount":1,"hasActivePrompt":false}これは生のライブセッションビューであるため、clientCount と hasActivePrompt は GET /workspace/:id/sessions の対応するエントリと一致しますが、2 つのルートはバイト単位で同一ではありません。リストエンドポイントは、永続化されたセッションストアデータで各アイテムを充実させます。その createdAt は永続化された最初のプロンプトの時間であり、updatedAt と、保存されたタイトルまたは最初のプロンプトから派生した displayName が追加されます。一方、/status はライブセッション自体の createdAt を報告し、updatedAt を省略し、displayName はライブセッションに設定されている場合にのみ返します。
GET /session/:id/lsp は、セッションごとの構造化された LSP ステータスを返します。生成されたエージェントセッションで LSP を有効にするには、--experimental-lsp を指定してデーモンを起動します。それ以外の場合、ルートはサーバーなしで enabled: false を返します。
GET /daemon/status は統合されたトラブルシューティングスナップショットです。デフォルトの detail=summary はインメモリ上のデーモン状態(セッション、権限、SSE/ACP トランスポートカウント、レート制限拒否、プロセスメモリ、解決された制限)のみを読み取り、ACP 子は起動しません。問題の調査中に、セッションごとの診断、ACP 接続の詳細、認証デバイスフローカウント、およびワークスペースステータスセクションを取得するには、GET /daemon/status?detail=full を使用します。
GET /workspace/mcp、GET /workspace/skills、および GET /workspace/providers はライブ ACP ランタイムを報告し、アイドル時には ACP 子を起動しません。アイドル状態のデーモンは空のスナップショットで initialized: false を返します。セッションが存続すると、initialized: true に切り替わり、実際の状態を表面化します。
CLI の /skills パネルをリモートでミラーするには、workspace_skill_toggle ケーパビリティを確認した後、POST /workspace/skills/:name/enable を { "enabled": true | false } で呼び出します。複数のスキルを変更するには、workspace_skill_batch_toggle を確認し、POST /workspace/skills/enable を { "skillNames": ["review", "deploy"], "enabled": false } で呼び出します。レスポンスは成功した results をターゲットごとの errors から分離し、有効なターゲットをまとめて永続化し、アクティブな ACP セッションを一度にリフレッシュします。これらのルートはワークスペースの skills.disabled と skills.enabled を必要に応じて更新し、不明、非表示、非アクティブな拡張、より高スコープでロック、および信頼されていないターゲットを拒否し、アクティブな ACP セッションを即座にリフレッシュします。skills.defaultDisabled のスキルを有効にすると、skills.enabled に正規のオプトインが書き込まれます。より高スコープから継承されたハードな skills.disabled エントリは引き続きオーバーライドできません。スキルのステータスセルは disabledReason(hard、default、または inactive_extension)とオプションの lockedScope を公開します。deferred レスポンスは、ACP 子が実行されていない間に設定が保存されたことを意味します。子の起動時に適用されます。skills.disabled は手動とモデルの両方の使用を無効にします。disable-model-invocation: true とは異なり、後者は直接の /skill-name 呼び出しを有効にしたままにします。V2 Extension バッチの場合は extension_batch_activation_v2 を確認してください。PUT /extensions/activation はグローバルデフォルトを変更し、PUT /workspaces/:workspace/extensions/activation は選択したワークスペースのオーバーライドを変更し、"inherit" を受け付けてクリアします。どちらも extensionNames で名前を受け付けます。enabled と disabled はインストール前に宣言可能ですが、不明な名前の inherit は no-op です。各リクエストは 1 つのポーリング対象の操作を返します。
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 検出、スキル、プロバイダー、ツールレジストリ、エグレス)にはライブの ACP 子が必要です。デーモンがアイドル状態の場合、それらを入力するためだけに ACP を生成するのではなく、status: 'not_started' プレースホルダーを出力します。失敗はクローズドな errorKind 列挙型(missing_binary、auth_env_error、init_timeout、restore_timeout、protocol_error、missing_file、parse_error、blocked_egress)にマッピングされるため、クライアント UI は構造化された修復策をレンダリングできます。
デーモンはワークスペースファイルヘルパーも公開します。
GET /fileはテキストファイルを読み取ります。フルスナップショットのレスポンスは生バイトのsha256:<hex>ハッシュを返します。256 KiB を超えるファイルからの有限行ウィンドウはそれを省略します。GET /file/bytesは境界付きの生バイトウィンドウを読み取り、base64 コンテンツを返します。POST /file/writeはテキストファイルを作成または置換します。POST /file/editは 1 回の正確なテキスト置換を適用します。
書き込み / 編集は厳格な変更ルートです。ループバックでも設定されたベアラートークンが必要であり、そうでない場合は 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 をポストすると、400 workspace_mismatch が返されます。
デフォルトの sessionScope: 'single' では、解決されたワークスペースランタイムに対して /session にポストする 2 番目のクライアントは "attached": true を取得し、そのランタイムのエージェントセッションを共有します。cwd を省略するとプライマリに解決され、別の登録済みワークスペースを選択するとそのランタイムの個別のデフォルトセッションが作成またはアタッチされます。
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?})であり、1 行に JSON 文字列化されています。ACP ペイロード(この例では sessionUpdate ブロック)は、そのエンベロープ内の data の下に配置されます。SSE レベルの id: / event: 行は EventSource クライアント向けの利便性ですが、同じ値が JSON エンベロープ内にも含まれているため、生の fetch コンシューマーもそれらを取得できます。
プロンプトを送信する前にこれを開きます。SSE リプレイバッファは最後の 8000 個のイベントを保持するため、遅れて購読したクライアントは Last-Event-ID 経由でキャッチアップできますが、単純な「1 つのプロンプトを監視する」ケースでは、最初に購読してライブストリーミングさせるのが最も簡単です。
ストリームは 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 は、フレームが到着するたびに出力します。
オプションの Todo 停止ガード
長時間実行するデーモンクライアントは、現在の作業チェーンがトップレベルの Todo リストを正常に書き込み、まだ保留中または進行中のアイテムがある状態で停止した場合に、バウンドされた継続をオプトインできます。これを settings.json に追加してデーモンを再起動します。
{
"experimental": {
"todoStopGuard": true
}
}ガードは、新しいユーザー入力なしで最大 2 回の連続したプライマリモデル呼び出しを追加します。ターン中のユーザーメッセージが最初に実行され、新しい 2 試行ステージを開始します。リトライ / 続行および関連するバックグラウンドの結果は現在のステージのバジェットを保持します。すべての呼び出しと最終的な使い切り状態は、_meta.source: "todo_stop_guard" を持つリプレイ可能な session_update イベントとして表示されます。メタデータには試行回数と未完了数が含まれますが、Todo テキストは含まれません。キューに入れられた完全なプロンプトも最初に実行されます。既存の権限 / キャンセルルールは変更されません。
武装されたチェーンが関連するバックグラウンドワークを待っている間、関係のない cron/loop の発火と古いタスクの通知は延期されます。定期実行されるワークはバウンドされ、チェーンが yield するまでタスクごとに統合されます。
このオプションはデフォルトで false であり、再起動が必要です。セーフモード、ベアモード、および Approval plan モードでは強制的にオフになります。インメモリのみです。ディスクから Todo 状態をロードするかデーモンを再起動しても武装されません。新しい通常のプロンプトは独自のトップレベルの todo_write を正常に実行する必要があります。リトライ / 続行とライブクライアントの再接続は現在のインメモリの作業チェーンを保持します。セッションの作業ディレクトリを正常に変更するとクリアされるため、古い Todo が新しいワークスペースで再開されることはありません。
認証
ループバック以外の場合、ベアラートークンを渡す必要があります。
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 はループバックバインドの場合のみ免除されるため、ポッド内の k8s/Compose の Liveness プローブ(デーモンが 127.0.0.1 をリッスンしている場所)は資格情報を必要としません。非ループバックバインド(--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 レスポンスは「ヘッダーなし」、「不正なスキーム」、「不正なトークン」で統一されているため、サイドチャネルで区別できません。
HTTPS / TLS(モバイル / クロスデバイスアクセス用)
デフォルトでは、デーモンはプレーンな HTTP を提供します。これは localhost では問題ありませんが、LAN IP(https://192.168.x.x:4170)にアクセスするスマートフォンやタブレットは、http:// 経由ではセキュアコンテキスト ではありません。そのため、ブラウザは getUserMedia(音声入力)、WebRTC、およびその他のセキュアコンテキスト専用 API をブロックします。--tls-cert と --tls-key を渡して、Web Shell を HTTPS 経由で提供し、それらの API を有効化します。
# 1. ローカル CA をインストールして信頼します(一度だけ)。モバイルデバイスも
# この CA を信頼する必要があります。mkcert はルート証明書が保存される場所を表示します。
mkcert -install
# 2. マシンの LAN IP 用の証明書を生成します。localhost / 127.0.0.1 も
# SAN に追加します。`--open` を指定すると、デーモンはブラウザの URL を
# 127.0.0.1 に書き換えるため、LAN IP のみにスコープされた証明書は
# ERR_CERT_COMMON_NAME_INVALID で拒否されます(mkcert はすべてのホスト名に基づいて出力ファイルに名前を付けます)。
mkcert 192.168.1.100 localhost 127.0.0.1
# 3. HTTPS 経由でデーモンを起動します。ループバック以外のバインドには引き続きトークンが必要であり、
# ブラウザの Origin は CORS で許可されている必要があります。
qwen serve \
--hostname 0.0.0.0 \
--token "$(openssl rand -hex 32)" \
--tls-cert "./192.168.1.100+2.pem" \
--tls-key "./192.168.1.100+2-key.pem" \
--allow-origin "https://192.168.1.100:4170"
# → qwen serve が https://0.0.0.0:4170 でリッスンしています注意事項:
- 両方のフラグ、またはどちらも指定しない — どちらか一方だけを指定すると起動に失敗します(キーのない証明書では HTTPS リスナーを開始できません)。
- TLS は認証と直交します — HTTPS はトランスポートを暗号化しますが、Bearer トークンが引き続きすべての API ルートを保護します。ループバック以外のバインドでは、TLS の有無にかかわらずトークンが必要です。
- スコープは TLS ターミネーションのみ — 自動生成や ACME / Let’s Encrypt はサポートしていません。これは LAN / 開発環境向けの便利な機能です。インターネットに公開するデプロイメントでは、リバースプロキシで TLS をターミネートしてください(後述の脅威モデルを参照)。
CLI フラグ
| フラグ | デフォルト | 目的 |
|---|---|---|
--port <n> | 4170 | TCP ポート。0 = OS によって割り当てられるエフェメラルポート。 |
--hostname <addr> | 127.0.0.1 | バインドするインターフェース。ループバック以外を指定する場合はトークンが必要です。 |
--local-control | false | 選択された 1 つのプライベート IPv4 インターフェース上で、デーモンが所有する取り消し可能なペアリングトークン、ターミナル QR コード、正確なブラウザオリジン、およびベストエフォートのスリープ抑制を使用して Web Shell を共有します。--token、--allow-origin、および --port 0 と組み合わせ可能です。--no-web およびデフォルト以外の --hostname と競合します。複数の LAN 候補がある場合は --local-control-address を使用し、音声入力などのセキュアコンテキストのブラウザ API には --tls-cert + --tls-key を追加します。 |
--local-control-address <ip> | — | ホストに複数の候補がある場合に共有する LAN IPv4 アドレス。--local-control が曖昧な選択を報告する場合にのみ必要です。 |
--token <str> | — | Bearer トークン。指定がない場合は QWEN_SERVER_TOKEN 環境変数にフォールバックします(先頭と末尾の空白は削除されるため、$(cat token.txt) などに便利です)。 |
--require-auth | false | Bearer トークンなしでの起動を拒否します(ループバックでも同様)。ローカルユーザーが誰でもリスナーにアクセスできる共有開発ホスト / CI ランナー / マルチテナントワークステーションにおいて、127.0.0.1 という開発者向けデフォルト設定を強化します。--token または QWEN_SERVER_TOKEN が設定されている場合にのみ起動し、/health も Bearer トークンで保護します。 |
--tls-cert <path> | — | PEM 証明書ファイルへのパス。HTTP ではなく HTTPS で提供します。--tls-key とペアで指定する必要があります(どちらか一方だけを指定すると起動に失敗します)。LAN IP 経由で、ブラウザがプレーンな http:// ではブロックするセキュアコンテキストのブラウザ API(音声入力(getUserMedia)、WebRTC など)を有効化します。TLS ターミネーションのみ。自動生成 / ACME はサポートしていません。後述の HTTPS / TLS を参照してください。 |
--tls-key <path> | — | PEM 秘密鍵ファイルへのパス。--tls-cert とペアで指定する必要があります。 |
--max-sessions <n> | 32 | 同時実行ライブセッションの上限。上限に達した場合、新しい子プロセスを生成する POST /session リクエストは 503(Retry-After: 5 付き)を返します。既存セッションへのアタッチはカウントされません。無効にするには 0 を設定します。シングルユーザー / 小規模チームでの使用を想定したサイズです。デプロイメントに RAM / FD の余裕がある場合は、この値を上げてください(1セッションあたり約 30〜50 MB)。 |
--max-total-sessions <n> | 派生 | オプションの非負整数のデーモン全体の新規セッション作成上限。登録されたすべてのワークスペースランタイムに適用されます。新しい子セッション、セッションの復元、およびブランチ / フォークで作成されたセッションに適用されます。既存のライブセッションへのアタッチはスロットを消費しません。無制限にするには 0 を設定します。複数の起動 / 復元ワークスペースがある場合に省略すると、デーモンはワークスペースごとの上限と起動ワークスペース数から固定の上限を導出します。後続の動的登録は再計算しません。 |
--max-pending-prompts-per-session <n> | 5 | 1セッションあたりの、POST /session/:id/prompt で受け付けられたがまだ確定していないプロンプトの上限(キューイングされたプロンプトとアクティブなプロンプトを含む)。上限を超えた場合、ブリッジは promptId を返す前に、503、Retry-After: 5、および code: "prompt_queue_full" を同期的に返却して拒否します。無効にするには 0 を設定します。branchSession は同じ FIFO で直列化されますが、このプロンプト上限にはカウントされません。 |
--workspace <path> | process.cwd() | このデーモンによって登録される絶対ワークスペースディレクトリ。フラグを繰り返して、1 つのプロセスで複数のワークスペースをホストします。最初がプライマリであり、リクエストが cwd を省略した場合のデフォルトとなります。相対値は拒否されます。正規化された cwd が登録されていないセッションリクエストは 400 workspace_mismatch を返します。 |
--memory-project-scope <mode> | workspace | プロジェクトメモリのパーティショニングモード。workspace(デフォルト)は正確な登録ワークスペースディレクトリによってメモリをキーイングし、各デーモンワークスペースが独自の隔離されたメモリを持ちます。git-root は同じ Git ルートに解決されるワークスペース間で共有されるレガシーな互換モードです。指定された場合は QWEN_CODE_MEMORY_PROJECT_SCOPE をオーバーライドします。空の環境変数値は未設定として扱われ、認識されない非空の値は一度限りの警告とともに無視され、レガシーな git-root の動作が保持されます。新しいデフォルトは既存の git-root プロジェクトメモリを移行しません。移行期間中にそれらのエントリを読み取るには、明示的な git-root スコープを使用してください。 |
--channel <name|all> | — | 実験的なデーモン管理チャネルワーカー。フラグを繰り返して複数の設定済みチャネルを選択するか、all を渡してすべての設定済みチャネルを起動します。all は名前付きチャネルと組み合わせることはできません。選択したチャネルの cwd 値は登録されたワークスペースに解決される必要があります。マルチワークスペースのデーモンは所有ワークスペースごとに 1 つのワーカーを実行します。ワーカーは qwen serve が所有します。serve 管理チャネルを停止するにはデーモンを停止します。 |
--max-connections <n> | 256 | リスナーレベルの TCP 接続上限(server.maxConnections)。セッション数に関係なく、生のソケット数を制限します。上限に達すると、低速 / ファントムの SSE クライアントは accept 時に拒否されます。デプロイメントで 1セッションあたりの SSE サブスクライバーが多くなることが予想される場合は、--max-sessions と合わせてこの値を上げてください。 |
--memory-budget-mb <n> | cgroup/ホストの 50% | デーモンプロセスツリー全体の MB 単位の合計メモリバジェット。未設定の場合、cgroup 制限またはホストメモリの 50% として導出されます。どちらの場合でも、有効値は解決された利用可能メモリでキャップされ、設定値と有効値の両方が報告されます。qwen --acp 子のサイジング方法は変更しません。現在の唯一のコンシューマーはアダプティブなライブジャーナルの成長です。有効なバジェットの 5% として導出されるデーモン全体の成長プール(1024 MB にキャップされる。insufficientMemory を報告するホストではプールは 0 であり、アダプティブ成長は無効化される)は、すべてのワークスペースブリッジで共有されます — --max-journal-bytes を参照してください。解決された数値は GET /daemon/status の limits.memory に、登録およびライブの子カウントと runtime.memory 下のアドバイザリな子ごとのシェアとともに表示されます。最小値を満たさないホストは上方クランプされず、insufficientMemory を報告します。導出される割合は 50% であるため、約 2 GB 未満のホストはすべてこれに該当します。そのようなホストでは明示的な --memory-budget-mb 1024 を渡して導出値をオーバーライドします(フラグは警告をクリアするために少なくとも 1024 MB の利用可能メモリを必要とします)。[1024, 1048576] の整数である必要があります。 |
--memory-pressure-mode <mode> | observe | デーモンが自身のメモリ読み取りを判定に変換するかどうか。observe(デフォルト)は GET /daemon/status の runtime.memory.pressure にプレッシャーレベルを報告し、レベルが normal を離れるたびに daemon_memory_pressure issue(warning なので全体の status は ok のまま)を発生させます。off はレベルを含むすべての数値を報告しますが、issue を発生させないため、全体の status は変わりません。キャリブレーション中、またはトップレベルのステータスでアラートを出す場合に使用します。レベルは 2 つの比率の悪い方です。利用可能メモリに対する RSS(cgroup OOM キラーが監視するもの)と、このプロセスのヒープ上限に対する V8 ヒープ使用量です。デーモンのルートプロセスのみをカバーします。子プロセスについては runtime.memory.children.rssBytes と比較してください。どちらのモードでも何も修復しません。off、observe のいずれか。 |
--child-heap-mode <mode> | observe | デーモンが --memory-budget-mb の子ごとのヒープパーティションをモデル化するかどうか。observe(デフォルト)は適用されるもの — limits.memory.childHeap.perChildCeilingMb と maxConcurrentChildren — を報告し、制限を超えたはずのスポーンをカウントします。何も適用されません。どの子もバジェットからサイジングされず、どのスポーンも拒否されません。off は何もモデル化せず、それをワイヤー上で伝えます。maxConcurrentChildren と perChildCeilingMb は両方とも null であり、オフにしたパーティションを運びません。拒否カウントが 0 であっても、パーティションを適用しても安全とは限りません。子は引き続きホスト由来のずっと大きな上限で実行されるため、モデル化された上限よりも多くの old space を必要とするワークロードはここでは完全に健全に見えます。パーティションの適用は、それに答えられる計測とともに出荷されます。 |
--event-ring-size <n> | 8000 | 1セッションあたりの SSE リプレイリングの深さ(#3803 §02 ターゲット)。Last-Event-ID: N を指定した GET /session/:id/events で利用可能なバックログを設定します。値を大きくすると、1セッションあたり数百 KB の追加 RAM を消費する代わりに、再接続の余裕が増えます。SDK クライアントは、?maxQueued=N(範囲 [16, 2048]、デフォルト 256)を介して、特定のサブスクリプションに対してサブスクライバーごとのより大きなバックログ上限を追加で要求できます。デーモンはまた、キューが 75% 埋まった時点で非ターミナルの slow_client_warning SSE フレームを出力し、クライアントが排除される前にドレイン / 再接続できるようにします。プリフライト: caps.features.slow_client_warning。 |
--compacted-replay-max-bytes <n> | 4194304 | POST /session/:id/load が返すバウンドされたスナップショット内の保持されたリプレイイベントの、ライブセッションごとのバイト上限。この上限は compactedReplay に適用されます。現在の進行中の liveJournal は --max-journal-events と --max-journal-bytes によって別に制限されます(アダプティブ成長が引き上げることができるベースラインキャップ — --max-journal-bytes を参照)。値は正の安全な整数である必要があります。無効な値は起動時に失敗し、ハード ceiling は 256 MiB です。古い保持リプレイが削除されると、スナップショットは history_truncated で始まります。これはディスク上のトランスクリプトを制限しません。 |
--max-journal-events <n> | 10000 | 現在の未完了ターンの進行中の liveJournal に保持されるリプレイエントリのセッションごとのベースライン上限。連続する互換性のあるテキストまたは思考のチャンクは 1 エントリを共有し、1 エントリあたり最大 256 のソースイベントを持ちます。他のイベント境界は保持されます。上限を超えた場合、デーモンはまずアダプティブ成長を試みます(--max-journal-bytes を参照)。ヘッドルームが付与されないか、付与がオーバーシュートをカバーするのに十分でない場合、最も古いエントリが削除され、history_truncated マーカーが先頭に追加されます。マーカーの truncatedEvents と retainedEvents カウントはソースイベントを記述します。正の安全な整数である必要があります。このフラグ(または --max-journal-bytes)をピン留めすると、アダプティブ成長が無効になります。 |
--max-journal-bytes <n> | 8388608 | 進行中の liveJournal のセッションごとのベースラインバイト上限。互換性のあるチャンクがリプレイエントリを共有する場合でも、シリアライズされたソースイベントから計算されます。ターンが上限を超えて成長すると、アダプティブ成長はセッションの上限を 2 倍に向けて引き上げます(セッションごとのハードキャップ 256 MiB まで、残りのプールヘッドルームによって制限される)。デーモンのすべてのライブセッションに付与される成長は、デーモンの有効なメモリバジェットの 5% でサイジングされた 1 つの共有成長プールに収まります — 渡された場合は --memory-budget-mb の値(解決された利用可能メモリでキャップされる)、それ以外の場合は自動検出されたメモリの 50%(--memory-budget-mb を参照)— 1024 MB にキャップされる。insufficientMemory を報告するホストではプールは 0 であり、アダプティブ成長は無効化される。成長はオンデマンドで発生し、プールが許可する範囲内でのみ行われる。成長が拒否された場合、プールが枯渇した場合、または付与がオーバーシュートをカバーしない場合、最も古いエントリがまとめて削除される(少なくとも 1 つのエントリは常に保持されるため、保持される末尾は上限よりずっと小さくなる場合があります)。このフラグ(または --max-journal-events)をピン留めすると、アダプティブ成長が無効になります。正の安全な整数である必要があります。デフォルトは 8 MiB です。 |
--mcp-client-budget <n> | — | ライブ MCP クライアントの正の整数上限。mcp_workspace_pool が通知されている場合、上限とトランスポートはワークスペースランタイムごとに共有されます。タグがない場合、レガシーなセッションごとのマネージャーが強制します。--mcp-budget-mode と組み合わせて使用します。未設定の場合、アカウンティングによる強制は行われません(ただし、GET /workspace/mcp は引き続き clientCount を報告します)。起動の同時実行を制限する claude-code の MCP_SERVER_CONNECTION_BATCH_SIZE とは異なり、こちらはライブクライアントの総数を制限します。プリフライト: caps.features.mcp_guardrails および caps.features.mcp_workspace_pool。 |
--mcp-budget-mode <m> | warn / off | --mcp-client-budget の強制方法。warn(予算設定時のデフォルト): 拒否はせず、スナップショットの budgets[0].status が予算の 75% 以上で warning に切り替わります。enforce: 上限を超える接続は拒否され、サーバーごとのセルに disabledReason: 'budget' が表示されます(mcpServers の宣言順序によって決定的に決定)。off(予算未設定時のデフォルト): 純粋な観測性。予算なしで enforce を指定すると起動時に拒否されます。 |
--external-tool-guard-mode <m> | off | 管理された ACP の外部実行前ポリシー。off はプロバイダー呼び出しを行わず、ケーパビリティも通知しません。required は互換性のあるプロバイダーが v1 ハンドシェイクを完了しない限り起動に失敗し、サポートされたトップレベルのツール呼び出しごとに、単一の prepare リクエストが許可されない限りクローズドに失敗します。 |
--external-tool-guard-endpoint <url> | — | required モードで使用されるオリジン専用のループバック HTTP(S) プロバイダー URL。例: http://127.0.0.1:8787。パス、URL 資格情報、リダイレクト、非ループバックホスト、およびプロキシルーティングは受け付けられません。 |
--external-tool-guard-timeout-ms <n> | 3000 | 整数 100..30000。起動ハンドシェイクと各 prepare リクエストに独立して適用されます。 |
--http-bridge | true | ステージ 1 モード: 本番環境では互換性のためにプライマリの qwen --acp 子の事前起動を試み、失敗時は初回使用時にリトライします。信頼されたセカンダリはそれぞれオンデマンドで 1 つの子を起動できます。ランタイムをターゲットにするセッションは ACP の newSession() を介してその子プロセスに多重化されます。信頼されていないセカンダリは ACP を起動できません。ステージ 2 のネイティブなインプロセス処理は後日利用可能になる予定です。 |
--initialize-timeout-ms <n> | 10000 | ACP 子リクエストタイムアウト。initialize ハンドシェイクを含む(ミリ秒)。2147483647 までの正の整数である必要があります。JS タイマー上限(2^31-1)を超える値は、Node がそれらを 1 ms にサイレントに圧縮するため、起動時に拒否されます。子プロセスの起動に追加の余裕が必要なコールドコンテナデプロイメントではこの値を上げることができます。同じ値が newSession、ワークスペースステータスポール、およびその他の ACP ext メソッドのデッドラインを管理します。 |
--session-restore-timeout-ms <n> | 60000 | ミリ秒単位の ACP セッションのロード / 再開デッドライン。2147483647 までの正の整数である必要があります。0 は無効です。省略した場合、デフォルトは 60 秒です。明示的に指定された --initialize-timeout-ms がそれより大きい場合はその値に引き上げられます。短い initialize タイムアウトが復元バジェットを下げることはありません。SDK と WebUI は 10 秒および 15 秒のクライアント余裕を追加します。タイムアウトはリトライ可能な 504 session_restore_timeout を返します。デーモン自身が終了したことを意味するものではありません。 |
--allow-origin <pat> | — | T2.4 (#4514 )。ブラウザ Web UI クライアントのクロスオリジン許可リスト。繰り返し指定可能です。各値は *(任意のオリジン。Bearer トークンが設定されていない場合は起動を拒否。ループバックでの --require-auth を推奨。これにより /health も Bearer トークンで保護されます。デフォルトではループバックで事前認証のため。Web Shell の静的アセットはすべてのモードで事前認証のままなので、それらを削除するには --no-web を渡してください)または正規の URL オリジン(<scheme>://<host>[:<port>]。末尾のスラッシュ / パス / userinfo / クエリは不可)です。サブドメインワイルドカード(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/*、および GET /session/<id> のドキュメントナビゲーション)で提供します。これらのエントリポイントは Bearer 認証ゲートの前に登録されます。ブラウザは <script> サブリソースやアドレスバーのナビゲーションにトークンを付加できないため、シェルには秘密情報が含まれません。すべての API ルートは引き続きトークンで保護され、他のすべてのパスの SPA ディープリンクフォールバックも Bearer ゲートの背後にあります。ループバック以外のバインドでは、UI が認証なしでアクセス可能である旨の 1 行の stderr 警告が表示されます。API のみのデーモンとして使用する場合は --no-web を使用します。ビルド時に Web Shell アセットが省略されている場合は効果がありません(デーモンはパンくずログを出力し、API のみで実行されます)。 |
--open | false | リスナーが起動した後、デーモンの URL でデフォルトブラウザの Web Shell を開きます(トークンが設定されている場合は #token= が URL フラグメントとして追加されます。フラグメントはサーバーに送信されないため、トークンがアクセスログや Referer ヘッダーに残るのを防ぎます)。--no-web を指定した場合、またはブラウザが利用できないヘッドレス / CI / SSH 環境では無効(no-op)です。 |
メモリプロジェクトスコープの注意事項。
- デーモン vs. スタンドアロン CLI。 フラグまたはデーモンの起動環境が、 そのデーモンが所有するすべてのランタイムに対して 1 つの凍結されたスコープを 設定します。ワークスペースの
.envまたはsettings.envは、登録された ワークスペースの 1 つに対してこれをオーバーライドできません。スタンドアロンのqwenTUI は引き続き git-root スコープがデフォルトです。両方のエントリ ポイントを一貫して保つには、それらを起動するシェルまたはサービス環境でQWEN_CODE_MEMORY_PROJECT_SCOPEをエクスポートしてください。- ディレクトリ名の衝突。 保存キーは
sanitizeCwdによって導出され、 英数字以外のすべての文字を-に置換します。句読点のみが異なる 兄弟ディレクトリ(例:feature_1とfeature-1)は、workspaceスコープでも同じメモリディレクトリにマッピングされます。ワークスペースの 隔離に依存する場合は、このような命名を避けてください。- フラグと環境変数で正規化が異なります。 環境変数は トリミングされ小文字化されます(
" Workspace "も動作します)。CLI フラグは yargs のchoicesによって大文字小文字を区別してマッチングされます(--memory-project-scope Workspaceは拒否されます)。両者間でコピーする場合は小文字の値を使用してください。
組み込みデーモン Git 再配置ガード
管理されたデーモンの ACP セッションはすべて、モデルのシェルコマンドに対して組み込みの実行前ガードを適用します。これは --external-tool-guard-mode とは独立しており、ケイパビリティの通知も行われません。デーモンはバインドされたワークスペースとセッションの現在の有効な作業ディレクトリを所有します。どちらも信頼されたセッション状態から提供され、ACP 子プロセスから受け入れられることはありません。
このガードはシェルコマンドラインを実行するツール(run_shell_command と monitor)を検査し、リポジトリの位置がセッションの有効な作業ディレクトリの外部に解決される場合、実行前に変更を伴う Git コマンドを拒否します。再配置は、git -C <path>、git --git-dir[=]<path>、git --work-tree[=]<path> のリテラル形式、先頭の GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE 代入(export/declare/readonly を介して行われた場合も含む。これらはチェーン内の後続のコマンドの環境に保持されます)、ディレクトリシフトのラッパーフラグ(env -C、sudo -D)、および同じコマンドチェーン内の cd、pushd、または popd 組み込みコマンドについて認識されます。一般的なラッパープレフィックス(sh -c、bash -c、eval、sudo、nohup、timeout、exec、command、builtin、env、パス修飾の git バイナリ、および { …; } / ! … シェル構文)はアンラップされ、内部の Git 呼び出しに同じポリシーが適用されます。また、$(…) またはバックティックの置換ボディは独自のコマンドとして分析されます。
独自のワークツリーにピン留めされたサブエージェントは、セッションのディレクトリではなくそのワークツリーに閉じ込められます。デーモンが実行ディレクトリを配置できないシェル呼び出しは拒否されます。
相対ターゲットは、正規パス解決後にコマンドの有効な開始ディレクトリから解決されます(存在する場合は arguments.directory、存在しない場合はセッションの現在の有効な作業ディレクトリ)。.git gitfile リダイレクト、シンボリックリンク、およびワークツリーごとの管理ディレクトリを含みます。実行前に完全に解決できない再配置ターゲット(動的ターゲット($VAR、バックティック、~、グロブ)、まだ存在しないパス、または読み取り不能な間接参照)は、変更を伴うまたは分類不能なサブコマンドに対して拒否されます。解決できない再配置ターゲットは、サブコマンドに関係なく拒否されます(読み取り専用のものも含む)。再配置されたコマンドのサブコマンドが検証済みの読み取り専用セット(rev-parse、cat-file)の 1 つである場合、ターゲットが解決されると許可されます。ただし、コマンド実行の -c 設定、または --output、--textconv、--filters フラグが含まれる場合は除きます(これらはファイルを書き込むか、ターゲットリポジトリの設定されたドライバーを実行するため)。認識される再配置がないコマンドは既存の動作を保持します。
拒否は最終的で、解決済み、動的、または解決不能なリポジトリ位置の場合は Daemon shell guard denied a mutating Git command… としてモデルに報告され、コマンドを解析できなかった、ペイロードを解決できなかった、または認識されないプログラムが再配置された Git コマンドを実行する可能性がある場合は Daemon shell guard denied a shell command… として報告されます。
このガードは上記のリテラル形式で書かれた Git 再配置に対して信頼できます(この制御が存在する誤ったターゲットのコマンドに対して)。これを打ち負かすために書かれたシェルテキストに対してはベストエフォートであり、バウンダリではありません。静的な読み手から再配置を隠す構成は通過する可能性があり、新しいものが引き続き見つかるでしょう。これに基づいてデーモンにより広い信頼を付与しないでください。スクリプトファイルの解釈、コマンド間の環境変数値の追跡、または heredoc ボディの分析は行いません(heredoc 内の Git 形状のテキストは、シェルが決して実行しない場合でも拒否される可能性があります)。/fork およびエージェントバックのワークスペースメモリ remember/dream は組み込みガードの下で引き続き利用可能です。これらは以下の外部プロバイダーモードがアクティブな間のみ制限されます。オプションの外部ツールガードは追加のポリシーであり、組み込みポリシーが許可した後にのみ同じリクエストを受け取ります。
Required external Tool Guard
このオプトインは、最終的なツール実行バウンダリで外部の許可 / 拒否の判断が必要な管理された ACP デプロイメントのためのものです。--external-tool-guard-mode=required が存在しない限り、完全に無効です。
export QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN='replace-with-local-secret'
qwen serve \
--external-tool-guard-mode=required \
--external-tool-guard-endpoint=http://127.0.0.1:8787 \
--external-tool-guard-timeout-ms=3000プロバイダーは POST /v1/handshake と POST /v1/prepare を公開し、Authorization: Bearer <token> を要求し、JSON を返し、提供された nonce またはリクエスト ID をエコーし、プロトコルバージョン 1 を使用する必要があります。トークンは非空白で、最大 8192 UTF-16 コードユニットで、制御文字を含んではなりません。リクエストは 1 MiB に、レスポンスは 64 KiB に、オプションの拒否理由は制御文字なしの 500 UTF-16 コードユニットに制限されます。成功した prepare レスポンスは次のとおりです。
{ "protocolVersion": 1, "requestId": "<echo>", "allowed": true }拒否は allowed:false を使用し、短い reason を追加できます。既存の権限および PreToolUse ゲートを通過して最終実行バウンダリに到達した、サポートされたトップレベルのツール呼び出しごとに、Qwen Code は 1 つの prepare リクエストを送信し、それをリトライすることはありません。以前の権限 / フックの拒否は prepare リクエストを送信しません。タイムアウト、キャンセル、トランスポート障害、不正な形式または不一致のレスポンス、および明示的な拒否は、実行ツールが実行されるのを防ぎます。各生成された ACP チャネルは、必要なコールバックをインストールしたことを確認する必要があります。欠落または非互換の確認は、Session 作成前にチャネルを拒否します。
プロバイダーリクエストは sessionId、promptId、toolCallId、正規の toolName、および最終的な arguments を運びます。toolCallId は相関ラベルであり、認証 ID またはスタンドアロンの冪等性キーではありません。
最終的な引数には機密性の高いアプリケーションデータが含まれる可能性があります。プロバイダーのログと監査ストレージでそれに応じて扱ってください。
PreToolUse フックは、この最終実行者の判断の前に実行されます。Required Guard モードはフックの動作を認可またはサンドボックス化しません。考えられるすべての副作用の周りにバウンダリが必要なデプロイメントは、フックを無効にするか、その実装を別途管理する必要があります。
スラッシュコマンドのアクションも、モデル / ツールのスケジューリング前に実行され、Guard の呼び出しではありません。一部のビルトインはファイルや設定を直接変更できます。すべての副作用のバウンダリが必要な管理デプロイメントは、スラッシュコマンド入力を拒否するか、slashCommands.disabled または --disabled-slash-commands を通じて未承認のコマンドをすべて無効にする必要があります。
v1 の管理スコープは、アクティブなフォアグラウンドの管理プロンプトによって呼び出されたトップレベルツールです。ネストされたまたは委譲する agent、workflow、create_sub_session、send_message、直接の /fork、およびエージェントバックのワークスペースメモリ remember/dream 制御は、required モードがアクティブな間は拒否されます。トップレベルのバックグラウンドシェルまたはモニター開始は引き続き 1 つのガードされた呼び出しであり、その最終引数はプロバイダーに到達しますが、この機能はプロセスを継続的に認可したり、プロセス完了の監査プロトコルを追加したりしません。フォアグラウンドの完了を必要とするポリシーは、それらの形状を拒否する必要があります。ガードされた MCP 呼び出しは、トランスポートエラー後の自動再接続 / リプレイも無効にします。成功した起動ハンドシェイクの後、/capabilities は external_tool_guard を通知します。その欠如は、クライアントが強制を想定してはならないことを意味します。
この機能は、明示的なデーモンの REST/ACP 管理呼び出しを認可するものではありません。それらは引き続きデーモンの既存の認証とルート契約を使用します。また、許可されたツールやシェルコマンドを決定論的にしたり、その内部をサンドボックス化したりするものでもありません。管理デプロイメントは、プロバイダーの判断を通常のツールポリシーと分離バウンダリと組み合わせる必要があります。
負荷調整ノブのサイジング。
--max-sessionsはワークスペースごとの新規セッション上限です。--max-total-sessionsは設定された場合、デーモン全体の新規セッション上限です。 他の 3 つのレイヤーも負荷を制限します。高並行性のデプロイメント向けにサイジングする場合は、これらをまとめてチューニングしてください。
- リスナーレベル:
--max-connections/server.maxConnections=256は生の TCP 接続を制限します(低速クライアントのバックプレッシャー)。- セッションごとのサブスクライバー: EventBus はデフォルトで SSE サブスクライバーをセッションあたり 64 に制限します。65 番目のクライアントはターミナルの
stream_errorを受け取り、クローズされます。- セッションごとのプロンプトアドミッション:
--max-pending-prompts-per-session=5は、1 つのセッションで受け入れられるキューイング済みおよびアクティブなプロンプトを制限します。オーバーフローするとRetry-After: 5付きの503が返されます。- デーモン全体の新規セッション:
--max-total-sessions=Nはデーモン全体の新規セッション作成を制限します。オーバーフローするとscope: "total"を持つ同じsession_limit_exceededの形状が返されます。- サブスクライバーごとのバックログ: SSE クライアントあたり 256 フレームのキュー。容量を超えたクライアントはターミナルの
client_evictedフレームを受け取り、クローズされます(1 つの低速なコンシューマーがデーモンを占有しないようにします)。これらの上限は相互に関連します。各ランタイムは
--max-sessionsによって制限され、--max-total-sessionsはそれらの集約を制限します。有効なセッションの上限は、有限のデーモン全体の上限と集約ランタイムごとの上限の低い方です(ワークスペースごとの上限が無制限の場合は、その集約を無制限として扱います)。どちらも有限でない場合、有限のセッション上限は存在しません。有限の上限 × 64 サブスクライバー × 256 フレームは EventBus レイヤーでのワーストケースのインフライトメモリであり、それに--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'が表示されます。スロットの予約はサーバー名ごとに行われ、再接続や検出タイムアウト後も維持されます。拒否されたサーバーが正常なサーバーからスロットを奪うことはありません。現在のスコープはケーパビリティ駆動です。
mcp_workspace_poolが存在する場合、1 つのワークスペースランタイム内のすべてのセッションがその MCP トランスポートプールとバジェットコントローラーを共有します。GET /workspace/mcpはscope: 'workspace'を出力します。2 つ目のワークスペースは独立したプールとバジェットを持ちます。タグがない場合(QWEN_SERVE_NO_MCP_POOL=1を含む)、デーモンはレガシーなセッションごとのMcpClientManagerを使用し、scope: 'session'を出力します。そのフォールバックでは、N 個のセッションがそれぞれ設定された上限を消費する可能性があります。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(起動時の同時実行を制限するもの)とは同じではありません。これらは直交するものです。クライアントはmcp_workspace_poolで分岐する必要があり、プロトコルバージョンだけではスコープを想定してはなりません。プッシュイベント(issue #4175 PR 14b)。
GET /session/:id/eventsをサブスクライブしている SDK クライアントは、予算のしきい値を超えたときに型付きフレームを受け取ります。mcp_budget_warning(合成フレームで、75% を超えるたびに 1 回発生し、37.5% でヒステリシス再武装され、mcp_guardrail_events経由で通知されます)とmcp_child_refused_batch(enforceモードでの検出パスごとに 1 回に統合されます。readResourceの遅延生成拒否による長さ 1 のフレーム)です。GET /workspace/mcpのスナップショットは、再接続後の状態の信頼できる情報源です。イベントは変化のエッジです。ポーリングなしでリアルタイムにダッシュボード化する際に便利です。
デフォルトのデプロイメント脅威モデル
- 127.0.0.1 のみ — ループバックバインド、認証不要。
--hostname 0.0.0.0はトークンを要求 — トークンなしでは起動が拒否されます。LOOPBACK_BINDSには IPv6 が含まれる —::1および[::1]は、トークン不要ルールにおいてループバックとしてカウントされます。- 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 を返します。特定のブラウザの Origin を許可するには--allow-origin <pattern>(繰り返し指定可能、T2.4 #4514)を渡します。各値はリテラルの*(任意の Origin。ベアラートークンが設定されていない場合は起動が拒否されます。/healthはデフォルトでループバックでは事前認証のままになるため、完全なハードニングにはループバックでの--require-authを推奨します。Web Shell の静的アセット(/、/assets/*、/session/:idのドキュメントナビゲーション)はすべてのモードで Bearer の前にマウントされ、--require-authを使用しても事前認証のままです。残りのブラウザ表面が問題になる場合は--no-webを使用してください)または正規化された URL オリジン(<scheme>://<host>[:<port>]、末尾のスラッシュ / パス / userinfo なし)のいずれかです。一致した Origin は適切な CORS レスポンスヘッダー(Access-Control-Allow-Origin: <echoed>、Vary: Origin、および標準のメソッド / ヘッダー / max-age、公開されるRetry-After)を受け取ります。一致しない Origin は、デフォルトのウォールと同じエンベロープで 403 を受け取ります。caps.features.allow_originは条件付きで通知されるため、SDK / webui クライアントはリクエストを発行する前に、デーモンがクロスオリジンヒットを許可するかどうかをプリフライトで確認できます。例:qwen serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173。ループバックのセルフオリジンヒット(例: Web Shell UI)は影響を受けません。--allow-originに関係なく、別の Origin ストリップシムがこれらを処理します。--allow-originが設定されていないブラウザの webui は、以前と同じ Stage 1 のオプションにフォールバックします。Originヘッダーが送信されないようにネイティブシェル(Electron/Tauri)としてパッケージ化するか、同じオリジンのリバースプロキシでデーモンの前に配置します。 - Chrome 拡張ブラウザ自動化はフレーミングとは別です。
qwen serve --allow-origin chrome-extension://<id>により、拡張機能が Web Shell をフレームしてデーモンに接続できます。コンソール / ネットワーク / スクリーンショット / クリックツールには外部 CDP MCP アダプターコマンドが必要です。QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter qwen serve --allow-origin chrome-extension://<id>。メイン CLI パッケージにはブラウザ自動化アダプターがバンドルされていません。クライアントはそれらのツールを利用可能として表示する前にcaps.features.includes('browser_automation_mcp')を確認できます。 - 生成された
qwen --acp子は、所有ランタイムの実効環境を受け取ります。 デーモンはプロセス環境のベースをフリーズし、そのワークスペースの設定 / 環境ファイルオーバーレイをランタイムローカルのスナップショットに適用し、オーバーレイをprocess.envに書き戻すことはありません。別のランタイムの同名のキーがクロスオーバーすることはありません。QWEN_SERVER_TOKENは生成前にスクラブされます。エージェントはデーモンのベアラを必要としないためです。ローダーに影響する変数(NODE_OPTIONS、npm_config_node_optionsと npm の設定ファイルリダイレクト、NODE_PATH、OPENSSL_CONF、NODE_REPL_EXTERNAL_MODULE、npm_config_node_gyp、npm_config_init_module、LD_PRELOAD、LD_AUDIT、DYLD_INSERT_LIBRARIES、BASH_ENV、ZDOTDIR、エクスポートされた bash 関数定義BASH_FUNC_*)も同様にセッションサブプロセスに渡されることはありません。デーモンはそれらを自身のprocess.envと、セッションをホストする子が生成するフリーズされたベース環境からスクラブします(ベース環境は.tsエントリがまだ tsx ローダーを必要とするDEV=trueハーネス下でのみそれらを保持します)。.env/settings.jsonのenvソースもそれらを拒否します(設定を参照)。これはデーモンがホストするすべてのセッションに適用されます。OPENAI_API_KEY、ANTHROPIC_API_KEY、QWEN_*、DASHSCOPE_API_KEYなどの基本資格情報は、ランタイムオーバーレイが変更しない限りそのまま渡されます。これは意図的なものであり、サンドボックスではありません。 エージェントは同じ UID でシェルツールアクセス権を持って実行されるため、~/.bashrc/~/.aws/credentials/~/.npmrcの中の何にでもプロンプトインジェクションによってアクセス可能です。ランタイム間の環境分離はオペレーティングシステムのセキュリティバウンダリではありません。エージェントに信頼させたくない資格情報を持つ ID でqwen serveを実行しないでください。 - エージェントのテキスト読み取りは子プロセスローカルであり、ワークスペースファイルシステムのバウンダリではなく、通常の CLI 権限ルールに従います。 直接の
read_fileは、登録されたワークスペース外のホストテキストパスに到達できます。外部パスはデフォルトで確認が必要ですが、許可ルールや承認モードによって自動的に承認される場合があります。承認された読み取りは、ワークスペースファイルシステムの返却出力、フルスナップショット、および大型テキストスキャンのキャップではなく、設定可能な CLI 出力制限を使用します。これは共有テキスト読み取りコンシューマーすべてに適用されるため、write、edit、notebook、sed、および artifact 操作が実行するプリリードは、ワークスペースファイルシステムの読み取り監査、シンボリックリンク拒否、および読み取り側の TOCTOU 保護とともにそれらのキャップを失います — 正確なリストについてはリードのデザインを参照してください。確認ペイロードはファイルを読み取ることで構築されるため、ワークスペース外の差分は誰かが承認する前にすべての接続された SSE サブスクライバーにファンアウトされます — インタラクティブ CLI では、その内容はターミナルにいる人にのみ表示されます。認証されたデーモンクライアントも同じセキュリティプリンシパルとして扱ってください。HTTP ファイルシステムルートはワークスペーススコープのままであり、エージェントの検出ツールの動作は変更されません。 - ビルトインのテキストツールによる承認済みの最終書き込みには、狭い同一ホストのルートがあります。
write_file、edit、notebook_edit、およびシェルツールのシミュレートされた sed エディタは、既存の権限ポリシーが実行を許可した後にのみ内部の出所を添付します。したがって、最終的な ACP テキスト書き込みは、2 回目の確認なしに所有ワークスペース外の絶対パスをターゲットにできます。許可ルール、AUTO/AUTO_EDIT、および YOLO は CLI と同じように動作します。一方、拒否、Plan、Hook/Guard の拒否、および実行前のキャンセルは最終書き込みを送信しません。キャンセルがツールがすでにキャンセル不可能なファイルシステム操作に入った後に行われた場合、そのツールの既存の動作が保持されます。ワークスペースのターゲットは引き続き WFS を使用します。外部のターゲットは、同じ信頼スナップショット、5 MiB のエンコードされた制限、leaf シンボリックリンクの拒否、正規パスロック、アトミックなリネーム、モードの保持、0600の新規ファイルモード(デフォルト — 設定可能 — エージェントのテキスト書き込みの新規ファイルモードを参照)、世代ガード、およびファイルシステム監査を備えたデーモンホストライターを使用します。HTTP 書き込み、一般またはマークのない ACP 書き込み、注入されたブリッジ / ワークスペースレジストリ / ファクトリの統合、および任意のシェルリダイレクトはこの例外を受け取りません。外部書き込みのデザインを参照してください。 - サブスクライバーごとの制限付き SSE キュー — キューをオーバーフローさせた低速クライアントは
client_evictedターミナルフレームを受け取り、クローズされます。1 つの停滞したコンシューマーがデーモンを占有することはありません。 - セッションごとのプロンプトアドミッションキャップ — デフォルトでセッションあたり 5 つの受け入れ済みだが未解決のプロンプトに制限されます。バグのあるクライアントは、1 つのセッションに対して無制限のプロミスや一時的な SSE 待機をキューに入れることはできません。
- グレースフルシャットダウン — SIGINT/SIGTERM は、リスナーをクローズする前にエージェントの子プロセスをドレインします(子プロセスあたり 10 秒のデッドライン)。
⚠️ Stage 1 の既知のギャップ — 権限はデーモン全体で有効であり、セッションごとではありません (BUy4H)。
pendingPermissionsはデーモンスコープに存在します。ベアラートークンを持つ任意のクライアントは、表示可能な任意のセッションの任意のrequestIdに対して投票できます(また、SSE のpermission_requestイベントはペイロードに requestId を含みます)。これは、認証されたすべてのクライアントが同じ人間、または信頼できる協力者であるシングルユーザー / 小規模チームの信頼モデルでは許容されます。Stage 1.5 では、POST /session/:id/permission/:requestId+ セッションスコープの保留中マップ + クライアントごとの ID に移行します(ダウンストリームレビューからの must-have #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 のキープアライブプローブがタイムアウトするまで(Linux のデフォルトでは通常約 2 時間)、カーネルレベルのソケットを「生存」させます。このような NAT の背後にある
--hostname 0.0.0.0デプロイメントでは、ファントム SSE 接続が蓄積し、最終的に 256 のserver.maxConnections上限に達する可能性があります。明示的なアプリケーションレベルのアイドルデッドラインを設定する
--writer-idle-timeout-ms <n>(issue #4514 T2.9)を設定して、このギャップを埋めます。nms 間書き込みが正常にフラッシュされない場合、デーモンはreason: 'writer_idle_timeout'を含むターミナルのclient_evictedフレームを発行し、ストリームをクローズします。このフラグは、レガシーな契約を維持するためにデフォルトでオフになっています。RST を飲み込むネットワーク上のオペレーターは、正当なアイドル接続が追い出されないように、15 秒のハートビート間隔よりも十分に大きい値(例:60000〜300000)を選択し、本当に停滞したライターを迅速に回収する必要があります。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 にもミラーリングされる)を含むターミナルの client_evicted フレームを発行し、ストリームをクローズします。15 秒のハートビートよりも十分に大きい値(例: 30000〜300000)を選択して、正当なアイドルストリームが追い出されないようにします。< 15000 の値は、最初のハートビートが発生する前に、そうでなければ健全なアイドル接続を追い出します(テスト / 短命な開発セッションのみを意図)。機能タグ(条件付き): writer_idle_timeout。 |
どちらのフラグもミリ秒単位の正の整数を受け入ります。0、NaN、非整数、または負の値は、起動時に明確なエラーメッセージとともに拒否されます。CLI フラグは環境変数より優先されます。明示的な ServeOptions フィールド(組み込み呼び出し元)は環境変数より優先されます。SDK コンシューマーは、いずれの動作にも依存する前に、一致する機能タグをプリフライトする必要があります。この PR より前のデーモンは両方のタグを省略し、リクエストの deadlineMs フィールドはサイレントにドロップされます。
エージェントのテキスト書き込みの新規ファイルモード
エージェントのテキスト書き込み(write_file、edit、notebook_edit、およびシェルツールのシミュレートされた sed エディタ)は、デーモンのアトミックライターを通じて公開されます。既存のターゲットのモードを保持し、新規ファイルの場合はオーナーのみがアクセス可能な 0600 をデフォルトとし、デーモンプロセスの umask を無視します。このフェイルクローズなデフォルトは意図的なものです。新しく作成されたファイルは、スーパーバイザーの umask がどれだけ寛容であっても、誤ってグループ/ワールドから読み取り可能になることはありません。
umask 駆動のデプロイメント規則を持つオペレーター(例: UMask=0002 の systemd ユニット、共有グループリポジトリ)は、新規ファイルを標準的な POSIX 処理にオプトインできます。
| 環境変数 | 値 | デフォルト | 動作 |
|---|---|---|---|
QWEN_SERVE_NEW_FILE_MODE | owner | system | owner | system は新規ファイルを 0o666 & ~umask で作成し、エージェントが作成したファイルがマシンの他のプロセスと同様にデーモンプロセスの umask に従います。owner は umask に依存しない 0600 のデフォルトを保持します。値は大文字小文字を区別しません。リテラル 0600 は owner のエイリアスとして受け付けられます(他の8進モードはサポートされていません)。それ以外の値は stderr に警告を出して拒否され、0600 のデフォルトが保持されます。 |
スコープと制限:
- テキスト書き込みルートによって作成された新規ファイルに適用されます(ワークスペースターゲット、同一ホストの外部ホストライター、および HTTP テキスト書き込み)。既存のファイルは常にディスク上のモードを保持します。
0600のシークレットを編集しても0600のまま、実行可能ファイルは+xを保持します。 - バイナリアップロード(
POST /file/upload)は、この設定に関係なく常に0600で作成されます。 - デーモンはワークスペースファイルシステムの構築時にこの変数を読み取ります。変更後はデーモンを再起動してください。
マルチセッションおよびマルチワークスペースデプロイメント
--workspace を複数回渡して、1 つの qwen serve プロセスで重複しない複数のワークスペースを登録します。最初のパスがプライマリです。各登録ワークスペースは隔離されたランタイムバウンダリを所有し、デーモン全体のリスナー、認証ポリシー、および合計セッション制限は共有されます。本番環境では互換性のためにプライマリ ACP 子の事前起動を試み、失敗時は初回使用時にリトライします。信頼されたセカンダリはオンデマンドで独自の子を起動し、信頼されていないセカンダリは ACP を起動しません。リクエストは正規の cwd で登録されたワークスペースを選択できます。cwd を省略したリクエストはプライマリワークスペースを使用します。ユーザーまたはセキュリティプリンシパルごとに 1 つのデーモンを使用してください。ワークスペースの信頼は実行ゲートであり、ACL ではありません。
信頼されていないセカンダリワークスペースは、Web Shell で untrusted かつ read-only として表示されます。永続化されたセッションカタログを検査するために展開できますが、Web Shell で選択または開いたり、再開したり、セッションを作成したり、完全にエクスポートしたりすることはまだできません。REST API は既存のバウンドされたファイルシステム読み取りポリシーに従い、永続化されたセッショングループカタログと、workspace_persisted_transcript が通知されている場合は、バウンドされたワークスペース修飾ページャーを通じてアクティブな永続化トランスクリプトも公開します。これらの読み取りにはライブランタイム状態は含まれず、ACP 子も起動しません。完全なワークスペース修飾エクスポートには信頼されたワークスペースと別の workspace_session_export ケーパビリティが必要です。実行、ミューテーション、またはエクスポート機能を使用する前に、ワークスペースを信頼してデーモンを再起動してください。信頼されていないプライマリは Web Shell で無効のままです。
より小さい障害またはセキュリティバウンダリ、独立したベアラートークン、クォータ、監査バウンダリ、オペレーティングシステムの分離、または独立したリソース監督が必要な場合は、個別のデーモンプロセスを使用してください。マルチワークスペースモードは、1 人のオペレーターが複数のリポジトリをホストすることを目的としています。マルチテナントの分離バウンダリではありません。単一のデーモントークンは、デーモンが公開するすべてのルート(登録されたすべてのワークスペースの許可された読み取り専用カタログを含む)を認可します。
modelServiceIdをポストする前にサブスクライブしてください。 クライアントがmodelServiceIdを含むPOST /sessionを実行し、ワークスペースにすでに異なるモデルを実行しているセッションがある場合、デーモンは内部でsetSessionModel呼び出しを発行します。失敗は HTTP エラーとして伝播されません(セッションは現在のモデルで動作し続けます)。目に見える失敗シグナルは、セッションの SSE ストリーム上のmodel_switch_failedイベントです。POST /sessionを呼び出し、その後にGET /session/:id/eventsを開くと、失敗イベントを見逃し、サイレントに間違ったモデルと話し続けることになります。最初に SSE ストリームを開くか、サブスクライブ時にLast-Event-ID: 0を渡して、リングの最も古い利用可能なイベントをリプレイしてください。
複数のユーザーまたはセキュリティプリンシパル(それぞれに独立したトークン、クォータ、監査ログ、サンドボックス、またはプロセス障害バウンダリを持つ)を処理する場合、または 1 つのプロセスの範囲を超えてスケーリングする場合(コールドスタート予算、FD 数、RSS)は、プリンシパルごとに 1 つのデーモンを外部オーケストレーターの背後に生成します。各デーモンはそのプリンシパル用に複数のワークスペースをホストできます。そのオーケストレーター(マルチテナンシー / OIDC / クォータ / 監査 / k8s)は、qwen-code プロジェクトのスコープ外です。設計のポインターについては、issue #3803 の「External Reference Architecture」を参照してください。
永続化されたセッションのロードと再開
デーモンは、ACP の session/load および再開フローを HTTP 経由で公開します。加えて、別の読み取り専用のトランスクリプトページャーも提供します。
| ルート | 使用タイミング |
|---|---|
POST /session/:id/load | クライアントにレンダリングされた有用なローカル履歴がない場合(コールド再接続、ピッカーからのオープン)。ライブセッションの場合、デーモンは現在のバウンドされたリプレイスナップショットウィンドウを返して注入します。古いリプレイが削除された場合、スナップショットは history_truncated で始まります。機能タグ: session_load。 |
POST /session/:id/resume | クライアントがすでに画面上にターンを持っており、デーモン側のハンドルだけを必要とする場合。モデルコンテキストはエージェント側で UI リプレイなしに復元され、SSE ストリームはクリーンなままです。機能タグ: session_resume(unstable_session_resume は古いクライアント向けの非推奨エイリアスのままです)。 |
GET /session/:id/transcript | クライアントが完全なアクティブな永続化トランスクリプトを必要とする場合。カーソルページで ID なしのリプレイフレームを返し、/load を呼び出したり、クライアントをアタッチしたり、ライブ EventBus をシードしたり、ライブセッションを作成したり、ライブリプレイウィンドウを変更したりすることはありません。機能タグ: session_transcript。 |
GET /workspaces/:workspace/session/:id/transcript | クライアントが、ACP を開始したりワークスペース設定をロードしたりせずに、選択したワークスペースからアクティブな永続化トランスクリプトを必要とする場合。登録された信頼されていないセカンダリワークスペースもこの読み取り専用パスを使用できます。機能タグ: workspace_persisted_transcript。 |
GET /workspaces/:workspace/session/:id/export | クライアントが選択した信頼されたワークスペースから完全な html、md、json、または jsonl 添付ファイルを必要とする場合。ACP を開始したりプライマリにフォールバックしたりせずにアクティブな永続化ストレージを読み取ります。機能タグ: workspace_session_export。 |
GET /workspaces/:workspace/session/:id/archive/export | クライアントが選択した信頼されたワークスペースのアーカイブされた永続化ストレージから同じ添付ファイル形式を必要とする場合。アーカイブ解除、ACP の開始、またはアクティブまたはプライマリセッションへのフォールバックは行いません。機能タグ: workspace_archived_session_export。 |
ロードとリスタートの場合、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 the bounded snapshot window 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、または caps.features.session_transcript をプリフライトしてください。古いデーモンは 404 を返します。unstable_session_resume はまだ非推奨の互換性エイリアスとして通知されます。同じ ID に対する同時の同じアクションリクエストは統合されます。クロスアクションの競合(load と resume の競合)、および呼び出し元指定 ID の生成と復元の競合は、Retry-After: 5 を伴う 409 restore_in_progress になります。limits.sessionRestoreTimeoutMs を超える復元は、バジェットから導出された Retry-After(5〜120 秒にクランプされる)を伴うリトライ可能な 504 session_restore_timeout を返します。まだ実行中の子リクエストは、クリーンアップが確定するまでフェンスされたままです。そのウィンドウ中の同じ ID のリトライは、固定の 5 秒遅延ではなく、5〜120 秒にクランプされたバジェットから導出された Retry-After を伴う 409 restore_in_progress(reason: awaiting_abandoned_cleanup)を受け取ります。クリーンアップが不確かな場合、または放棄された復元がデッドライン後に完全な復元バジェット仍未に確定していない場合、新しいセッションの作業は一時的に 503 acp_channel_unavailable(reason: restore_cleanup_failed または restore_settlement_overdue)を受け取ります。すでにライブのセッションは引き続き使用可能です。完全なエラーエンベロープについては、プロトコルリファレンスを参照してください。
完全な永続化リプレイの場合は、DaemonClient.getSessionTranscriptPage(sessionId, { cursor, limit }) または生の REST ルートでページングします。
curl "http://127.0.0.1:4170/session/$SESSION_ID/transcript?limit=100"登録されたワークスペースの場合は、client.workspaceById(workspaceId).getSessionTranscriptPage(sessionId, { cursor, limit }) または /workspaces/:workspace/session/:id/transcript を使用します。ワークスペース修飾メソッドは、SDK クライアントに置換可能な ACP トランスポートがある場合でも、常にネイティブ REST を使用します。そのカーソルはデーモンの存続期間のみ有効であり、デーモンの再起動後はページ 1 から再開する必要があります。
信頼された登録ワークスペースからの完全な添付ファイルの場合は、workspace_session_export をプリフライトし、client.workspaceById(workspaceId).exportSession(sessionId, { format: 'html' }) または生の /workspaces/:workspace/session/:id/export ルートを呼び出します。session_export または workspace_qualified_rest_core からサポートを推測しないでください。古いデーモンは両方を通知しながらプライマリのみのエクスポートを保持する可能性があります。現在の Web Shell のエクスポートアクションはプライマリのみのままです。別のワークスペースの場合は SDK または REST ルートを使用してください。
アーカイブされた添付ファイルの場合は、workspace_archived_session_export をプリフライトし、client.workspaceById(workspaceId).exportArchivedSession(sessionId, { format: 'html' }) または /workspaces/:workspace/session/:id/archive/export を呼び出します。このパスはアーカイブされたストレージをそのまま読み取り、アクティブのみの ID に対して 409 session_not_archived を返します。セッションのアーカイブ解除は行いません。Web Shell は、ケーパビリティが存在する場合、信頼されたプライマリおよびセカンダリワークスペースのアーカイブされた行に対して同じエクスポートを公開します。
limit はアクティブなチャットレコードをカウントし、出力されたリプレイフレームはカウントしません。1 つのレコードが複数の session_update イベントを生成する可能性があります。最初のレスポンスは JSONL スナップショットサイズをフリーズし、hasMore が true の間に nextCursor を返します。後のページはページ 1 以降の追加を無視しますが、ファイルが削除、切り詰め、置換、アーカイブされた場合、またはフリーズされたカーソルと競合する場合は 409 を返します。非常に大きなスナップショットは、デーモンがリクエストパスで無制限のトランスクリプトファイルをスキャンしないように、インデックス作成前に 413 transcript_too_large を返します。
レガシーな単数ルートを通じて繰り返しページングする場合は、--channel-idle-timeout-ms を正の値に設定してください。デフォルトの 0 では、アイドル状態のワークスペースの ACP 子 — およびそれが保持するインプロセスのトランスクリプトインデックスキャッシュ — は各ページ後に回収されるため、各ページは子を再スポーンし、フリーズされたプレフィックス全体を再スキャンしてインデックスを再構築します(ページあたり O(snapshotSize))。正のタイムアウトはカーソルウォークをまたいで子を存続させるため、キャッシュされたトランスクリプトインデックスとリプレイ設定を再利用します。ワークスペース修飾の永続化ルートは ACP 子を起動せず、このタイムアウトの影響を受けません。
注: ライブセッションの履歴リプレイは 2 重に制限されます。Last-Event-ID 再接続用の SSE リングと、POST /session/:id/load が返すスナップショット用の --compacted-replay-max-bytes です。会話の多い長い履歴はどちらの制限を超える可能性があります。デーモンはスナップショットの切り詰めを history_truncated で表面化します。完全なアクティブな永続化履歴が必要な場合は /transcript を使用してください。
耐久性モデル
Stage 1 では、デーモンの再起動をまたいでセッションはまだエフェメラルですが、ディスク上の永続化されたセッションはリロード可能です。
- 子プロセスのクラッシュは
session_diedを発行し、ライブセッションをデーモンのマップから削除します。ディスク上に永続化されたセッションは、新しいエージェント子プロセスを生成可能であれば、POST /session/:id/load経由でリロードできます。 - デーモンの再起動は、進行中のすべてのライブセッションを失います。永続化されたセッションはディスク上に残り、同じワークスペースバインディングルールに従って、新しいデーモンプロセスに対してロードできます。
- 長時間のクライアント切断(会話の多いターンで 5 分以上)は、SSE リプレイリング(デフォルト 8000 フレーム)を追い越す可能性があります。
Last-Event-ID再接続はstate_resync_requiredをトリガーします。モバイル / 不安定なネットワークのクライアントの場合は、長時間の切断時に SSE を再度開くか、POST /session/:id/loadを呼び出して現在のバウンドされたリプレイスナップショットを回復してください。そのルートが完全なトランスクリプトを返すことを想定しないでください。 - ファイル操作(
writeTextFile)はクラッシュをまたいでアトミックです(書き込んでからリネーム)。リプレイの意味ではデーモンの再起動をまたいでアトミックではありません。ファイルの書き込みは成功したか、失敗したかのどちらかです。
あなたのインテグレーションが session/load がカバーするものを超えるサーバー側の再起動をまたぐ耐久性(例: サーバー管理のリトライキュー)を必要とする場合、アプリケーションレベルの状態回復が依然として必要です。デーモンのセッション内に、長時間実行される再起動に敏感な状態を保持しないでください。
Stage 1.5+ のランタイム保証
Stage 1 の契約はプロトタイピング向けにサイジングされています。#3889 chiga0 downstream-consumer review に従い、以下は Stage 1 には含まれません。本番グレードのインテグレーションは、それらに依存する前に Stage 1.5 以降が必要です。
本格的な下流利用におけるブロッカー:
- HTTP 経由の
loadSession/unstable_resumeSession— これがないと、どのインテグレーションも子プロセスのクラッシュやデーモンの再起動に耐えられず、デーモンを調整するオーケストレーターも状態を回復できません。 - 永続的なクライアント ID(ペアトークン + クライアントごとの取り消し) — Stage 1 では 1 つの共有 Bearer トークンを使用します。トークンが漏洩すると全員が無効化され、
originatorClientIdは認証された ID からデーモンによってスタンプされるのではなく、クライアント自身によって宣言されたものになります。
信頼性のベースライン:
クライアント開始のハートビートパス— #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)を介して実行時に追加 / 削除できるようになりました — Runtime MCP server management を参照してください。
モード 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 のみが approval-mode / memory / MCP サーバーリスト / agents / tools 許可リスト / auth 状態を参照 / ミューテーションします。
モード 2 における結果: リモートクライアント UI がセッション設定をミラーリングしようとした場合、TUI のスラッシュコマンド実行後にドリフト(乖離)が発生する可能性があります。リモートクライアントは、アタッチ / 再接続時に状態を再取得するべきです(model_switched などのために Last-Event-ID: 0 を使用してリングの最も古いイベントをリプレイします)。TUI 側のミューテーションに対してインクリメンタルイベントに依存すべきではありません。
なぜ (B) ではなく (A) なのか(ミューテーションを session_state_changed イベントファミリーに昇格させること)
(B) はより野心的な回答ですが、Stage 1.5 を、計画されているプロセス内リファクタリングもクリーンに通過しなければならない、大幅に大きなワイヤーサーフェスに固定してしまいます。私たちは、より小さなスコープを誠実に進めることを選びます。セッション状態イベントの分類作業 — どの TUI フローが設計上ローカル限定であり、将来のオプトイン (B) 形式の拡張でワイヤーに昇格する可能性があるかを列挙すること — は、Stage 1.5 のコードではなく #3803 に移行します。
N 個の並列セッションがワークスペースランタイムごとに 1 つの qwen --acp 子を共有
同じ信頼されたワークスペース上の複数のセッションは、そのランタイムの qwen --acp 子プロセスを共有します。エージェントのネイティブマルチセッションサポート(packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>)を介してです。ブリッジは各セッションに対して connection.newSession({cwd, mcpServers}) を呼び出します。エージェントはそれらをセッションマップに保存し、呼び出しごとの sessionId をデマルチプレクスします。本番環境では、プライマリ子 1 つ(デフォルトで事前起動を試みる) plus 信頼されたセカンダリごとにオンデマンド 1 つを所有できます。信頼されていないセカンダリは子を所有しません。
同じワークスペース上の N=5 セッションの場合の具体的なコスト:
| リソース | セッションあたり | N=5 の場合 |
|---|---|---|
| デーモン Node プロセス | 1 つ | 30–50 MB(1 つのデーモン) |
qwen --acp 子プロセス | 共有 | 60–100 MB(1 つの子プロセス) |
| MCP サーバー子プロセス | 通知されている場合はワークスペースプール。それ以外の場合はセッションごと | 一致するプールエントリで共有、またはレガシーフォールバックでは最大 3×N |
FileReadCache(子プロセスヒープ内) | 共有 | 1 回だけパース |
CLAUDE.md / 階層メモリのパース | 共有 | 1 回だけパース |
| OAuth リフレッシュトークン状態 | 共有 | 1 つのリフレッシュパス |
| 自動メモリで学習したファクト | 共有 | 子プロセスごとに 1 つのナレッジベース |
| コールドスタート | 初回のみ | 最初のセッション以降は <200 ms |
各アクティブなワークスペースランタイムは1 つのブリッジバウンダリを保持します。本番環境ではプライマリチャネルの事前起動を試み、失敗時は初回使用時にリトライします。信頼されたセカンダリはオンデマンドでチャネルと子を開き、信頼されていないセカンダリは決して開きません。チャネルは少なくとも 1 つのセッションが存続している限り維持されます。最後の killSession の後、ランタイムはデフォルトですぐに、または設定されたチャネルアイドル猶予後に子をキルします。チャネルレベルのクラッシュも同様にそれをティアダウンし、別のランタイムを選択することはありません。
MCP サーバー子プロセスは、mcp_workspace_pool が通知されている場合、ワークスペーススコープのトランスポートプールを使用します。一致する (ワークスペースランタイム, サーバー名, 設定フィンガープリント) エントリがセッションをまたいで参照カウントされます。ケーパビリティがない場合、レガシーなセッションごとのマネージャーがそれらを独立してスポーンします。
ピアエージェント(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. フローを開始します。デーモンは IdP に連絡し、コード + URL を返します。
curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"providerId":"qwen-oauth"}'
# → 201 {
# "deviceFlowId": "fa07c61b-…",
# "userCode": "USER-1",
# "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device",
# "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1",
# "expiresAt": 1700000600000,
# "intervalMs": 5000,
# "attached": false
# }
# 2. スマートフォン / ノートパソコンで URL にアクセスし、ユーザーコードを入力します。
# 3. 完了をポーリングします(または auth_device_flow_authorized イベントの SSE をサブスクライブします)。
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
-H "Authorization: Bearer $TOKEN"
# → ステータスの遷移: pending → authorizedTypeScript SDK は両方のステップを 1 つのヘルパーにラップしています。
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 を実行すると、プロバイダーごとのシングルトンが取得されます。1 回目の呼び出しは新しい 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/daemon.logすべてのファイルレコードには、ランダムな起動ごとの runId とデーモン PID が含まれます。成功した安定オーナーは、シンボリックリンクをサポートするプラットフォームで debug/daemon/latest を daemon.log に更新します。macOS/Linux では、ローテーションをフォローするには:
tail -F ~/.qwen/debug/daemon/daemon.log他のプラットフォームでは、パスネームが置換された後にビューアーが再オープンするように設定してください。古いファイルハンドルのみを保持するビューアーは、ローテーション後にアーカイブに残ります。
ログはライフサイクルメッセージ、ルートエラー(route= および sessionId= コンテキスト付き)、ACP 子プロセスの stderr、および QWEN_SERVE_DEBUG=1 が設定されている場合は追加のブリッジブレッドクラムをキャプチャします。現在 stderr に出力される行は引き続き stderr に出力されます。ファイルログは追加であり、置き換えではありません。
アクティブファイルは 10 MiB を超える前にローテーションされます。各ファミリーは archive/ の下に 4 つのアーカイブを保持し、各ファイルレコードは 256 KiB に制限されます。インメモリキューは最大 4 MiB の未確定ファイルペイロードを受け入れます。キューの圧迫、ローテーションの失敗、またはファイルシステムの障害により、ファイルのコピーが失われる可能性があります。GET /daemon/status?detail=full はロガーのヘルス、問題、およびドロップされたレコード / バイトカウンターを公開します。
1 つのログ名前空間では、1 つのデーモンのみが安定ファミリーを所有できます。並行デーモンは debug/daemon/runs/run-<runId>/daemon.log に書き込みます。起動バナーと完全なステータスに正式なパスが含まれます。runs/recent-fallback は最近のフォールバックファミリーのベストエフォートのロケーターであり、まだライブのものを指している可能性があります。健全な名前空間は約 100 MiB に収束します。安定用約 50 MiB と 1 つの非アクティブなフォールバックファミリーです。ライブまたはまだ古くなっていないフォールバックファミリーは保持されるため、並行デーモンやクラッシュ / 再起動のストームで一時的にそれ以上使用することがあります。
1 つのランタイムディレクトリは 1 つの所有権と保持の名前空間です。デーモンが独立した履歴を必要とする場合は、異なる QWEN_RUNTIME_DIR 値を使用してください。新しいデーモンログディレクトリはユーザーにプライベート(0700)であり、新しいファイルは POSIX で 0600 を使用します。経過年数ベースの有効期限はありません。
無効化
ファイルログを完全にスキップするには、QWEN_DAEMON_LOG_FILE=0(または false/off/no)を設定します。stderr 出力は影響を受けません。
セッションデバッグログとの関係
セッションスコープのデバッグログ(~/.qwen/debug/<sessionId>.txt および ~/.qwen/debug/latest シンボリックリンク)は独立しています。デーモンログは兄弟ディレクトリである daemon/ サブディレクトリに存在します。セッションごとのデバッグセマンティクスは、この機能によって変更されません。
外部ローテーション
アクティブな daemon.log に外部の logrotate ルールを向けないでください。デーモンが唯一サポートされるライターおよびローテーターです。外部でのリネーム、削除、または切り詰めはサイズモデルを無効にします。ファミリーを変更せずにレコードをコピーまたは出荷することは安全です。古い serve-<pid>.log および serve-<pid>-<workspaceHash>.log ファイルはそのまま残され、新しい保持ポリシーではカウントされません。
実行時 MCP サーバー管理(issue #4514 )
デーモンを再起動せずに、実行時に MCP サーバーを追加または削除します。実行時エントリは、同じ名前の設定定義サーバーをシャドウする一時的なオーバーレイに存在します。基礎となる settings.json / mcpServers 設定が書き換えられることはありません。
プレフライト: どちらのルートを呼び出す前に、caps.features で mcp_server_runtime_mutation を確認してください。このタグがない古いデーモンは 404 を返します。
POST /workspace/mcp/servers — 実行時 MCP サーバーの追加
厳格なゲート(Bearer トークンが必要)。ライブの McpClientManager 経由でサーバーに即座に接続し、そのツールを検出します。
リクエスト:
{
"name": "my-server",
"config": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"]
}
}name は英数字と _、- のみで構成される必要があります(最大 256 文字)。config は 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 | 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[]配列に表示されます。今日のスナップショットでは、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)からの予算関連イベントも、実行時の追加が予算しきい値を超えた際に発生します。
次のステップ
- 長時間実行するデーモンのセットアップ? v0.16-alpha(ローカルのみ)向けの ローカル起動テンプレート(systemd / launchd / nohup / tmux)。
- クライアントの構築? DaemonClient TypeScript クイックスタート および HTTP プロトコルリファレンス を参照してください。
- ソースコードの確認? ブリッジコードは
packages/cli/src/serve/に、SDK クライアントはpackages/sdk-typescript/src/daemon/にあります。 - ロードマップの追跡? ステージ 1.5 / ステージ 2 の進捗は、issue #3803 で管理されています。