From 27f8f2c95de3f91c5f2df30007b2374014f22a1b Mon Sep 17 00:00:00 2001 From: jinye Date: Wed, 8 Jul 2026 08:52:36 +0800 Subject: [PATCH] feat(cli): Add serve env isolation and total admission (#6416) * feat(cli): add serve env isolation and total admission Add runtime-local serve env snapshots, explicit env injection for low-cost workspace-scoped consumers, and sourceEnv support for ACP child spawn. Add a daemon-wide maxTotalSessions admission reservation hook for fresh session creation while keeping multi-workspace sessions gated. Co-authored-by: Qwen-Coder * codex: address PR review feedback (#6416) Reject fractional maxTotalSessions values so the daemon-wide session cap remains an integer count and matches the documented limit semantics. Co-authored-by: Qwen-Coder * fix(cli): address PR review feedback (#6416) Always pass the runtime env to A2UI stdio transports, keep daemon runtime env metadata coherent after env reload fallback, and tighten total-admission coverage. Co-authored-by: Qwen-Coder * fix(cli): address total admission review feedback (#6416) Add retryable ACP error data for total session limits, log total-admission REST rejections, and keep session-limit response scopes explicit. Co-authored-by: Qwen-Coder * fix(cli): address env review feedback (#6416) Co-authored-by: Qwen-Coder * fix(cli): restore scheduled task serve deps (#6416) Co-authored-by: Qwen-Coder * fix(cli): isolate runtime env reload base (#6416) Co-authored-by: Qwen-Coder * codex: address PR review feedback (#6416) Co-authored-by: Qwen-Coder * codex: address PR review feedback (#6416) Co-authored-by: Qwen-Coder * codex: address PR review feedback (#6416) Co-authored-by: Qwen-Coder * codex: address PR review feedback (#6416) Co-authored-by: Qwen-Coder * codex: fix CI failure on PR #6416 Co-authored-by: Qwen-Coder * fix(cli): address daemon admission review feedback Co-authored-by: Qwen-Coder * fix(cli): address runtime env review feedback Scrub daemon bearer tokens from A2UI stdio MCP environments and prune reload-owned keys from the daemon runtime base before rebuilding runtime env snapshots. Co-authored-by: Qwen-Coder * fix(cli): preserve daemon env base on reload Keep runtime env rebuilds anchored to the boot-time daemon base snapshot, preventing reload-owned key pruning from dropping valid shell-exported values. Also carry env file read failure details into runtime metadata and daemon logs. Co-authored-by: Qwen-Coder * fix(cli): satisfy env metadata lint rules Co-authored-by: Qwen-Coder --------- Co-authored-by: Qwen-Coder --- ...daemon-multi-workspace-phase2a-sessions.md | 53 +++- docs/developers/qwen-serve-protocol.md | 8 +- docs/users/qwen-serve.md | 12 +- packages/acp-bridge/src/bridge.test.ts | 254 +++++++++++++++++ packages/acp-bridge/src/bridge.ts | 219 +++++++++----- packages/acp-bridge/src/bridgeErrors.ts | 10 + packages/acp-bridge/src/bridgeOptions.ts | 25 +- packages/acp-bridge/src/spawnChannel.test.ts | 38 +++ packages/acp-bridge/src/spawnChannel.ts | 6 +- packages/cli/src/commands/serve.test.ts | 18 ++ packages/cli/src/commands/serve.ts | 10 + packages/cli/src/config/environment.test.ts | 192 +++++++++++++ packages/cli/src/config/environment.ts | 267 ++++++++++++------ packages/cli/src/serve/acp-http/dispatch.ts | 22 +- .../cli/src/serve/acp-http/transport.test.ts | 76 +++++ packages/cli/src/serve/acp-session-bridge.ts | 4 + .../cli/src/serve/daemon-status-provider.ts | 20 +- packages/cli/src/serve/daemon-status.test.ts | 72 +++++ packages/cli/src/serve/daemon-status.ts | 31 +- packages/cli/src/serve/env-snapshot.test.ts | 40 +++ packages/cli/src/serve/env-snapshot.ts | 18 +- packages/cli/src/serve/fast-path.test.ts | 2 + packages/cli/src/serve/fast-path.ts | 1 + .../cli/src/serve/process-env-guard.test.ts | 80 ++++++ .../cli/src/serve/routes/a2ui-action.test.ts | 49 +++- packages/cli/src/serve/routes/a2ui-action.ts | 51 +++- .../cli/src/serve/routes/daemon-status.ts | 4 + packages/cli/src/serve/run-qwen-serve.test.ts | 255 +++++++++++++++++ packages/cli/src/serve/run-qwen-serve.ts | 155 +++++++++- packages/cli/src/serve/server.test.ts | 220 +++++++++++---- packages/cli/src/serve/server.ts | 54 +++- .../cli/src/serve/server/error-response.ts | 32 +++ .../src/serve/total-session-admission.test.ts | 89 ++++++ .../cli/src/serve/total-session-admission.ts | 80 ++++++ packages/cli/src/serve/types.ts | 6 + .../serve/voice/resolve-voice-config.test.ts | 91 ++++++ .../src/serve/voice/resolve-voice-config.ts | 30 +- packages/cli/src/serve/voice/voice-ws.ts | 14 +- .../serve/workspace-providers-status.test.ts | 24 ++ .../src/serve/workspace-providers-status.ts | 11 +- packages/cli/src/serve/workspace-registry.ts | 10 +- .../cli/src/services/voice-transcriber.ts | 7 +- .../src/ui/voice/voice-transcriber.test.ts | 26 ++ .../cli/src/utils/modelConfigUtils.test.ts | 13 + packages/cli/src/utils/modelConfigUtils.ts | 22 +- 45 files changed, 2422 insertions(+), 299 deletions(-) create mode 100644 packages/cli/src/config/environment.test.ts create mode 100644 packages/cli/src/serve/process-env-guard.test.ts create mode 100644 packages/cli/src/serve/total-session-admission.test.ts create mode 100644 packages/cli/src/serve/total-session-admission.ts create mode 100644 packages/cli/src/serve/voice/resolve-voice-config.test.ts diff --git a/docs/design/daemon-multi-workspace-phase2a-sessions.md b/docs/design/daemon-multi-workspace-phase2a-sessions.md index fcdc97cfdb..5ce013a919 100644 --- a/docs/design/daemon-multi-workspace-phase2a-sessions.md +++ b/docs/design/daemon-multi-workspace-phase2a-sessions.md @@ -2,17 +2,17 @@ ## Summary -This document records the Phase 2a foundation contract for issue #6378 after -the Phase 1 `WorkspaceRegistry` PR. The current implementation batch combines -the Phase 1 repeated `--workspace` follow-up, the Phase 2a prep guardrails, and -the first internal registry/runtime contract needed by later multi-workspace -session work. +This document records the Phase 2a contract for issue #6378 after the Phase 1 +`WorkspaceRegistry` PR and the Phase 2a foundation PR. Phase 2a is now split +into two implementation PRs: PR 1 lands env isolation and total-admission +guardrails while multi-workspace remains gated; PR 2 will wire non-primary live +session dispatch and publish the additive capabilities/status schema. Phase 2a remains sessions-only. It does not add plural routes, a `WorkspaceDaemonClient`, workspace-qualified ACP/WebSocket, file, memory, MCP, -settings, voice, channel-worker migration, env overlays, total-session -admission, capabilities `workspaces[]`, `multi_workspace_sessions`, route -dispatch, or non-primary runtime construction in this foundation batch. +settings, voice, or channel-worker migration. PR 1 does not add capabilities +`workspaces[]`, `multi_workspace_sessions`, route dispatch, or non-primary +runtime construction. ## Foundation Contract @@ -38,8 +38,10 @@ Phase 2a work: - `primary`: true for the primary runtime. - `trusted`: boot-time trust metadata; direct `createServeApp` fallback remains false unless production passes an explicit trusted value. -- `env`: metadata only. This foundation batch records parent-process mode and - empty overlay keys; it does not compute runtime-local env overlays. +- `env`: runtime-local env source metadata. In single-workspace production, + the primary runtime now receives a computed effective env snapshot and a + mutable env source that can be refreshed after daemon env reload. Direct + `createServeApp` fallback remains parent-process metadata. The internal `WorkspaceRegistry` supports exact cwd lookup, exact id lookup, `resolveWorkspaceCwd(undefined)` primary fallback, and live session owner @@ -94,14 +96,35 @@ after tests prove they depend solely on the owning live bridge. - Fail closed if more than one runtime reports the same live session id. - Keep non-primary session listing live-only unless persisted entries are explicitly marked non-resumable. -- Add runtime-local env overlays before non-primary child spawn. -- Add `maxTotalSessions` at the bridge fresh-creation seam so REST and primary - `/acp` cannot bypass it, while attach still bypasses admission. -- Publish `workspaces[]`, total limits, and `multi_workspace_sessions` only in - the final ungate PR. +- Reuse PR 1 runtime-local env overlays before non-primary child spawn. +- Reuse PR 1 `maxTotalSessions` admission at every future fresh-creation seam + so REST and primary `/acp` cannot bypass it, while attach still bypasses + admission. +- Publish `workspaces[]` and `multi_workspace_sessions` only in PR 2 when the + live session dispatch loop is complete. - Update SDK capability types when the additive capabilities schema ships, but do not add a workspace client in Phase 2a. +## PR 1 Guardrails + +- Runtime env is computed from daemon base env plus workspace `.env`, settings + env, and Cloud Shell defaults without mutating parent `process.env` during + runtime initialization. +- The env helper intentionally does not virtualize `QWEN_HOME`, Storage, or + global config routing. Those remain daemon boot/base-env responsibilities. +- ACP child spawn accepts an explicit `sourceEnv`, and low-cost + workspace-scoped status/config readers use injected env instead of direct + `process.env` reads. +- `maxTotalSessions` is an optional daemon-wide fresh-session cap. It covers + spawn, persisted load/resume restore, and branch/fork session creation; + attach bypasses it. +- The bridge admission seam is a synchronous reservation hook. Failed fresh + creation releases the reservation, preventing concurrent oversell across + runtimes once non-primary bridges exist. +- `/daemon/status.limits.maxTotalSessions` is additive. `/capabilities` and SDK + capability types remain unchanged until PR 2 ungates multi-workspace + sessions. + ## Audit Decisions - The foundation PR must not create non-primary runtimes or relax any REST diff --git a/docs/developers/qwen-serve-protocol.md b/docs/developers/qwen-serve-protocol.md index f8a5f046ed..793792c9e3 100644 --- a/docs/developers/qwen-serve-protocol.md +++ b/docs/developers/qwen-serve-protocol.md @@ -91,10 +91,13 @@ Use this to detect mismatch pre-flight: read `workspaceCwd` off `/capabilities` { "error": "Session limit reached (20)", "code": "session_limit_exceeded", - "limit": 20 + "limit": 20, + "scope": "workspace" } ``` +When `--max-total-sessions` rejects a fresh session, the same response shape is returned with `"scope": "total"`. + Attaches to existing sessions are NOT counted toward the cap, so an idle daemon's reconnects keep working even when at-capacity. `RestoreInProgressError` — only emitted by `POST /session/:id/load` and `POST /session/:id/resume` — returns `409` with a `Retry-After: 5` header (matching `session_limit_exceeded`) and: @@ -293,6 +296,7 @@ Response shape: }, "limits": { "maxSessions": 20, + "maxTotalSessions": null, "maxPendingPromptsPerSession": 5, "listenerMaxConnections": 256, "eventRingSize": 8000, @@ -364,6 +368,8 @@ runtime routes return `503`. `runtime.activity` reports daemon-wide prompt activity. `activePrompts` counts sessions with an in-flight prompt. `pendingPrompts` counts all accepted prompts that have not settled yet, including the running prompt and FIFO-waiting prompts. `queuedPrompts` counts FIFO-waiting prompts that have been accepted but not dispatched. `lastActivityAt` is the ISO 8601 timestamp of the last prompt start/end or session spawn; `null` when the daemon has never processed any activity since boot. `idleSinceMs` is computed from `lastActivityAt` at response generation time. +`limits.maxTotalSessions` is additive. `null` means the daemon-wide fresh-session cap is disabled. When set, it limits fresh session creation across the daemon and reports total-limit failures with the existing `session_limit_exceeded` error shape plus `scope: "total"`. It does not change `/capabilities`, does not advertise `workspaces[]`, and does not enable multi-workspace routing by itself. + `runtime.channel.live` reports the ACP bridge channel inside the daemon. It is not the channel-adapter worker. Daemon-managed channels use `runtime.channelWorker`, whose `state` is one of `disabled`, `starting`, diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index 99b20f2b4f..9f249e46ec 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -81,7 +81,7 @@ curl http://127.0.0.1:4170/daemon/status ``` The `workspaceCwd` field surfaces the bound workspace so clients can pre-flight check + omit `cwd` on `POST /session`. -The `limits.maxPendingPromptsPerSession` field advertises the active per-session prompt admission cap; `null` means the cap is disabled. +The `limits.maxPendingPromptsPerSession` field advertises the active per-session prompt admission cap; `null` means the cap is disabled. `limits.maxTotalSessions` advertises the optional daemon-wide fresh-session cap; `null` means unlimited. ### Run channels from the daemon @@ -303,6 +303,7 @@ Notes: | `--tls-cert ` | — | Path to a PEM certificate file. Serve over **HTTPS** instead of HTTP. Must be paired with `--tls-key` (boot fails if only one is given). Unlocks secure-context browser APIs — voice input (`getUserMedia`), WebRTC — over a LAN IP, which browsers otherwise block on plain `http://`. TLS termination only; no auto-generation / ACME. See [HTTPS / TLS](#https--tls-for-mobile--cross-device-access) below. | | `--tls-key ` | — | Path to a PEM private key file. Must be paired with `--tls-cert`. | | `--max-sessions ` | `20` | Cap on concurrent live sessions. New `POST /session` requests that would spawn a fresh child return `503` (with `Retry-After: 5`) when the cap is hit; attaches to existing sessions are NOT counted. Set to `0` to disable. Sized for single-user / small-team usage; raise it if your deployment has the RAM/FD headroom (~30–50 MB per session). | +| `--max-total-sessions ` | unlimited | Optional non-negative integer daemon-wide cap on fresh session creation across the runtime. It applies to new child sessions, session restore, and branch/fork-created sessions; attaching to an existing live session does not consume a slot. Set to `0` or omit the flag for unlimited. This is a guardrail for future multi-workspace sessions and does not enable multi-workspace serving by itself. | | `--max-pending-prompts-per-session ` | `5` | Per-session cap on prompts accepted by `POST /session/:id/prompt` but not yet settled, including queued prompts and the active prompt. The bridge rejects overflow synchronously with `503`, `Retry-After: 5`, and `code: "prompt_queue_full"` before returning a `promptId`. Set to `0` to disable. `branchSession` serializes on the same FIFO but does not count against this prompt cap. | | `--workspace ` | `process.cwd()` | Absolute workspace path this daemon binds to (per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02 — 1 daemon = 1 workspace). `POST /session` requests with a mismatched `cwd` return `400 workspace_mismatch`. For multi-workspace deployments, run one `qwen serve` per workspace on separate ports. | | `--channel ` | — | Experimental daemon-managed channel worker. Repeat the flag to select multiple configured channels, or pass `all` to start every configured channel. `all` cannot be combined with named channels. Selected channel `cwd` values must resolve to the daemon workspace. The worker is owned by `qwen serve`; stop the daemon to stop serve-managed channels. | @@ -315,7 +316,7 @@ Notes: | `--web` / `--no-web` | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and SPA deep-link fallback). The static shell is registered **before** the bearer-auth gate — a browser can't attach a token to a `