@qwen-code/qwen-code 0.15.12-preview.2 → 0.16.0-nightly.20260522.48b0a8bfc
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.
- package/bundled/qc-helper/docs/configuration/settings.md +69 -71
- package/bundled/qc-helper/docs/features/_meta.ts +2 -0
- package/bundled/qc-helper/docs/features/approval-mode.md +119 -2
- package/bundled/qc-helper/docs/features/auto-mode.md +263 -0
- package/bundled/qc-helper/docs/features/commands.md +11 -10
- package/bundled/qc-helper/docs/features/lsp.md +87 -10
- package/bundled/qc-helper/docs/features/skills.md +3 -0
- package/bundled/qc-helper/docs/features/structured-output.md +309 -0
- package/bundled/qc-helper/docs/features/sub-agents.md +47 -5
- package/bundled/qc-helper/docs/qwen-serve.md +179 -24
- package/bundled/qc-helper/docs/reference/keyboard-shortcuts.md +11 -11
- package/bundled/review/SKILL.md +12 -3
- package/bundled/stuck/SKILL.md +124 -0
- package/chunks/{agent-UQY6A6OS.js → agent-GRCSD6XI.js} +15 -17
- package/chunks/{anthropicContentGenerator-4QE6LTVV.js → anthropicContentGenerator-RQJNXJIY.js} +7 -4
- package/chunks/{askUserQuestion-QFSCBTUO.js → askUserQuestion-PQPMPNM3.js} +2 -2
- package/chunks/{ca-VQSV6JHA.js → ca-UZ7BANMN.js} +29 -3
- package/chunks/{chunk-B7ZL7HUA.js → chunk-4AOCVI6J.js} +2 -1
- package/chunks/{chunk-PCL3EJGY.js → chunk-4U3O4VDA.js} +4722 -3823
- package/chunks/{chunk-AEJ2DKLP.js → chunk-C6WMLUNB.js} +1 -1
- package/chunks/{chunk-FSYKVGER.js → chunk-C7PY32RJ.js} +115 -23
- package/chunks/{chunk-JMZQICAL.js → chunk-CAVZVZX6.js} +2 -2
- package/chunks/{chunk-CAWKL3UC.js → chunk-CSWBPY3P.js} +2 -2
- package/chunks/{chunk-G27O2LD2.js → chunk-D5NTAHYL.js} +1 -1
- package/chunks/{chunk-BXNCPI75.js → chunk-DMIMF3CG.js} +2 -2
- package/chunks/{chunk-OCC4MZRS.js → chunk-F23NCRJ2.js} +1 -1
- package/chunks/{chunk-SOIEFHIK.js → chunk-G7YTSRES.js} +1 -100
- package/chunks/{chunk-SQNQIOD5.js → chunk-GGNTZ2NH.js} +92 -21
- package/chunks/{chunk-FKVKVE6N.js → chunk-KXZ4TJB4.js} +1 -1
- package/chunks/{chunk-CBVB66WY.js → chunk-L5E26RN6.js} +2 -2
- package/chunks/{chunk-UXW7MYAW.js → chunk-MAY32HXD.js} +376 -1
- package/chunks/{chunk-2WFU3IUH.js → chunk-OZO6R4KI.js} +11796 -4086
- package/chunks/{chunk-CM2IESUE.js → chunk-PR4T27R7.js} +1 -1
- package/chunks/{chunk-FYMSCRHM.js → chunk-PVVL5Q3W.js} +32 -1
- package/chunks/{chunk-MXBWOU2L.js → chunk-TM4RMEYN.js} +17 -349
- package/chunks/{chunk-YHEAJFCI.js → chunk-USE2VQ5P.js} +3 -0
- package/chunks/chunk-UY2AYRZF.js +19126 -0
- package/chunks/{chunk-GJXIKCKL.js → chunk-XP27SJMH.js} +76 -5
- package/chunks/chunk-XTYIPAUG.js +379 -0
- package/chunks/{chunk-TPGOGCWM.js → chunk-YJLGXDQJ.js} +1 -1
- package/chunks/{contextCommand-MQRG6RMG.js → contextCommand-O2OLCMOG.js} +17 -19
- package/chunks/{cron-create-WUTD5ZTH.js → cron-create-IGYXQVG4.js} +28 -2
- package/chunks/{cron-delete-N3UQYCRA.js → cron-delete-ETKIZCWT.js} +2 -2
- package/chunks/{cron-list-Z6RJJ4YH.js → cron-list-BVCUSWRU.js} +2 -2
- package/chunks/{de-M2IPQRBS.js → de-V4IE2OOZ.js} +29 -3
- package/chunks/{dist-RRYNPBOE.js → dist-4L54HRX2.js} +2 -2
- package/chunks/{dist-WP4AH3VK.js → dist-BXDUQ2QY.js} +1 -1
- package/chunks/{dist-M6GFCZ7S.js → dist-MN2PDDPR.js} +1 -1
- package/chunks/{edit-3KCBTA25.js → edit-NRL5ZYWP.js} +32 -18
- package/chunks/{en-N5GMPCVT.js → en-HGJ2SPLM.js} +32 -3
- package/chunks/{enter-worktree-VWS5QZTU.js → enter-worktree-TLT6EXVN.js} +41 -17
- package/chunks/{exit-worktree-RVXFWAPD.js → exit-worktree-PE7RZMLE.js} +41 -17
- package/chunks/{exitPlanMode-UL5DILDG.js → exitPlanMode-JYMKAHYD.js} +15 -17
- package/chunks/{fr-BTHRYEXO.js → fr-CJULI7ZX.js} +29 -3
- package/chunks/{geminiContentGenerator-O2OPGHJG.js → geminiContentGenerator-GYIOVT4A.js} +3 -3
- package/chunks/{glob-57BSREPN.js → glob-T7KIVIWT.js} +15 -17
- package/chunks/{grep-XO5JOC7T.js → grep-4KI7ZJR5.js} +15 -17
- package/chunks/{ja-D63TAEBO.js → ja-L7CHRQEW.js} +29 -3
- package/chunks/{keychain-token-storage-DMFP5IJM.js → keychain-token-storage-335UOLJ6.js} +2 -2
- package/chunks/{ls-SUILOZZB.js → ls-7HD6XG3V.js} +3 -3
- package/chunks/{lsp-6TQBWVMZ.js → lsp-ZZSFCIWD.js} +2 -2
- package/chunks/{monitor-BECPGO3K.js → monitor-L5YPPGVO.js} +45 -26
- package/chunks/notebook-edit-HYXLIC3D.js +756 -0
- package/chunks/{openaiContentGenerator-KEZQHIRM.js → openaiContentGenerator-XRP5JM6P.js} +12 -11
- package/chunks/{pt-XUV7FSKC.js → pt-M6JULLEQ.js} +29 -3
- package/chunks/{qwenContentGenerator-RPMRXTNH.js → qwenContentGenerator-EOX3HZXX.js} +17 -19
- package/chunks/{qwenOAuth2-JSQ7EPR3.js → qwenOAuth2-EEJGROP7.js} +9 -3
- package/chunks/{read-file-LGHEIQNH.js → read-file-3XOYPTIB.js} +7 -8
- package/chunks/{ripGrep-6SFSXZ2G.js → ripGrep-5QUURWGU.js} +15 -17
- package/chunks/{ru-7KHWMN3A.js → ru-QILM4HBC.js} +29 -3
- package/chunks/{send-message-Q2JRAC3J.js → send-message-ULK4MQXJ.js} +23 -2
- package/chunks/{serve-27O2AFE3.js → serve-AZ44ZNWF.js} +8864 -1351
- package/chunks/{shell-J7K5KYCH.js → shell-O6OTTSIP.js} +15 -17
- package/chunks/{skill-2R7P4ATS.js → skill-MUGS3H4M.js} +23 -10
- package/chunks/{src-CGEDVW67.js → src-YCQBAYCP.js} +292 -28
- package/chunks/{syntheticOutput-S4DRGMQM.js → syntheticOutput-IS2X5OZ2.js} +3 -3
- package/chunks/{task-stop-7THHVAQS.js → task-stop-7QSJGSSP.js} +2 -2
- package/chunks/{todoWrite-WKUGUTPX.js → todoWrite-7CVACFUX.js} +3 -3
- package/chunks/{tool-search-XOH3ZWVS.js → tool-search-UFDQJOXT.js} +7 -8
- package/chunks/{web-fetch-OZE6ZQUF.js → web-fetch-ENQ2I5JA.js} +7 -4
- package/chunks/{write-file-74NQ27Q2.js → write-file-GBLDDYQT.js} +29 -18
- package/chunks/{zh-VGHU6XBB.js → zh-PWL2NKY3.js} +32 -3
- package/chunks/{zh-TW-O36Q4V7E.js → zh-TW-S3YGWICZ.js} +32 -3
- package/cli.js +61338 -76795
- package/locales/ca.js +44 -5
- package/locales/de.js +44 -5
- package/locales/en.js +49 -5
- package/locales/fr.js +45 -5
- package/locales/ja.js +43 -5
- package/locales/pt.js +43 -5
- package/locales/ru.js +43 -5
- package/locales/zh-TW.js +46 -4
- package/locales/zh.js +46 -4
- package/package.json +2 -2
- package/chunks/chunk-5P5XGNYH.js +0 -93
- package/chunks/chunk-SYCJMSIJ.js +0 -82
- package/chunks/chunk-Y6Z2O3WR.js +0 -33
|
@@ -12,6 +12,7 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
|
|
|
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
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
|
+
- **Remote runtime control** ([#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 17) — change a session's approval mode (`POST /session/:id/approval-mode`), toggle a tool per workspace (`POST /workspace/tools/:name/enable`), scaffold an empty `QWEN.md` (`POST /workspace/init`, mechanical only — does NOT call the model; for AI-fill, follow up with `POST /session/:id/prompt`), or restart a single MCP server with a budget pre-check (`POST /workspace/mcp/:server/restart`). All four are strict-gated — configure `--token` first.
|
|
15
16
|
|
|
16
17
|
## Quickstart
|
|
17
18
|
|
|
@@ -38,6 +39,53 @@ curl http://127.0.0.1:4170/capabilities
|
|
|
38
39
|
|
|
39
40
|
The `workspaceCwd` field surfaces the bound workspace so clients can pre-flight check + omit `cwd` on `POST /session`.
|
|
40
41
|
|
|
42
|
+
The daemon also exposes read-only runtime snapshots for client UIs:
|
|
43
|
+
`GET /workspace/mcp`, `GET /workspace/skills`, `GET /workspace/providers`,
|
|
44
|
+
`GET /workspace/env`, `GET /workspace/preflight`,
|
|
45
|
+
`GET /session/:id/context`, and `GET /session/:id/supported-commands`.
|
|
46
|
+
|
|
47
|
+
`GET /workspace/mcp`, `GET /workspace/skills`, and `GET /workspace/providers`
|
|
48
|
+
report the live ACP runtime and do not start the ACP child when idle; an
|
|
49
|
+
idle daemon returns `initialized: false` with an empty snapshot. Once a
|
|
50
|
+
session is alive they switch to `initialized: true` and surface the real
|
|
51
|
+
state.
|
|
52
|
+
|
|
53
|
+
`GET /workspace/env` and `GET /workspace/preflight` always answer with
|
|
54
|
+
`initialized: true` regardless of ACP state. `env` never consults ACP
|
|
55
|
+
(daemon-process info only); `preflight` answers daemon-level cells from
|
|
56
|
+
`process.*` and emits `status: 'not_started'` placeholders for ACP-level
|
|
57
|
+
cells when the child is idle.
|
|
58
|
+
|
|
59
|
+
`GET /workspace/env` reports the daemon process's runtime, platform, sandbox,
|
|
60
|
+
proxy, and the **presence** (never the value) of whitelisted secret env vars
|
|
61
|
+
such as `OPENAI_API_KEY`. Proxy URLs are stripped of credentials and reduced
|
|
62
|
+
to `host:port` before they hit the wire. The route always answers from the
|
|
63
|
+
daemon process directly and never spawns an ACP child.
|
|
64
|
+
|
|
65
|
+
`GET /workspace/preflight` returns a list of readiness checks. **Daemon-level
|
|
66
|
+
cells** (Node version, CLI entry, workspace directory, ripgrep, git, npm)
|
|
67
|
+
always render. **ACP-level cells** (auth, MCP discovery, skills, providers,
|
|
68
|
+
tool registry, egress) require a live ACP child — when the daemon is idle
|
|
69
|
+
they emit `status: 'not_started'` placeholders rather than spawning ACP just
|
|
70
|
+
to populate them. Failures map to a closed `errorKind` enum (`missing_binary`,
|
|
71
|
+
`auth_env_error`, `init_timeout`, `protocol_error`, `missing_file`,
|
|
72
|
+
`parse_error`, `blocked_egress`) so client UIs can render structured
|
|
73
|
+
remediation.
|
|
74
|
+
|
|
75
|
+
The daemon also exposes workspace file helpers:
|
|
76
|
+
|
|
77
|
+
- `GET /file` reads text files and returns a raw-byte `sha256:<hex>` hash.
|
|
78
|
+
- `GET /file/bytes` reads bounded raw byte windows and returns base64 content.
|
|
79
|
+
- `POST /file/write` creates or replaces text files.
|
|
80
|
+
- `POST /file/edit` applies one exact text replacement.
|
|
81
|
+
|
|
82
|
+
Write/edit are **strict mutation routes**: even on loopback they require a
|
|
83
|
+
configured bearer token, otherwise they return `token_required`. Replacements
|
|
84
|
+
and edits require the latest `expectedHash` from `GET /file` (or a full-window
|
|
85
|
+
`GET /file/bytes`). `create` never overwrites. Explicit writes to ignored paths
|
|
86
|
+
are allowed but audited. Binary writes, delete/move/mkdir, and recursive parent
|
|
87
|
+
creation are not part of this surface.
|
|
88
|
+
|
|
41
89
|
### 3. Open a session
|
|
42
90
|
|
|
43
91
|
```bash
|
|
@@ -97,6 +145,17 @@ qwen serve --hostname 0.0.0.0 --port 4170
|
|
|
97
145
|
|
|
98
146
|
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
147
|
|
|
148
|
+
> **Hardened loopback (`--require-auth`).** The default loopback no-token behavior is fine for a single-user laptop but unsafe on shared dev hosts, CI runners, or multi-tenant workstations where any local user can `curl 127.0.0.1:4170`. Pass `--require-auth` to make the bearer token mandatory on every route — including `/health` and `/capabilities` — even when bound to `127.0.0.1`. Boot fails without a token. With the flag on, an **unauthenticated** client can't read `/capabilities` to discover that auth is required; the discovery surface is the 401 response body itself. Once authenticated, the `caps.features.require_auth` tag is a post-auth confirmation that the deployment is hardened (useful for audit / compliance UIs):
|
|
149
|
+
>
|
|
150
|
+
> ```bash
|
|
151
|
+
> qwen serve --require-auth --token "$(openssl rand -hex 32)"
|
|
152
|
+
> # → /health, /capabilities, /session, … all require Authorization: Bearer …
|
|
153
|
+
> curl http://127.0.0.1:4170/health
|
|
154
|
+
> # → 401
|
|
155
|
+
> curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4170/capabilities | jq '.features | index("require_auth")'
|
|
156
|
+
> # → 13 (or whatever index — non-null after authenticating means the tag is present)
|
|
157
|
+
> ```
|
|
158
|
+
|
|
100
159
|
```bash
|
|
101
160
|
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
|
|
102
161
|
# → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
|
|
@@ -107,15 +166,19 @@ The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401
|
|
|
107
166
|
|
|
108
167
|
## CLI flags
|
|
109
168
|
|
|
110
|
-
| Flag
|
|
111
|
-
|
|
|
112
|
-
| `--port <n>`
|
|
113
|
-
| `--hostname <addr>`
|
|
114
|
-
| `--token <str>`
|
|
115
|
-
| `--
|
|
116
|
-
| `--
|
|
117
|
-
| `--
|
|
118
|
-
| `--
|
|
169
|
+
| Flag | Default | Purpose |
|
|
170
|
+
| ------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
171
|
+
| `--port <n>` | `4170` | TCP port. `0` = OS-assigned ephemeral port. |
|
|
172
|
+
| `--hostname <addr>` | `127.0.0.1` | Bind interface. Anything beyond loopback requires a token. |
|
|
173
|
+
| `--token <str>` | — | Bearer token. Falls back to `QWEN_SERVER_TOKEN` env var (with leading/trailing whitespace stripped — handy for `$(cat token.txt)`). |
|
|
174
|
+
| `--require-auth` | `false` | Refuse to start without a bearer token, even on loopback. Hardens the `127.0.0.1` developer default for shared dev hosts / CI runners / multi-tenant workstations where any local user can hit the listener. Boots only with `--token` or `QWEN_SERVER_TOKEN` set; gates `/health` behind the bearer too. |
|
|
175
|
+
| `--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). |
|
|
176
|
+
| `--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. |
|
|
177
|
+
| `--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. |
|
|
178
|
+
| `--event-ring-size <n>` | `8000` | Per-session SSE replay ring depth (#3803 §02 target). Sets the backlog available to `GET /session/:id/events` with `Last-Event-ID: N`. Larger = more reconnect headroom at the cost of a few hundred KB extra RAM per session. SDK clients can additionally request a larger per-subscriber backlog cap on a specific subscription via `?maxQueued=N` (range `[16, 2048]`, default 256). Daemons also emit a non-terminal `slow_client_warning` SSE frame at 75% queue fill so clients can drain / reconnect before getting evicted. Pre-flight `caps.features.slow_client_warning`. |
|
|
179
|
+
| `--mcp-client-budget <n>` | — | Positive integer cap on live MCP clients **per ACP session** (issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 14 v1; PR 23 graduates this to per-workspace via the shared MCP pool). Combine with `--mcp-budget-mode`. When unset, no accounting-driven enforcement (but `GET /workspace/mcp` still reports `clientCount`). Distinct from claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE` which gates startup concurrency, not the total client count. Pre-flight `caps.features.mcp_guardrails`. |
|
|
180
|
+
| `--mcp-budget-mode <m>` | `warn` / `off` | How `--mcp-client-budget` is enforced. `warn` (default when budget set): no refusal, snapshot's `budgets[0].status` flips to `warning` at ≥75% of budget. `enforce`: connects past the cap are refused, per-server cell shows `disabledReason: 'budget'`, deterministic by `mcpServers` declaration order. `off` (default when budget unset): pure observability. Boot rejects `enforce` without a budget. |
|
|
181
|
+
| `--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
182
|
|
|
120
183
|
> **Sizing the load knobs.** `--max-sessions` is the **new-child** cap.
|
|
121
184
|
> Three other layers also limit load — when sizing for a high-concurrency
|
|
@@ -135,6 +198,20 @@ The token comparison is constant-time (SHA-256 + `crypto.timingSafeEqual`); 401
|
|
|
135
198
|
> sizing assumes single-user / small-team load; raise progressively
|
|
136
199
|
> (and watch RSS) for multi-tenant deployments.
|
|
137
200
|
|
|
201
|
+
> **MCP client guardrails (issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 14).** A workspace declaring 30 MCP servers in `mcpServers` will start 30 clients with no upstream cap unless you set one. `--mcp-client-budget=N` caps the live MCP client count; `--mcp-budget-mode={enforce,warn,off}` chooses the behavior. Default is `warn` when a budget is set (snapshot surfaces the warning but no client is refused — useful for measuring real-world fanout before flipping on enforcement). Refused servers under `enforce` mode get `disabledReason: 'budget'` on their per-server cell, and the `budgets[0]` cell shows `status: 'error'` + `errorKind: 'budget_exhausted'`. Slot reservation is by server name and survives reconnects / discovery timeouts — a refused server can't take a slot from a healthy one.
|
|
202
|
+
>
|
|
203
|
+
> ⚠️ **v1 scope: per-session, not per-workspace.** Each ACP session inside the daemon has its own `Config`/`McpClientManager` (created via `newSessionConfig` per session). The budget caps live MCP clients **per session**, not aggregated across all sessions in the workspace. Snapshot at `GET /workspace/mcp` reflects the bootstrap session's view (the cell carries `scope: 'session'` for honesty). If you run 5 concurrent ACP sessions with `--mcp-client-budget=10`, you may have up to 50 live MCP clients across the daemon — the cap holds per session. **Wave 5 PR 23 (shared MCP pool)** introduces a workspace-scoped manager and graduates this to true per-workspace enforcement.
|
|
204
|
+
>
|
|
205
|
+
> ```sh
|
|
206
|
+
> qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn
|
|
207
|
+
> # later, after telemetry shows your real-world distribution:
|
|
208
|
+
> qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforce
|
|
209
|
+
> ```
|
|
210
|
+
>
|
|
211
|
+
> This is **not** the same as claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE` (which gates startup concurrency); they're orthogonal. PR 23 will add a real shared MCP pool (a `scope: 'workspace'` cell in `budgets[]` alongside the per-session cell); PR 14 v1 is the in-process counter + soft enforcement on the existing per-session manager.
|
|
212
|
+
>
|
|
213
|
+
> **Push events (issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 14b).** SDK clients subscribed to `GET /session/:id/events` receive typed frames when budget thresholds cross — `mcp_budget_warning` (synthetic, fires once per upward 75% crossing with hysteresis re-arm at 37.5%, advertised via `mcp_guardrail_events`) and `mcp_child_refused_batch` (coalesced once per discovery pass under `enforce` mode; length-1 from `readResource` lazy-spawn refusal). The snapshot at `GET /workspace/mcp` is still the source-of-truth for state-after-reconnect; events are change-edges. Useful when dashboarding in real-time without polling.
|
|
214
|
+
|
|
138
215
|
## Default deployment threat model
|
|
139
216
|
|
|
140
217
|
- **127.0.0.1 only** — loopback bind, no auth needed.
|
|
@@ -173,16 +250,48 @@ To host **multiple workspaces** (one user, several repos; or several users on th
|
|
|
173
250
|
|
|
174
251
|
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
252
|
|
|
253
|
+
## Loading and resuming a persisted session
|
|
254
|
+
|
|
255
|
+
The daemon exposes ACP's `session/load` and `session/unstable_resumeSession` over HTTP via two routes:
|
|
256
|
+
|
|
257
|
+
| Route | Use when |
|
|
258
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
259
|
+
| `POST /session/:id/load` | The client has **no** history rendered (cold reconnect, picker-then-open). The daemon replays every persisted turn through SSE so subscribers see the full transcript. Capability tag: `session_load`. |
|
|
260
|
+
| `POST /session/:id/resume` | The client already has the turns on screen and only needs the daemon-side handle back. Model context is restored on the agent side without UI replay — the SSE stream stays clean. Capability tag: `unstable_session_resume`. |
|
|
261
|
+
|
|
262
|
+
The TypeScript SDK exposes both as static factories on `DaemonSessionClient`:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';
|
|
266
|
+
|
|
267
|
+
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
|
|
268
|
+
|
|
269
|
+
// Cold reconnect — daemon will replay history through SSE.
|
|
270
|
+
const session = await DaemonSessionClient.load(client, 'persisted-id');
|
|
271
|
+
|
|
272
|
+
// Or, if your UI already has the history, skip the replay:
|
|
273
|
+
// const session = await DaemonSessionClient.resume(client, 'persisted-id');
|
|
274
|
+
|
|
275
|
+
for await (const event of session.events()) {
|
|
276
|
+
// First the replayed `session_update` frames (load only),
|
|
277
|
+
// then live events.
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Pre-flight `caps.features.session_load` / `caps.features.unstable_session_resume` before calling — older daemons return `404`. Concurrent same-action requests for the same id coalesce; cross-action races (a `load` racing a `resume`) get `409 restore_in_progress` with `Retry-After: 5`. See the [protocol reference](../developers/qwen-serve-protocol.md) for the full error envelope.
|
|
282
|
+
|
|
283
|
+
Note: history replay is bounded by the SSE ring (default 4000 frames). Long histories with chatty turns can exceed that — earliest frames are dropped silently. For very long sessions, prefer `resume` and rely on the client's local persisted UI.
|
|
284
|
+
|
|
176
285
|
## Durability model
|
|
177
286
|
|
|
178
|
-
**Sessions are ephemeral in Stage 1
|
|
287
|
+
**Sessions are still ephemeral in Stage 1 across daemon restarts**, but persisted sessions on disk can be reloaded:
|
|
179
288
|
|
|
180
|
-
- A child process crash publishes `session_died` and removes the session from the daemon's maps.
|
|
181
|
-
- A daemon restart loses every in-flight session.
|
|
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-
|
|
289
|
+
- A child process crash publishes `session_died` and removes the live session from the daemon's maps. The persisted on-disk session **can** be reloaded via `POST /session/:id/load` if a fresh agent child is spawnable.
|
|
290
|
+
- A daemon restart loses every in-flight live session. The persisted sessions remain on disk and can be loaded against a new daemon process, subject to the same workspace binding rules.
|
|
291
|
+
- 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-open SSE on long drops or call `POST /session/:id/load` to replay from disk.
|
|
183
292
|
- 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
293
|
|
|
185
|
-
If your integration needs cross-restart durability
|
|
294
|
+
If your integration needs server-side cross-restart durability beyond what `session/load` covers (e.g. server-managed retry queues), you still need application-level state recovery. Don't hold long-running, restart-sensitive state inside the daemon's session.
|
|
186
295
|
|
|
187
296
|
## Stage 1.5+ runtime guarantees
|
|
188
297
|
|
|
@@ -190,22 +299,21 @@ Stage 1's contract is sized for prototyping. Per [#3889 chiga0 downstream-consum
|
|
|
190
299
|
|
|
191
300
|
**Blockers for serious downstream use:**
|
|
192
301
|
|
|
193
|
-
1.
|
|
194
|
-
2.
|
|
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.
|
|
302
|
+
1. **`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.
|
|
303
|
+
2. **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
304
|
|
|
197
305
|
**Reliability baseline:**
|
|
198
306
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
307
|
+
3. ~~**Client-initiated heartbeat path**~~ — shipped via [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 9. `POST /session/:id/heartbeat` records last-seen timestamps on the daemon (capability tag `client_heartbeat`); SDK helpers are `DaemonClient.heartbeat()` / `DaemonSessionClient.heartbeat()`.
|
|
308
|
+
4. **`permission_already_resolved` event** when a vote loses the first-responder race — currently UIs have to infer state from a `404`.
|
|
309
|
+
5. **Larger / per-session-configurable replay ring** — default 4000 covers short drops; mobile / chatty-turn workloads need 8000+ or per-session config.
|
|
310
|
+
6. **`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
311
|
|
|
204
312
|
**Integration ergonomics:**
|
|
205
313
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
314
|
+
7. **`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.
|
|
315
|
+
8. **`/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".
|
|
316
|
+
9. **First-class durability documentation** (this section) — already shipped above.
|
|
209
317
|
|
|
210
318
|
The full convergence roadmap is tracked on [#3803](https://github.com/QwenLM/qwen-code/issues/3803).
|
|
211
319
|
|
|
@@ -269,6 +377,53 @@ The bridge keeps **one channel per daemon** (one daemon per workspace, per §02)
|
|
|
269
377
|
|
|
270
378
|
**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
379
|
|
|
380
|
+
## Logging in to a remote daemon (issue #4175 PR 21)
|
|
381
|
+
|
|
382
|
+
When the daemon runs on a remote pod (no shared display with you), you can still log in to a Qwen account by triggering an OAuth device flow over HTTP. The daemon polls the IdP itself; your job is just to open a URL on whatever device has a browser.
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# 1. Start a flow. The daemon contacts the IdP, returns a code + URL.
|
|
386
|
+
curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \
|
|
387
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
388
|
+
-H "Content-Type: application/json" \
|
|
389
|
+
-d '{"providerId":"qwen-oauth"}'
|
|
390
|
+
# → 201 {
|
|
391
|
+
# "deviceFlowId": "fa07c61b-…",
|
|
392
|
+
# "userCode": "USER-1",
|
|
393
|
+
# "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device",
|
|
394
|
+
# "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1",
|
|
395
|
+
# "expiresAt": 1700000600000,
|
|
396
|
+
# "intervalMs": 5000,
|
|
397
|
+
# "attached": false
|
|
398
|
+
# }
|
|
399
|
+
|
|
400
|
+
# 2. Visit the URL on your phone / laptop, enter the user code.
|
|
401
|
+
# 3. Poll for completion (or subscribe to SSE for the auth_device_flow_authorized event):
|
|
402
|
+
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
|
|
403
|
+
-H "Authorization: Bearer $TOKEN"
|
|
404
|
+
# → status transitions: pending → authorized
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The TypeScript SDK wraps both steps into a single helper:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { DaemonClient } from '@qwen-code/sdk';
|
|
411
|
+
|
|
412
|
+
const client = new DaemonClient({ baseUrl, token });
|
|
413
|
+
const flow = await client.auth.start({ providerId: 'qwen-oauth' });
|
|
414
|
+
console.log(`Open ${flow.verificationUri}\nCode: ${flow.userCode}`);
|
|
415
|
+
const result = await flow.awaitCompletion({ signal: abortCtrl.signal });
|
|
416
|
+
// result.status === 'authorized'
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**The daemon never opens a browser on your behalf.** Even when running locally, the daemon stays passive — it returns the URL and lets the SDK / user choose where to open it. This is intentional: a daemon on a headless pod that called `xdg-open` would silently fail, masking the actual auth surface. Mirror `gh auth login`'s "Press Enter to open browser" UX in your client.
|
|
420
|
+
|
|
421
|
+
**`--require-auth` and dev convenience.** The device-flow routes use the strict mutation gate (PR 15), which means a token-less loopback default returns `401 token_required`. Locally, the simplest way around this during development is `qwen serve --token=dev-token`; you don't need `--require-auth` unless you're hardening the loopback default.
|
|
422
|
+
|
|
423
|
+
**Cross-daemon limitation.** `oauth_creds.json` is daemon-shared (`~/.qwen/oauth_creds.json`), so a successful login in daemon A is automatically picked up by daemon B's next token refresh — but daemon B's SDK clients won't receive the `auth_device_flow_authorized` event (events are per-daemon).
|
|
424
|
+
|
|
425
|
+
**Cross-client take-over.** Two SDK clients on the same daemon that both `POST /workspace/auth/device-flow` for the same provider get the per-provider singleton: the first call starts a fresh IdP request and returns `attached: false`; the second call returns the EXISTING in-flight entry with `attached: true`. The take-over is recorded on the audit trail (under the second client's `X-Qwen-Client-Id`) but does NOT emit a separate event — both clients eventually observe the SAME `auth_device_flow_authorized` once the user finishes the IdP page. If your UI distinguishes "I started this" from "someone else's flow I joined", branch on the `attached` field returned by `start()`.
|
|
426
|
+
|
|
272
427
|
## What's next
|
|
273
428
|
|
|
274
429
|
- **Build a client?** See the [DaemonClient TypeScript quickstart](../developers/examples/daemon-client-quickstart.md) and the [HTTP protocol reference](../developers/qwen-serve-protocol.md).
|
|
@@ -24,11 +24,11 @@ This document lists the available keyboard shortcuts in Qwen Code.
|
|
|
24
24
|
| `!` | Toggle shell mode when the input is empty. |
|
|
25
25
|
| `?` | Toggle keyboard shortcuts display when the input is empty. |
|
|
26
26
|
| `\` (at end of line) + `Enter` | Insert a newline. |
|
|
27
|
-
| `Down Arrow` |
|
|
27
|
+
| `Down Arrow` | Row down, then snap to end, then history next. |
|
|
28
28
|
| `Enter` | Submit the current prompt. |
|
|
29
29
|
| `Meta+Delete` / `Ctrl+Delete` | Delete the word to the right of the cursor. |
|
|
30
30
|
| `Tab` | Autocomplete the current suggestion if one exists. |
|
|
31
|
-
| `Up Arrow` |
|
|
31
|
+
| `Up Arrow` | Row up, then snap to start, then history prev. |
|
|
32
32
|
| `Ctrl+A` / `Home` | Move the cursor to the beginning of the line. |
|
|
33
33
|
| `Ctrl+B` / `Left Arrow` | Move the cursor one character to the left. |
|
|
34
34
|
| `Ctrl+C` | Clear the input prompt |
|
|
@@ -39,8 +39,8 @@ This document lists the available keyboard shortcuts in Qwen Code.
|
|
|
39
39
|
| `Ctrl+H` / `Backspace` | Delete the character to the left of the cursor. |
|
|
40
40
|
| `Ctrl+K` | Delete from the cursor to the end of the line. |
|
|
41
41
|
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. |
|
|
42
|
-
| `Ctrl+N` |
|
|
43
|
-
| `Ctrl+P` |
|
|
42
|
+
| `Ctrl+N` | Row down, then snap to end, then history next. |
|
|
43
|
+
| `Ctrl+P` | Row up, then snap to start, then history prev. |
|
|
44
44
|
| `Ctrl+R` | Reverse search through input/shell history. |
|
|
45
45
|
| `Ctrl+Y` | Retry the last failed request. |
|
|
46
46
|
| `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F` | Move the cursor one word to the right. |
|
|
@@ -59,13 +59,13 @@ This document lists the available keyboard shortcuts in Qwen Code.
|
|
|
59
59
|
|
|
60
60
|
## Radio Button Select
|
|
61
61
|
|
|
62
|
-
| Shortcut
|
|
63
|
-
|
|
|
64
|
-
| `Down Arrow` / `j` | Move selection down. |
|
|
65
|
-
| `Enter`
|
|
66
|
-
| `Up Arrow` / `k` | Move selection up. |
|
|
67
|
-
| `1-9`
|
|
68
|
-
| (multi-digit)
|
|
62
|
+
| Shortcut | Description |
|
|
63
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `Down Arrow` / `j` / `Ctrl+N` | Move selection down. |
|
|
65
|
+
| `Enter` | Confirm selection. |
|
|
66
|
+
| `Up Arrow` / `k` / `Ctrl+P` | Move selection up. |
|
|
67
|
+
| `1-9` | Select an item by its number. |
|
|
68
|
+
| (multi-digit) | For items with numbers greater than 9, press the digits in quick succession to select the corresponding item. |
|
|
69
69
|
|
|
70
70
|
## IDE Integration
|
|
71
71
|
|
package/bundled/review/SKILL.md
CHANGED
|
@@ -18,8 +18,10 @@ You are an expert code reviewer. Your job is to review code changes and provide
|
|
|
18
18
|
|
|
19
19
|
**Critical rules (most commonly violated — read these first):**
|
|
20
20
|
|
|
21
|
-
1. **
|
|
22
|
-
2. **Step
|
|
21
|
+
1. **For same-repo PR reviews (PR number, or URL whose owner/repo matches a local remote), the worktree is MANDATORY.** After argument parsing and remote detection (early in Step 1), the first command that touches code state MUST be `qwen review fetch-pr`. Do NOT use `gh pr checkout`, `git checkout <branch>`, `git switch`, `git pull`, `git reset --hard`, or any other command that modifies the user's current HEAD or working tree. After `fetch-pr` returns, ALL subsequent reads, linters, builds, tests, and edits MUST happen inside the `worktreePath` it created. Violating this contaminates the user's local branch state. (Cross-repo PRs with no matching remote use lightweight mode and do NOT create a worktree — see Step 1.)
|
|
22
|
+
2. **If `--comment` was specified, Step 8 (Autofix) is SKIPPED entirely.** `--comment` means the user wants inline PR comments posted, not code mutations. Do not ask "Apply auto-fixes? (y/n)" — go straight from Step 7 to Step 9.
|
|
23
|
+
3. **Match the language of the PR.** If the PR is in English, ALL your output (terminal + PR comments) MUST be in English. If in Chinese, use Chinese. Do NOT switch languages. For **local reviews** (no PR), if the system prompt includes an output language preference, use that language; otherwise follow the user's input language.
|
|
24
|
+
4. **Step 9: use Create Review API** with `comments` array for inline comments. Do NOT use `gh api .../pulls/.../comments` to post individual comments. See Step 9 for the JSON format.
|
|
23
25
|
|
|
24
26
|
**Design philosophy: Silence is better than noise.** Every comment you make should be worth the reader's time. If you're unsure whether something is a problem, DO NOT MENTION IT. Low-quality feedback causes "cry wolf" fatigue — developers stop reading all AI comments and miss real issues.
|
|
25
27
|
|
|
@@ -44,6 +46,8 @@ Based on the remaining arguments:
|
|
|
44
46
|
- If both diffs are empty, inform the user there are no changes to review and stop here — do not proceed to the review agents
|
|
45
47
|
|
|
46
48
|
- **PR number or same-repo URL** (e.g., `123` or a URL whose owner/repo matches the current repo — cross-repo URLs are handled by the lightweight mode above):
|
|
49
|
+
|
|
50
|
+
> ⚠️ **MANDATORY worktree flow.** Do NOT use `gh pr checkout`, `git checkout <branch>`, `git switch`, `git pull`, `git reset --hard`, or any other command that changes the user's current HEAD or working tree contents. The ONLY entry point is `qwen review fetch-pr` (below) — it isolates the PR into an ephemeral worktree so the user's local state is never touched. After it returns, every subsequent command in Steps 2-8 MUST operate inside the returned `worktreePath` (e.g. `cd <worktreePath>` first, or pass the path as a `--cwd` / explicit argument).
|
|
47
51
|
- **Run `qwen review fetch-pr`** to set up the working state in one pass — it cleans any stale worktree, fetches the PR HEAD into `qwen-review/pr-<n>`, queries `gh pr view` for metadata, and creates an ephemeral worktree at `.qwen/tmp/review-pr-<n>`:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
@@ -442,7 +446,12 @@ If the user responds with "post comments" (or similar intent like "yes post them
|
|
|
442
446
|
|
|
443
447
|
## Step 8: Autofix
|
|
444
448
|
|
|
445
|
-
|
|
449
|
+
**Skip this entire step (do not even ask) if EITHER of the following is true:**
|
|
450
|
+
|
|
451
|
+
- `--comment` was specified in the arguments — the user explicitly asked for inline PR comments, not code edits. Go straight to Step 9.
|
|
452
|
+
- The review target is a cross-repo PR running in lightweight mode (no local files to edit).
|
|
453
|
+
|
|
454
|
+
Otherwise, if there are **Critical** or **Suggestion** findings with clear, unambiguous fixes, offer to auto-apply them. (If there are no such findings, this step is also a no-op — fall through to Step 9.)
|
|
446
455
|
|
|
447
456
|
1. Count the number of auto-fixable findings (those with concrete suggested fixes that can be expressed as file edits).
|
|
448
457
|
2. If there are fixable findings, ask the user:
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stuck
|
|
3
|
+
description: Diagnose frozen, stuck, or slow Qwen Code sessions on this machine. Scans for problematic processes, high CPU/memory usage, hung subprocesses, and debug logs. Use /stuck or /stuck <PID> to focus on a specific process.
|
|
4
|
+
argument-hint: '[PID or symptom]'
|
|
5
|
+
allowedTools:
|
|
6
|
+
- run_shell_command
|
|
7
|
+
- read_file
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /stuck — diagnose frozen/slow Qwen Code sessions
|
|
11
|
+
|
|
12
|
+
The user thinks another Qwen Code session on this machine is frozen, stuck, or very slow. Investigate and present a diagnostic report.
|
|
13
|
+
|
|
14
|
+
## What to look for
|
|
15
|
+
|
|
16
|
+
Scan for other Qwen Code processes (excluding the current one — exclude the PID you see running this prompt). Since Qwen Code is a Node.js CLI (`#!/usr/bin/env node`), the process name (`comm` column) is always `node` (or `bun` if run with Bun). Identify Qwen Code sessions by looking at the `command` column for a script path inside a directory whose name starts with `qwen-code` (matches `qwen-code/`, `qwen-code-dev/`, worktree clones, etc.) — anchored to the start of the path or after `/` so unrelated names like `analyze-qwen-code/` don't false-match — or a bin invocation ending in `/qwen` (the global symlink). Avoid loose `qwen-code` substring matching: it false-positives on plugin brokers that merely pass a qwen-code path as `--cwd`.
|
|
17
|
+
|
|
18
|
+
Signs of a stuck session:
|
|
19
|
+
|
|
20
|
+
- **High CPU (>=90%) sustained** — likely an infinite loop. Sample twice, 1-2s apart, to confirm it's not a transient spike.
|
|
21
|
+
- **Process state `D` / `U` (uninterruptible sleep)** — often an I/O hang. Linux uses `D`, macOS/BSD uses `U`. The `state` column in `ps` output; first character matters (ignore modifiers like `+`, `s`, `<`).
|
|
22
|
+
- **Process state `T` (stopped)** — user probably hit Ctrl+Z by accident.
|
|
23
|
+
- **Process state `Z` (zombie)** — parent isn't reaping.
|
|
24
|
+
- **Very high RSS (>=4GB)** — possible memory leak making the session sluggish.
|
|
25
|
+
- **State `S` with low CPU** — the most common hang signature: a hung HTTPS request to the model API. Not a process-level red flag on its own, but combined with the user reporting "stuck", treat it as a strong signal to run the network check in step 3.
|
|
26
|
+
- **Stuck child process** — a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -P <pid>` (then `ps -p` for state — see step 3) for each session.
|
|
27
|
+
|
|
28
|
+
## Argument validation
|
|
29
|
+
|
|
30
|
+
If the user gave an argument, treat it as a PID **only if it consists entirely of digits 0-9**. Anything else — letters, whitespace, punctuation — fails the check, in which case treat it as a free-text symptom description (guidance for the report only, never substituted into shell commands). The strict digit-only whitelist is safer than enumerating shell metacharacters.
|
|
31
|
+
|
|
32
|
+
## Investigation steps
|
|
33
|
+
|
|
34
|
+
**Preamble — resolve the runtime base directory.** Required for both paths below (sidecar enumeration in step 1, debug log lookup in step 3, and the PID fast path). The base directory is taken from (in priority order): `QWEN_RUNTIME_DIR` env var, the `advanced.runtimeOutputDir` setting, `QWEN_HOME` env var, and finally `~/.qwen`.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}"
|
|
38
|
+
[ -z "$RUNTIME_DIR" ] && command -v jq >/dev/null && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' "${QWEN_HOME:-$HOME/.qwen}/settings.json" 2>/dev/null)
|
|
39
|
+
# `advanced.runtimeOutputDir` may be `~/...` or relative; mirror Storage.resolvePath() before using in globs
|
|
40
|
+
[ -n "$RUNTIME_DIR" ] && RUNTIME_DIR="${RUNTIME_DIR/#\~/$HOME}"
|
|
41
|
+
[ -n "$RUNTIME_DIR" ] && case "$RUNTIME_DIR" in /*) ;; *) RUNTIME_DIR="$(cd "$RUNTIME_DIR" 2>/dev/null && pwd)" || RUNTIME_DIR="" ;; esac
|
|
42
|
+
RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(If `jq` isn't installed, the settings layer is silently skipped — the env-var / default fallback covers the common case.)
|
|
46
|
+
|
|
47
|
+
**Fast path for targeted diagnosis** — if a digit-only PID argument was given, skip step 1 enumeration. Validate that the PID is a live current-user Qwen Code process before dumping any details:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
kill -0 <pid> 2>/dev/null || { echo "PID <pid> is dead, or owned by another user"; exit 0; }
|
|
51
|
+
ps -p <pid> -o command= -ww 2>/dev/null | grep -qE '((^|/)qwen-code[^ /]*/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))' || { echo "PID <pid> is yours but is not a Qwen Code process — refusing to dump details"; exit 0; }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If either guard prints, stop the diagnostic and surface the message verbatim. Otherwise, gather stats and the sidecar mapping, then jump to step 3:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
ps -p <pid> -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww
|
|
58
|
+
grep -El '"pid"[[:space:]]*:[[:space:]]*<pid>\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Note: as in step 2, the `command=` column may include credentials passed as CLI args (e.g., `--openai-api-key=sk-…`). Redact such values to `***` before quoting them in the report.
|
|
62
|
+
|
|
63
|
+
`-E` is required so `\b` is interpreted as word boundary (BSD `grep` without `-E` treats `\b` as a backspace character, silently returning nothing on macOS). The `-l` flag returns the matching sidecar file path; the basename (stripped of `.runtime.json`) is the session ID for step 3's debug log read. If multiple sidecars match (rare — happens only after PID reuse leaves a stale file), prefer the most recently modified one: `ls -t <matches> | head -n 1`.
|
|
64
|
+
|
|
65
|
+
Otherwise (no arg, or symptom-only arg), run the general path below:
|
|
66
|
+
|
|
67
|
+
1. **Enumerate live sessions via the runtime sidecar** (preferred, reliable):
|
|
68
|
+
|
|
69
|
+
Qwen Code writes a `runtime.json` sidecar for each interactive session at `"$RUNTIME_DIR"/projects/<sanitized-cwd>/chats/<sessionId>.runtime.json`. Each file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` — the authoritative source of `(pid, session_id, work_dir)` mappings.
|
|
70
|
+
|
|
71
|
+
Filter to live `(pid, sidecar-path)` pairs in one shot. Use Node (guaranteed available — qwen-code requires it) instead of `jq` (often missing on default macOS / minimal Linux) so this path doesn't silently degrade:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
node -e 'const fs=require("fs"); for (const f of process.argv.slice(1)) { try { const p=JSON.parse(fs.readFileSync(f,"utf8")).pid; if (p) { try { process.kill(p,0); console.log(p+" "+f); } catch {} } } catch {} }' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
PID reuse is rare but possible — when you cross-reference with `ps` in step 2, skip pairs whose live PID's command line no longer looks like a Qwen Code process.
|
|
78
|
+
|
|
79
|
+
**If the command emits nothing** (no sidecars, or no live PIDs), fall through to step 2 — `ps` is the working fallback.
|
|
80
|
+
|
|
81
|
+
2. **List Qwen Code processes via `ps`** (macOS/Linux) — used to enrich each live session with CPU/RSS/state/uptime, and to catch sessions that may have started before the sidecar feature existed:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
ps -xo pid=,pcpu=,rss=,etime=,state=,comm=,command= -u "$(id -u)" -ww | grep -E '((^|/)qwen-code[^ /]*/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))' | grep -v grep
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`-u "$(id -u)"` restricts the scan to the current user — on shared hosts this avoids exposing other users' Qwen process paths/arguments into the chat. `-ww` disables column truncation so long "qwen" paths aren't cut off. The `comm` column will be `node` or `bun`, not `qwen`; filter to rows where the `command` column contains a qwen path (e.g., `qwen-code/dist/cli.js`, or a bin symlink ending in `/qwen`). Cross-reference with the PIDs from step 1.
|
|
88
|
+
|
|
89
|
+
Note: `ps` reports `rss` in **kilobytes** on both macOS and Linux. To report in MB, divide by 1024; to report in GB, divide by 1048576. The 4GB threshold is `4194304` KB — compare the raw `rss` value against that, or compare the GB value against 4. Do not divide once and then compare against 4; that would flag every process >4MB as "very high RSS".
|
|
90
|
+
|
|
91
|
+
Note: full command lines may contain credentials passed as CLI args (e.g., `--openai-api-key=sk-…`). Redact such values to `***` before quoting them in the report.
|
|
92
|
+
|
|
93
|
+
3. **For anything suspicious**, gather more context. If the process state alone explains the problem (`T` = accidentally stopped, `Z` = parent not reaping), skip directly to the report — child / log / stack inspection adds nothing. Otherwise:
|
|
94
|
+
- Child processes (with state, so a hung `git` / `node` shows up): `CHILDREN=$(pgrep -P <pid> | tr '\n' ',' | sed 's/,$//'); [ -n "$CHILDREN" ] && ps -p "$CHILDREN" -o pid=,ppid=,pcpu=,state=,etime=,command= -ww`. Single `ps` call (avoids forking one per child) and `-ww` so long child command lines aren't truncated.
|
|
95
|
+
- If high CPU: sample again after 1-2s to confirm it's sustained
|
|
96
|
+
- **Network hang** — if CPU is low and state is `S` despite the user reporting "stuck", the most likely cause is a hung HTTPS request to the model API. macOS: `lsof -nP -i -p <pid> 2>/dev/null | head -20` (the `-nP` flags skip reverse-DNS and port lookups, which can themselves hang). If `lsof` itself feels slow, prefix with `timeout 10` (or `gtimeout 10` on macOS with Homebrew coreutils). Linux: `ss -tnp 2>/dev/null | grep "pid=<pid>,"`. Note that `ss -tnp`'s `-p` requires root or `CAP_NET_ADMIN` — without it, the PID column shows `-` and the grep returns empty. If you see no matches but `ss -t 2>/dev/null` does show ESTABLISHED sockets, fall back to `lsof -nP -i -p <pid>` rather than reporting "no connections". A long-lived `ESTABLISHED` connection to a model host (dashscope, openai, anthropic, etc.) with no recent traffic is the smoking gun.
|
|
97
|
+
- **Debug log** — start with `"$RUNTIME_DIR"/debug/latest` (symlink to the most recent session); if it matches the suspicious PID's session, that's usually the right one. Otherwise infer the session ID from the sidecar and read `"$RUNTIME_DIR"/debug/<session-id>.txt`. Bound the read with `tail -n 200 <path>` — debug logs can be GB-sized. The last few hundred lines typically show what the session was doing before hanging. Debug logs may contain prompts, file contents, or tokens from other sessions — paste only lines relevant to the hang, and never quote secrets/API keys you happen to see.
|
|
98
|
+
|
|
99
|
+
4. **Consider a stack dump** for a truly frozen process (advanced, optional):
|
|
100
|
+
- macOS: `sample <pid> 3` gives a 3-second native stack sample. If `sample` itself seems to hang (the target's Mach task port may be wedged on a kernel-level freeze), wrap it: `timeout 15 sample <pid> 3` (or `gtimeout 15 ...` on Homebrew coreutils). Stack frames may include function arguments containing API keys or tokens held in memory — redact such values to `***` before including the dump in the report.
|
|
101
|
+
- Linux: `cat /proc/<pid>/stack` for kernel stack (read-only, no `ptrace` permissions needed). Avoid `strace -p` for this purpose: it requires `CAP_SYS_PTRACE` (often denied under `kernel.yama.ptrace_scope=1`), and `strace -c` blocks until the target exits — it would hang on the very kind of stuck process you are diagnosing.
|
|
102
|
+
- This is big — only grab it if the process is clearly hung and you want to know _why_
|
|
103
|
+
|
|
104
|
+
## Report
|
|
105
|
+
|
|
106
|
+
Present a structured diagnostic report directly to the user with these sections:
|
|
107
|
+
|
|
108
|
+
**For each stuck/slow session found:**
|
|
109
|
+
|
|
110
|
+
- PID, CPU%, RSS (in MB), process state, uptime, full command line
|
|
111
|
+
- Child processes and their states
|
|
112
|
+
- Your diagnosis of what's likely wrong
|
|
113
|
+
- Relevant debug log tail if you captured it
|
|
114
|
+
- Stack dump output if you captured it
|
|
115
|
+
- Suggested next step for the user to decide (e.g., "user may consider `kill <pid>` if the session is unresponsive", "likely waiting on I/O — check disk", "accidentally stopped — user can resume with `kill -CONT <pid>`"). Do not execute these actions yourself — present them as options for the user.
|
|
116
|
+
|
|
117
|
+
**If every session looks healthy**, tell the user directly — no diagnostic dump needed. Mention how many sessions you checked and that none showed signs of being stuck.
|
|
118
|
+
|
|
119
|
+
**If no sessions are found at all** (zero sidecars and zero matching `ps` rows), say so explicitly: which `RUNTIME_DIR` you searched and that `ps` returned no qwen-related processes for the current user. Suggest the session may have already exited.
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- Don't kill or signal any processes — this is diagnostic only.
|
|
124
|
+
- If the user gave an argument (e.g., a specific PID or symptom), focus there first.
|
|
@@ -7,32 +7,30 @@ import {
|
|
|
7
7
|
hasRebuiltToolRegistry,
|
|
8
8
|
rebuildToolRegistryOnOverride,
|
|
9
9
|
resolveSubagentApprovalMode
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-5P5XGNYH.js";
|
|
10
|
+
} from "./chunk-OZO6R4KI.js";
|
|
12
11
|
import "./chunk-K5PGHDBN.js";
|
|
13
12
|
import "./chunk-O4PICXES.js";
|
|
14
13
|
import "./chunk-TW522KN6.js";
|
|
15
14
|
import "./chunk-MLZQVCF3.js";
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-CAVZVZX6.js";
|
|
16
|
+
import "./chunk-G7YTSRES.js";
|
|
17
|
+
import "./chunk-4AOCVI6J.js";
|
|
19
18
|
import "./chunk-77WXWU44.js";
|
|
20
|
-
import "./chunk-
|
|
21
|
-
import "./chunk-
|
|
22
|
-
import "./chunk-
|
|
23
|
-
import "./chunk-SYCJMSIJ.js";
|
|
19
|
+
import "./chunk-F23NCRJ2.js";
|
|
20
|
+
import "./chunk-CSWBPY3P.js";
|
|
21
|
+
import "./chunk-4U3O4VDA.js";
|
|
24
22
|
import "./chunk-UWCTAVOD.js";
|
|
25
23
|
import "./chunk-OFEVLU4C.js";
|
|
26
|
-
import "./chunk-
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-PR4T27R7.js";
|
|
25
|
+
import "./chunk-MAY32HXD.js";
|
|
26
|
+
import "./chunk-D5NTAHYL.js";
|
|
29
27
|
import "./chunk-T4VD6OJ4.js";
|
|
30
28
|
import "./chunk-RDYWTWEM.js";
|
|
31
|
-
import "./chunk-
|
|
32
|
-
import "./chunk-
|
|
33
|
-
import "./chunk-
|
|
34
|
-
import "./chunk-
|
|
35
|
-
import "./chunk-
|
|
29
|
+
import "./chunk-YJLGXDQJ.js";
|
|
30
|
+
import "./chunk-PVVL5Q3W.js";
|
|
31
|
+
import "./chunk-GGNTZ2NH.js";
|
|
32
|
+
import "./chunk-KXZ4TJB4.js";
|
|
33
|
+
import "./chunk-XP27SJMH.js";
|
|
36
34
|
import "./chunk-E7E2MFYM.js";
|
|
37
35
|
import "./chunk-ZERZSAZL.js";
|
|
38
36
|
import "./chunk-QN5NZ3UQ.js";
|