@qwen-code/qwen-code 0.15.12-preview.0 → 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.
@@ -45,12 +45,11 @@ Commands for adjusting interface appearance and work environment.
45
45
 
46
46
  Commands specifically for controlling interface and output language.
47
47
 
48
- | Command | Description | Usage Examples |
49
- | --------------------- | --------------------------------------------------------------------------- | -------------------------- |
50
- | `/language` | View or change language settings | `/language` |
51
- | → `ui [language]` | Set UI interface language | `/language ui zh-CN` |
52
- | → `output [language]` | Set LLM output language | `/language output Chinese` |
53
- | → `translate on/off` | Toggle AI translation for dynamic slash command descriptions (default: off) | `/language translate on` |
48
+ | Command | Description | Usage Examples |
49
+ | --------------------- | -------------------------------- | -------------------------- |
50
+ | `/language` | View or change language settings | `/language` |
51
+ | → `ui [language]` | Set UI interface language | `/language ui zh-CN` |
52
+ | → `output [language]` | Set LLM output language | `/language output Chinese` |
54
53
 
55
54
  - Available built-in UI languages: `zh-CN` (Simplified Chinese), `en-US` (English), `ru-RU` (Russian), `de-DE` (German), `ja-JP` (Japanese), `pt-BR` (Portuguese - Brazil), `fr-FR` (French), `ca-ES` (Catalan)
56
55
  - Output language examples: `Chinese`, `English`, `Japanese`, etc.
@@ -30,13 +30,14 @@ Hooks are user-defined scripts or programs that are automatically executed by Qw
30
30
 
31
31
  ## Hook Types
32
32
 
33
- Qwen Code supports three hook executor types:
33
+ Qwen Code supports four hook executor types:
34
34
 
35
35
  | Type | Description |
36
36
  | :--------- | :--------------------------------------------------------------------------------------------- |
37
37
  | `command` | Execute a shell command. Receives JSON via `stdin`, returns results via `stdout`. |
38
38
  | `http` | Send JSON as a `POST` request body to a specified URL. Returns results via HTTP response body. |
39
39
  | `function` | Directly call a registered JavaScript function (session-level hooks only). |
40
+ | `prompt` | Use an LLM to evaluate hook input and return a decision. |
40
41
 
41
42
  ### Command Hooks
42
43
 
@@ -134,6 +135,102 @@ Function hooks directly call registered JavaScript/TypeScript functions. They ar
134
135
 
135
136
  **Note**: For most use cases, use **command hooks** or **HTTP hooks** instead, which can be configured in settings files.
136
137
 
