From 3078934261e1658547d9db80c9ef7d9c85221cb1 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 14 May 2026 10:40:09 +0800 Subject: [PATCH 01/15] feat(skills): add /stuck diagnostic skill for frozen sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the /stuck diagnostic capability to qwen-code as a bundled skill. Scans for stuck processes, high CPU/memory, hung subprocesses, and debug logs, then presents a structured diagnostic report. Adapted from claude-code's internal /stuck skill with: - Process identification via command path (node-based CLI, not compiled binary) - Debug log path updated to ~/.qwen/debug/ - Cross-platform stack dump support (macOS sample + Linux /proc/stack) - Direct user-facing output (no Slack dependency) ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/core/src/skills/bundled/stuck/SKILL.md diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md new file mode 100644 index 00000000000..48b9ed713a2 --- /dev/null +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -0,0 +1,64 @@ +--- +name: stuck +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 to focus on a specific process. +argument-hint: '[PID or symptom]' +--- + +# /stuck โ€” diagnose frozen/slow Qwen Code sessions + +The user thinks another Qwen Code session on this machine is frozen, stuck, or very slow. Investigate and present a diagnostic report. + +## What to look for + +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 paths containing "qwen" (e.g., `node /path/to/qwen-code/dist/cli.js` or a symlinked `qwen` script). + +Signs of a stuck session: + +- **High CPU (>=90%) sustained** โ€” likely an infinite loop. Sample twice, 1-2s apart, to confirm it's not a transient spike. +- **Process state `D` (uninterruptible sleep)** โ€” often an I/O hang. The `state` column in `ps` output; first character matters (ignore modifiers like `+`, `s`, `<`). +- **Process state `T` (stopped)** โ€” user probably hit Ctrl+Z by accident. +- **Process state `Z` (zombie)** โ€” parent isn't reaping. +- **Very high RSS (>=4GB)** โ€” possible memory leak making the session sluggish. +- **Stuck child process** โ€” a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -lP ` for each session. + +## Investigation steps + +1. **List all Qwen Code processes** (macOS/Linux): + + ``` + ps -axo pid=,pcpu=,rss=,etime=,state=,comm=,command= | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep + ``` + + The `comm` column will be `node` or `bun`, not `qwen`. Filter to rows where the `command` column contains a path related to qwen (e.g., `qwen-code/dist/cli.js`, or a bin symlink ending in `/qwen`). + +2. **For anything suspicious**, gather more context: + - Child processes: `pgrep -lP ` + - If high CPU: sample again after 1-2s to confirm it's sustained + - If a child looks hung (e.g., a git command), note its full command line with `ps -p -o command=` + - Check the session's debug log if you can infer the session ID: `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging) + - The `~/.qwen/debug/latest` symlink points to the most recent session's log + +3. **Consider a stack dump** for a truly frozen process (advanced, optional): + - macOS: `sample 3` gives a 3-second native stack sample + - Linux: `cat /proc//stack` for kernel stack, or `strace -p -c -f` for syscall profile + - This is big โ€” only grab it if the process is clearly hung and you want to know _why_ + +## Report + +Present a structured diagnostic report directly to the user with these sections: + +**For each stuck/slow session found:** + +- PID, CPU%, RSS (in MB), process state, uptime, full command line +- Child processes and their states +- Your diagnosis of what's likely wrong +- Relevant debug log tail if you captured it +- Stack dump output if you captured it +- Recommended action (e.g., "safe to kill with `kill `", "waiting on I/O โ€” check disk", "accidentally stopped โ€” resume with `kill -CONT `") + +**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. + +## Notes + +- Don't kill or signal any processes โ€” this is diagnostic only. +- If the user gave an argument (e.g., a specific PID or symptom), focus there first. From d92021db6764b411d9e14a3d03324b2845847c35 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 14 May 2026 11:46:09 +0800 Subject: [PATCH 02/15] fix(skills): respect QWEN_RUNTIME_DIR/QWEN_HOME for debug log path in /stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 48b9ed713a2..5200355e2bd 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -35,7 +35,7 @@ Signs of a stuck session: - Child processes: `pgrep -lP ` - If high CPU: sample again after 1-2s to confirm it's sustained - If a child looks hung (e.g., a git command), note its full command line with `ps -p -o command=` - - Check the session's debug log if you can infer the session ID: `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging) + - Check the session's debug log if you can infer the session ID: `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. - The `~/.qwen/debug/latest` symlink points to the most recent session's log 3. **Consider a stack dump** for a truly frozen process (advanced, optional): From a5f85e7c36fbdf75aebfc9ca9e8be7c4ef0881d3 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 14 May 2026 14:20:19 +0800 Subject: [PATCH 03/15] fix(skills): add allowedTools and clarify diagnostic-only boundary in /stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add allowedTools (run_shell_command, read_file) for convention consistency - Rephrase recommended actions as user-facing options, not model-executable commands ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 5200355e2bd..5067c6b10dd 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -2,6 +2,9 @@ name: stuck 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 to focus on a specific process. argument-hint: '[PID or symptom]' +allowedTools: + - run_shell_command + - read_file --- # /stuck โ€” diagnose frozen/slow Qwen Code sessions @@ -54,7 +57,7 @@ Present a structured diagnostic report directly to the user with these sections: - Your diagnosis of what's likely wrong - Relevant debug log tail if you captured it - Stack dump output if you captured it -- Recommended action (e.g., "safe to kill with `kill `", "waiting on I/O โ€” check disk", "accidentally stopped โ€” resume with `kill -CONT `") +- Suggested next step for the user to decide (e.g., "user may consider `kill ` if the session is unresponsive", "likely waiting on I/O โ€” check disk", "accidentally stopped โ€” user can resume with `kill -CONT `"). Do not execute these actions yourself โ€” present them as options for the user. **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. From 72639a0a50b105bcf5a1be424d1c692e25772570 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Fri, 15 May 2026 14:50:29 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix(skills):=20address=20review=20feedbac?= =?UTF-8?q?k=20for=20/stuck=20=E2=80=94=20security,=20accuracy,=20sidecar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit PID argument validation (reject shell metacharacters) to prevent the model from substituting injection payloads into shell commands - Mention macOS/BSD `U` state alongside Linux `D` for uninterruptible sleep, so I/O-blocked macOS sessions are not silently missed - Add `-ww` to `ps` to disable column truncation, so long qwen paths don't fall outside the grep window and cause sessions to be missed - Use `~/.qwen/projects/*/chats/*.runtime.json` sidecars as the primary source of (pid, sessionId, workDir) mappings; `ps` is now a supplement for CPU/RSS/state enrichment ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 5067c6b10dd..2a4c0900953 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -18,30 +18,44 @@ Scan for other Qwen Code processes (excluding the current one โ€” exclude the PI Signs of a stuck session: - **High CPU (>=90%) sustained** โ€” likely an infinite loop. Sample twice, 1-2s apart, to confirm it's not a transient spike. -- **Process state `D` (uninterruptible sleep)** โ€” often an I/O hang. The `state` column in `ps` output; first character matters (ignore modifiers like `+`, `s`, `<`). +- **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`, `<`). - **Process state `T` (stopped)** โ€” user probably hit Ctrl+Z by accident. - **Process state `Z` (zombie)** โ€” parent isn't reaping. - **Very high RSS (>=4GB)** โ€” possible memory leak making the session sluggish. - **Stuck child process** โ€” a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -lP ` for each session. +## Argument validation + +If the user gave an argument, first check whether it is a positive integer (PID). If it contains shell metacharacters (`$`, `;`, `|`, `&`, `` ` ``, `(`, `)`, `<`, `>`, `*`, `?`, backslash, quote, whitespace, etc.), refuse to proceed and tell the user the argument does not look like a valid PID. Treat free-text symptom descriptions as guidance for the report, not as values to substitute into shell commands. + ## Investigation steps -1. **List all Qwen Code processes** (macOS/Linux): +1. **Enumerate live sessions via the runtime sidecar (preferred, reliable)**: + + Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The default `` is `~/.qwen` (overridable by `QWEN_RUNTIME_DIR` or `QWEN_HOME`). Each file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}`. Use this as the authoritative source of `(pid, session_id, work_dir)` mappings: + + ``` + ls -1 ~/.qwen/projects/*/chats/*.runtime.json 2>/dev/null + ``` + + For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. + +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: ``` - ps -axo pid=,pcpu=,rss=,etime=,state=,comm=,command= | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep + ps -axo pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep ``` - The `comm` column will be `node` or `bun`, not `qwen`. Filter to rows where the `command` column contains a path related to qwen (e.g., `qwen-code/dist/cli.js`, or a bin symlink ending in `/qwen`). + The `-ww` flag disables column truncation โ€” without it, long command paths get cut off and `grep` may miss sessions whose "qwen" path component falls beyond the terminal width. The `comm` column will be `node` or `bun`, not `qwen`. Filter to rows where the `command` column contains a path related to qwen (e.g., `qwen-code/dist/cli.js`, or a bin symlink ending in `/qwen`). Cross-reference with the PIDs from step 1. -2. **For anything suspicious**, gather more context: +3. **For anything suspicious**, gather more context. Substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): - Child processes: `pgrep -lP ` - If high CPU: sample again after 1-2s to confirm it's sustained - If a child looks hung (e.g., a git command), note its full command line with `ps -p -o command=` - - Check the session's debug log if you can infer the session ID: `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. + - Check the session's debug log if you can infer the session ID (from the sidecar): `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. - The `~/.qwen/debug/latest` symlink points to the most recent session's log -3. **Consider a stack dump** for a truly frozen process (advanced, optional): +4. **Consider a stack dump** for a truly frozen process (advanced, optional): - macOS: `sample 3` gives a 3-second native stack sample - Linux: `cat /proc//stack` for kernel stack, or `strace -p -c -f` for syscall profile - This is big โ€” only grab it if the process is clearly hung and you want to know _why_ From e7875ae12a3b941e56199d76ddc8f5553d3f03e4 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Fri, 15 May 2026 17:03:42 +0800 Subject: [PATCH 05/15] fix(skills): apply minimal review fixes for /stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filter ps to current UID via -u "$(id -u)" โ€” avoid leaking other users' Qwen processes on shared hosts - Note that ps `rss=` is in KB; divide by 1024 before MB comparison - Replace `pgrep -lP` with `pgrep -P` + `ps -p` so child state shows up - Mention `advanced.runtimeOutputDir` setting alongside QWEN_RUNTIME_DIR / QWEN_HOME in the runtime-base description - Add half-line about PID reuse handling and not quoting secrets from debug logs (without inflating the prompt into a full workflow) ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 2a4c0900953..5ae313d5c41 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -32,27 +32,28 @@ If the user gave an argument, first check whether it is a positive integer (PID) 1. **Enumerate live sessions via the runtime sidecar (preferred, reliable)**: - Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The default `` is `~/.qwen` (overridable by `QWEN_RUNTIME_DIR` or `QWEN_HOME`). Each file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}`. Use this as the authoritative source of `(pid, session_id, work_dir)` mappings: + Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The default `` is `~/.qwen` (overridable by `QWEN_RUNTIME_DIR`, `QWEN_HOME`, or the `advanced.runtimeOutputDir` setting). Each file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}`. Use this as the authoritative source of `(pid, session_id, work_dir)` mappings: ``` ls -1 ~/.qwen/projects/*/chats/*.runtime.json 2>/dev/null ``` - For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. + For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. PID reuse is rare but possible, so when you cross-reference with `ps` in step 2, also skip records whose live PID's command line no longer looks like a Qwen Code process. 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: ``` - ps -axo pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep + ps -xo pid=,pcpu=,rss=,etime=,state=,comm=,command= -u "$(id -u)" -ww | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep ``` - The `-ww` flag disables column truncation โ€” without it, long command paths get cut off and `grep` may miss sessions whose "qwen" path component falls beyond the terminal width. The `comm` column will be `node` or `bun`, not `qwen`. Filter to rows where the `command` column contains a path related to qwen (e.g., `qwen-code/dist/cli.js`, or a bin symlink ending in `/qwen`). Cross-reference with the PIDs from step 1. + `-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. + + Note: `ps` reports `rss` in **kilobytes** on both macOS and Linux. Divide by 1024 before comparing to the >=4GB threshold or reporting in MB. 3. **For anything suspicious**, gather more context. Substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): - - Child processes: `pgrep -lP ` + - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P ` to get child PIDs, then `ps -p -o pid=,ppid=,pcpu=,state=,etime=,command=` for their state - If high CPU: sample again after 1-2s to confirm it's sustained - - If a child looks hung (e.g., a git command), note its full command line with `ps -p -o command=` - - Check the session's debug log if you can infer the session ID (from the sidecar): `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. + - Check the session's debug log if you can infer the session ID (from the sidecar): `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. Debug logs may contain prompts, file contents, or tokens from other sessions โ€” paste only the lines relevant to the hang, and never quote secrets/API keys you happen to see. - The `~/.qwen/debug/latest` symlink points to the most recent session's log 4. **Consider a stack dump** for a truly frozen process (advanced, optional): From abb105fe2562634f0aef85e85fc0c282a4a13e44 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Fri, 15 May 2026 21:20:44 +0800 Subject: [PATCH 06/15] fix(skills): round-3 review fixes for /stuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolve RUNTIME_DIR from QWEN_RUNTIME_DIR/QWEN_HOME and use it in the sidecar `ls`, debug log path, and `latest` symlink โ€” the previous round only updated the prose and left the actual commands hardcoded - Add explicit fallthrough: when sidecar enumeration finds nothing, fall through to step 2 instead of getting stuck trying to make sidecar work - Replace metacharacter blacklist with digit-only PID whitelist โ€” safer and shorter; "etc." in a blacklist outsourced completeness to the LLM - Drop `strace -p -c -f` from the Linux stack-dump branch: `-c` blocks until the target exits, hanging the diagnostic on the very conditions it should diagnose; `ptrace_scope=1` would also misreport permission errors as process symptoms. Keep `cat /proc//stack` - Warn that `ps -ww` command lines may include CLI-arg credentials and that `sample` stack frames may include in-memory secrets โ€” redact before quoting in the report - Cover the "no sessions found at all" case so a fresh machine doesn't get reported as "all healthy" when zero data was collected ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 5ae313d5c41..3d40a1b39f4 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -26,20 +26,25 @@ Signs of a stuck session: ## Argument validation -If the user gave an argument, first check whether it is a positive integer (PID). If it contains shell metacharacters (`$`, `;`, `|`, `&`, `` ` ``, `(`, `)`, `<`, `>`, `*`, `?`, backslash, quote, whitespace, etc.), refuse to proceed and tell the user the argument does not look like a valid PID. Treat free-text symptom descriptions as guidance for the report, not as values to substitute into shell commands. +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. ## Investigation steps -1. **Enumerate live sessions via the runtime sidecar (preferred, reliable)**: +1. **Resolve the runtime base directory**, then enumerate live sessions via the runtime sidecar (preferred, reliable): - Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The default `` is `~/.qwen` (overridable by `QWEN_RUNTIME_DIR`, `QWEN_HOME`, or the `advanced.runtimeOutputDir` setting). Each file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}`. Use this as the authoritative source of `(pid, session_id, work_dir)` mappings: + Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The base directory is taken from (in priority order): `QWEN_RUNTIME_DIR`, `QWEN_HOME`, the `advanced.runtimeOutputDir` setting, and finally `~/.qwen`. Use the resolved value in every command below โ€” substituting the literal default would silently miss sessions on machines that override it. ``` - ls -1 ~/.qwen/projects/*/chats/*.runtime.json 2>/dev/null + RUNTIME_DIR="${QWEN_RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" + ls -1 "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null ``` + (If the user has set `advanced.runtimeOutputDir` in `settings.json`, ask them or read it from settings; otherwise the env-var resolution above is correct.) Each sidecar file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` โ€” the authoritative source of `(pid, session_id, work_dir)` mappings. + For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. PID reuse is rare but possible, so when you cross-reference with `ps` in step 2, also skip records whose live PID's command line no longer looks like a Qwen Code process. + **If no sidecar files are found** (empty output, or the directory does not exist), fall through to step 2 โ€” `ps` is the working fallback. + 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: ``` @@ -50,15 +55,17 @@ If the user gave an argument, first check whether it is a positive integer (PID) Note: `ps` reports `rss` in **kilobytes** on both macOS and Linux. Divide by 1024 before comparing to the >=4GB threshold or reporting in MB. + 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. + 3. **For anything suspicious**, gather more context. Substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P ` to get child PIDs, then `ps -p -o pid=,ppid=,pcpu=,state=,etime=,command=` for their state - If high CPU: sample again after 1-2s to confirm it's sustained - - Check the session's debug log if you can infer the session ID (from the sidecar): `~/.qwen/debug/.txt` (the last few hundred lines often show what it was doing before hanging). If `QWEN_RUNTIME_DIR` or `QWEN_HOME` is set, the debug directory is `$QWEN_RUNTIME_DIR/debug/` or `$QWEN_HOME/debug/` instead of the default. Debug logs may contain prompts, file contents, or tokens from other sessions โ€” paste only the lines relevant to the hang, and never quote secrets/API keys you happen to see. - - The `~/.qwen/debug/latest` symlink points to the most recent session's log + - Check the session's debug log if you can infer the session ID (from the sidecar): `"$RUNTIME_DIR"/debug/.txt` using the same `RUNTIME_DIR` resolved in step 1. The last few hundred lines often show what it was doing before hanging. Debug logs may contain prompts, file contents, or tokens from other sessions โ€” paste only the lines relevant to the hang, and never quote secrets/API keys you happen to see. + - The `"$RUNTIME_DIR"/debug/latest` symlink points to the most recent session's log 4. **Consider a stack dump** for a truly frozen process (advanced, optional): - - macOS: `sample 3` gives a 3-second native stack sample - - Linux: `cat /proc//stack` for kernel stack, or `strace -p -c -f` for syscall profile + - macOS: `sample 3` gives a 3-second native stack sample. 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. + - Linux: `cat /proc//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. - This is big โ€” only grab it if the process is clearly hung and you want to know _why_ ## Report @@ -76,6 +83,8 @@ Present a structured diagnostic report directly to the user with these sections: **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. +**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. + ## Notes - Don't kill or signal any processes โ€” this is diagnostic only. From e232ddb433ea22febbb957af7b41adfec358fdfe Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Fri, 15 May 2026 23:56:11 +0800 Subject: [PATCH 07/15] fix(skills): /stuck overview-vs-step3 consistency and self-explanatory state triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update "What to look for" overview from `pgrep -lP ` to `pgrep -P ` to match step 3 (overview was left behind in the previous round when step 3 was upgraded to capture child state) - Add a triage sentence to step 3: when the state alone explains the problem (`T` = stopped, `Z` = zombie), skip child/log/stack inspection and go straight to the report ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 3d40a1b39f4..984e16ec8e3 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -22,7 +22,7 @@ Signs of a stuck session: - **Process state `T` (stopped)** โ€” user probably hit Ctrl+Z by accident. - **Process state `Z` (zombie)** โ€” parent isn't reaping. - **Very high RSS (>=4GB)** โ€” possible memory leak making the session sluggish. -- **Stuck child process** โ€” a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -lP ` for each session. +- **Stuck child process** โ€” a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -P ` (then `ps -p` for state โ€” see step 3) for each session. ## Argument validation @@ -57,7 +57,7 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o 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. -3. **For anything suspicious**, gather more context. Substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): +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, substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P ` to get child PIDs, then `ps -p -o pid=,ppid=,pcpu=,state=,etime=,command=` for their state - If high CPU: sample again after 1-2s to confirm it's sustained - Check the session's debug log if you can infer the session ID (from the sidecar): `"$RUNTIME_DIR"/debug/.txt` using the same `RUNTIME_DIR` resolved in step 1. The last few hundred lines often show what it was doing before hanging. Debug logs may contain prompts, file contents, or tokens from other sessions โ€” paste only the lines relevant to the hang, and never quote secrets/API keys you happen to see. From 21d419fbf1ef59c5b0386be9ec2809acc388f104 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 00:04:04 +0800 Subject: [PATCH 08/15] fix(skills): correct /stuck runtime base priority order and resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The actual priority in `Storage.getRuntimeBaseDir()` is `QWEN_RUNTIME_DIR` > `advanced.runtimeOutputDir` setting > `QWEN_HOME` > `~/.qwen`. The previous round merged the `advanced.runtimeOutputDir` mention but listed it after `QWEN_HOME`, and the shell snippet skipped the settings layer entirely โ€” so on a machine where only the setting was configured, the skill would silently look in `~/.qwen` and miss all sessions. - Reorder the prose priority list to match the source - Add a `jq`-based read of `~/.qwen/settings.json` between the env-var and `QWEN_HOME`/default fallbacks. Gracefully degrades if `jq` is absent or the setting is unset. ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 984e16ec8e3..7d51f80257c 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -32,14 +32,16 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o 1. **Resolve the runtime base directory**, then enumerate live sessions via the runtime sidecar (preferred, reliable): - Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. The base directory is taken from (in priority order): `QWEN_RUNTIME_DIR`, `QWEN_HOME`, the `advanced.runtimeOutputDir` setting, and finally `~/.qwen`. Use the resolved value in every command below โ€” substituting the literal default would silently miss sessions on machines that override it. + Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. 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`. Use the resolved value in every command below โ€” substituting the literal default would silently miss sessions on machines that override it. ``` - RUNTIME_DIR="${QWEN_RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" + RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}" + [ -z "$RUNTIME_DIR" ] && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' ~/.qwen/settings.json 2>/dev/null) + RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" ls -1 "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null ``` - (If the user has set `advanced.runtimeOutputDir` in `settings.json`, ask them or read it from settings; otherwise the env-var resolution above is correct.) Each sidecar file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` โ€” the authoritative source of `(pid, session_id, work_dir)` mappings. + (The `jq` line gracefully degrades โ€” if `jq` isn't installed or `settings.json` doesn't exist, the next line falls back to `QWEN_HOME` or the default.) Each sidecar file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` โ€” the authoritative source of `(pid, session_id, work_dir)` mappings. For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. PID reuse is rare but possible, so when you cross-reference with `ps` in step 2, also skip records whose live PID's command line no longer looks like a Qwen Code process. From 21dfc9a6271e6eed8dc0f58549918c75584d20d6 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 00:18:31 +0800 Subject: [PATCH 09/15] feat(skills): improve /stuck diagnostic flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Functional upgrades found in self-review (no reviewer raised these): - Add network-hang detection bullet to step 3. Hung HTTPS requests to the model API are the most common qwen-code "stuck" mode and showed as healthy under all previous heuristics (low CPU + S state). macOS uses `lsof -i -p`, Linux uses `ss -tnp`. - Add a fast path at the top of "Investigation steps": when the user passes a digit-only PID, skip enumeration and go straight to per-PID ps + step 3. Avoids a full sidecar+ps scan in the targeted case. - Replace per-file sidecar liveness check with a single bash loop that emits only live (pid, sidecar-path) pairs. On machines with many stale sidecars this drops 50+ separate reads. - Promote `~/.qwen/debug/latest` to the primary debug-log entry point (it usually points to the suspicious session). Sidecar-derived path becomes the fallback. - Bound the debug-log read with `tail -n 200` so the model doesn't attempt to load multi-GB log files. - Replace the placeholder `` for `ps -p` with a runnable `pgrep -P | xargs -I{} ps -p {} -o ...` composition. - Drop the redundant "substitute only after validation" note in step 3 โ€” the digit-only whitelist in Argument validation already enforces this; PIDs from ps/sidecar are inherently digit-only. End-to-end tmux smoke test confirms the flow runs to completion with a correct structured report. ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 7d51f80257c..e3c96faceba 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -30,6 +30,16 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o ## Investigation steps +**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip steps 1-2 (enumeration). Verify the PID is alive, grab its stats, and jump to step 3: + +``` +kill -0 2>/dev/null && \ + ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww \ + || echo "PID is not running" +``` + +Otherwise (no arg, or symptom-only arg), run the general path below: + 1. **Resolve the runtime base directory**, then enumerate live sessions via the runtime sidecar (preferred, reliable): Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. 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`. Use the resolved value in every command below โ€” substituting the literal default would silently miss sessions on machines that override it. @@ -43,7 +53,16 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o (The `jq` line gracefully degrades โ€” if `jq` isn't installed or `settings.json` doesn't exist, the next line falls back to `QWEN_HOME` or the default.) Each sidecar file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` โ€” the authoritative source of `(pid, session_id, work_dir)` mappings. - For each file, read it and check whether the recorded `pid` is still alive (`kill -0 ` returns 0). Stale files where the PID is gone mean the session has exited โ€” skip them silently. PID reuse is rare but possible, so when you cross-reference with `ps` in step 2, also skip records whose live PID's command line no longer looks like a Qwen Code process. + Then filter to live PIDs in one pass (avoids reading every stale sidecar individually): + + ``` + for f in "$RUNTIME_DIR"/projects/*/chats/*.runtime.json; do + pid=$(jq -r .pid "$f" 2>/dev/null) + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && echo "$pid $f" + done + ``` + + Only the live `(pid, sidecar-path)` pairs reach the report. 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. **If no sidecar files are found** (empty output, or the directory does not exist), fall through to step 2 โ€” `ps` is the working fallback. @@ -59,11 +78,11 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o 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. -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, substitute `` only after the validation in "Argument validation" above (or after taking it from `ps` / sidecar output, which is trusted): - - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P ` to get child PIDs, then `ps -p -o pid=,ppid=,pcpu=,state=,etime=,command=` for their state +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: + - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P | xargs -I{} ps -p {} -o pid=,ppid=,pcpu=,state=,etime=,command=` - If high CPU: sample again after 1-2s to confirm it's sustained - - Check the session's debug log if you can infer the session ID (from the sidecar): `"$RUNTIME_DIR"/debug/.txt` using the same `RUNTIME_DIR` resolved in step 1. The last few hundred lines often show what it was doing before hanging. Debug logs may contain prompts, file contents, or tokens from other sessions โ€” paste only the lines relevant to the hang, and never quote secrets/API keys you happen to see. - - The `"$RUNTIME_DIR"/debug/latest` symlink points to the most recent session's log + - **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 -i -p 2>/dev/null | head -20`. Linux: `ss -tnp 2>/dev/null | grep "pid=,"`. A long-lived `ESTABLISHED` connection to a model host (dashscope, openai, anthropic, etc.) with no recent traffic is the smoking gun. + - **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/.txt`. Bound the read with `tail -n 200 ` โ€” 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. 4. **Consider a stack dump** for a truly frozen process (advanced, optional): - macOS: `sample 3` gives a 3-second native stack sample. 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. From c1fb70d761b504f98a30417cdea22bf872043cf0 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 00:38:57 +0800 Subject: [PATCH 10/15] =?UTF-8?q?fix(skills):=20/stuck=20=E2=80=94=20RUNTI?= =?UTF-8?q?ME=5FDIR=20preamble=20+=20jq-free=20sidecar=20liveness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues caught by Codex review: 1. **PID fast path left $RUNTIME_DIR unset.** Step 3 references `"$RUNTIME_DIR"/debug/.txt` but the fast path skipped step 1 where it was resolved, so debug-log lookup degraded to `/debug/latest` (broken absolute path). Fix: extract RUNTIME_DIR resolution into a preamble that runs before both paths. Also add a `grep -l "pid": ` lookup in the fast path so it can match the given PID to its sidecar and recover the session ID for log lookup. 2. **Sidecar liveness loop required `jq`.** Default macOS / minimal Linux images don't ship `jq`, so the loop emitted nothing for every sidecar โ€” the "preferred reliable" path silently failed and the skill fell back to the less accurate `ps | grep`. Replace with a single-spawn `node -e` script: node is guaranteed present (qwen-code itself runs on it). The settings.json `jq` lookup stays โ€” that one gracefully degrades to QWEN_HOME/default if `jq` is missing. Both verified by hand: liveness loop correctly emits live PID/sidecar pairs (56219, 33534), `grep -l` lookup correctly finds the sidecar for a given PID and emits empty for non-matches. ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index e3c96faceba..9c3cd482185 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -30,41 +30,42 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o ## Investigation steps -**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip steps 1-2 (enumeration). Verify the PID is alive, grab its stats, and jump to step 3: +**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`. ``` -kill -0 2>/dev/null && \ - ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww \ - || echo "PID is not running" +RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}" +[ -z "$RUNTIME_DIR" ] && command -v jq >/dev/null && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' ~/.qwen/settings.json 2>/dev/null) +RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" ``` -Otherwise (no arg, or symptom-only arg), run the general path below: +(If `jq` isn't installed, the settings layer is silently skipped โ€” the env-var / default fallback covers the common case.) + +**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip step 1 enumeration but still match the PID to its sidecar so step 3 has the right session ID for log lookup: + +``` +kill -0 2>/dev/null \ + && ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww \ + || echo "PID is not running" +grep -l '"pid"[[:space:]]*:[[:space:]]*\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null +``` -1. **Resolve the runtime base directory**, then enumerate live sessions via the runtime sidecar (preferred, reliable): +The `grep -l` returns the sidecar file path (if any); the basename (stripped of `.runtime.json`) is the session ID for step 3's debug log read. Then jump to step 3. - Qwen Code writes a `runtime.json` sidecar for each interactive session at `/projects//chats/.runtime.json`. 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`. Use the resolved value in every command below โ€” substituting the literal default would silently miss sessions on machines that override it. +Otherwise (no arg, or symptom-only arg), run the general path below: - ``` - RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}" - [ -z "$RUNTIME_DIR" ] && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' ~/.qwen/settings.json 2>/dev/null) - RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" - ls -1 "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null - ``` +1. **Enumerate live sessions via the runtime sidecar** (preferred, reliable): - (The `jq` line gracefully degrades โ€” if `jq` isn't installed or `settings.json` doesn't exist, the next line falls back to `QWEN_HOME` or the default.) Each sidecar file contains `{schema_version, pid, session_id, work_dir, hostname, started_at, qwen_version}` โ€” the authoritative source of `(pid, session_id, work_dir)` mappings. + Qwen Code writes a `runtime.json` sidecar for each interactive session at `"$RUNTIME_DIR"/projects//chats/.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. - Then filter to live PIDs in one pass (avoids reading every stale sidecar individually): + 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: ``` - for f in "$RUNTIME_DIR"/projects/*/chats/*.runtime.json; do - pid=$(jq -r .pid "$f" 2>/dev/null) - [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && echo "$pid $f" - done + 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 ``` - Only the live `(pid, sidecar-path)` pairs reach the report. 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. + 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. - **If no sidecar files are found** (empty output, or the directory does not exist), fall through to step 2 โ€” `ps` is the working fallback. + **If the command emits nothing** (no sidecars, or no live PIDs), fall through to step 2 โ€” `ps` is the working fallback. 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: From 52f5bcc51253f6dbd0d6198688d240fc4c55505b Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 09:13:15 +0800 Subject: [PATCH 11/15] =?UTF-8?q?fix(skills):=20/stuck=20=E2=80=94=20valid?= =?UTF-8?q?ate=20fast-path=20PID=20is=20a=20Qwen=20Code=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review caught that the targeted PID fast path accepted any digit-only PID and dumped its full command line, bypassing the Qwen- process filter that the general scan applies via `grep -E '(qwen|node.*qwen|bun.*qwen)'`. Cross-user PIDs are already filtered (`kill -0` returns EPERM), but **same-user non-Qwen processes** would have their argv (potentially including secret CLI flags) printed into the chat. Fix: add a single-line validation pipeline before the stats dump: `kill -0 && ps -p -o command= -ww | grep -qE '(qwen|node.*qwen|bun.*qwen)'`. If it returns non-zero, refuse with "PID is not a current-user Qwen Code session" and stop the diagnostic. Otherwise proceed. Verified by manual test against a real Qwen Code session PID (matches) and PID 1 / launchd (correctly rejected). ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 9c3cd482185..22302ef6275 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -40,16 +40,20 @@ RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" (If `jq` isn't installed, the settings layer is silently skipped โ€” the env-var / default fallback covers the common case.) -**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip step 1 enumeration but still match the PID to its sidecar so step 3 has the right session ID for log lookup: +**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip step 1 enumeration. First validate that the PID is a live current-user Qwen Code process; if not, refuse and stop. Without this check, a same-user non-Qwen PID (e.g., a `node my-other-app.js --api-key=...`) would have its full command line dumped into the report. ``` -kill -0 2>/dev/null \ - && ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww \ - || echo "PID is not running" +kill -0 2>/dev/null && ps -p -o command= -ww 2>/dev/null | grep -qE '(qwen|node.*qwen|bun.*qwen)' +``` + +If this pipeline returns non-zero (PID dead, owned by another user, or not a Qwen Code process), stop the diagnostic and tell the user "PID `` is not a current-user Qwen Code session". Otherwise, gather stats and the sidecar mapping, then jump to step 3: + +``` +ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww grep -l '"pid"[[:space:]]*:[[:space:]]*\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null ``` -The `grep -l` returns the sidecar file path (if any); the basename (stripped of `.runtime.json`) is the session ID for step 3's debug log read. Then jump to step 3. +The `grep -l` returns the sidecar file path (if any); the basename (stripped of `.runtime.json`) is the session ID for step 3's debug log read. Otherwise (no arg, or symptom-only arg), run the general path below: From d3f4bfb8d62d5aecf4271cca474a6e209cb06460 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 16:37:00 +0800 Subject: [PATCH 12/15] =?UTF-8?q?fix(skills):=20/stuck=20=E2=80=94=20setti?= =?UTF-8?q?ngs=20path,=20sidecar=20grep,=20ps=20regex,=20lsof=20safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four issues from PR review: 1. **Settings path honors QWEN_HOME.** The `jq` lookup in the preamble hardcoded `~/.qwen/settings.json`, but `getGlobalSettingsPath()` resolves to `$QWEN_HOME/settings.json` when set. Now uses `"${QWEN_HOME:-$HOME/.qwen}/settings.json"`. 2. **Sidecar grep uses `-El`.** Without `-E`, BSD `grep` on macOS may not treat `\b` as a word boundary in BRE. Also added a note: when PID reuse makes multiple sidecars match, prefer the most recently modified file via `ls -t | head -n 1`. 3. **Process regex tightened to avoid false positives.** The old `(qwen|node.*qwen|bun.*qwen)` matched any path containing "qwen" anywhere โ€” so `qwen-playground/server.js`, `qwen-polyfill.js`, and even unrelated processes that pass a qwen-code path as `--cwd` (e.g., Codex plugin brokers) all matched. Replaced with `(qwen-code/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))` โ€” requires the `qwen-code/` substring to be followed by a script-file path, OR the bin invocation to end in `/qwen`. Verified on the local machine that broker processes are no longer matched while real Qwen sessions (worktree dev, dist/cli.js, qwen serve daemons) all are. 4. **lsof safety.** Added `-nP` to skip reverse-DNS and port lookups which can themselves hang. Mentioned `timeout 10` / `gtimeout 10` as an optional prefix when available โ€” qwen-code's shell tool already has a backstop timeout, so this is belt-and-suspenders. Note: tested `\b` in BSD ERE on macOS โ€” it does work correctly with `-E`, so the `-El` switch alone fully addresses concern #2's portability claim (BRE-without-E remains broken but is no longer used). ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 22302ef6275..cedb8edebc2 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -13,7 +13,7 @@ The user thinks another Qwen Code session on this machine is frozen, stuck, or v ## What to look for -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 paths containing "qwen" (e.g., `node /path/to/qwen-code/dist/cli.js` or a symlinked `qwen` script). +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 `qwen-code/` directory (matches dev / worktree / install layouts) or a bin invocation ending in `/qwen` (matches the global symlink). Avoid loose `qwen-code` substring matching โ€” it false-positives on unrelated processes that merely pass a qwen-code path as a `--cwd` argument (e.g., plugin brokers). Signs of a stuck session: @@ -34,7 +34,7 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o ``` RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}" -[ -z "$RUNTIME_DIR" ] && command -v jq >/dev/null && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' ~/.qwen/settings.json 2>/dev/null) +[ -z "$RUNTIME_DIR" ] && command -v jq >/dev/null && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' "${QWEN_HOME:-$HOME/.qwen}/settings.json" 2>/dev/null) RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" ``` @@ -43,17 +43,17 @@ RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" **Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip step 1 enumeration. First validate that the PID is a live current-user Qwen Code process; if not, refuse and stop. Without this check, a same-user non-Qwen PID (e.g., a `node my-other-app.js --api-key=...`) would have its full command line dumped into the report. ``` -kill -0 2>/dev/null && ps -p -o command= -ww 2>/dev/null | grep -qE '(qwen|node.*qwen|bun.*qwen)' +kill -0 2>/dev/null && ps -p -o command= -ww 2>/dev/null | grep -qE '(qwen-code/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))' ``` If this pipeline returns non-zero (PID dead, owned by another user, or not a Qwen Code process), stop the diagnostic and tell the user "PID `` is not a current-user Qwen Code session". Otherwise, gather stats and the sidecar mapping, then jump to step 3: ``` ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww -grep -l '"pid"[[:space:]]*:[[:space:]]*\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null +grep -El '"pid"[[:space:]]*:[[:space:]]*\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null ``` -The `grep -l` returns the sidecar file path (if any); the basename (stripped of `.runtime.json`) is the session ID for step 3's debug log read. +`-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 | head -n 1`. Otherwise (no arg, or symptom-only arg), run the general path below: @@ -74,7 +74,7 @@ Otherwise (no arg, or symptom-only arg), run the general path below: 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: ``` - ps -xo pid=,pcpu=,rss=,etime=,state=,comm=,command= -u "$(id -u)" -ww | grep -E '(qwen|node.*qwen|bun.*qwen)' | grep -v grep + 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 ``` `-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. @@ -86,7 +86,7 @@ Otherwise (no arg, or symptom-only arg), run the general path below: 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: - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P | xargs -I{} ps -p {} -o pid=,ppid=,pcpu=,state=,etime=,command=` - If high CPU: sample again after 1-2s to confirm it's sustained - - **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 -i -p 2>/dev/null | head -20`. Linux: `ss -tnp 2>/dev/null | grep "pid=,"`. A long-lived `ESTABLISHED` connection to a model host (dashscope, openai, anthropic, etc.) with no recent traffic is the smoking gun. + - **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 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=,"`. A long-lived `ESTABLISHED` connection to a model host (dashscope, openai, anthropic, etc.) with no recent traffic is the smoking gun. - **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/.txt`. Bound the read with `tail -n 200 ` โ€” 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. 4. **Consider a stack dump** for a truly frozen process (advanced, optional): From 2d987d935c336d1129a91f751a4f40da87e35347 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 18:58:00 +0800 Subject: [PATCH 13/15] =?UTF-8?q?fix(skills):=20/stuck=20=E2=80=94=20expan?= =?UTF-8?q?d=20~=20and=20resolve=20relative=20paths=20in=20RUNTIME=5FDIR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Storage.resolvePath()` in qwen-code expands `~` and resolves relative paths before using `advanced.runtimeOutputDir`. The shell preamble was reading the raw JSON value via `jq`, so a user with `"runtimeOutputDir": "~/.qwen-runtime"` would pass the literal string to the glob โ€” bash does not expand `~` inside double quotes โ€” and the sidecar scan would silently find nothing and fall back to ps-only mode. Add two bash lines after the jq lookup: - `${RUNTIME_DIR/#\~/$HOME}` to substitute leading tilde - `case ... cd && pwd` to resolve relative paths to absolute (clears RUNTIME_DIR if cd fails so the chain falls through to QWEN_HOME) Smoke tested: tilde paths expand, absolute paths pass through, relative paths resolve, nonexistent dirs clear cleanly, empty stays empty. ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/core/src/skills/bundled/stuck/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index cedb8edebc2..59d8309d8f4 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -35,6 +35,9 @@ If the user gave an argument, treat it as a PID **only if it consists entirely o ``` RUNTIME_DIR="${QWEN_RUNTIME_DIR:-}" [ -z "$RUNTIME_DIR" ] && command -v jq >/dev/null && RUNTIME_DIR=$(jq -r '.advanced.runtimeOutputDir // empty' "${QWEN_HOME:-$HOME/.qwen}/settings.json" 2>/dev/null) +# `advanced.runtimeOutputDir` may be `~/...` or relative; mirror Storage.resolvePath() before using in globs +[ -n "$RUNTIME_DIR" ] && RUNTIME_DIR="${RUNTIME_DIR/#\~/$HOME}" +[ -n "$RUNTIME_DIR" ] && case "$RUNTIME_DIR" in /*) ;; *) RUNTIME_DIR="$(cd "$RUNTIME_DIR" 2>/dev/null && pwd)" || RUNTIME_DIR="" ;; esac RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" ``` From 42c3ca3bc2585730a68ab291a2698b4242a39f87 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 19:24:59 +0800 Subject: [PATCH 14/15] =?UTF-8?q?fix(skills):=20/stuck=20=E2=80=94=20round?= =?UTF-8?q?-N=20review=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopted 9 of the 16 review suggestions; declined 5; 1 already done. - Anchor process regex to `(^|/)qwen-code[^ /]*/`. Now matches renamed clones (`qwen-code-dev`, `qwen-code-x1`, worktrees) AND rejects prefix false positives (`analyze-qwen-code/`, `my-qwen-code-tool/`). Verified against 10 cases. - Clarify RSS unit conversion: KB รท 1024 = MB, KB รท 1048576 = GB. The 4GB threshold is `4194304` KB raw, or 4 in GB. Prevents the model from dividing once and comparing to 4, which would over-alert by 1024ร—. - Add `State S with low CPU` to the Signs list so initial triage flags the most common hang signature (hung HTTPS to model API) instead of only catching it inside step 3. - Split fast path validation into two guards with distinct messages: dead/wrong-user vs. yours-but-not-Qwen. Plus add the same credential-redaction note that step 2 already has. - Replace `pgrep | xargs -I{} ps` with a single `ps -p $CHILDREN` call (avoids forking N times) and add `-ww` so long child cmdlines don't truncate. - Wrap macOS `sample 3` with optional `timeout 15` (or `gtimeout 15`). Same belt-and-suspenders pattern used for `lsof`. - Note that `ss -tnp -p` requires root/CAP_NET_ADMIN; non-root sees `-` in the PID column. Tell the model to fall back to `lsof` instead of concluding "no connections". Declined: self-PID via `$$` (wrong PID โ€” `$$` is the spawned shell, not qwen), pgrep fallback for distroless (over-engineering), `\b` matches negative numbers (false alarm โ€” `:[[:space:]]*` won't match through `-`), regex DRY abstraction (no value in markdown prompts), project-level settings.json read (already declined; same trade-off). ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../core/src/skills/bundled/stuck/SKILL.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/core/src/skills/bundled/stuck/SKILL.md b/packages/core/src/skills/bundled/stuck/SKILL.md index 59d8309d8f4..351be4f34b2 100644 --- a/packages/core/src/skills/bundled/stuck/SKILL.md +++ b/packages/core/src/skills/bundled/stuck/SKILL.md @@ -13,7 +13,7 @@ The user thinks another Qwen Code session on this machine is frozen, stuck, or v ## What to look for -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 `qwen-code/` directory (matches dev / worktree / install layouts) or a bin invocation ending in `/qwen` (matches the global symlink). Avoid loose `qwen-code` substring matching โ€” it false-positives on unrelated processes that merely pass a qwen-code path as a `--cwd` argument (e.g., plugin brokers). +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`. Signs of a stuck session: @@ -22,6 +22,7 @@ Signs of a stuck session: - **Process state `T` (stopped)** โ€” user probably hit Ctrl+Z by accident. - **Process state `Z` (zombie)** โ€” parent isn't reaping. - **Very high RSS (>=4GB)** โ€” possible memory leak making the session sluggish. +- **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. - **Stuck child process** โ€” a hung `git`, `node`, or shell subprocess can freeze the parent. Check `pgrep -P ` (then `ps -p` for state โ€” see step 3) for each session. ## Argument validation @@ -43,19 +44,22 @@ RUNTIME_DIR="${RUNTIME_DIR:-${QWEN_HOME:-$HOME/.qwen}}" (If `jq` isn't installed, the settings layer is silently skipped โ€” the env-var / default fallback covers the common case.) -**Fast path for targeted diagnosis** โ€” if a digit-only PID argument was given, skip step 1 enumeration. First validate that the PID is a live current-user Qwen Code process; if not, refuse and stop. Without this check, a same-user non-Qwen PID (e.g., a `node my-other-app.js --api-key=...`) would have its full command line dumped into the report. +**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: ``` -kill -0 2>/dev/null && ps -p -o command= -ww 2>/dev/null | grep -qE '(qwen-code/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))' +kill -0 2>/dev/null || { echo "PID is dead, or owned by another user"; exit 0; } +ps -p -o command= -ww 2>/dev/null | grep -qE '((^|/)qwen-code[^ /]*/[^ ]*\.(js|ts|mjs|cjs)( |$)|/qwen( |$))' || { echo "PID is yours but is not a Qwen Code process โ€” refusing to dump details"; exit 0; } ``` -If this pipeline returns non-zero (PID dead, owned by another user, or not a Qwen Code process), stop the diagnostic and tell the user "PID `` is not a current-user Qwen Code session". Otherwise, gather stats and the sidecar mapping, then jump to step 3: +If either guard prints, stop the diagnostic and surface the message verbatim. Otherwise, gather stats and the sidecar mapping, then jump to step 3: ``` ps -p -o pid=,pcpu=,rss=,etime=,state=,comm=,command= -ww grep -El '"pid"[[:space:]]*:[[:space:]]*\b' "$RUNTIME_DIR"/projects/*/chats/*.runtime.json 2>/dev/null ``` +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. + `-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 | head -n 1`. Otherwise (no arg, or symptom-only arg), run the general path below: @@ -77,23 +81,23 @@ Otherwise (no arg, or symptom-only arg), run the general path below: 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: ``` - 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 + 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 ``` `-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. - Note: `ps` reports `rss` in **kilobytes** on both macOS and Linux. Divide by 1024 before comparing to the >=4GB threshold or reporting in MB. + 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". 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. 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: - - Child processes (with state, so a hung `git` / `node` shows up): `pgrep -P | xargs -I{} ps -p {} -o pid=,ppid=,pcpu=,state=,etime=,command=` + - Child processes (with state, so a hung `git` / `node` shows up): `CHILDREN=$(pgrep -P | 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. - If high CPU: sample again after 1-2s to confirm it's sustained - - **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 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=,"`. A long-lived `ESTABLISHED` connection to a model host (dashscope, openai, anthropic, etc.) with no recent traffic is the smoking gun. + - **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 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=,"`. 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 ` 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. - **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/.txt`. Bound the read with `tail -n 200 ` โ€” 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. 4. **Consider a stack dump** for a truly frozen process (advanced, optional): - - macOS: `sample 3` gives a 3-second native stack sample. 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. + - macOS: `sample 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 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. - Linux: `cat /proc//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. - This is big โ€” only grab it if the process is clearly hung and you want to know _why_ From 161fa70f82da06cd68b258c3f4c2f4651347f855 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Sat, 16 May 2026 21:34:24 +0800 Subject: [PATCH 15/15] test(skills): add integration test that parses every bundled SKILL.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled skill loader (`SkillManager.parseSkillFileInternal`) silently catches and debug-logs frontmatter parse errors, so a typo in any SKILL.md (missing `description`, broken `---` delimiter, `allowedTools` written as a scalar) merges with green CI and only surfaces when a user invokes the skill โ€” at which point the skill is missing from autocomplete with no indication why. Add a tiny integration test that walks `packages/core/src/skills/bundled/`, runs every `SKILL.md` through the real `parseSkillContent` (no mocks), and asserts: name matches the directory, description is non-empty, body is non-empty, and `allowedTools` (if present) is an array. Lives in its own file because `skill-load.test.ts` mocks `fs/promises` and the YAML parser, which would defeat the purpose of an integration test. New file uses real fs and the real loader. Negative-case verified: deliberately corrupting `stuck/SKILL.md`'s frontmatter delimiter makes only that file's test fail; restoring it returns the suite to all-green. Addresses wenshao's standing [Critical] review (2026-05-15 12:29Z) about the bundled skill system lacking automated tests for SKILL.md parsing. ๐Ÿค– Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../skills/bundled-skills.integration.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/core/src/skills/bundled-skills.integration.test.ts diff --git a/packages/core/src/skills/bundled-skills.integration.test.ts b/packages/core/src/skills/bundled-skills.integration.test.ts new file mode 100644 index 00000000000..d692ba7cdc8 --- /dev/null +++ b/packages/core/src/skills/bundled-skills.integration.test.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { parseSkillContent } from './skill-load.js'; + +// Bundled skills are loaded from disk at runtime by SkillManager. A typo in +// frontmatter (missing `description`, malformed YAML, broken `---` delimiter, +// `allowedTools` written as a scalar instead of a list, ...) currently fails +// only when a user invokes the skill โ€” `skill-manager.ts` swallows the parse +// error and emits a debug log, so CI stays green. This integration test parses +// every shipped SKILL.md against the real loader so any frontmatter regression +// fails CI immediately. + +const bundledDir = path.join( + path.dirname(fileURLToPath(import.meta.url)), + 'bundled', +); + +const skillNames = fs + .readdirSync(bundledDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + +describe('bundled SKILL.md files', () => { + it('discovers at least one bundled skill', () => { + expect(skillNames.length).toBeGreaterThan(0); + }); + + it.each(skillNames)('%s/SKILL.md parses with required fields', (name) => { + const file = path.join(bundledDir, name, 'SKILL.md'); + const content = fs.readFileSync(file, 'utf8'); + const cfg = parseSkillContent(content, file); + + expect(cfg.name).toBe(name); + expect(cfg.description).toBeTruthy(); + expect(cfg.body.length).toBeGreaterThan(0); + if (cfg.allowedTools !== undefined) { + expect(Array.isArray(cfg.allowedTools)).toBe(true); + } + }); +});