Skip to content

RFC: Support multiple workspaces in one qwen serve daemon #6378

Description

@doudouOUC

Summary

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:

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 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.

Each registered workspace gets its own runtime:

Express app
  |
  +-- global auth / cors / rate limit / access log
  |
  +-- WorkspaceRegistry
        |
        +-- primary runtime
        |     +-- AcpSessionBridge(boundWorkspace=A)
        |     +-- DaemonWorkspaceService(boundWorkspace=A)
        |     +-- WorkspaceFileSystemFactory(boundWorkspaces=[A, ...A roots])
        |     +-- ClientMcpSenderRegistry(A)
        |
        +-- runtime B
              +-- AcpSessionBridge(boundWorkspace=B)
              +-- DaemonWorkspaceService(boundWorkspace=B)
              +-- WorkspaceFileSystemFactory(boundWorkspaces=[B, ...B roots])
              +-- ClientMcpSenderRegistry(B)

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;
  • update SDK ACP route tables so HTTP fallback uses plural paths.

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.

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:

  • workspaceCwd continues to mean primary workspace.
  • 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:

  • multiple explicit workspace startup;
  • capabilities/status workspaces[], stable id, primary, trusted, limits, multi_workspace_sessions;
  • session creation by cwd;
  • 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;
  • untrusted workspace session/mutation gate; the original Phase 2a boot-frozen limitation is superseded by feat(serve): Hot-reload workspace trust changes #7268 runtime-generation reconciliation;
  • 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.

Phase 4b: channel workers and voice

  • Add workspace grouping for daemon-managed channel workers.
  • Add additive pidfile/status worker-list schema while preserving old single-worker fields.
  • Add /workspaces/:workspace/voice/stream with 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:

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-sessions per-workspace meaning, maxSessionsPerWorkspace, maxTotalSessions, and the "Multi-session & multi-workspace deployment" section.
  • docs/developers/qwen-serve-protocol.md: capabilities schema, workspaces[], stable id, trusted, limits, multi_workspace_sessions, plural route selector semantics, and workspace_mismatch / boundWorkspaces.
  • 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;
  • legacy primary-only routes;
  • read-only parameterized legacy routes accepting registered workspaces;
  • env reload isolation and per-runtime effective env;
  • max total session atomic reservation, rollback, attach behavior, and 503 semantics;
  • preheat primary-only behavior;
  • capabilities/status workspaces[], stable id, trusted, limits, feature cache scoping;
  • 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

Reviewer-confirmed baseline

  • 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.

Follow-up phases

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:

  • 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:

  • Secondary-workspace branch/fork owner routing and multi-workspace cd semantics 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.
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions