You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is an RFC for supporting multiple workspaces from a single qwen serve daemon process while preserving the current single-workspace behavior for existing clients.
The current public model is effectively:
1 daemon = 1 workspace x N sessions
That assumption appears across startup config, HTTP routing, ACP bridge ownership, SDK capability shape, channel workers, and public docs. At the same time, the codebase already has partial multi-root and per-workspace session storage building blocks, but those are not enough to provide true multi-workspace daemon semantics.
Full drafted design document is available on this branch for review:
Update after maintainer review: this RFC now scopes the first ungated milestone down to multi-workspace sessions only. The broader workspace-qualified REST, ACP/Web Shell, channel worker grouping, voice, SDK workspace client, and dynamic workspace surfaces remain in later phases.
Update after additional code review: this RFC now tightens env isolation, untrusted workspace/session semantics, trust-state changes, fault-radius documentation, and several implementation guardrails. In multi-workspace mode the daemon parent env must stay as daemon base env; all runtimes, including primary, use runtime-local env overlays. Untrusted workspaces can be registered and shown read-only, but cannot create/resume sessions or spawn ACP children.
Update after reviewer scope feedback: the first feature tag is now multi_workspace_sessions, not generic multi_workspace. Phase 2a only proves session creation/listing and the minimum live session routes. File, memory, MCP, settings, voice, workspace-qualified ACP/WebSocket, channel workers, persisted operations, and WorkspaceDaemonClient are explicit follow-ups.
Update after reviewer confirmation: the 7 open decisions are now folded into the RFC. Phase 1 landed with a single-runtime registry, daemon-scoped identity, repeatable --workspace parsing, and a temporary multi-workspace boot-error gate. Phase 2a exposes stable workspaces[].id, keeps live session scan misses as 404 with no primary fallback, does not implement non-primary persisted load/resume, reuses existing workspace trust storage, and adds lint guards so workspace-scoped serve/bridge code cannot directly read process.env.
Update after code-seam verification: Phase 2a is now explicitly conditional on four additional implementation contracts. Trust is boot-frozen in Phase 2a and runtime trust apply moves to the phase that lands workspace-qualified trust mutation / watcher / reload-time re-evaluation. maxTotalSessions admission must be anchored at bridge fresh-creation seams so REST and /acp dispatcher paths cannot bypass it. The RFC no longer assumes a runtime-level event bus; idle workspace state changes are discoverable by polling status/capabilities. Phase 2a classified all /session/:id/* routes before implementation, with basic live-session lifecycle routes such as heartbeat/detach/pending-prompts dispatched by ownership.
Update after Phase 2a sessions merge: Phase 1, the Phase 2a foundation, the env/admission guardrails, and the sessions-only closed loop are now merged. #6394 landed the single-runtime WorkspaceRegistry boundary plus daemon-scoped log/telemetry identity. #6410 landed the repeated --workspace parser/fast-path guardrail, duplicate/nested explicit-workspace validation, the expanded internal registry/runtime contract, and the Phase 2a route-classification design doc. #6416 landed the runtime-local env overlay foundation, process-env guardrails, explicit ACP child spawn env, --max-total-sessions / daemon maxTotalSessions, total-session admission at bridge fresh-creation seams, and additive daemon-status limit surfacing. #6511 landed the sessions closed loop and ungated the narrow multi_workspace_sessions milestone: multiple distinct explicit workspaces can boot as registered runtimes, capabilities/status publish workspaces[], live session creation can target a registered workspace by cwd, and the classified live session routes dispatch by session ownership. This issue remains open for Phase 2b+ work: persisted non-primary load/resume/export/archive/delete, plural workspace routes, workspace-qualified ACP/WebSocket, file/memory/MCP/settings/voice/channel-worker migration, SDK workspace client APIs, and dynamic workspace management.
Update after Phase 2b persisted-list merge: #6540 and #6558 are now merged. Phase 2b has landed lifecycle-backed session owner index hardening for workspace runtimes, workspace-aware persisted load/resume, the first plural workspace sessions alias, and trusted non-primary active persisted session listing with live-session merge. Live session ownership no longer depends only on scanning registered runtimes, persisted restore no longer has to guess across workspace stores, and trusted non-primary session list routes can now discover active persisted sessions. Archived/organized non-primary listing and non-primary export/archive/delete intentionally remain later work. The issue remains open for the next Phase 2b/3+ surfaces: non-primary export/archive/delete, archived and organized non-primary listing, broader plural workspace routes such as /workspaces/:workspace/..., workspace-qualified ACP/WebSocket, and migration of file/memory/MCP/settings/voice/channel-worker and broader SDK workspace client APIs.
Update after Phase 3 core REST PR: #6567 is open against main for the workspace-qualified core REST slice. It adds /workspaces/:workspace/... plural routes for core daemon surfaces, selector resolution by workspace id then encoded absolute cwd, workspace_qualified_rest_core, SDK workspaceById() / workspaceByCwd() helpers, telemetry attribution for plural routes, and docs/tests for trust and scope semantics. This PR intentionally excludes auth, voice, extensions, ACP/WebSocket transport, and channel-worker routing; those remain in later phases.
Update after Phase 4 ACP PR: #6567 (Phase 3 core REST) is now merged, and #6621 is open against main for the workspace-qualified ACP transport slice. It mounts /workspaces/:workspace/acp (Streamable HTTP + reverse WebSocket) with a per-workspace ACP dispatcher, connection registry, device-flow registry, remember lane, and reverse client-MCP, all bound to that runtime's bridge; the daemon's single WebSocket upgrade listener dispatches by URL path. Legacy /acp stays primary-only, untrusted (untrusted_workspace) and unknown (workspace_mismatch) selectors are rejected, and workspace_qualified_acp is advertised when more than one runtime is registered. Web Shell workspace picker (front-end), per-workspace voice/channel workers, and untrusted read-only relaxation remain later phases.
Update after Phase 2b listing-closure merge: #6631 is now merged. It closes the last Phase 2b listing gap — trusted non-primary workspaces now support archiveState=archived, the organized view=organized list, and group filters on the workspace session list routes (/workspace/:id/sessions and /workspaces/:workspace/sessions), reading from their own chats/, chats/archive/, and session-organization stores. The persisted read path is forced for organized and archived views, untrusted workspaces are still refused, and the primary workspace and legacy routes are unchanged. Behavior note: for a non-primary workspace an unknown group id now returns 404 group_not_found (matching primary) instead of the former phase-limitation error. Per-session organization mutation (pin/assign) for non-primary workspaces later landed in #6724.
Update after non-primary session organization mutation merge: #6724 is now merged, closing the write-side gap tracked in #6646. Trusted non-primary workspaces can update per-session pin, group, and color state through PATCH /workspaces/:workspace/session/:id/organization for active persisted, archived persisted, and live-only sessions, using the selected runtime’s bridge and organization sidecar with no cross-workspace fallback. The TypeScript SDK exposes the mutation through workspaceById() and workspaceByCwd(). The legacy PATCH /session/:id/organization route remains primary-only; ACP, capability tags, and the sidecar schema are unchanged.
Update after Extension Management V2 merge: #6825 is now merged (merge commit fea3ab385496afdbcf73c1fe1c31f16ac7088c83), superseding #6638. It lands globally shared extension artifacts, transactional and journal-recoverable global mutation coordination, bounded concurrent preparation with serialized durable commits, exact per-workspace activation/projection, runtime reconciliation, structured post-commit warnings, revisioned sensitive-setting bundles, TypeScript SDK helpers, and the additive extension_management_v2 capability while preserving the existing workspace_extensions compatibility surface. The workspace-qualified ACP and Web Shell workspace-management slices remain complete through #6621 and #6625, and per-workspace daemon-managed channel workers remain complete through #6635. This umbrella issue remains open for the still-unfinished multi-workspace surfaces, including Web Shell per-workspace Settings/Memory/MCP control parity tracked in #6974, remaining persisted-session mutations, and other follow-ups described below. Workspace-qualified Voice later landed in #6839; its secondary-workspace Web Shell controls are tracked in #6972.
Update after untrusted read-only session catalog merge: #6717 is now merged. It allows registered untrusted secondary workspaces to expose persisted-only session and session-group catalogs without live bridge access, ACP startup, settings loading, storage repair, or mutation. Web Shell can expand these workspaces and display historical sessions as non-interactive read-only rows; selecting the workspace, opening sessions, creating or resuming sessions, transcript access, and other runtime-backed surfaces remain blocked. Untrusted primary behavior is unchanged.
Update after persistent dynamic registration and runtime-removal merges: #6716 and #6745 are now merged. #6716 makes dynamically registered workspaces durable across daemon restarts through a bounded user-level registration store, adds registration listing and forget APIs, negotiates persistence through the SDK and Web Shell, and supports lazy workspace-qualified ACP mounts for runtimes registered after startup. #6745 completes the lifecycle with synchronous drain and removal of dynamically added or persisted-restored secondary runtimes. It advertises workspace_runtime_removal, keeps primary and explicitly configured startup workspaces non-removable, returns 409 workspace_busy for non-force removal with active resources, supports explicit force removal, deletes known persistent registration aliases, disposes workspace-owned sessions, ACP connections, memory tasks, bridges, sub-session launchers, and channel workers, and releases the path for a clean re-add without deleting project files, settings, transcripts, archives, or other workspace data. The TypeScript SDK, WebUI action layer, Web Shell confirmation/busy/force flow, protocol documentation, and lifecycle/concurrency coverage landed with the daemon implementation.
Update after non-primary Web Shell and session-mutation follow-ups: #6705, #6737, and #6746 are now merged. #6705 restored the full multi-workspace session-row experience while keeping unsupported non-primary actions read-only, #6737 migrated model and approval-mode changes to the owning workspace runtime so newly registered non-primary sessions can be created and controlled without primary-only errors, and #6746 surfaces trusted cross-workspace sessions in Split View and Session Overview, labels sessions by workspace, and attaches each pane through the session's owning workspace.
Update after persisted transcript and runtime channel-control merges: #6740 and #6741 are now merged. #6740 adds the unconditional workspace_persisted_transcript capability plus a workspace-qualified, daemon-local pager for active persisted transcripts. Trusted workspaces and registered untrusted secondary workspaces can read the existing visible transcript projection without ACP startup, live bridge access, settings loading, storage repair, or creation of the legacy persisted cursor-key file; cursors are intentionally valid only for the current daemon lifetime, and the TypeScript workspace client forces native REST. Transcript attach/live follow, archived reads, and the safe Web Shell read-only viewer remain separate follow-ups. #6741 adds authenticated runtime lifecycle control for daemon-managed channel workers: a daemon started without --channel can enable, replace, query, reload, and stop its current selection through HTTP, the TypeScript SDK, or qwen channel CLI commands. Runtime selections remain daemon-lifetime only; multi-workspace isolation, replacement rollback, PID-lease safety, and existing boot-time channel behavior are preserved.
Update after persisted transcript page-hardening merge: #6769 is now merged. Workspace-qualified persisted transcript reads now enforce a 4 MiB persisted-source budget per page, a 32 MiB serialized-response cap, and 64 KiB cursor bounds. Pagination stops at record boundaries, oversized aggregate records return the structured 413 transcript_page_too_large error, and oversized replay state returns the successful page as terminal partial content without issuing an unsafe continuation cursor. The legacy singular transcript route remains unchanged. The Web Shell safe read-only viewer remains tracked separately in #6770.
Update after multi-workspace session rewind/shell merge: #6826 is now merged (merge commit b933b90172058d1908b769799b062fae107b5ee4). The singular GET /session/:id/rewind/snapshots, POST /session/:id/rewind, and POST /session/:id/shell routes now resolve a live session’s owning trusted runtime while preserving their existing URLs and failure contracts. SDK rewind calls remain authenticated native REST even when ACP is configured, shell retains its configured transport, and the additive multi_workspace_session_rewind / multi_workspace_session_shell tags expose secondary-workspace support only when available. rewindFiles must now be omitted or boolean; file-history storage, primary/single-workspace behavior, and the lack of an ACP rewind vendor method remain unchanged.
Update after P0 secondary live-session mutation routing merge: #6833 is now merged (merge commit 9dd8389ebe4ad56e054f33f8f74189354a54a77f). The singular POST /session/:id/continue, POST /session/:id/language, POST /session/:id/artifacts, and DELETE /session/:id/artifacts/:artifactId routes now dispatch to the owning trusted runtime for live sessions while preserving their existing URLs, authentication gates, validation order, archive lease, bridge error mapping, request/response contracts, and primary/single-workspace behavior. No capability, SDK API, type, or response field was added. Artifact mutations remain live-session-only; workspace-qualified active persisted export later landed in #6844, while archived export later landed in #6911. ACP-over-HTTP artifact dispatch, branch/fork admission, cd ownership, and any separately scoped delete work remained separate follow-ups at that point. Legacy session-route telemetry later landed in #7003.
Update after Phase 4b workspace-qualified Voice merge: #6839 is now merged (merge commit c7250df8eacc47bfaa2eb6912351797c33c488ea). Trusted registered runtimes can read and update Voice settings, submit batch transcription, and open streaming transcription through workspace-qualified REST and WebSocket routes selected by workspace id or encoded absolute cwd. Requests are bound to the selected runtime’s cwd, effective environment, bridge, and workspace-scoped settings persistence; unknown or untrusted selectors never fall back to primary, and legacy Voice routes remain primary-only. A process-level coordinator caps legacy and qualified REST/WebSocket Voice work at eight and integrates voiceSessions with workspace drain, removal, and daemon shutdown; force removal aborts only the selected runtime’s Voice leases. TypeScript workspace-client methods and the workspace_qualified_voice capability landed with the daemon implementation. Secondary-workspace Web Shell Voice controls remain out of scope.
Update after workspace-qualified session export and Web Shell path-lock merges: #6844 and #6853 are now merged. #6844 adds GET /workspaces/:workspace/session/:id/export for trusted registered workspaces and active persisted sessions, the independent workspace_session_export capability, native-REST TypeScript workspace-client support, and workspace-attributed telemetry while preserving the primary-bound legacy export route. Archived-session export later landed in #6911; workspace-qualified archive/unarchive/delete REST and SDK support was already present through #6567. #6853 lets embedders select or lock Web Shell by workspaceCwd, persistently registers an unknown secondary workspace when locked, hides unrelated workspace-management surfaces, and keeps session and Scheduled Tasks operations scoped to the locked workspace.
Update after daemon-global deep-health merge: #6961 is now merged (merge commit ad65ba3bdeeb3b84b5e366c4a7acc3fb2a1d5693). It makes GET /health?deep=1 a daemon-wide aggregate across every managed workspace runtime, including draining runtimes, adds workspaceCount, preserves the exact shallow /health contract, and returns retryable degraded responses during bootstrap or aggregation failure. This closes the primary-only deep-health observability gap while keeping per-workspace readiness checks and atomic reclaim leases out of scope; this umbrella issue remains open for the other unfinished multi-workspace surfaces.
Update after legacy session workspace telemetry merge: #7003 is merged (merge commit e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy /session, /sessions, and /permission routes now emit canonical request spans. The 41 handler-resolved routes bind qwen-code.workspace.hash to the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.
Update after workspace trust hot-reload merge: #7268 is now merged (merge commit c4859627a77ed7c3fe015d505d82645cd672188a). It supersedes the Phase 2a boot-frozen trust limitation with semantic trust-policy snapshots and monitoring, per-workspace runtime-generation reconciliation, and fail-closed route behavior while reconciliation is applying, failed, blocked, draining, or otherwise unavailable. Trust grants rebuild settings, environment, filesystem, tools, services, and caches from fresh state; revocations close the old trusted generation before asynchronous drain. The additive v2 trust status exposes configured/effective trust, policy revision, and reconciliation state through the TypeScript SDK while the existing v1 trust-request behavior remains compatible. Web Shell trust approval and presentation remain outside this merged slice.
Goals
Allow one daemon process to serve multiple explicit workspaces.
Keep legacy workspace-less routes bound to the primary workspace.
Let new clients target a specific workspace for session creation, session listing, file/workspace APIs, status, and ACP/Web Shell connections.
Isolate sessions, settings, MCP state, permissions, memory tasks, filesystem writes, env overlays, and child processes by workspace.
Avoid rewriting AcpSessionBridge into a multi-tenant bridge for the first version.
Add clear process-level capacity limits so one daemon cannot grow without bounds as workspaces are added.
Non-goals for v1
Dynamic arbitrary workspace registration.
One ACP child serving multiple workspaces.
Shared default sessions across workspaces.
Automatically promoting IDE multi-folder roots into full daemon workspaces.
Breaking or rewriting all legacy URL shapes at once.
Recommended architecture
Use a single daemon process with multiple independent workspace runtimes.
Multi-workspace behavior stays in the serve layer: registry, route resolution, status aggregation, env overlays, and SDK facades. Each runtime remains a familiar single-workspace bridge/service stack.
Key design points
1. Workspace registry
Introduce a WorkspaceRegistry that owns:
the primary runtime;
the list of registered runtimes;
workspace path canonicalization and lookup;
sessionId -> workspaceKey routing;
aggregated status and shutdown.
Unknown workspace paths should return workspace_mismatch; they should not dynamically create runtimes.
Nested workspace lists should be rejected in v1 to avoid ambiguous path ownership.
resolveWorkspace(undefined) is the only primary fallback. A string workspace selector must resolve to a registered workspace or fail with workspace_mismatch; unknown strings must not silently fall back to primary.
2. Legacy compatibility
Legacy workspace-less routes remain primary-only:
/workspace/...
/file
/list
/glob
/session when no cwd is provided
/acp
/voice/stream
Already-parameterized read-only legacy routes can be relaxed to registered workspaces, for example:
GET /workspace/:id/sessions
GET /workspace/:id/session-groups
Singular mutation routes should remain primary-only, use the plural namespace, or be explicitly owner-routed by live session id after authentication and failure-contract review.
3. New workspace-qualified routes
Use a plural namespace:
/workspaces/:workspace/...
Phase 2a does not add new plural URL surfaces; it only relaxes existing read-only GET /workspace/:id/sessions style routes. Starting in Phase 2a, capabilities/status expose a stable workspaces[].id derived from the existing per-workspace storage/identity hash. Before Phase 2b ships the first plural route, the plural resolver must accept both that id and encoded cwd: selectors that are platform absolute paths are canonicalized as paths, all others are id lookups. Resolver tests must cover POSIX paths, Windows drive paths, UNC paths, stable ids, unknown ids, and unknown paths.
The route resolver must:
decode and canonicalize path selectors, or look up stable workspace ids;
require an exact registered workspace match;
reject prefix/startsWith ownership checks;
register plural routes before catch-all legacy routes;
Rate limiting must explicitly map plural routes to the same tier as equivalent singular routes: read, mutation, or prompt.
Deployment docs should call out that encoded absolute cwd values contain %2F; reverse proxies must preserve those paths when clients choose encoded cwd. New clients and proxy deployments should prefer workspaces[].id once plural routes land.
workspaces[] is the authoritative list of workspaces served by this daemon.
workspaces[].id is stable and should be preferred by new clients and proxy deployments once plural routes land.
workspaces[].trusted lets clients disable mutation UI before a 403.
multi_workspace_sessions only advertises multi-workspace session creation/listing and minimum session-scoped dispatch. It does not advertise workspace-qualified file/memory/MCP/settings/ACP/voice/channel support.
multi_workspace_session_rewind advertises owner-routed rewind snapshots and rewind for secondary live sessions only when multiple runtimes are registered.
multi_workspace_session_shell advertises owner-routed shell for secondary live sessions only when multiple runtimes are registered and session shell is effectively enabled.
features[] stays daemon/primary-oriented. Workspace-settings-derived tags such as workspace_voice_transcription should be treated as primary-derived at the top level. Workspace-specific truth should come from workspace-qualified status/voice status.
Feature cache invalidation must be scoped: non-primary settings_changed must not invalidate or flip primary-derived top-level feature tags.
Feature cache invalidation must be implemented at a runtime construction seam shared by production daemon and server fallback paths; do not put the scoping only in a fallback/test publish wrapper.
5. Session routing
POST /session:
omitted cwd uses primary;
provided cwd must resolve to a registered workspace;
target workspace must be trusted; untrusted workspaces return 403 untrusted_workspace and must not spawn a child;
successful creation records the session's workspace ownership.
/session/:id/... routes should route by sessionId -> workspaceKey without requiring clients to resend workspace.
Recommended implementation: the registry should subscribe to or inject bridge session lifecycle callbacks so every bridge session registration/removal updates the index. This covers both REST routes and ACP dispatcher paths such as session/new, load, and resume. If lifecycle callbacks are not introduced immediately, every session creation path must call noteSession, including runtime-bound ACP dispatchers.
Phase 2a uses live-session scanning across registered runtimes for owner resolution, because the first enabled milestone only covers live session prompt/events/cancel/permission and basic live-session lifecycle routes. The bridge lifecycle index remains the recommended hardening before workspace-qualified ACP/Web Shell, broader session-scoped REST, telemetry attribution, or persisted-session management depend on it. Stale index entries after child crash can be lazily cleaned when the target bridge returns not found. Total session capacity should be computed from live bridge sessionCount plus in-flight reservations.
Hard Phase 2a rules:
scan miss returns 404 session_not_found, never primary fallback;
non-primary persisted load/resume is not implemented in Phase 2a. Legacy resume remains primary-storage only; workspace-aware load/resume waits for Phase 2b lifecycle index and an explicit workspace selector.
all /session/:id/* routes must be classified before implementation as Phase 2a dispatch, later, or explicit 404/primary-only. Basic live-session lifecycle routes such as heartbeat, detach, and pending-prompts should dispatch by session ownership in Phase 2a.
if multiple runtimes report the same session id, fail closed with an internal ambiguous-owner error rather than selecting the first runtime.
6. Env reload isolation
reloadDaemonEnv currently mutates daemon-global process.env, which becomes last-writer-wins across workspaces.
For multi-workspace:
parent process.env must remain daemon base env and must not contain any workspace .env / workspace settings.env contribution;
every runtime, including primary, should maintain its own effective env / child env overlay;
POST /workspaces/:workspace/reload recomputes only that runtime's overlay;
response should indicate whether the child must restart for env changes to apply;
GET /workspaces/:workspace/env should return the target runtime's redacted effective env and must not expose another workspace's overlay;
daemon-side workspace helpers, providers, voice, auth, and preflight logic must use the target runtime effective env, not temporary global env mutation.
Phase 2a should add lint/grep guards for serve-layer and @qwen-code/acp-bridge workspace-scoped code: direct process.env reads are disallowed except for base env initialization, serve fast-path env loading, process-level flags, and tests covered by explicit whitelist.
Multi-workspace mode changes primary behavior too: primary .env / settings.env no longer enters parent process.env. Boot logs should show a compatibility warning with the primary workspace env keys moved into the runtime overlay.
Single-workspace mode may keep the current parent-env behavior for compatibility; the clean parent env rule applies once multiple explicit workspaces are enabled.
6b. Trust state
Explicit untrusted workspaces may be registered so clients can show read-only state, but mutating routes and session creation/resume must fail with 403 untrusted_workspace in multi-workspace mode. Do not invent a new trust default: each workspace reuses the existing workspace trust storage, so a never-trusted registered directory is trusted: false. Single-workspace mode keeps today's trust behavior for compatibility.
Phase 2a originally shipped with boot-frozen trust state. #7268 supersedes that limitation: the daemon now monitors semantic trust-policy snapshots and replaces the affected WorkspaceRuntime generation under the same registry key, so explicit trust-policy changes take effect without process restart. The existing /workspace/trust/request remains a request/event surface rather than a direct trust mutation, while the additive v2 status reports configured trust, effective trust, policy revision, and reconciliation state.
Trust changes are runtime-scoped:
legacy /workspace/trust/request remains primary-only and keeps its v1 request/event behavior;
v2 status exposes configured/effective trust plus applying, failed, blocked, and stable reconciliation state;
untrusted -> trusted constructs a fresh generation with current settings, environment, filesystem boundary, tools, services, and caches before admitting future work;
trusted -> untrusted closes the old generation before draining its sessions and services, rejects further work, and never falls back to primary.
7. ACP and WebSocket surfaces
/acp remains primary-only.
Add:
/workspaces/:workspace/acp
Each ACP dispatcher is runtime-bound. JSON-RPC params may include workspaceCwd only as a consistency check, not as a way to switch runtime inside one connection.
/workspaces/:workspace/acp and /workspaces/:workspace/voice/stream are WebSocket upgrade paths. They will not automatically go through Express routes. The HTTP server upgrade listener must parse the URL, authenticate as today, resolve workspace, and dispatch to the target runtime's ACP dispatcher or voice handler.
These workspace-qualified WebSocket surfaces are not part of the first ungated milestone. Initially /acp and /voice/stream remain primary-only; workspace-qualified ACP/voice should land as separate follow-up phases once the upgrade-path dispatchers are ready.
8. Channel workers
qwen serve --channel currently assumes one workspace. Multi-workspace daemon support eventually needs:
selected channel cwd must resolve to a registered workspace;
worker processes should be grouped by workspace;
QWEN_DAEMON_WORKSPACE and worker cwd should use the owning workspace;
--channel all should remain primary-only in v1 to avoid implicit cross-workspace process fan-out;
pidfile/status should add worker-list data while preserving old single-worker fields.
For the first session closed-loop milestone, channel workers should remain primary-only, or the daemon should boot-error for multi-workspace plus non-primary channel selection until worker grouping, pidfile, and status are implemented.
9. Observability and daemon identity
Do not keep daemon identity tied to one workspace hash:
log file should become daemon-scoped, e.g. serve-<pid>.log;
telemetry service id should be daemon-scoped, e.g. daemon:<pid>;
workspace should be a log/span/metric attribute;
HTTP telemetry middleware must read resolved workspace from request context rather than closure-capturing primary;
daemon gauges should aggregate across runtimes;
child RSS/CPU should support total and per-workspace views;
PermissionAuditRing entries should include workspace, or per-runtime rings should be aggregated.
10. Capacity
Keep --max-sessions as per-workspace in multi-workspace mode, and add maxTotalSessions as a process-level hard cap.
Admission for total sessions should happen in serve/registry, not inside a single bridge:
aggregate live bridge session counts;
maintain in-flight reservations for fresh session creation;
atomically reserve before fresh spawn/load/resume/branch;
rollback on creation failure;
release on close/delete;
do not count attach to an existing session;
reject with existing capacity semantics: HTTP 503 + Retry-After: 5.
The admission hook must sit at the bridge fresh-creation seam, not only in REST handlers. It must cover REST POST /session and primary /acp dispatcher creation paths, while preserving attach-to-existing bypass.
The existing process-level rate limiter remains global in v1. This should be documented as a known limitation: one workspace can consume the shared HTTP quota.
Other v1 limitations that should be explicit in user/deploy docs:
single daemon fault radius is larger than multi-daemon: crash/restart/upgrade affects all registered workspace live sessions, and v1 static workspace add/remove requires daemon restart;
one daemon auth token covers all registered workspaces; v1 has no per-workspace ACL, and workspace isolation is not an access-control boundary;
default maxTotalSessions = maxSessionsPerWorkspace * workspaceCount preserves migration expectations but does not provide extra RSS protection unless the operator sets a lower total;
v1 has no independent workspace-level event bus; current workspace events depend on live session event buses. Idle workspace state changes may only be discoverable by polling /capabilities or daemon status unless a future daemon/workspace-level event aggregation API is added;
encoded cwd plural routes contain %2F and need reverse proxy validation, especially before workspace-qualified ACP/voice WebSocket is exposed.
Rollout plan
Phase 1: single-runtime registry and gated prep
Status: complete. #6394 merged the primary single-runtime registry and daemon-scoped identity work; #6410 completed the remaining repeatable---workspace guardrail and kept multiple explicit workspaces gated until the later Phase 2a sessions ungate in #6511.
Introduce WorkspaceRegistry / WorkspaceRuntime.
Wrap the current single bridge/service/fsFactory as the primary runtime.
Move daemon log filename and telemetry service id to daemon-scoped identity, with workspace as an attribute. This is low risk but observable, so the changelog should name it.
Explicitly handle repeatable --workspace / yargs array input; before the feature gate is removed, multiple explicit workspaces boot-error instead of entering a half-enabled state.
Test both yargs string and array shapes. v1 does not add --primary-workspace; the first --workspace value is primary and workspaces[].primary is observable.
Keep existing tests passing.
Phase 2a: multi-workspace sessions only
Status: complete for the sessions-only milestone. #6410 established the internal contract and route-classification baseline for this phase. #6416 added runtime env isolation guardrails and total-session admission, including --max-total-sessions and daemon-status limit surfacing. #6511 enabled non-primary runtimes for the live session closed loop, published the additive workspaces[] capabilities/status shape and multi_workspace_sessions feature tag when multiple runtimes are registered, and updated the SDK capability types plus daemon documentation for the Phase 2a boundary.
Phase 2a was split across PRs and is now ungated for the sessions-only closed loop. Future phases should keep the same pattern: unsupported surfaces must remain primary-only or return clear unsupported / workspace_mismatch errors until their complete slice lands.
Phase 2a is the first ungated milestone. Its acceptance bar is intentionally narrow:
session-scoped dispatch for prompt/events/cancel/session-scoped permission plus basic live-session lifecycle routes such as heartbeat/detach/pending-prompts, after a complete /session/:id/* route classification pass;
GET /workspace/:id/sessions for registered workspaces, with non-primary Phase 2a results live-only or persisted entries marked resumable: false;
live session owner resolution may scan registered runtimes first; scan miss is 404 session_not_found, never primary fallback; bridge lifecycle session index is Phase 2b hardening;
non-primary persisted load/resume remains out of scope for Phase 2a;
max total session admission through the bridge fresh-creation seam, covering both REST and /acp dispatcher creation paths;
boot logs resolved session caps and per-workspace trusted state;
primary-only preheat and lazy non-primary runtime spawn;
clean parent env plus per-runtime env overlays, backed by direct-process.env lint guards and primary-env compatibility boot logs;
public docs and changelog updates before removing the feature gate.
Surfaces outside this sessions-only loop remain primary-only or return a clear unsupported / workspace_mismatch until their plural route or workspace-qualified transport lands. This explicitly excludes file APIs, memory, MCP, settings, voice, workspace-qualified ACP/WebSocket, channel workers, dynamic add/remove, persisted-only operations, and a new SDK workspace client.
Phase 2b: session hardening and basic workspace-qualified reads
Status: complete for the persisted session foundation described here. #6540 and #6558 landed owner tracking, workspace-aware persisted load/resume, and active persisted listing; #6631 completed archived and organized listing; #6567 added workspace-qualified export/archive/unarchive/delete REST plus SDK clients; #6844 added the independent active-export capability; #6911 added archived export with its own capability and SDK method.
Add bridge lifecycle session index.
Treat the lifecycle index as new bridge API surface, not simple wiring; bridge options currently need a new session registered/closed callback seam.
Add workspace-aware load/resume with explicit workspace selector; do not scan every workspace persisted store.
Add model/mode, metadata/status/tasks, A2UI, and other non-persisted session-adjacent routes.
Add /workspaces/:workspace/sessions and any other minimal read-only workspace status/list endpoints needed by clients.
Relax parameterized read-only legacy routes where compatible.
Add rate-limit tier mapping for plural routes so read/mutation/prompt semantics match the corresponding singular routes.
Make plural route workspace selectors accept both workspaces[].id and encoded cwd.
Phase 3: workspace-qualified REST
Status: complete. #6567 merged the Phase 3 core REST slice.
Add plural routes for core persisted session storage/organization/export/archive/delete, files, memory, agents, settings, trust, permissions, tools, lifecycle, and MCP.
Keep workspace-less batch routes primary-only.
Add WorkspaceDaemonClient via workspaceById() and workspaceByCwd().
Advertise workspace_qualified_rest_core when the binary supports the plural core REST contract.
Exclude auth, voice, extensions, ACP/WebSocket transport, and channel-worker routing from this phase.
Phase 4: workspace-qualified ACP
Status: complete for the workspace-qualified ACP and Web Shell workspace-management slices. #6621 merged the runtime-bound workspace-qualified ACP transport, and #6625 merged the Web Shell workspace sidebar plus runtime registration through POST /workspaces.
Mount /workspaces/:workspace/acp for both the Streamable HTTP endpoint and the reverse WebSocket, dispatched from the daemon's single upgrade listener by URL path; legacy /acp stays bound to the primary runtime.
Give each workspace runtime its own ACP dispatcher, connection registry, device-flow registry, remember lane, and reverse client-MCP, all bound to that runtime's bridge.
Advertise workspace_qualified_acp when more than one runtime is registered; reject untrusted (untrusted_workspace) and unknown (workspace_mismatch) selectors.
#6511 removed the sessions-only multi-workspace feature gate and updated daemon documentation for that milestone. Future PRs that add Phase 2b/3 surfaces must keep public docs aligned at the same time:
docs/users/qwen-serve.md: repeatable --workspace, --max-sessions per-workspace meaning, maxSessionsPerWorkspace, maxTotalSessions, and the "Multi-session & multi-workspace deployment" section.
docs/developers/examples/daemon-client-quickstart.md: use the session workspace picker first; document WorkspaceDaemonClient later with workspace-qualified REST; keep multi-daemon as fallback.
docs/users/qwen-serve-deploy-local.md: update "Cross-host federation / multi-daemon coordination on one host" so multi-daemon is fallback/federation rather than the only multi-workspace model.
Other daemon/channel docs that still state "one daemon = one workspace".
changelog / compatibility note for --max-sessions, maxTotalSessions, daemon log/telemetry identity migration, new workspace-qualified routes, single-daemon fault radius, no per-workspace ACL, global rate-limit fairness limits, and encoded cwd reverse-proxy requirements.
Test plan
The implementation should include focused tests for:
workspace canonicalization, duplicate/nested rejection, and unknown workspace mismatch;
primary fallback for omitted workspace;
non-primary session creation and session-scoped prompt/events/cancel/permission routing;
singular rewind snapshots, rewind, and shell dispatch by live session owner, including strict rewindFiles boolean validation, authenticated REST rewind under ACP-configured SDK transports, owning-workspace shell cwd, capability gating, and fail-closed unknown/untrusted/ambiguous owners;
singular continue, language, and artifact add/delete dispatch by live session owner, including secondary-only bridge execution, parameter forwarding, primary/single-workspace non-regression, authentication compatibility, live-session-only artifact behavior, and fail-closed unknown/untrusted/ambiguous owners;
Phase 2a live owner resolution by scanning registered runtimes, with scan miss returning 404 and no primary fallback; Phase 2b registry session index hit, stale fallback, and ACP dispatcher-created sessions;
Phase 2a route classification for every /session/:id/* route, with heartbeat/detach/pending-prompts dispatched by ownership and persisted sidecar/storage routes explicitly later or primary-only;
Phase 2a non-primary persisted load/resume remaining unsupported; workspace-aware load/resume only after Phase 2b lifecycle index and explicit workspace selector;
maxTotalSessions admission at bridge fresh-creation seams for REST and /acp, with attach excluded from capacity accounting;
clean parent env plus per-runtime env overlay used for child spawn and daemon-side helpers, with no primary .env leakage to non-primary effective env/status views;
lint/grep guard that blocks direct workspace-scoped process.env reads in serve/acp-bridge code except whitelisted base env initialization and serve fast-path env loading;
boot log compatibility warning for primary env keys moved out of parent process.env in multi-workspace mode;
untrusted workspace session creation/resume returning 403 without child spawn; the original Phase 2a boot-frozen limitation is superseded by feat(serve): Hot-reload workspace trust changes #7268 runtime-generation reconciliation;
GET /workspace/:id/sessions accepting registered workspaces in Phase 2a, with non-primary live-only or resumable: false; workspace-qualified session list and storage operations later;
feature-cache invalidation scoping exercised through a production-and-fallback shared runtime construction seam;
boot logs for resolved session caps and per-workspace trusted state;
first milestone guards for unsupported surfaces, including primary-only channel/voice/ACP or clear boot/runtime errors;
later channel worker grouping and --channel all primary-only behavior;
rate-limit tier mapping for plural routes;
WS upgrade routing for workspace ACP and voice;
observability aggregation and workspace attributes;
idle workspace event limitation: status/capabilities polling is required when no live session event bus exists;
repeatable --workspace array handling for primary-only compatibility and multi-workspace sessions startup;
encoded cwd %2F route/proxy documentation or verification;
documentation updates shipped with feature gate removal, including the narrow multi_workspace_sessions tag.
Current implementation status
feat(cli): Add Phase 1 workspace runtime registry #6394 is merged: Phase 1 single-runtime WorkspaceRegistry / WorkspaceRuntime, primary runtime wrapping, daemon-scoped log/telemetry identity, and compatibility locals are in main.
feat(cli): Add Phase 2a workspace foundation #6410 is merged: the Phase 1 follow-up guardrail and Phase 2a foundation contract are in main, including repeatable --workspace parsing/fallback behavior, duplicate/nested multi-workspace input validation, internal runtime metadata, registry lookup/owner-resolution contracts, injected registry assembly checks, and the Phase 2a route-classification design doc.
feat(cli): Add serve env isolation and total admission #6416 is merged: Phase 2a env isolation and total-admission guardrails are in main, including runtime-local env snapshot plumbing, process-env guard tests, explicit ACP child spawn env, --max-total-sessions, total admission at bridge fresh-creation seams, and additive daemon-status limits.maxTotalSessions.
feat(cli): Enable multi-workspace session routing #6511 is merged: the Phase 2a sessions-only closed loop is in main, including multiple explicit workspace startup, non-primary runtime registration, multi_workspace_sessions / workspaces[] capabilities/status ungate, explicit-cwd session creation, live session owner dispatch for the classified routes, and live-only non-primary workspace session listing.
feat(cli): Add session owner index for workspace runtimes #6540 is merged: Phase 2b session owner index hardening is in main, adding lifecycle-backed ownership tracking for workspace runtimes, workspace-aware persisted load/resume, and the first plural workspace sessions alias.
feat(cli): Add workspace-qualified core REST routes #6567 is merged: Phase 3 workspace-qualified core REST is in main, including /workspaces/:workspace/... for core route surfaces, id/cwd workspace selectors, workspace_qualified_rest_core, and SDK workspace clients while keeping auth, voice, extensions, ACP/WebSocket, and channel-worker routing out of scope.
feat(web-shell): add workspace path lock #6853 is merged: embedders can select or lock Web Shell by workspace path; locked unknown secondary workspaces are persistently registered, unrelated workspace-management UI is hidden, and session plus Scheduled Tasks operations stay scoped to the locked workspace.
feat(serve): persist dynamic workspace registrations #6716 is merged: dynamic workspace registrations can persist across daemon restarts, with registration list/forget APIs, SDK and Web Shell persistence negotiation, and lazy workspace-qualified ACP mounts for restored or newly added runtimes.
feat(serve): support runtime workspace removal #6745 is merged: removable secondary runtimes support synchronous two-phase drain and removal through DELETE /workspaces/:workspace, with busy/force behavior, persistent-alias deletion, workspace-owned resource cleanup, SDK/WebUI/Web Shell integration, and clean same-path re-add while preserving workspace data.
fix(cli): Scope session organization mutations by workspace #6724 is merged: trusted non-primary active persisted, archived persisted, and live-only sessions can update pin, group, and color state through the workspace-qualified REST route and SDK workspace client while the legacy mutation remains primary-only.
feat(serve): Expose read-only untrusted session catalogs #6717 is merged: untrusted secondary workspaces expose persisted-only session and session-group catalogs, and Web Shell displays them as non-interactive read-only rows without ACP/live-runtime access or polling.
feat(serve): add workspace persisted transcript reader #6740 is merged: trusted workspaces and registered untrusted secondary workspaces can page active persisted transcript projections through daemon-local workspace-qualified REST without ACP/live bridge/settings access or workspace-storage writes. The SDK forces native REST, cursors expire on daemon restart, and the safe Web Shell viewer remains follow-up work.
feat(cli): Add runtime daemon channel control #6741 is merged: daemon-managed channel workers support authenticated runtime enable, replace, query, reload, and stop through HTTP, the TypeScript SDK, and daemon-aware qwen channel CLI commands. Runtime selections are not persisted; per-workspace isolation, rollback, PID-lease ownership, and boot-time compatibility are preserved.
feat(serve): Bound persisted transcript pages #6769 is merged: workspace-qualified persisted transcript pages are bounded by persisted-source, serialized-response, and cursor budgets while preserving record-boundary pagination, the existing successful response schema, legacy singular-route behavior, and the no-ACP/no-storage-write contract. The safe Web Shell viewer is tracked in feat(web-shell): Add safe read-only transcript viewer #6770.
fix(serve): route session actions to the owning workspace #6798 is merged: six live-session REST actions—metadata update, recap, BTW, mid-turn message, task cancellation, and goal clearing—now route to the owning trusted workspace runtime while preserving legacy URLs and contracts.
feat(serve): support multi-workspace rewind and shell #6826 is merged: rewind snapshots, rewind, and shell now route singular live-session requests to the owning trusted runtime; SDK rewind remains strict-auth REST, shell preserves its configured transport, and capability tags gate secondary-workspace support.
fix(serve): Route session continue, language, and artifacts by owner #6833 is merged: continue, language, and artifact add/delete now route singular live-session requests to the owning trusted runtime while preserving legacy URLs, authentication and validation contracts, live-session-only artifact behavior, and primary/single-workspace behavior.
feat(serve): Add workspace-qualified Voice #6839 is merged: Phase 4b workspace-qualified Voice is in main, including runtime-selected settings, batch transcription, and streaming transcription; runtime-local environment and settings isolation; process-wide admission across legacy and qualified REST/WebSocket operations; workspace drain/removal/shutdown integration; TypeScript workspace-client methods; and the workspace_qualified_voice capability. Legacy Voice routes remain primary-only, and secondary-workspace Web Shell Voice controls are tracked in feat(web-shell): Add secondary-workspace Voice controls #6972.
feat(cli): Add archived session export #6911 is merged (merge commit 7a1b182cd15dc67c8eb1c3333dbdeaadfbb574e6): trusted registered workspaces can export archived sessions through the independent workspace_archived_session_export capability and WorkspaceDaemonClient.exportArchivedSession, using selected-workspace-only archived storage under a shared archive lease without ACP startup or primary/active fallback.
feat(daemon): Aggregate deep health across workspaces #6961 is merged: GET /health?deep=1 now reports a conservative daemon-wide snapshot across active and draining workspace runtimes, including aggregate activity/session counters and workspaceCount, while shallow health remains unchanged and bootstrap or aggregation failures return retryable degraded responses.
feat(serve): Complete legacy session workspace telemetry #7003 is merged (merge commit e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy /session, /sessions, and /permission routes now emit canonical request spans. The 41 handler-resolved routes bind qwen-code.workspace.hash to the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.
fix(serve): Harden multi-workspace ownership guards #7005 is merged (merge commit 117adc32eef1818c05a9cdfdeb939b97b454c0fa): branch, fork, and cd are an explicit primary-only route set with parameterized secondary-workspace regression coverage; direct process.env access is limited to exact process-scoped allowances; workspace GitHub setup uses the owning runtime environment; and daemon-route ownership checks are part of the review rules.
docs(serve): Close multi-workspace hardening gaps #7019 is merged (merge commit 357660f32be9ee63b8d5e7598e8d96a5ca5fc918): the ownership and fail-closed model, current multi-workspace user/developer guidance, superseded single-workspace assumptions, and the exact conditional-capability documentation contract are in main.
feat(web-shell): add archived session export #6910 is merged (merge commit 660ae9f712db060586433f7849034cfd72b4ffc8): trusted archived sessions expose capability-gated, owner-routed export in Web Shell, with progress keyed by workspace and session id.
feat(serve): Hot-reload workspace trust changes #7268 is merged (merge commit c4859627a77ed7c3fe015d505d82645cd672188a): workspace trust policy changes now reconcile affected runtime generations in process, fail closed during transition/failure, and expose additive v2 configured/effective/reconciliation status through the SDK while preserving v1 request behavior.
Phase 1 landed registry/runtime wrapping, daemon-scoped identity, repeatable --workspace parsing, and the temporary multi-workspace boot-error gate.
Repeatable --workspace <dir> is the v1 CLI shape; first value is primary; no --primary-workspace in v1.
multi_workspace_sessions is the first feature tag and Phase 2a milestone boundary.
--max-sessions is per-workspace in multi-workspace mode, with maxTotalSessions as the process cap.
Trust state reuses existing workspace trust storage; never-trusted registered workspaces are read-only/untrusted. feat(serve): Hot-reload workspace trust changes #7268 replaces the historical Phase 2a boot-frozen behavior with monitored, fail-closed runtime-generation reconciliation and additive v2 configured/effective status.
v1 documents single-daemon fault radius and lack of per-workspace ACL; multi-daemon remains the cross-trust-domain fallback.
Phase 2a scans live runtimes for session owner resolution; scan miss is 404, and non-primary persisted load/resume waits for Phase 2b.
Stable workspaces[].id is exposed in Phase 2a; Phase 2b plural routes accept both id and encoded cwd using platform absolute-path detection, with Windows path tests.
Per-workspace rate-limit fairness remains a documented v1 limitation and can be evaluated after the process-global limiter ships with plural route tier mapping.
Update after secondary live-session owner-routing merge
#6798 is now merged (merge commit 3d5dd41bc740d04ce91deef26f82dbd5dc695251). Metadata update, recap, BTW, mid-turn message, task cancellation, and goal clearing now dispatch to the owning trusted workspace runtime. The existing workspace-less URLs, contracts, authentication, and primary-workspace behavior remain unchanged.
#6798, #6826, and #6833 are now merged. #6798 routes metadata update, recap, BTW, mid-turn message, task cancellation, and goal clearing to the owning trusted workspace runtime. #6826 closes the rewind snapshots, rewind, and shell gap. #6833 closes continue, language, and live-session artifact add/delete for secondary live sessions. #6844 separately closes the trusted active-persisted-session export gap through a workspace-qualified route while preserving the primary-bound legacy export route.
The following legacy session routes remain unsupported or primary-bound for non-primary live sessions:
Derived execution: POST /session/:id/branch and POST /session/:id/fork; these need independent ownership, admission, cleanup, and resulting-session registration review.
Workspace semantics change: POST /session/:id/cd; this needs an explicit design for whether session ownership moves, remains fixed, or rejects cross-workspace targets.
Workspace-qualified active export is merged in #6844, and archived export is merged in #6911. Workspace-qualified archive, unarchive, and delete REST/SDK support is already merged through #6567; WebShell archive/unarchive hardening is merged in #6912, while delete behavior remains unchanged and outside that PR.
These gaps should remain explicit primary-only/unsupported behavior until each complete owner-routing or workspace-qualified slice lands. They should be handled in follow-up PRs; they remain outside the merged scopes of #6798, #6826, #6833, and #6844.
Update after archive hardening and archived export merges (2026-07-15)
#6912 is now merged (merge commit ca5019968aa07e52d374322e2de863d6891de568), and #6911 is now merged (merge commit 7a1b182cd15dc67c8eb1c3333dbdeaadfbb574e6); both are based on the already-merged #6567. #6912 hardens the existing non-primary WebShell archive/unarchive path with exact capability and trust gates, errors[] handling, complete active/archived/workspace reconciliation, and (workspaceCwd, sessionId) row identity so equal ids do not collide; delete behavior, REST contracts, SDK types, and persistence stay unchanged. #6911 adds a separate trusted-only GET /workspaces/:workspace/session/:id/archive/export route, workspace_archived_session_export, and WorkspaceDaemonClient.exportArchivedSession; it reads only the selected workspace archive under a shared archive lease, never falls back or starts ACP, enforces a 256 MiB archived-source limit, and defines active-only, missing, conflict, transition, oversized, and untrusted boundaries. The final #6911 revision also centralizes SessionNotArchivedError in the shared ACP bridge error taxonomy, adds direct unit coverage for all assertSessionArchived states and the archived-source ENOENT path, and keeps the route, capability, SDK, export format, and archive-state contracts unchanged.
The PRs have no production dependency on each other and merged independently. The explicitly remaining roadmap is the safe WebShell transcript viewer (#6770), Web Shell per-workspace Settings/Memory/MCP control parity tracked in #6974, secondary-workspace Web Shell Voice controls tracked in #6972, derived-session branch/fork routing, cd ownership semantics, any separately scoped delete UI work, and review-discovered hardening or documentation.
Final multi-workspace hardening/docs closeout (2026-07-17)
#6961 is merged (merge commit ad65ba3bdeeb3b84b5e366c4a7acc3fb2a1d5693) and closes the primary-only deep-health observability gap by aggregating active and draining runtimes while preserving the exact shallow-health response.
#7003 is merged (merge commit e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy /session, /sessions, and /permission routes now emit canonical request spans. The 41 handler-resolved routes bind qwen-code.workspace.hash to the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.
#7005 is merged (merge commit 117adc32eef1818c05a9cdfdeb939b97b454c0fa) and completes the invariant-hardening batch. Branch, fork, and cd are the exact explicit primary-only live-session route set and return the stable fail-closed contract for secondary sessions; parameterized tests verify that neither the primary nor secondary bridge performs the rejected operation. Direct process.env access is restricted to exact process-scoped allowances, workspace GitHub setup receives the owning runtime environment, and daemon-route ownership review checks are recorded in AGENTS.md.
#7019 is merged (merge commit 357660f32be9ee63b8d5e7598e8d96a5ca5fc918) and completes the documentation/protocol-contract batch. The repository now records the process-global, legacy-primary, workspace-qualified, live-session-owner, and persisted-workspace ownership classes; fail-closed semantics and the no-primary-fallback invariant; process-wide authentication/rate-limit/fault-radius boundaries; current quickstart, local deployment, channel, resource, MCP pool, and Web Shell behavior; and superseded single-workspace assumptions. The documented conditional capability tags are contract-tested for exact equality with CONDITIONAL_SERVE_FEATURES.
#7268 is merged (merge commit c4859627a77ed7c3fe015d505d82645cd672188a) and closes the deferred runtime trust-apply gap. Semantic policy monitoring now closes and replaces affected workspace generations without restarting the daemon, fails closed across workspace-scoped consumers during transition or failure, and exposes additive v2 configured/effective/reconciliation status through the TypeScript SDK while preserving the v1 trust-request contract.
The hardened baseline is therefore fixed:
Unknown, untrusted, ambiguous, bootstrapping, draining, and removed workspace/session states do not silently fall back to primary.
Workspace-scoped env, bridge, service, filesystem, trust, and persistence access must follow the selected or owning runtime; new direct process.env reads fail the guard unless explicitly justified as process-scoped.
Trust-policy changes close the old generation before drain, publish no stale generation result, and expose configured/effective/reconciliation state without primary fallback.
Branch, fork, and cd remain deliberately primary-only with a stable compatibility response; no capability or unsupported secondary behavior was introduced by the closeout.
Shallow health remains exactly compatible, while deep health observes active and draining runtimes without exposing workspace paths.
Conditional capability documentation cannot drift from the implementation tag set.
Explicitly deferred limitations remain outside this umbrella:
A daemon token is not a per-workspace ACL. Rate limiting and fault radius remain process-global, so separate daemons remain the isolation boundary for different trust domains.
--channel all remains primary-only. Per-runtime ACP children and the current MCP workspace-pool/legacy fallback distinction remain the documented resource model.
No speculative Web Shell delete issue is created without a reproducible user need.
Independent roadmap work remains focused in #6770 (safe read-only transcript viewer), #6974 (workspace-scoped Settings/Memory/MCP controls), and #6972 (secondary-workspace Voice controls). #6910 is also merged and completes the Web Shell entry point for owner-routed archived-session export.
The #6378 fixed baseline is complete. Future correctness, security, isolation, data-loss, or regression findings should be filed as focused issues; new capabilities and pure refactors should not reopen this umbrella by default.
Summary
This is an RFC for supporting multiple workspaces from a single
qwen servedaemon process while preserving the current single-workspace behavior for existing clients.The current public model is effectively:
That assumption appears across startup config, HTTP routing, ACP bridge ownership, SDK capability shape, channel workers, and public docs. At the same time, the codebase already has partial multi-root and per-workspace session storage building blocks, but those are not enough to provide true multi-workspace daemon semantics.
Full drafted design document is available on this branch for review:
https://github.com/doudouOUC/qwen-code/blob/docs/daemon-multi-workspace-design/docs/design/daemon-multi-workspace.md
Related daemon tracking issues:
Update after maintainer review: this RFC now scopes the first ungated milestone down to multi-workspace sessions only. The broader workspace-qualified REST, ACP/Web Shell, channel worker grouping, voice, SDK workspace client, and dynamic workspace surfaces remain in later phases.
Update after additional code review: this RFC now tightens env isolation, untrusted workspace/session semantics, trust-state changes, fault-radius documentation, and several implementation guardrails. In multi-workspace mode the daemon parent env must stay as daemon base env; all runtimes, including primary, use runtime-local env overlays. Untrusted workspaces can be registered and shown read-only, but cannot create/resume sessions or spawn ACP children.
Update after reviewer scope feedback: the first feature tag is now
multi_workspace_sessions, not genericmulti_workspace. Phase 2a only proves session creation/listing and the minimum live session routes. File, memory, MCP, settings, voice, workspace-qualified ACP/WebSocket, channel workers, persisted operations, andWorkspaceDaemonClientare explicit follow-ups.Update after reviewer confirmation: the 7 open decisions are now folded into the RFC. Phase 1 landed with a single-runtime registry, daemon-scoped identity, repeatable
--workspaceparsing, and a temporary multi-workspace boot-error gate. Phase 2a exposes stableworkspaces[].id, keeps live session scan misses as 404 with no primary fallback, does not implement non-primary persisted load/resume, reuses existing workspace trust storage, and adds lint guards so workspace-scoped serve/bridge code cannot directly readprocess.env.Update after code-seam verification: Phase 2a is now explicitly conditional on four additional implementation contracts. Trust is boot-frozen in Phase 2a and runtime trust apply moves to the phase that lands workspace-qualified trust mutation / watcher / reload-time re-evaluation.
maxTotalSessionsadmission must be anchored at bridge fresh-creation seams so REST and/acpdispatcher paths cannot bypass it. The RFC no longer assumes a runtime-level event bus; idle workspace state changes are discoverable by polling status/capabilities. Phase 2a classified all/session/:id/*routes before implementation, with basic live-session lifecycle routes such as heartbeat/detach/pending-prompts dispatched by ownership.Update after Phase 2a sessions merge: Phase 1, the Phase 2a foundation, the env/admission guardrails, and the sessions-only closed loop are now merged. #6394 landed the single-runtime
WorkspaceRegistryboundary plus daemon-scoped log/telemetry identity. #6410 landed the repeated--workspaceparser/fast-path guardrail, duplicate/nested explicit-workspace validation, the expanded internal registry/runtime contract, and the Phase 2a route-classification design doc. #6416 landed the runtime-local env overlay foundation, process-env guardrails, explicit ACP child spawn env,--max-total-sessions/ daemonmaxTotalSessions, total-session admission at bridge fresh-creation seams, and additive daemon-status limit surfacing. #6511 landed the sessions closed loop and ungated the narrowmulti_workspace_sessionsmilestone: multiple distinct explicit workspaces can boot as registered runtimes, capabilities/status publishworkspaces[], live session creation can target a registered workspace bycwd, and the classified live session routes dispatch by session ownership. This issue remains open for Phase 2b+ work: persisted non-primary load/resume/export/archive/delete, plural workspace routes, workspace-qualified ACP/WebSocket, file/memory/MCP/settings/voice/channel-worker migration, SDK workspace client APIs, and dynamic workspace management.Update after Phase 2b persisted-list merge: #6540 and #6558 are now merged. Phase 2b has landed lifecycle-backed session owner index hardening for workspace runtimes, workspace-aware persisted load/resume, the first plural workspace sessions alias, and trusted non-primary active persisted session listing with live-session merge. Live session ownership no longer depends only on scanning registered runtimes, persisted restore no longer has to guess across workspace stores, and trusted non-primary session list routes can now discover active persisted sessions. Archived/organized non-primary listing and non-primary export/archive/delete intentionally remain later work. The issue remains open for the next Phase 2b/3+ surfaces: non-primary export/archive/delete, archived and organized non-primary listing, broader plural workspace routes such as
/workspaces/:workspace/..., workspace-qualified ACP/WebSocket, and migration of file/memory/MCP/settings/voice/channel-worker and broader SDK workspace client APIs.Update after Phase 3 core REST PR: #6567 is open against
mainfor the workspace-qualified core REST slice. It adds/workspaces/:workspace/...plural routes for core daemon surfaces, selector resolution by workspace id then encoded absolute cwd,workspace_qualified_rest_core, SDKworkspaceById()/workspaceByCwd()helpers, telemetry attribution for plural routes, and docs/tests for trust and scope semantics. This PR intentionally excludes auth, voice, extensions, ACP/WebSocket transport, and channel-worker routing; those remain in later phases.Update after Phase 4 ACP PR: #6567 (Phase 3 core REST) is now merged, and #6621 is open against
mainfor the workspace-qualified ACP transport slice. It mounts/workspaces/:workspace/acp(Streamable HTTP + reverse WebSocket) with a per-workspace ACP dispatcher, connection registry, device-flow registry, remember lane, and reverse client-MCP, all bound to that runtime's bridge; the daemon's single WebSocket upgrade listener dispatches by URL path. Legacy/acpstays primary-only, untrusted (untrusted_workspace) and unknown (workspace_mismatch) selectors are rejected, andworkspace_qualified_acpis advertised when more than one runtime is registered. Web Shell workspace picker (front-end), per-workspace voice/channel workers, and untrusted read-only relaxation remain later phases.Update after Phase 2b listing-closure merge: #6631 is now merged. It closes the last Phase 2b listing gap — trusted non-primary workspaces now support
archiveState=archived, the organizedview=organizedlist, andgroupfilters on the workspace session list routes (/workspace/:id/sessionsand/workspaces/:workspace/sessions), reading from their ownchats/,chats/archive/, and session-organization stores. The persisted read path is forced for organized and archived views, untrusted workspaces are still refused, and the primary workspace and legacy routes are unchanged. Behavior note: for a non-primary workspace an unknowngroupid now returns404 group_not_found(matching primary) instead of the former phase-limitation error. Per-session organization mutation (pin/assign) for non-primary workspaces later landed in #6724.Update after non-primary session organization mutation merge: #6724 is now merged, closing the write-side gap tracked in #6646. Trusted non-primary workspaces can update per-session pin, group, and color state through
PATCH /workspaces/:workspace/session/:id/organizationfor active persisted, archived persisted, and live-only sessions, using the selected runtime’s bridge and organization sidecar with no cross-workspace fallback. The TypeScript SDK exposes the mutation throughworkspaceById()andworkspaceByCwd(). The legacyPATCH /session/:id/organizationroute remains primary-only; ACP, capability tags, and the sidecar schema are unchanged.Update after Extension Management V2 merge: #6825 is now merged (merge commit
fea3ab385496afdbcf73c1fe1c31f16ac7088c83), superseding #6638. It lands globally shared extension artifacts, transactional and journal-recoverable global mutation coordination, bounded concurrent preparation with serialized durable commits, exact per-workspace activation/projection, runtime reconciliation, structured post-commit warnings, revisioned sensitive-setting bundles, TypeScript SDK helpers, and the additiveextension_management_v2capability while preserving the existingworkspace_extensionscompatibility surface. The workspace-qualified ACP and Web Shell workspace-management slices remain complete through #6621 and #6625, and per-workspace daemon-managed channel workers remain complete through #6635. This umbrella issue remains open for the still-unfinished multi-workspace surfaces, including Web Shell per-workspace Settings/Memory/MCP control parity tracked in #6974, remaining persisted-session mutations, and other follow-ups described below. Workspace-qualified Voice later landed in #6839; its secondary-workspace Web Shell controls are tracked in #6972.Update after untrusted read-only session catalog merge: #6717 is now merged. It allows registered untrusted secondary workspaces to expose persisted-only session and session-group catalogs without live bridge access, ACP startup, settings loading, storage repair, or mutation. Web Shell can expand these workspaces and display historical sessions as non-interactive read-only rows; selecting the workspace, opening sessions, creating or resuming sessions, transcript access, and other runtime-backed surfaces remain blocked. Untrusted primary behavior is unchanged.
Update after persistent dynamic registration and runtime-removal merges: #6716 and #6745 are now merged. #6716 makes dynamically registered workspaces durable across daemon restarts through a bounded user-level registration store, adds registration listing and forget APIs, negotiates persistence through the SDK and Web Shell, and supports lazy workspace-qualified ACP mounts for runtimes registered after startup. #6745 completes the lifecycle with synchronous drain and removal of dynamically added or persisted-restored secondary runtimes. It advertises
workspace_runtime_removal, keeps primary and explicitly configured startup workspaces non-removable, returns409 workspace_busyfor non-force removal with active resources, supports explicit force removal, deletes known persistent registration aliases, disposes workspace-owned sessions, ACP connections, memory tasks, bridges, sub-session launchers, and channel workers, and releases the path for a clean re-add without deleting project files, settings, transcripts, archives, or other workspace data. The TypeScript SDK, WebUI action layer, Web Shell confirmation/busy/force flow, protocol documentation, and lifecycle/concurrency coverage landed with the daemon implementation.Update after non-primary Web Shell and session-mutation follow-ups: #6705, #6737, and #6746 are now merged. #6705 restored the full multi-workspace session-row experience while keeping unsupported non-primary actions read-only, #6737 migrated model and approval-mode changes to the owning workspace runtime so newly registered non-primary sessions can be created and controlled without primary-only errors, and #6746 surfaces trusted cross-workspace sessions in Split View and Session Overview, labels sessions by workspace, and attaches each pane through the session's owning workspace.
Update after persisted transcript and runtime channel-control merges: #6740 and #6741 are now merged. #6740 adds the unconditional
workspace_persisted_transcriptcapability plus a workspace-qualified, daemon-local pager for active persisted transcripts. Trusted workspaces and registered untrusted secondary workspaces can read the existing visible transcript projection without ACP startup, live bridge access, settings loading, storage repair, or creation of the legacy persisted cursor-key file; cursors are intentionally valid only for the current daemon lifetime, and the TypeScript workspace client forces native REST. Transcript attach/live follow, archived reads, and the safe Web Shell read-only viewer remain separate follow-ups. #6741 adds authenticated runtime lifecycle control for daemon-managed channel workers: a daemon started without--channelcan enable, replace, query, reload, and stop its current selection through HTTP, the TypeScript SDK, orqwen channelCLI commands. Runtime selections remain daemon-lifetime only; multi-workspace isolation, replacement rollback, PID-lease safety, and existing boot-time channel behavior are preserved.Update after persisted transcript page-hardening merge: #6769 is now merged. Workspace-qualified persisted transcript reads now enforce a 4 MiB persisted-source budget per page, a 32 MiB serialized-response cap, and 64 KiB cursor bounds. Pagination stops at record boundaries, oversized aggregate records return the structured
413 transcript_page_too_largeerror, and oversized replay state returns the successful page as terminal partial content without issuing an unsafe continuation cursor. The legacy singular transcript route remains unchanged. The Web Shell safe read-only viewer remains tracked separately in #6770.Update after multi-workspace session rewind/shell merge: #6826 is now merged (merge commit
b933b90172058d1908b769799b062fae107b5ee4). The singularGET /session/:id/rewind/snapshots,POST /session/:id/rewind, andPOST /session/:id/shellroutes now resolve a live session’s owning trusted runtime while preserving their existing URLs and failure contracts. SDK rewind calls remain authenticated native REST even when ACP is configured, shell retains its configured transport, and the additivemulti_workspace_session_rewind/multi_workspace_session_shelltags expose secondary-workspace support only when available.rewindFilesmust now be omitted or boolean; file-history storage, primary/single-workspace behavior, and the lack of an ACP rewind vendor method remain unchanged.Update after P0 secondary live-session mutation routing merge: #6833 is now merged (merge commit
9dd8389ebe4ad56e054f33f8f74189354a54a77f). The singularPOST /session/:id/continue,POST /session/:id/language,POST /session/:id/artifacts, andDELETE /session/:id/artifacts/:artifactIdroutes now dispatch to the owning trusted runtime for live sessions while preserving their existing URLs, authentication gates, validation order, archive lease, bridge error mapping, request/response contracts, and primary/single-workspace behavior. No capability, SDK API, type, or response field was added. Artifact mutations remain live-session-only; workspace-qualified active persisted export later landed in #6844, while archived export later landed in #6911. ACP-over-HTTP artifact dispatch, branch/fork admission,cdownership, and any separately scoped delete work remained separate follow-ups at that point. Legacy session-route telemetry later landed in #7003.Update after Phase 4b workspace-qualified Voice merge: #6839 is now merged (merge commit
c7250df8eacc47bfaa2eb6912351797c33c488ea). Trusted registered runtimes can read and update Voice settings, submit batch transcription, and open streaming transcription through workspace-qualified REST and WebSocket routes selected by workspace id or encoded absolute cwd. Requests are bound to the selected runtime’s cwd, effective environment, bridge, and workspace-scoped settings persistence; unknown or untrusted selectors never fall back to primary, and legacy Voice routes remain primary-only. A process-level coordinator caps legacy and qualified REST/WebSocket Voice work at eight and integratesvoiceSessionswith workspace drain, removal, and daemon shutdown; force removal aborts only the selected runtime’s Voice leases. TypeScript workspace-client methods and theworkspace_qualified_voicecapability landed with the daemon implementation. Secondary-workspace Web Shell Voice controls remain out of scope.Update after workspace-qualified session export and Web Shell path-lock merges: #6844 and #6853 are now merged. #6844 adds
GET /workspaces/:workspace/session/:id/exportfor trusted registered workspaces and active persisted sessions, the independentworkspace_session_exportcapability, native-REST TypeScript workspace-client support, and workspace-attributed telemetry while preserving the primary-bound legacy export route. Archived-session export later landed in #6911; workspace-qualified archive/unarchive/delete REST and SDK support was already present through #6567. #6853 lets embedders select or lock Web Shell byworkspaceCwd, persistently registers an unknown secondary workspace when locked, hides unrelated workspace-management surfaces, and keeps session and Scheduled Tasks operations scoped to the locked workspace.Update after daemon-global deep-health merge: #6961 is now merged (merge commit
ad65ba3bdeeb3b84b5e366c4a7acc3fb2a1d5693). It makesGET /health?deep=1a daemon-wide aggregate across every managed workspace runtime, including draining runtimes, addsworkspaceCount, preserves the exact shallow/healthcontract, and returns retryable degraded responses during bootstrap or aggregation failure. This closes the primary-only deep-health observability gap while keeping per-workspace readiness checks and atomic reclaim leases out of scope; this umbrella issue remains open for the other unfinished multi-workspace surfaces.Update after legacy session workspace telemetry merge: #7003 is merged (merge commit
e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy/session,/sessions, and/permissionroutes now emit canonical request spans. The 41 handler-resolved routes bindqwen-code.workspace.hashto the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.Update after workspace trust hot-reload merge: #7268 is now merged (merge commit
c4859627a77ed7c3fe015d505d82645cd672188a). It supersedes the Phase 2a boot-frozen trust limitation with semantic trust-policy snapshots and monitoring, per-workspace runtime-generation reconciliation, and fail-closed route behavior while reconciliation is applying, failed, blocked, draining, or otherwise unavailable. Trust grants rebuild settings, environment, filesystem, tools, services, and caches from fresh state; revocations close the old trusted generation before asynchronous drain. The additive v2 trust status exposes configured/effective trust, policy revision, and reconciliation state through the TypeScript SDK while the existing v1 trust-request behavior remains compatible. Web Shell trust approval and presentation remain outside this merged slice.Goals
AcpSessionBridgeinto a multi-tenant bridge for the first version.Non-goals for v1
Recommended architecture
Use a single daemon process with multiple independent workspace runtimes.
Each registered workspace gets its own runtime:
Multi-workspace behavior stays in the serve layer: registry, route resolution, status aggregation, env overlays, and SDK facades. Each runtime remains a familiar single-workspace bridge/service stack.
Key design points
1. Workspace registry
Introduce a
WorkspaceRegistrythat owns:sessionId -> workspaceKeyrouting;Unknown workspace paths should return
workspace_mismatch; they should not dynamically create runtimes.Nested workspace lists should be rejected in v1 to avoid ambiguous path ownership.
resolveWorkspace(undefined)is the only primary fallback. A string workspace selector must resolve to a registered workspace or fail withworkspace_mismatch; unknown strings must not silently fall back to primary.2. Legacy compatibility
Legacy workspace-less routes remain primary-only:
/workspace/.../file/list/glob/sessionwhen nocwdis provided/acp/voice/streamAlready-parameterized read-only legacy routes can be relaxed to registered workspaces, for example:
GET /workspace/:id/sessionsGET /workspace/:id/session-groupsSingular mutation routes should remain primary-only, use the plural namespace, or be explicitly owner-routed by live session id after authentication and failure-contract review.
3. New workspace-qualified routes
Use a plural namespace:
/workspaces/:workspace/...Phase 2a does not add new plural URL surfaces; it only relaxes existing read-only
GET /workspace/:id/sessionsstyle routes. Starting in Phase 2a, capabilities/status expose a stableworkspaces[].idderived from the existing per-workspace storage/identity hash. Before Phase 2b ships the first plural route, the plural resolver must accept both that id and encoded cwd: selectors that are platform absolute paths are canonicalized as paths, all others are id lookups. Resolver tests must cover POSIX paths, Windows drive paths, UNC paths, stable ids, unknown ids, and unknown paths.The route resolver must:
Rate limiting must explicitly map plural routes to the same tier as equivalent singular routes:
read,mutation, orprompt.Deployment docs should call out that encoded absolute cwd values contain
%2F; reverse proxies must preserve those paths when clients choose encoded cwd. New clients and proxy deployments should preferworkspaces[].idonce plural routes land.4. Capabilities and status
Keep existing fields primary-compatible:
{ "workspaceCwd": "/repo/a" }Add fields:
{ "workspaceCwd": "/repo/a", "workspaces": [ { "id": "ws_a_hash", "cwd": "/repo/a", "primary": true, "trusted": true }, { "id": "ws_b_hash", "cwd": "/repo/b", "primary": false, "trusted": false } ], "limits": { "maxSessionsPerWorkspace": 20, "maxTotalSessions": 40 }, "features": ["multi_workspace_sessions"] }Important compatibility rules:
workspaceCwdcontinues to mean primary workspace.workspaces[]is the authoritative list of workspaces served by this daemon.workspaces[].idis stable and should be preferred by new clients and proxy deployments once plural routes land.workspaces[].trustedlets clients disable mutation UI before a 403.multi_workspace_sessionsonly advertises multi-workspace session creation/listing and minimum session-scoped dispatch. It does not advertise workspace-qualified file/memory/MCP/settings/ACP/voice/channel support.multi_workspace_session_rewindadvertises owner-routed rewind snapshots and rewind for secondary live sessions only when multiple runtimes are registered.multi_workspace_session_shelladvertises owner-routed shell for secondary live sessions only when multiple runtimes are registered and session shell is effectively enabled.features[]stays daemon/primary-oriented. Workspace-settings-derived tags such asworkspace_voice_transcriptionshould be treated as primary-derived at the top level. Workspace-specific truth should come from workspace-qualified status/voice status.settings_changedmust not invalidate or flip primary-derived top-level feature tags.5. Session routing
POST /session:cwduses primary;cwdmust resolve to a registered workspace;untrusted_workspaceand must not spawn a child;/session/:id/...routes should route bysessionId -> workspaceKeywithout requiring clients to resend workspace.Recommended implementation: the registry should subscribe to or inject bridge session lifecycle callbacks so every bridge session registration/removal updates the index. This covers both REST routes and ACP dispatcher paths such as
session/new, load, and resume. If lifecycle callbacks are not introduced immediately, every session creation path must callnoteSession, including runtime-bound ACP dispatchers.Phase 2a uses live-session scanning across registered runtimes for owner resolution, because the first enabled milestone only covers live session prompt/events/cancel/permission and basic live-session lifecycle routes. The bridge lifecycle index remains the recommended hardening before workspace-qualified ACP/Web Shell, broader session-scoped REST, telemetry attribution, or persisted-session management depend on it. Stale index entries after child crash can be lazily cleaned when the target bridge returns not found. Total session capacity should be computed from live bridge
sessionCountplus in-flight reservations.Hard Phase 2a rules:
session_not_found, never primary fallback;/session/:id/*routes must be classified before implementation as Phase 2a dispatch, later, or explicit 404/primary-only. Basic live-session lifecycle routes such as heartbeat, detach, and pending-prompts should dispatch by session ownership in Phase 2a.6. Env reload isolation
reloadDaemonEnvcurrently mutates daemon-globalprocess.env, which becomes last-writer-wins across workspaces.For multi-workspace:
process.envmust remain daemon base env and must not contain any workspace.env/ workspacesettings.envcontribution;POST /workspaces/:workspace/reloadrecomputes only that runtime's overlay;GET /workspaces/:workspace/envshould return the target runtime's redacted effective env and must not expose another workspace's overlay;@qwen-code/acp-bridgeworkspace-scoped code: directprocess.envreads are disallowed except for base env initialization, serve fast-path env loading, process-level flags, and tests covered by explicit whitelist..env/settings.envno longer enters parentprocess.env. Boot logs should show a compatibility warning with the primary workspace env keys moved into the runtime overlay.Single-workspace mode may keep the current parent-env behavior for compatibility; the clean parent env rule applies once multiple explicit workspaces are enabled.
6b. Trust state
Explicit untrusted workspaces may be registered so clients can show read-only state, but mutating routes and session creation/resume must fail with 403
untrusted_workspacein multi-workspace mode. Do not invent a new trust default: each workspace reuses the existing workspace trust storage, so a never-trusted registered directory istrusted: false. Single-workspace mode keeps today's trust behavior for compatibility.Phase 2a originally shipped with boot-frozen trust state. #7268 supersedes that limitation: the daemon now monitors semantic trust-policy snapshots and replaces the affected
WorkspaceRuntimegeneration under the same registry key, so explicit trust-policy changes take effect without process restart. The existing/workspace/trust/requestremains a request/event surface rather than a direct trust mutation, while the additive v2 status reports configured trust, effective trust, policy revision, and reconciliation state.Trust changes are runtime-scoped:
/workspace/trust/requestremains primary-only and keeps its v1 request/event behavior;7. ACP and WebSocket surfaces
/acpremains primary-only.Add:
/workspaces/:workspace/acpEach ACP dispatcher is runtime-bound. JSON-RPC params may include
workspaceCwdonly as a consistency check, not as a way to switch runtime inside one connection./workspaces/:workspace/acpand/workspaces/:workspace/voice/streamare WebSocket upgrade paths. They will not automatically go through Express routes. The HTTP server upgrade listener must parse the URL, authenticate as today, resolve workspace, and dispatch to the target runtime's ACP dispatcher or voice handler.These workspace-qualified WebSocket surfaces are not part of the first ungated milestone. Initially
/acpand/voice/streamremain primary-only; workspace-qualified ACP/voice should land as separate follow-up phases once the upgrade-path dispatchers are ready.8. Channel workers
qwen serve --channelcurrently assumes one workspace. Multi-workspace daemon support eventually needs:QWEN_DAEMON_WORKSPACEand worker cwd should use the owning workspace;--channel allshould remain primary-only in v1 to avoid implicit cross-workspace process fan-out;For the first session closed-loop milestone, channel workers should remain primary-only, or the daemon should boot-error for multi-workspace plus non-primary channel selection until worker grouping, pidfile, and status are implemented.
9. Observability and daemon identity
Do not keep daemon identity tied to one workspace hash:
serve-<pid>.log;daemon:<pid>;PermissionAuditRingentries should include workspace, or per-runtime rings should be aggregated.10. Capacity
Keep
--max-sessionsas per-workspace in multi-workspace mode, and addmaxTotalSessionsas a process-level hard cap.Admission for total sessions should happen in serve/registry, not inside a single bridge:
Retry-After: 5.The admission hook must sit at the bridge fresh-creation seam, not only in REST handlers. It must cover REST
POST /sessionand primary/acpdispatcher creation paths, while preserving attach-to-existing bypass.The existing process-level rate limiter remains global in v1. This should be documented as a known limitation: one workspace can consume the shared HTTP quota.
Other v1 limitations that should be explicit in user/deploy docs:
maxTotalSessions = maxSessionsPerWorkspace * workspaceCountpreserves migration expectations but does not provide extra RSS protection unless the operator sets a lower total;/capabilitiesor daemon status unless a future daemon/workspace-level event aggregation API is added;%2Fand need reverse proxy validation, especially before workspace-qualified ACP/voice WebSocket is exposed.Rollout plan
Phase 1: single-runtime registry and gated prep
Status: complete. #6394 merged the primary single-runtime registry and daemon-scoped identity work; #6410 completed the remaining repeatable-
--workspaceguardrail and kept multiple explicit workspaces gated until the later Phase 2a sessions ungate in #6511.WorkspaceRegistry/WorkspaceRuntime.--workspace/ yargs array input; before the feature gate is removed, multiple explicit workspaces boot-error instead of entering a half-enabled state.--primary-workspace; the first--workspacevalue is primary andworkspaces[].primaryis observable.Phase 2a: multi-workspace sessions only
Status: complete for the sessions-only milestone. #6410 established the internal contract and route-classification baseline for this phase. #6416 added runtime env isolation guardrails and total-session admission, including
--max-total-sessionsand daemon-status limit surfacing. #6511 enabled non-primary runtimes for the live session closed loop, published the additiveworkspaces[]capabilities/status shape andmulti_workspace_sessionsfeature tag when multiple runtimes are registered, and updated the SDK capability types plus daemon documentation for the Phase 2a boundary.Phase 2a was split across PRs and is now ungated for the sessions-only closed loop. Future phases should keep the same pattern: unsupported surfaces must remain primary-only or return clear unsupported /
workspace_mismatcherrors until their complete slice lands.Phase 2a is the first ungated milestone. Its acceptance bar is intentionally narrow:
workspaces[], stableid,primary,trusted,limits,multi_workspace_sessions;cwd;/session/:id/*route classification pass;GET /workspace/:id/sessionsfor registered workspaces, with non-primary Phase 2a results live-only or persisted entries markedresumable: false;session_not_found, never primary fallback; bridge lifecycle session index is Phase 2b hardening;/acpdispatcher creation paths;process.envlint guards and primary-env compatibility boot logs;Surfaces outside this sessions-only loop remain primary-only or return a clear unsupported /
workspace_mismatchuntil their plural route or workspace-qualified transport lands. This explicitly excludes file APIs, memory, MCP, settings, voice, workspace-qualified ACP/WebSocket, channel workers, dynamic add/remove, persisted-only operations, and a new SDK workspace client.Phase 2b: session hardening and basic workspace-qualified reads
Status: complete for the persisted session foundation described here. #6540 and #6558 landed owner tracking, workspace-aware persisted load/resume, and active persisted listing; #6631 completed archived and organized listing; #6567 added workspace-qualified export/archive/unarchive/delete REST plus SDK clients; #6844 added the independent active-export capability; #6911 added archived export with its own capability and SDK method.
/workspaces/:workspace/sessionsand any other minimal read-only workspace status/list endpoints needed by clients.workspaces[].idand encoded cwd.Phase 3: workspace-qualified REST
Status: complete. #6567 merged the Phase 3 core REST slice.
WorkspaceDaemonClientviaworkspaceById()andworkspaceByCwd().workspace_qualified_rest_corewhen the binary supports the plural core REST contract.Phase 4: workspace-qualified ACP
Status: complete for the workspace-qualified ACP and Web Shell workspace-management slices. #6621 merged the runtime-bound workspace-qualified ACP transport, and #6625 merged the Web Shell workspace sidebar plus runtime registration through
POST /workspaces./workspaces/:workspace/acpfor both the Streamable HTTP endpoint and the reverse WebSocket, dispatched from the daemon's single upgrade listener by URL path; legacy/acpstays bound to the primary runtime.workspace_qualified_acpwhen more than one runtime is registered; reject untrusted (untrusted_workspace) and unknown (workspace_mismatch) selectors.Phase 4b: channel workers and voice
/workspaces/:workspace/voice/streamwith correct workspace settings and process-level or explicit per-workspace + total concurrency limits.Phase 5: optional dynamic workspace
Only after static multi-workspace is stable:
POST /workspaces, persistence in feat(serve): persist dynamic workspace registrations #6716, and remove/drain in feat(serve): support runtime workspace removal #6745);Public documentation updates required
#6511 removed the sessions-only multi-workspace feature gate and updated daemon documentation for that milestone. Future PRs that add Phase 2b/3 surfaces must keep public docs aligned at the same time:
docs/users/qwen-serve.md: repeatable--workspace,--max-sessionsper-workspace meaning,maxSessionsPerWorkspace,maxTotalSessions, and the "Multi-session & multi-workspace deployment" section.docs/developers/qwen-serve-protocol.md: capabilities schema,workspaces[], stableid,trusted,limits,multi_workspace_sessions, plural route selector semantics, andworkspace_mismatch/boundWorkspaces.docs/developers/examples/daemon-client-quickstart.md: use the session workspace picker first; documentWorkspaceDaemonClientlater with workspace-qualified REST; keep multi-daemon as fallback.docs/users/qwen-serve-deploy-local.md: update "Cross-host federation / multi-daemon coordination on one host" so multi-daemon is fallback/federation rather than the only multi-workspace model.--max-sessions,maxTotalSessions, daemon log/telemetry identity migration, new workspace-qualified routes, single-daemon fault radius, no per-workspace ACL, global rate-limit fairness limits, and encoded cwd reverse-proxy requirements.Test plan
The implementation should include focused tests for:
rewindFilesboolean validation, authenticated REST rewind under ACP-configured SDK transports, owning-workspace shell cwd, capability gating, and fail-closed unknown/untrusted/ambiguous owners;/session/:id/*route, with heartbeat/detach/pending-prompts dispatched by ownership and persisted sidecar/storage routes explicitly later or primary-only;maxTotalSessionsadmission at bridge fresh-creation seams for REST and/acp, with attach excluded from capacity accounting;.envleakage to non-primary effective env/status views;process.envreads in serve/acp-bridge code except whitelisted base env initialization and serve fast-path env loading;process.envin multi-workspace mode;GET /workspace/:id/sessionsaccepting registered workspaces in Phase 2a, with non-primary live-only orresumable: false; workspace-qualified session list and storage operations later;workspaces[], stableid,trusted,limits, feature cache scoping;--channel allprimary-only behavior;--workspacearray handling for primary-only compatibility and multi-workspace sessions startup;%2Froute/proxy documentation or verification;multi_workspace_sessionstag.Current implementation status
WorkspaceRegistry/WorkspaceRuntime, primary runtime wrapping, daemon-scoped log/telemetry identity, and compatibility locals are in main.--workspaceparsing/fallback behavior, duplicate/nested multi-workspace input validation, internal runtime metadata, registry lookup/owner-resolution contracts, injected registry assembly checks, and the Phase 2a route-classification design doc.--max-total-sessions, total admission at bridge fresh-creation seams, and additive daemon-statuslimits.maxTotalSessions.multi_workspace_sessions/workspaces[]capabilities/status ungate, explicit-cwdsession creation, live session owner dispatch for the classified routes, and live-only non-primary workspace session listing./workspaces/:workspace/...for core route surfaces, id/cwd workspace selectors,workspace_qualified_rest_core, and SDK workspace clients while keeping auth, voice, extensions, ACP/WebSocket, and channel-worker routing out of scope.POST /workspacesfor runtime add, workspace picker UI, per-workspace session display, AddWorkspaceDialog, and route test coverage.DELETE /workspaces/:workspace, with busy/force behavior, persistent-alias deletion, workspace-owned resource cleanup, SDK/WebUI/Web Shell integration, and clean same-path re-add while preserving workspace data.qwen channelCLI commands. Runtime selections are not persisted; per-workspace isolation, rollback, PID-lease ownership, and boot-time compatibility are preserved.workspace_qualified_voicecapability. Legacy Voice routes remain primary-only, and secondary-workspace Web Shell Voice controls are tracked in feat(web-shell): Add secondary-workspace Voice controls #6972.7a1b182cd15dc67c8eb1c3333dbdeaadfbb574e6): trusted registered workspaces can export archived sessions through the independentworkspace_archived_session_exportcapability andWorkspaceDaemonClient.exportArchivedSession, using selected-workspace-only archived storage under a shared archive lease without ACP startup or primary/active fallback.GET /health?deep=1now reports a conservative daemon-wide snapshot across active and draining workspace runtimes, including aggregate activity/session counters andworkspaceCount, while shallow health remains unchanged and bootstrap or aggregation failures return retryable degraded responses.e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy/session,/sessions, and/permissionroutes now emit canonical request spans. The 41 handler-resolved routes bindqwen-code.workspace.hashto the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.117adc32eef1818c05a9cdfdeb939b97b454c0fa): branch, fork, andcdare an explicit primary-only route set with parameterized secondary-workspace regression coverage; directprocess.envaccess is limited to exact process-scoped allowances; workspace GitHub setup uses the owning runtime environment; and daemon-route ownership checks are part of the review rules.357660f32be9ee63b8d5e7598e8d96a5ca5fc918): the ownership and fail-closed model, current multi-workspace user/developer guidance, superseded single-workspace assumptions, and the exact conditional-capability documentation contract are in main.660ae9f712db060586433f7849034cfd72b4ffc8): trusted archived sessions expose capability-gated, owner-routed export in Web Shell, with progress keyed by workspace and session id.c4859627a77ed7c3fe015d505d82645cd672188a): workspace trust policy changes now reconcile affected runtime generations in process, fail closed during transition/failure, and expose additive v2 configured/effective/reconciliation status through the SDK while preserving v1 request behavior.cdremain explicitly primary-only; feat(serve): Route session branch and fork by live workspace owner #7014 and design(serve): Define multi-workspace session cd ownership semantics #7015 are closed as not planned and can be reopened only if concrete demand justifies their ownership designs. Dynamic add, persistence, runtime removal/drain, workspace-qualified Voice, active and archived persisted session export, cross-workspace Split View, Web Shell path locking, archive hardening, archived export UI, and the hardening/docs baseline are complete through feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) #6625, feat(serve): persist dynamic workspace registrations #6716, feat(serve): support runtime workspace removal #6745, feat(serve): Add workspace-qualified Voice #6839, feat(serve): Add workspace-qualified session export #6844, feat(web-shell): flesh out the multi-workspace split view (cross-workspace sessions, workspace labels, responsive layout) #6746, feat(web-shell): add workspace path lock #6853, feat(cli): Add archived session export #6911, fix(web-shell): Harden non-primary session archive actions #6912, feat(web-shell): add archived session export #6910, feat(daemon): Aggregate deep health across workspaces #6961, feat(serve): Complete legacy session workspace telemetry #7003, fix(serve): Harden multi-workspace ownership guards #7005, and docs(serve): Close multi-workspace hardening gaps #7019.Reviewer-confirmed baseline
--workspaceparsing, and the temporary multi-workspace boot-error gate.--workspace <dir>is the v1 CLI shape; first value is primary; no--primary-workspacein v1.multi_workspace_sessionsis the first feature tag and Phase 2a milestone boundary.--max-sessionsis per-workspace in multi-workspace mode, withmaxTotalSessionsas the process cap.workspaces[].idis exposed in Phase 2a; Phase 2b plural routes accept both id and encoded cwd using platform absolute-path detection, with Windows path tests.Follow-up phases
WorkspaceDaemonClientlanded in feat(cli): Add workspace-qualified core REST routes #6567, while dynamic workspace add, persistence, and runtime removal landed across feat(web-shell): workspace management sidebar with dynamic registration (daemon multi-workspace phase 4) #6625, feat(serve): persist dynamic workspace registrations #6716, and feat(serve): support runtime workspace removal #6745.Update after secondary live-session owner-routing merge
#6798 is now merged (merge commit
3d5dd41bc740d04ce91deef26f82dbd5dc695251). Metadata update, recap, BTW, mid-turn message, task cancellation, and goal clearing now dispatch to the owning trusted workspace runtime. The existing workspace-less URLs, contracts, authentication, and primary-workspace behavior remain unchanged.Remaining secondary live-session route gaps (2026-07-14)
#6798, #6826, and #6833 are now merged. #6798 routes metadata update, recap, BTW, mid-turn message, task cancellation, and goal clearing to the owning trusted workspace runtime. #6826 closes the rewind snapshots, rewind, and shell gap. #6833 closes continue, language, and live-session artifact add/delete for secondary live sessions. #6844 separately closes the trusted active-persisted-session export gap through a workspace-qualified route while preserving the primary-bound legacy export route.
The following legacy session routes remain unsupported or primary-bound for non-primary live sessions:
POST /session/:id/branchandPOST /session/:id/fork; these need independent ownership, admission, cleanup, and resulting-session registration review.POST /session/:id/cd; this needs an explicit design for whether session ownership moves, remains fixed, or rejects cross-workspace targets.Workspace-qualified active export is merged in #6844, and archived export is merged in #6911. Workspace-qualified archive, unarchive, and delete REST/SDK support is already merged through #6567; WebShell archive/unarchive hardening is merged in #6912, while delete behavior remains unchanged and outside that PR.
These gaps should remain explicit primary-only/unsupported behavior until each complete owner-routing or workspace-qualified slice lands. They should be handled in follow-up PRs; they remain outside the merged scopes of #6798, #6826, #6833, and #6844.
Update after archive hardening and archived export merges (2026-07-15)
#6912 is now merged (merge commit
ca5019968aa07e52d374322e2de863d6891de568), and #6911 is now merged (merge commit7a1b182cd15dc67c8eb1c3333dbdeaadfbb574e6); both are based on the already-merged #6567. #6912 hardens the existing non-primary WebShell archive/unarchive path with exact capability and trust gates,errors[]handling, complete active/archived/workspace reconciliation, and(workspaceCwd, sessionId)row identity so equal ids do not collide; delete behavior, REST contracts, SDK types, and persistence stay unchanged. #6911 adds a separate trusted-onlyGET /workspaces/:workspace/session/:id/archive/exportroute,workspace_archived_session_export, andWorkspaceDaemonClient.exportArchivedSession; it reads only the selected workspace archive under a shared archive lease, never falls back or starts ACP, enforces a 256 MiB archived-source limit, and defines active-only, missing, conflict, transition, oversized, and untrusted boundaries. The final #6911 revision also centralizesSessionNotArchivedErrorin the shared ACP bridge error taxonomy, adds direct unit coverage for allassertSessionArchivedstates and the archived-source ENOENT path, and keeps the route, capability, SDK, export format, and archive-state contracts unchanged.The PRs have no production dependency on each other and merged independently. The explicitly remaining roadmap is the safe WebShell transcript viewer (#6770), Web Shell per-workspace Settings/Memory/MCP control parity tracked in #6974, secondary-workspace Web Shell Voice controls tracked in #6972, derived-session branch/fork routing,
cdownership semantics, any separately scoped delete UI work, and review-discovered hardening or documentation.Final multi-workspace hardening/docs closeout (2026-07-17)
#6961 is merged (merge commit
ad65ba3bdeeb3b84b5e366c4a7acc3fb2a1d5693) and closes the primary-only deep-health observability gap by aggregating active and draining runtimes while preserving the exact shallow-health response.#7003 is merged (merge commit
e74c0cd33c50cdc8a8f4438dae370b74b91844c6) and completes legacy session workspace telemetry. All 48 explicit legacy/session,/sessions, and/permissionroutes now emit canonical request spans. The 41 handler-resolved routes bindqwen-code.workspace.hashto the runtime actually selected by creation, restore, transcript, or live-owner routing, while the seven primary-bound routes retain primary attribution. Unresolved or ambiguous requests omit the workspace hash, rewind/shell no longer perform duplicate owner resolution, and long-lived session event streams are separated from ordinary HTTP duration/count metrics without changing HTTP, SDK, persistence, session-execution, or metric-cardinality contracts.#7005 is merged (merge commit
117adc32eef1818c05a9cdfdeb939b97b454c0fa) and completes the invariant-hardening batch. Branch, fork, andcdare the exact explicit primary-only live-session route set and return the stable fail-closed contract for secondary sessions; parameterized tests verify that neither the primary nor secondary bridge performs the rejected operation. Directprocess.envaccess is restricted to exact process-scoped allowances, workspace GitHub setup receives the owning runtime environment, and daemon-route ownership review checks are recorded inAGENTS.md.#7019 is merged (merge commit
357660f32be9ee63b8d5e7598e8d96a5ca5fc918) and completes the documentation/protocol-contract batch. The repository now records the process-global, legacy-primary, workspace-qualified, live-session-owner, and persisted-workspace ownership classes; fail-closed semantics and the no-primary-fallback invariant; process-wide authentication/rate-limit/fault-radius boundaries; current quickstart, local deployment, channel, resource, MCP pool, and Web Shell behavior; and superseded single-workspace assumptions. The documented conditional capability tags are contract-tested for exact equality withCONDITIONAL_SERVE_FEATURES.#7268 is merged (merge commit
c4859627a77ed7c3fe015d505d82645cd672188a) and closes the deferred runtime trust-apply gap. Semantic policy monitoring now closes and replaces affected workspace generations without restarting the daemon, fails closed across workspace-scoped consumers during transition or failure, and exposes additive v2 configured/effective/reconciliation status through the TypeScript SDK while preserving the v1 trust-request contract.The hardened baseline is therefore fixed:
process.envreads fail the guard unless explicitly justified as process-scoped.cdremain deliberately primary-only with a stable compatibility response; no capability or unsupported secondary behavior was introduced by the closeout.Explicitly deferred limitations remain outside this umbrella:
cdsemantics remain unsupported. feat(serve): Route session branch and fork by live workspace owner #7014 and design(serve): Define multi-workspace session cd ownership semantics #7015 are closed as not planned because they are not current priorities; closing them does not broaden the contract.--channel allremains primary-only. Per-runtime ACP children and the current MCP workspace-pool/legacy fallback distinction remain the documented resource model.Independent roadmap work remains focused in #6770 (safe read-only transcript viewer), #6974 (workspace-scoped Settings/Memory/MCP controls), and #6972 (secondary-workspace Voice controls). #6910 is also merged and completes the Web Shell entry point for owner-routed archived-session export.
The #6378 fixed baseline is complete. Future correctness, security, isolation, data-loss, or regression findings should be filed as focused issues; new capabilities and pure refactors should not reopen this umbrella by default.