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 `