@qwen-code/qwen-code 0.15.11 → 0.15.12-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ # Daemon mode (`qwen serve`)
2
+
3
+ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs, CI scripts, custom CLIs) share one agent session over HTTP + Server-Sent Events instead of each spawning their own subprocess.
4
+
5
+ > **Status:** Stage 1 (experimental). The protocol surface is locked at the §04 routes table from issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803). Stage 1.5 (`qwen --serve` flag — TUI co-hosts the same HTTP server) and Stage 2 (in-process refactor + `mDNS`/OpenAPI/WebSocket/Prometheus polish) are immediately downstream.
6
+ >
7
+ > **Scope honesty:** Stage 1 is sized for **developers prototyping clients against the protocol surface** and for **local single-user / small-team collaboration**. Production-grade multi-client / long-running / network-flaky workloads (mobile companions, IM bots reaching 1000+ chats) need Stage 1.5+ guarantees that aren't in this release. See [Stage 1.5+ runtime guarantees](#stage-15-runtime-guarantees) for the full gap list and #3803 for the convergence roadmap.
8
+
9
+ ## What it gives you
10
+
11
+ - **One agent process, many clients** — under the default `sessionScope: 'single'`, every client connecting to the daemon shares one ACP session. Live cross-client collaboration on the same conversation, the same file diffs, the same permission prompts.
12
+ - **Reconnect-safe streaming** — SSE with `Last-Event-ID` reconnect lets a client drop and pick up exactly where it left off (within the ring's replay window).
13
+ - **First-responder permissions** — when the agent asks for permission to run a tool, every connected client sees the request; whichever client answers first wins.
14
+ - **One daemon, one workspace** — each `qwen serve` process binds to exactly one workspace at boot (per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02). Multi-workspace deployments run one daemon per workspace on separate ports (or behind an orchestrator).
15
+
16
+ ## Quickstart
17
+
18
+ ### 1. Start the daemon (loopback, no auth)
19
+
20
+ ```bash
21
+ cd your-project/
22
+ qwen serve
23
+ # → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
24
+ # → qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
25
+ ```
26
+
27
+ The default bind is `127.0.0.1:4170`. Bearer auth is **off** on loopback so local development "just works". The daemon binds to the current working directory; use `--workspace /path/to/dir` to override.
28
+
29
+ ### 2. Sanity-check it
30
+
31
+ ```bash
32
+ curl http://127.0.0.1:4170/health
33
+ # → {"status":"ok"}
34
+
35
+ curl http://127.0.0.1:4170/capabilities
36
+ # → {"v":1,"mode":"http-bridge","features":["health","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"}
37
+ ```
38
+
39
+ The `workspaceCwd` field surfaces the bound workspace so clients can pre-flight check + omit `cwd` on `POST /session`.
40
+
41
+ ### 3. Open a session
42
+
43
+ ```bash
44
+ curl -X POST http://127.0.0.1:4170/session \
45
+ -H 'Content-Type: application/json' \
46
+ -d '{}'
47
+ # → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}
48
+ ```
49
+
50
+ `cwd` may be omitted — the route falls back to the daemon's bound workspace. Posting a `cwd` that doesn't match the bound workspace returns `400 workspace_mismatch` (the daemon is bound to exactly one workspace; start a separate daemon for a different one).
51
+
52
+ A second client posting to `/session` (any matching `cwd` or none) gets `"attached": true` — they're now sharing the agent.
53
+
54
+ ### 4. Subscribe to the event stream (in another terminal first)
55
+
56
+ ```bash
57
+ SESSION_ID="<from step 3>"
58
+ curl -N http://127.0.0.1:4170/session/$SESSION_ID/events
59
+ # → id: 1
60
+ # event: session_update
61
+ # data: {"id":1,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}}
62
+ ```
63
+
64
+ The `data:` line is the **full event envelope** — `{id?, v, type, data, originatorClientId?}` — JSON-stringified on a single line. The ACP payload (the `sessionUpdate` block in this example) sits under `data` inside that envelope. The SSE-level `id:` / `event:` lines are convenience for EventSource clients; the same values appear inside the JSON envelope so raw-`fetch` consumers get them too.
65
+
66
+ Open this **before** sending the prompt — the SSE replay buffer holds the
67
+ last 4000 events so a late subscriber can catch up via `Last-Event-ID`,
68
+ but for the simple "watch a single prompt" case it's easiest to subscribe
69
+ first and let it stream live.
70
+
71
+ The stream emits `session_update` (LLM chunks, tool calls, usage),
72
+ `permission_request` (tool needs approval), `permission_resolved`
73
+ (someone voted), `model_switched`, `model_switch_failed`, and the terminal
74
+ frames `session_died` (agent child crashed — SSE then closes) and
75
+ `client_evicted` (your queue overflowed — SSE then closes).
76
+
77
+ ### 5. Send a prompt (back in the original terminal)
78
+
79
+ ```bash
80
+ curl -X POST http://127.0.0.1:4170/session/$SESSION_ID/prompt \
81
+ -H 'Content-Type: application/json' \
82
+ -d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}'
83
+ # → {"stopReason":"end_turn"}
84
+ ```
85
+
86
+ The `curl -N` from step 4 will print frames as they arrive.
87
+
88
+ ## Authentication
89
+
90
+ For anything beyond loopback, you **must** pass a bearer token:
91
+
92
+ ```bash
93
+ export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
94
+ qwen serve --hostname 0.0.0.0 --port 4170
95
+ # → boot refuses without QWEN_SERVER_TOKEN
96
+ ```
97
+
98
+ Clients then send `Authorization: Bearer $QWEN_SERVER_TOKEN` on every request. `/health` is exempted **only on loopback binds** so k8s/Compose liveness probes inside the pod (where the daemon listens on `127.0.0.1`) don't need credentials. On non-loopback binds (`--hostname 0.0.0.0` etc.) `/health` requires the token like every other route — otherwise an attacker can probe arbitrary addresses to confirm the daemon's existence. Use `/capabilities` to verify your token is correct end-to-end (it always requires auth):
99
+
100
+ ```bash
101
+ curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
102
+ # → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
103
+ # Wrong token → 401
104
+ ```
105
+
106
+ The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401 responses are uniform across "missing header", "wrong scheme", and "wrong token" so a side-channel can't distinguish.
107
+
108
+ ## CLI flags
109
+
110
+ | Flag | Default | Purpose |
111
+ | ----------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | `--port <n>` | `4170` | TCP port. `0` = OS-assigned ephemeral port. |
113
+ | `--hostname <addr>` | `127.0.0.1` | Bind interface. Anything beyond loopback requires a token. |
114
+ | `--token <str>` | — | Bearer token. Falls back to `QWEN_SERVER_TOKEN` env var (with leading/trailing whitespace stripped — handy for `$(cat token.txt)`). |
115
+ | `--max-sessions <n>` | `20` | Cap on concurrent live sessions. New `POST /session` requests that would spawn a fresh child return `503` (with `Retry-After: 5`) when the cap is hit; attaches to existing sessions are NOT counted. Set to `0` to disable. Sized for single-user / small-team usage; raise it if your deployment has the RAM/FD headroom (~30–50 MB per session). |
116
+ | `--workspace <path>` | `process.cwd()` | Absolute workspace path this daemon binds to (per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02 — 1 daemon = 1 workspace). `POST /session` requests with a mismatched `cwd` return `400 workspace_mismatch`. For multi-workspace deployments, run one `qwen serve` per workspace on separate ports. |
117
+ | `--max-connections <n>` | `256` | Listener-level TCP connection cap (`server.maxConnections`). Bounds raw socket count irrespective of session count — slow / phantom SSE clients get rejected at accept time once full. Raise alongside `--max-sessions` if your deployment expects many SSE subscribers per session. |
118
+ | `--http-bridge` | `true` | Stage 1 mode: one `qwen --acp` child per daemon (bound to one workspace at boot, per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02); N sessions multiplex onto that child via ACP `newSession()`. Stage 2 native in-process becomes available later. |
119
+
120
+ > **Sizing the load knobs.** `--max-sessions` is the **new-child** cap.
121
+ > Three other layers also limit load — when sizing for a high-concurrency
122
+ > deployment, tune them together:
123
+ >
124
+ > - **listener-level**: `--max-connections` / `server.maxConnections=256`
125
+ > bounds raw TCP connections (slow-client back-pressure).
126
+ > - **per-session subscribers**: the EventBus caps SSE subscribers at
127
+ > 64 per session by default; the 65th client gets a terminal
128
+ > `stream_error` and is closed.
129
+ > - **per-subscriber backlog**: a 256-frame queue per SSE client; an
130
+ > over-capacity client gets a terminal `client_evicted` frame and is
131
+ > closed (one slow consumer can't pin the daemon).
132
+ >
133
+ > The four caps interact: `--max-sessions × 64 subscribers × 256 frames`
134
+ > is the worst-case in-flight memory at the EventBus layer. Default
135
+ > sizing assumes single-user / small-team load; raise progressively
136
+ > (and watch RSS) for multi-tenant deployments.
137
+
138
+ ## Default deployment threat model
139
+
140
+ - **127.0.0.1 only** — loopback bind, no auth needed.
141
+ - **`--hostname 0.0.0.0` requires a token** — boot refuses without one.
142
+ - **`LOOPBACK_BINDS` includes IPv6** — `::1` and `[::1]` count as loopback for the no-token rule.
143
+ - **Host header allowlist** — on **loopback** binds the daemon checks `Host:` matches `localhost:port` / `127.0.0.1:port` / `[::1]:port` / `host.docker.internal:port` (case-insensitive per RFC 7230 §5.4) to defend against DNS rebinding. **Non-loopback binds (`--hostname 0.0.0.0`) intentionally bypass the Host allowlist** — the operator has chosen the surface area, so the bearer-token gate is the sole authentication layer; reverse proxies / SNI / client cert pinning are the operator's responsibility, not the daemon's. If you need Host-based isolation on a non-loopback bind, terminate TLS + check Host at a front proxy.
144
+ - **CORS denies any browser Origin** — returns `403` JSON. **Implication for browser-served webuis** (BUy4e): any `packages/webui`-style frontend that lives on a separate origin will get 403 at the wire. Stage 1 options for browser-style consumption: (a) package the webui as a native shell (Electron/Tauri) so no `Origin` header is sent, or (b) front the daemon with a same-origin reverse proxy that strips/rewrites `Origin` for a known frontend. Stage 1.5 will add `--allow-origin <pattern>` for opt-in named frontends.
145
+ - **Spawned `qwen --acp` child inherits the daemon's environment** with one explicit scrub: `QWEN_SERVER_TOKEN` is removed before the child starts (the daemon's own bearer; the agent doesn't need it). Everything else — `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `QWEN_*` / `DASHSCOPE_API_KEY` / your custom `modelProviders[].envKey` / etc. — passes through, because the agent legitimately needs those to authenticate to the LLM. **This is intentional, not a sandbox.** The agent runs as the same UID with shell-tool access, so anything in `~/.bashrc` / `~/.aws/credentials` / `~/.npmrc` is reachable by prompt injection regardless. The env passthrough is not the security boundary; the user-as-trust-root is. Don't run `qwen serve` under an identity that has env-resident credentials you wouldn't trust the agent with.
146
+ - **Per-subscriber bounded SSE queues** — a slow client that overflows its queue gets a `client_evicted` terminal frame and is closed; one stuck consumer can't pin the daemon.
147
+ - **Graceful shutdown** — SIGINT/SIGTERM drain the agent children before closing the listener (10s deadline per child).
148
+
149
+ > ⚠️ **Stage 1 known gap — permissions are daemon-global, not per-session (BUy4H).** `pendingPermissions` lives at daemon scope; any client holding the bearer token can vote on any `requestId` for any session it can see (and SSE `permission_request` events carry the requestId in their payload). This is acceptable under the single-user / small-team trust model where every authenticated client is the same human or collaborators they trust. Stage 1.5 will move to `POST /session/:id/permission/:requestId` + session-scoped pending map + per-client identity (must-have #3 from the downstream review); until then, don't run `qwen serve` behind a bearer shared with untrusted parties.
150
+ >
151
+ > ⚠️ **Stage 1 known gap — `POST /session/:id/prompt` body capped at 10 MB (BUy4L).** Multimodal prompts containing images / PDFs / audio that exceed 10 MB will fail at body-parse time before route logic runs (no streaming, no mid-upload abort). Workaround: shrink the content client-side, or pass a path reference and let the agent read the file via `readTextFile`. Stage 1.5 will accept `multipart/form-data` or chunked encoding on `/prompt` so large prompts don't hit a cliff.
152
+ >
153
+ > ⚠️ **Stage 1 known gap — phantom SSE connections behind NAT.** The
154
+ > daemon detects dead clients via TCP back-pressure on heartbeats
155
+ > (15s interval). A client that vanishes WITHOUT a TCP RST (e.g. a
156
+ > NAT box silently dropping idle flows) keeps the kernel-level socket
157
+ > "alive" until Node's keepalive probes time out — typically ~2 hours
158
+ > on Linux defaults. On `--hostname 0.0.0.0` deployments behind such
159
+ > NATs, phantom SSE connections can accumulate and eventually hit the
160
+ > 256 `server.maxConnections` ceiling. Stage 2 will add an
161
+ > application-level idle deadline (last-byte-written tracking +
162
+ > per-connection timeout). Until then, operators on networks that
163
+ > swallow RSTs may want to lower `server.keepAliveTimeout` via a
164
+ > reverse proxy or accept periodic daemon restarts.
165
+
166
+ ## Multi-session & multi-workspace deployment
167
+
168
+ Per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02, each `qwen serve` process binds to **one workspace** at boot. Within that workspace it multiplexes N sessions onto a single `qwen --acp` child via the agent's native session map — sessions share the child's process / OAuth state / file-read cache / hierarchy-memory parse.
169
+
170
+ To host **multiple workspaces** (one user, several repos; or several users on the same host), run **multiple daemon processes** — one per workspace, each on its own port, supervised by systemd / docker-compose / k8s / a `qwen-coordinator` reference orchestrator. The trade-off is intentional: one workspace per child means `loadSettings(cwd)` / OAuth / MCP server scope stay aligned with the bound directory and don't drift across requests.
171
+
172
+ > **Subscribe BEFORE posting `modelServiceId` on attach.** When a client `POST /session` with a `modelServiceId` and the workspace already has a session running a different model, the daemon issues an internal `setSessionModel` call — failures are NOT propagated as an HTTP error (the session stays operational on its current model). The visible failure signal is a `model_switch_failed` event on the session's SSE stream. If you call `POST /session` and only THEN open `GET /session/:id/events`, you'll miss the failure event and silently keep talking to the wrong model. Open the SSE stream first, or pass `Last-Event-ID: 0` on subscribe to replay the ring's oldest available event.
173
+
174
+ To handle multiple **users** (each with their own quota, audit log, sandbox) or to scale beyond one process's reach (cold-start budget, FD count, RSS), spawn one daemon per workspace per user behind an external orchestrator. That orchestrator (multi-tenancy / OIDC / Quota / Audit / k8s) is **out of scope** for the qwen-code project — see issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803) "External Reference Architecture" for the design pointers.
175
+
176
+ ## Durability model
177
+
178
+ **Sessions are ephemeral in Stage 1.** Plan accordingly:
179
+
180
+ - A child process crash publishes `session_died` and removes the session from the daemon's maps. There is **no resume** — clients must `POST /session` again.
181
+ - A daemon restart loses every in-flight session. ACP's `loadSession` / `unstable_resumeSession` are **not exposed via HTTP** in Stage 1; sessions don't outlive the daemon.
182
+ - Long client disconnects (>5 min on a chatty turn) can outrun the SSE replay ring (default 4000 frames) — `Last-Event-ID` reconnect succeeds but state may be incoherent. For mobile / flaky-network clients, plan to re-create the session and re-open SSE on long drops.
183
+ - File operations (`writeTextFile`) are atomic across crashes (write-then-rename); they aren't atomic across daemon restarts in the sense of replaying — the file write either landed or it didn't.
184
+
185
+ If your integration needs cross-restart durability, you need either Stage 1.5+ (`loadSession` over HTTP, persistence layer) or your own application-level state recovery. Don't hold long-running, restart-sensitive state inside the daemon's session.
186
+
187
+ ## Stage 1.5+ runtime guarantees
188
+
189
+ Stage 1's contract is sized for prototyping. Per [#3889 chiga0 downstream-consumer review](https://github.com/QwenLM/qwen-code/pull/3889#issuecomment-4427875644), the following are **not** in Stage 1 — production-grade integrations need Stage 1.5+ before relying on them:
190
+
191
+ **Blockers for serious downstream use:**
192
+
193
+ 1. **Per-request `sessionScope` override** on `POST /session` — today the daemon-wide default is the only setting; a VSCode extension can't say "I want a private session for this window" against a daemon configured for shared sessions.
194
+ 2. **`loadSession` / `unstable_resumeSession` over HTTP** — without this, no integration can survive a child crash or daemon restart, and any orchestrator coordinating the daemon can't recover state either.
195
+ 3. **Persistent client identity (pair tokens + per-client revocation)** — Stage 1 uses one shared bearer; a leaked token revokes everyone, and `originatorClientId` is client-self-declared rather than daemon-stamped from authenticated identity.
196
+
197
+ **Reliability baseline:**
198
+
199
+ 4. **Client-initiated heartbeat path** — distinguish "agent thinking" from "daemon dead" without waiting for the 15s server heartbeat.
200
+ 5. **`permission_already_resolved` event** when a vote loses the first-responder race — currently UIs have to infer state from a `404`.
201
+ 6. **Larger / per-session-configurable replay ring** — default 4000 covers short drops; mobile / chatty-turn workloads need 8000+ or per-session config.
202
+ 7. **`slow_client_warning` event before `client_evicted`** — soft backpressure so well-behaved slow clients can self-throttle (trim render depth, drop chunks) before being terminated.
203
+
204
+ **Integration ergonomics:**
205
+
206
+ 8. **`POST /session/:id/_meta` for IM-style context** — per-session key-value attached to subsequent prompts (chat id, sender, thread id) replaces the per-channel improvisation.
207
+ 9. **`/capabilities` actual feature negotiation** — `protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }` so clients can detect drift instead of falling through to "unknown frame, ignore".
208
+ 10. **First-class durability documentation** (this section) — already shipped above.
209
+
210
+ The full convergence roadmap is tracked on [#3803](https://github.com/QwenLM/qwen-code/issues/3803).
211
+
212
+ ## Stage 1 scope boundaries — what we won't fix in Stage 1.5
213
+
214
+ Two structural choices are explicit non-goals for the Stage 1 / 1.5 / 2 main-line roadmap. If your use case depends on either, plan around them rather than waiting for us.
215
+
216
+ ### Session state is local-mutation-only (per [LaZzyMan review #4270256721](https://github.com/QwenLM/qwen-code/pull/3889#pullrequestreview-4270256721))
217
+
218
+ The Stage 1.5 plan describes TUI as an in-process EventBus subscriber. In practice **TUI UI is strictly larger than the wire protocol**:
219
+
220
+ - **Local-only UI** — the ~15 Ink dialog components (`ModelDialog`, `MemoryDialog`, `PermissionsDialog`, `SessionPicker`, `WelcomeBackDialog`, `FolderTrustDialog`, …) and the `local-jsx` slash commands (`/ide`, `/auth`, `/init`, `/resume`, `/rename`, `/delete`, `/language`, `/arena`, …) render terminal-specific Ink JSX. Remote clients on HTTP/SSE can't equivalently render Ink, and these flows emit no wire event.
221
+ - **Session-state mutations without wire events** — `/approval-mode`, `/memory add`, `/mcp add-server`, `/agents`, `/tools enable/disable`, `/auth`, `/init` (writing `CLAUDE.md`) all change agent behavior, but only `/model` currently publishes an event (`model_switched`).
222
+
223
+ **Stage 1 choice — option (A) from the review**: don't promote these mutations to wire events. The two deployment modes have different consequences.
224
+
225
+ #### Mode 1 — headless `qwen serve` (this PR)
226
+
227
+ No TUI shell runs inside the daemon. The slash commands listed above **don't exist** in this mode — there's no terminal UI to issue them from. Session state is therefore:
228
+
229
+ - **Boot-time-frozen** for `approval-mode` / `memory` / `mcp servers` / `agents` / `tools` allowlist / `auth` — all loaded from settings + disk when the daemon's `qwen --acp` child starts; immutable for the session's lifetime.
230
+ - **Mutable over HTTP** only via the routes this PR exposes — primarily `POST /session/:id/model` (publishes `model_switched`). Permission votes (`POST /permission/:requestId`) are per-request, not per-session-state.
231
+
232
+ **Consequence:** remote clients in headless mode see the **full session state**. No TUI hides additional state; no drift is possible. If you want to change `approval-mode` or add an MCP server, restart the daemon with new settings — the daemon doesn't expose runtime mutation for those today.
233
+
234
+ #### Mode 2 — Stage 1.5 `qwen --serve` co-hosted TUI (not in this PR)
235
+
236
+ When Stage 1.5 lands `qwen --serve` (TUI process co-hosts the same HTTP server), the TUI **does** exist alongside remote clients. A local operator typing `/approval-mode yolo` or `/mcp add-server` mutates session state, and remote clients on HTTP have no event to observe the change.
237
+
238
+ In this mode, TUI is a **"super-client"** — it observes the same agent conversation remote clients see, AND can mutate session state remote clients can't. The asymmetry is:
239
+
240
+ - ✅ Both TUI and remote clients see the same agent messages, tool calls, file diffs, permission prompts.
241
+ - ❌ Only TUI sees / mutates approval-mode / memory / MCP server list / agents / tools allowlist / auth state.
242
+
243
+ **Consequence in Mode 2:** if a remote-client UI tries to mirror session settings, it can drift after any TUI slash command. Remote clients should **re-fetch state on attach / reconnect** (use `Last-Event-ID: 0` to replay the ring's oldest event for things like `model_switched`); they should NOT rely on incremental events for TUI-side mutations.
244
+
245
+ #### Why (A) and not (B) (promote mutations to `session_state_changed` event family)
246
+
247
+ (B) is the more ambitious answer but locks Stage 1.5 into a substantially larger wire surface that must also pass cleanly through the planned in-process refactor. We'd rather walk the smaller scope honestly. The session-state-event taxonomy work — enumerating which TUI flows are local-only by design vs. could plausibly graduate to wire under a future opt-in (B)-flavor extension — moves to [#3803](https://github.com/QwenLM/qwen-code/issues/3803), not Stage 1.5 code.
248
+
249
+ ### N parallel sessions share one `qwen --acp` child
250
+
251
+ Multiple sessions on the same workspace **share one `qwen --acp` child process** via the agent's native multi-session support (`packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>`). The bridge calls `connection.newSession({cwd, mcpServers})` for each session — the agent stores them in its sessions map and demultiplexes per-call sessionId.
252
+
253
+ Concrete cost at N=5 sessions on the same workspace:
254
+
255
+ | Resource | Per session | At N=5 |
256
+ | ------------------------------------ | ----------- | ---------------------------- |
257
+ | Daemon Node process | one | **30–50 MB** (one daemon) |
258
+ | `qwen --acp` child | shared | **60–100 MB** (one child) |
259
+ | MCP server children | per-session | 3×N if configs differ |
260
+ | `FileReadCache` (in-child heap) | shared | parsed once |
261
+ | `CLAUDE.md` / hierarchy memory parse | shared | parsed once |
262
+ | OAuth refresh-token state | shared | **one refresh path** |
263
+ | Auto-memory learned facts | shared | one knowledge base per child |
264
+ | Cold start | first only | <200 ms after first session |
265
+
266
+ The bridge keeps **one channel per daemon** (one daemon per workspace, per §02). The channel stays alive while at least one session is live; the last `killSession` (or a channel-level crash) kills the child.
267
+
268
+ **MCP server children** are still per-session today — each session's config can specify different servers, so they're independently spawned. Stage 1.5 follow-up: refcount MCP server children by `(workspace, config-hash)` so identical configs share. Not in scope for this PR.
269
+
270
+ **Peer agents (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) all do single-process multi-session.** qwen-code matches them at the agent layer; the Stage 1 bridge in this PR makes the same architecture visible over HTTP.
271
+
272
+ ## What's next
273
+
274
+ - **Build a client?** See the [DaemonClient TypeScript quickstart](../developers/examples/daemon-client-quickstart.md) and the [HTTP protocol reference](../developers/qwen-serve-protocol.md).
275
+ - **Reading the source?** Bridge code lives at `packages/cli/src/serve/`; SDK client at `packages/sdk-typescript/src/daemon/`.
276
+ - **Tracking the roadmap?** Stage 1.5 / Stage 2 progress is tracked on issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803).