138
+ ### Prompt Hooks
139
+
140
+ Prompt hooks use an LLM to evaluate hook input and return a decision. This is useful for making intelligent decisions based on context, such as determining whether to allow or block an operation.
141
+
142
+ **How it works:**
143
+
144
+ 1. The hook input JSON is injected into your prompt using the `$ARGUMENTS` placeholder
145
+ 2. The prompt is sent to an LLM (default: your current model)
146
+ 3. The LLM returns a JSON response with the decision
147
+ 4. Qwen Code processes the decision and continues or blocks execution accordingly
148
+
149
+ **Configuration:**
150
+
151
+ | Field | Type | Required | Description |
152
+ | :-------------- | :--------- | :------- | :-------------------------------------------------- |
153
+ | `type` | `"prompt"` | Yes | Hook type |
154
+ | `prompt` | `string` | Yes | Prompt sent to LLM. Use `$ARGUMENTS` for hook input |
155
+ | `model` | `string` | No | Model to use (defaults to your current model) |
156
+ | `timeout` | `number` | No | Timeout in seconds, default 30 |
157
+ | `name` | `string` | No | Hook name (for logging) |
158
+ | `description` | `string` | No | Hook description |
159
+ | `statusMessage` | `string` | No | Status message displayed during execution |
160
+
161
+ **Response Format:**
162
+
163
+ The LLM must return JSON with the following structure:
164
+
165
+ ```json
166
+ {
167
+ "ok": true,
168
+ "reason": "Explanation of the decision",
169
+ "additionalContext": "Optional context to inject into the conversation"
170
+ }
171
+ ```
172
+
173
+ | Field | Description |
174
+ | :------------------ | :------------------------------------------------------------------------- |
175
+ | `ok` | `true` to allow/continue, `false` to block/stop |
176
+ | `reason` | Required when `ok` is `false`. Shown to the model to explain the block |
177
+ | `additionalContext` | Optional. Additional context to inject into the conversation when allowing |
178
+
179
+ **Supported Events:**
180
+
181
+ Prompt hooks can be used with most hook events, including:
182
+
183
+ - `PreToolUse` - Evaluate whether to allow a tool call
184
+ - `PostToolUse` - Evaluate tool results and potentially inject context
185
+ - `Stop` - Determine whether to continue or stop
186
+ - `SubagentStop` - Evaluate subagent results
187
+ - `UserPromptSubmit` - Evaluate or enrich user prompts
188
+
189
+ **Example: Stop Hook**
190
+
191
+ ```json
192
+ {
193
+ "hooks": {
194
+ "Stop": [
195
+ {
196
+ "hooks": [
197
+ {
198
+ "type": "prompt",
199
+ "prompt": "You are evaluating whether Qwen Code should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.",
200
+ "timeout": 30
201
+ }
202
+ ]
203
+ }
204
+ ]
205
+ }
206
+ }
207
+ ```
208
+
209
+ When `ok` is `false`, Qwen Code will continue working and use the `reason` as context for the next response.
210
+
211
+ **Example: PreToolUse Hook**
212
+
213
+ ```json
214
+ {
215
+ "hooks": {
216
+ "PreToolUse": [
217
+ {
218
+ "matcher": "Bash",
219
+ "hooks": [
220
+ {
221
+ "type": "prompt",
222
+ "prompt": "Evaluate this tool call for security concerns. Tool input: $ARGUMENTS\n\nCheck for:\n- Dangerous commands (rm -rf, curl | sh, etc.)\n- Unauthorized access attempts\n- Data exfiltration patterns\n\nRespond with {\"ok\": true} if safe, or {\"ok\": false, \"reason\": \"concern\"} if blocked.",
223
+ "model": "sonnet",
224
+ "timeout": 30,
225
+ "name": "security-evaluator"
226
+ }
227
+ ]
228
+ }
229
+ ]
230
+ }
231
+ }
232
+ ```
233
+
137
234
  ## Hook Events
138
235
 
139
236
  Hooks fire at specific points during a Qwen Code session. Different events support different matchers to filter trigger conditions.
@@ -152,6 +249,8 @@ Hooks fire at specific points during a Qwen Code session. Different events suppo
152
249
  | `PreCompact` | Before conversation compaction | Trigger (`manual`, `auto`) |
153
250
  | `Notification` | When notifications are sent | Type (`permission_prompt`, `idle_prompt`, `auth_success`) |
154
251
  | `PermissionRequest` | When permission dialog is shown | Tool name |
252
+ | `TodoCreated` | When a new todo item is created | None (always fires) |
253
+ | `TodoCompleted` | When a todo item is marked as completed | None (always fires) |
155
254
 
156
255
  ### Matcher Patterns
157
256
 
@@ -165,6 +264,7 @@ Hooks fire at specific points during a Qwen Code session. Different events suppo
165
264
  | Session Events | `SessionEnd` | ✅ Regex | Reason: `clear`, `logout`, `prompt_input_exit`, etc. |
166
265
  | Notification Events | `Notification` | ✅ Exact match | Type: `permission_prompt`, `idle_prompt`, `auth_success` |
167
266
  | Compact Events | `PreCompact` | ✅ Exact match | Trigger: `manual`, `auto` |
267
+ | Todo Events | `TodoCreated`, `TodoCompleted` | ❌ No | N/A |
168
268
  | Prompt Events | `UserPromptSubmit` | ❌ No | N/A |
169
269
  | Stop Events | `Stop` | ❌ No | N/A |
170
270
 
@@ -754,6 +854,204 @@ Hook output supports three categories of fields:
754
854
  }
755
855
  ```
756
856
 
857
+ #### TodoCreated
858
+
859
+ **Purpose**: Executed when a new todo item is created via the `todo_write` tool. Allows validation, logging, or blocking of todo creation.
860
+
861
+ Todo hooks run in two phases:
862
+
863
+ - `validation`: runs before persistence. Use this phase for validation only; returning `block` or `deny` prevents the write.
864
+ - `postWrite`: runs after persistence. Use this phase for side effects such as logging or syncing; `block` or `deny` is ignored in this phase.
865
+
866
+ **Event-specific fields**:
867
+
868
+ ```json
869
+ {
870
+ "todo_id": "unique identifier for the todo item",
871
+ "todo_content": "content/description of the todo item",
872
+ "todo_status": "pending | in_progress | completed",
873
+ "all_todos": "array of all todo items in the current list",
874
+ "phase": "validation | postWrite"
875
+ }
876
+ ```
877
+
878
+ **Output Options**:
879
+
880
+ - `decision`: "allow", "block", or "deny"
881
+ - `reason`: human-readable explanation for the decision (required when blocking)
882
+
883
+ **Blocking Behavior**:
884
+
885
+ During the `validation` phase, when `decision` is `block` or `deny` (exit code 2), todo creation is prevented. The todo list remains unchanged, and the reason is provided as feedback to the model.
886
+
887
+ During the `postWrite` phase, the todo has already been persisted. Hooks may still return output, but `block` / `deny` does not undo the write and should not be used for validation.
888
+
889
+ **Example Output (Allow)**:
890
+
891
+ ```json
892
+ {
893
+ "decision": "allow",
894
+ "reason": "Todo content validated successfully"
895
+ }
896
+ ```
897
+
898
+ **Example Output (Block)**:
899
+
900
+ ```json
901
+ {
902
+ "decision": "block",
903
+ "reason": "Todo content too short. Minimum 5 characters required."
904
+ }
905
+ ```
906
+
907
+ **Example Hook Script**:
908
+
909
+ ```bash
910
+ #!/bin/bash
911
+ # ~/.qwen/hooks/todo-validator.sh
912
+ # Validates todo content before creation
913
+
914
+ INPUT=$(cat)
915
+ CONTENT=$(echo "$INPUT" | jq -r '.todo_content')
916
+
917
+ # Check minimum length
918
+ if [ ${#CONTENT} -lt 5 ]; then
919
+ echo '{"decision": "block", "reason": "Todo content must be at least 5 characters"}'
920
+ exit 2
921
+ fi
922
+
923
+ # Block test-related todos
924
+ if [[ "$CONTENT" =~ "test" ]]; then
925
+ echo '{"decision": "block", "reason": "Test todos are not allowed in production"}'
926
+ exit 2
927
+ fi
928
+
929
+ echo '{"decision": "allow"}'
930
+ exit 0
931
+ ```
932
+
933
+ **Example Configuration**:
934
+
935
+ ```json
936
+ {
937
+ "hooks": {
938
+ "TodoCreated": [
939
+ {
940
+ "hooks": [
941
+ {
942
+ "type": "command",
943
+ "command": "$HOME/.qwen/hooks/todo-validator.sh",
944
+ "name": "todo-validator",
945
+ "timeout": 5000
946
+ }
947
+ ]
948
+ }
949
+ ]
950
+ }
951
+ }
952
+ ```
953
+
954
+ #### TodoCompleted
955
+
956
+ **Purpose**: Executed when a todo item is marked as completed. Allows validation, logging, or blocking of todo completion.
957
+
958
+ Todo hooks run in two phases:
959
+
960
+ - `validation`: runs before persistence. Use this phase for validation only; returning `block` or `deny` prevents the write.
961
+ - `postWrite`: runs after persistence. Use this phase for side effects such as logging or syncing; `block` or `deny` is ignored in this phase.
962
+
963
+ **Event-specific fields**:
964
+
965
+ ```json
966
+ {
967
+ "todo_id": "unique identifier for the todo item",
968
+ "todo_content": "content/description of the todo item",
969
+ "previous_status": "pending | in_progress (status before completion)",
970
+ "all_todos": "array of all todo items in the current list",
971
+ "phase": "validation | postWrite"
972
+ }
973
+ ```
974
+
975
+ **Output Options**:
976
+
977
+ - `decision`: "allow", "block", or "deny"
978
+ - `reason`: human-readable explanation for the decision (required when blocking)
979
+
980
+ **Blocking Behavior**:
981
+
982
+ During the `validation` phase, when `decision` is `block` or `deny` (exit code 2), todo completion is prevented. The todo item remains in its previous status, and the reason is provided as feedback to the model.
983
+
984
+ During the `postWrite` phase, the todo has already been persisted. Hooks may still return output, but `block` / `deny` does not undo the write and should not be used for validation.
985
+
986
+ **Example Output (Allow)**:
987
+
988
+ ```json
989
+ {
990
+ "decision": "allow",
991
+ "reason": "Todo completion approved"
992
+ }
993
+ ```
994
+
995
+ **Example Output (Block)**:
996
+
997
+ ```json
998
+ {
999
+ "decision": "block",
1000
+ "reason": "Cannot complete this todo until dependent tasks are finished."
1001
+ }
1002
+ ```
1003
+
1004
+ **Example Hook Script**:
1005
+
1006
+ ```bash
1007
+ #!/bin/bash
1008
+ # ~/.qwen/hooks/todo-completion-validator.sh
1009
+ # Validates todo completion conditions
1010
+
1011
+ INPUT=$(cat)
1012
+ TODO_ID=$(echo "$INPUT" | jq -r '.todo_id')
1013
+ ALL_TODOS=$(echo "$INPUT" | jq -r '.all_todos')
1014
+
1015
+ # Check if there are incomplete dependent todos (example logic)
1016
+ INCOMPLETE_COUNT=$(echo "$ALL_TODOS" | jq '[.[] | select(.status != "completed")] | length')
1017
+
1018
+ if [ "$INCOMPLETE_COUNT" -gt 5 ]; then
1019
+ echo '{"decision": "block", "reason": "Too many incomplete todos. Complete other tasks first."}'
1020
+ exit 2
1021
+ fi
1022
+
1023
+ echo '{"decision": "allow"}'
1024
+ exit 0
1025
+ ```
1026
+
1027
+ **Example Configuration**:
1028
+
1029
+ ```json
1030
+ {
1031
+ "hooks": {
1032
+ "TodoCompleted": [
1033
+ {
1034
+ "hooks": [
1035
+ {
1036
+ "type": "command",
1037
+ "command": "$HOME/.qwen/hooks/todo-completion-validator.sh",
1038
+ "name": "completion-validator",
1039
+ "timeout": 5000
1040
+ }
1041
+ ]
1042
+ }
1043
+ ]
1044
+ }
1045
+ }
1046
+ ```
1047
+
1048
+ **Use Cases**:
1049
+
1050
+ - **Logging**: Track todo creation and completion for audit or analytics
1051
+ - **Validation**: Enforce content quality standards (minimum length, required keywords)
1052
+ - **Workflow Control**: Block completion until prerequisites are met
1053
+ - **Integration**: Sync todos with external task management systems (Jira, Trello, etc.)
1054
+
757
1055
  ## Hook Configuration
758
1056
 
759
1057
  Hooks are configured in Qwen Code settings, typically in `.qwen/settings.json` or user configuration files:
@@ -55,22 +55,6 @@ Detection priority:
55
55
  3. System locale via JavaScript Intl API
56
56
  4. Default: English
57
57
 
58
- ### Dynamic Command Translation
59
-
60
- Dynamic slash command descriptions from skills, extensions, file commands, and
61
- MCP prompts can be translated with AI. This is **off by default** to avoid
62
- unexpected model calls, latency, and token usage.
63
-
64
- ```bash
65
- /language translate status # Show current status
66
- /language translate on # Enable AI translation for dynamic descriptions
67
- /language translate off # Disable AI translation
68
- ```
69
-
70
- Use `/language translate cache refresh` to re-translate cached dynamic
71
- descriptions after enabling translation, or `/language translate cache clear` to
72
- remove cached translations.
73
-
74
58
  ## LLM Output Language
75
59
 
76
60
  The LLM output language controls what language the AI assistant responds in, regardless of what language you type your questions in.
@@ -145,6 +129,29 @@ User directory takes precedence over built-in translations.
145
129
  > Contributions are welcome! If you’d like to improve built-in translations or add new languages.
146
130
  > For a concrete example, see [PR #1238: feat(i18n): add Russian language support](https://github.com/QwenLM/qwen-code/pull/1238).
147
131
 
132
+ ### Maintaining `zh-TW` (Traditional Chinese for Taiwan)
133
+
134
+ `zh-TW` is **not** an automatic OpenCC s2t conversion of `zh.js` — it is a hand-maintained Taiwan-vocabulary translation. When adding or updating keys, please follow the conventions below.
135
+
136
+ The "CI enforced?" column indicates whether `npm run check-i18n` will fail the build on a violation. Rows marked **No** are style guidance enforced by review only — typically because the offending form has a legitimate non-UI meaning (`文件` can mean "document", `打開` is colloquially fine in Taiwan).
137
+
138
+ | Avoid | Use instead | CI enforced? | Reason |
139
+ | --------------------- | --------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | 文件 (file) | 檔案 | No | Taiwan term for filesystem files (but `文件` can legitimately mean "document") |
141
+ | 服務器 / 服务器 | 伺服器 | Yes | Taiwan term for "server" |
142
+ | 菜單 / 菜单 | 選單 | Yes | Taiwan term for "menu" |
143
+ | 鏈接 / 链接 | 連結 | Yes | Taiwan term for "link" (bare `鏈` is fine — e.g. 區塊鏈) |
144
+ | 打開 | 開啟 | No | Taiwan-preferred verb for "open" (UI); `打開` is colloquially common |
145
+ | 爲 / 啓 / 曆史 / 鏈接 | 為 / 啟 / 歷史 / 連結 | Yes | Variant Traditional forms from raw OpenCC s2t. Note: `曆` is context-dependent and correct in calendar terms (日曆, 農曆, 西曆); CI only flags the bigram `曆史`, not bare `曆`. |
146
+
147
+ If you are not a Traditional Chinese speaker and need to bootstrap a value, **do not paste raw OpenCC `s2t` output**: the default s2t profile emits variant Traditional characters (e.g. 爲, 啓) that Taiwan does not use, and never rewrites Mainland-Chinese vocabulary (服務器, 菜單). Prefer `s2twp.json` (Simplified → Taiwan with phrase mapping) as a starting point and then ask a Taiwan-Chinese speaker to review.
148
+
149
+ The `check-i18n` script (run in CI via `npm run check-i18n`) will fail the build if any of the CI-enforced substrings above end up in a `zh-TW` value. See `scripts/check-i18n.ts → ZH_TW_FORBIDDEN_PATTERNS` for the full list. If a translation legitimately needs to contain a CI-forbidden substring, add its key to `ZH_TW_ALLOWED_EXCEPTIONS` in the same file with a brief justification.
150
+
151
+ > [!note]
152
+ >
153
+ > The check uses plain substring matching, which does not understand Chinese word boundaries. A bigram pattern can therefore false-positive across compound-word boundaries — for example, `區塊鏈接口` (= `區塊鏈` + `接口`) contains the substring `鏈接` even though neither word is incorrect. If you hit a surprising CI failure of this kind, add the translation key to `ZH_TW_ALLOWED_EXCEPTIONS` rather than removing the pattern.
154
+
148
155
  ### Language Pack Format
149
156
 
150
157
  ```javascript
@@ -8,9 +8,10 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
8
8
 
9
9
  ## What it gives you
10
10
 
11
- - **One agent process, many clients** — under the default `sessionScope: 'single'`, every client connecting to the same workspace shares one ACP session. Live cross-client collaboration on the same conversation, the same file diffs, the same permission prompts.
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
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
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).
14
15
 
15
16
  ## Quickstart
16
17
 
@@ -19,11 +20,11 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
19
20
  ```bash
20
21
  cd your-project/
21
22
  qwen serve
22
- # → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge)
23
+ # → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
23
24
  # → qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
24
25
  ```
25
26
 
26
- The default bind is `127.0.0.1:4170`. Bearer auth is **off** on loopback so local development "just works".
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.
27
28
 
28
29
  ### 2. Sanity-check it
29
30
 
@@ -32,19 +33,23 @@ curl http://127.0.0.1:4170/health
32
33
  # → {"status":"ok"}
33
34
 
34
35
  curl http://127.0.0.1:4170/capabilities
35
- # → {"v":1,"mode":"http-bridge","features":["health","capabilities","session_create",...]}
36
+ # → {"v":1,"mode":"http-bridge","features":["health","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"}
36
37
  ```
37
38
 
39
+ The `workspaceCwd` field surfaces the bound workspace so clients can pre-flight check + omit `cwd` on `POST /session`.
40
+
38
41
  ### 3. Open a session
39
42
 
40
43
  ```bash
41
44
  curl -X POST http://127.0.0.1:4170/session \
42
45
  -H 'Content-Type: application/json' \
43
- -d '{"cwd":"'"$PWD"'"}'
46
+ -d '{}'
44
47
  # → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}
45
48
  ```
46
49
 
47
- A second client posting to `/session` with the same `cwd` gets `"attached": true` they're now sharing the agent.
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.
48
53
 
49
54
  ### 4. Subscribe to the event stream (in another terminal first)
50
55
 
@@ -94,7 +99,7 @@ Clients then send `Authorization: Bearer $QWEN_SERVER_TOKEN` on every request. `
94
99
 
95
100
  ```bash
96
101
  curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
97
- # → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[]}
102
+ # → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
98
103
  # Wrong token → 401
99
104
  ```
100
105
 
@@ -102,14 +107,15 @@ The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401
102
107
 
103
108
  ## CLI flags
104
109
 
105
- | Flag | Default | Purpose |
106
- | ----------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107
- | `--port <n>` | `4170` | TCP port. `0` = OS-assigned ephemeral port. |
108
- | `--hostname <addr>` | `127.0.0.1` | Bind interface. Anything beyond loopback requires a token. |
109
- | `--token <str>` | — | Bearer token. Falls back to `QWEN_SERVER_TOKEN` env var (with leading/trailing whitespace stripped — handy for `$(cat token.txt)`). |
110
- | `--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). |
111
- | `--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. |
112
- | `--http-bridge` | `true` | Stage 1 mode: per-session `qwen --acp` child process. Stage 2 native in-process becomes available later. |
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. |
113
119
 
114
120
  > **Sizing the load knobs.** `--max-sessions` is the **new-child** cap.
115
121
  > Three other layers also limit load — when sizing for a high-concurrency
@@ -157,13 +163,15 @@ The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401
157
163
  > swallow RSTs may want to lower `server.keepAliveTimeout` via a
158
164
  > reverse proxy or accept periodic daemon restarts.
159
165
 
160
- ## Multi-session & remote deployment
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.
161
169
 
162
- A single `qwen serve` process can manage sessions for any workspace path passed via `cwd` on `POST /session` under the default `sessionScope: 'single'` it keeps one ACP session per canonicalized workspace, sharing it across every client that posts the same `cwd`. So one daemon will happily host sessions for many workspaces at once.
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.
163
171
 
164
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.
165
173
 
166
- 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), you spawn multiple daemon instances 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.
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.
167
175
 
168
176
  ## Durability model
169
177
 
@@ -255,7 +263,7 @@ Concrete cost at N=5 sessions on the same workspace:
255
263
  | Auto-memory learned facts | shared | one knowledge base per child |
256
264
  | Cold start | first only | <200 ms after first session |
257
265
 
258
- The bridge keeps **one channel per workspace** (cross-workspace sharing is intentionally not done — different workspaces have different settings/auth scope, and `acpAgent.ts:601` reloads settings per newSession `cwd`, which would interfere). The channel stays alive while at least one session is live; the last `killSession` (or a channel-level crash) kills the child.
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.
259
267
 
260
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.
261
269