Skip to content

feat(cli): Add daemon status API#5174

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
doudouOUC:feat/daemon-status-api
Jun 16, 2026
Merged

feat(cli): Add daemon status API#5174
wenshao merged 4 commits into
QwenLM:mainfrom
doudouOUC:feat/daemon-status-api

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a read-only GET /daemon/status endpoint for qwen serve with summary and full detail levels. The summary response reports daemon runtime state from in-memory counters, including session counts, permission pressure, REST SSE and ACP transport counts, rate-limit rejects, process memory, resolved limits, auth posture, and advertised capabilities. The full response adds per-session diagnostics, ACP connection diagnostics, auth device-flow counts, and independently degraded workspace status sections for MCP, skills, tools, providers, env, preflight, hooks, and extensions.

The change also exposes a daemon_status capability tag, adds bridge and ACP registry snapshot helpers, keeps sensitive values out of the status response, and documents the new endpoint for operators and protocol clients.

Why it's needed

Operators currently have to query several narrower endpoints or infer daemon state from logs when troubleshooting issues like stuck sessions, pending permissions, transport fan-out, MCP budget pressure, preflight failures, or rate-limit rejects. A consolidated status API gives dashboards and support tooling one stable JSON surface for quick triage without spawning sessions or starting the ACP child for the default summary view.

Reviewer Test Plan

How to verify

Run qwen serve and request GET /daemon/status; the default response should have detail: "summary", v: 1, daemon/security/limits/runtime/capabilities sections, and no full section. Request GET /daemon/status?detail=full; the response should include full.sessions, full.acpConnections, full.auth, and full.workspace sections. Request an unsupported detail value such as GET /daemon/status?detail=verbose; the response should be 400 with code: "invalid_detail". When a bearer token is configured, the status route should require Authorization: Bearer ... like other normal daemon APIs rather than using the /health loopback exemption.

Local validation ran: npm run lint, npm run build -- --cli-only, npm run typecheck, cd packages/cli && npx vitest run src/serve/server.test.ts -t "GET /daemon/status", cd packages/cli && npx vitest run src/serve/acpHttp/connectionRegistry.test.ts, and cd packages/cli && npx vitest run src/serve/server.test.ts -t "serve capability registry".

Evidence (Before & After)

N/A; this is a JSON API and documentation change, not a TUI or visual UI change.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node v22.22.3, npm 10.9.8, Darwin 25.4.0 arm64.

Risk & Scope

  • Main risk or tradeoff: The full detail mode aggregates several existing workspace diagnostic surfaces, so large workspaces may return a larger payload; each workspace section is independently capped by a short timeout and failures degrade that section instead of failing the whole status response.
  • Not validated / out of scope: No live browser dashboard is added in this PR; this only adds the JSON API and capability/documentation surface.
  • Breaking changes / migration notes: None; the endpoint and capability tag are additive.

Linked Issues

N/A

中文说明

What this PR does

新增只读的 GET /daemon/status 接口,支持 summaryfull 两种 detail。summary 响应只从内存计数读取 daemon 运行态,包括 session 数、permission 压力、REST SSE 与 ACP transport 计数、rate-limit 拒绝数、进程内存、已解析 limits、鉴权姿态和已发布 capabilities。full 响应额外包含 session 诊断、ACP 连接诊断、auth device-flow 数量,以及 MCP、skills、tools、providers、env、preflight、hooks、extensions 等 workspace status sections,并且每个 section 独立降级。

这次改动还暴露 daemon_status capability tag,新增 bridge 与 ACP registry 的 snapshot helper,避免在 status 响应中泄露敏感值,并为 operator 和 protocol client 补充新接口文档。

Why it's needed

排查 stuck session、pending permission、transport fan-out、MCP budget 压力、preflight 失败或 rate-limit 拒绝等问题时,operator 目前需要查询多个更窄的接口,或者从日志里推断 daemon 状态。统一的 status API 给 dashboard 和支持工具提供一个稳定的 JSON 入口,便于快速定位问题;默认 summary 视图不会 spawn session,也不会启动 ACP child。

Reviewer Test Plan

How to verify

运行 qwen serve 后请求 GET /daemon/status;默认响应应包含 detail: "summary"v: 1、daemon/security/limits/runtime/capabilities sections,并且没有 full section。请求 GET /daemon/status?detail=full;响应应包含 full.sessionsfull.acpConnectionsfull.authfull.workspace sections。请求不支持的 detail,例如 GET /daemon/status?detail=verbose;响应应为 400,并包含 code: "invalid_detail"。配置 bearer token 时,status 路由应像其他普通 daemon API 一样要求 Authorization: Bearer ...,而不是使用 /health 的 loopback 免鉴权例外。

本地验证已运行:npm run lintnpm run build -- --cli-onlynpm run typecheckcd packages/cli && npx vitest run src/serve/server.test.ts -t "GET /daemon/status"cd packages/cli && npx vitest run src/serve/acpHttp/connectionRegistry.test.tscd packages/cli && npx vitest run src/serve/server.test.ts -t "serve capability registry"

Evidence (Before & After)

N/A;这是 JSON API 和文档改动,不是 TUI 或可视 UI 改动。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node v22.22.3,npm 10.9.8,Darwin 25.4.0 arm64。

Risk & Scope

  • Main risk or tradeoff: full detail 模式会聚合多个既有 workspace diagnostic surface,因此大 workspace 可能返回更大的 payload;每个 workspace section 都有短超时并独立降级,单个失败不会导致整个 status 响应失败。
  • Not validated / out of scope: 本 PR 不新增实时浏览器 dashboard;只新增 JSON API、capability 和文档。
  • Breaking changes / migration notes: 无;接口和 capability tag 都是增量新增。

Linked Issues

N/A

Comment thread packages/cli/src/serve/daemonStatus.ts
Comment thread packages/cli/src/serve/daemonStatus.ts
Comment thread packages/cli/src/serve/daemonStatus.ts
Comment thread packages/cli/src/serve/daemonStatus.ts
Comment thread packages/cli/src/serve/daemonStatus.ts
@doudouOUC
doudouOUC marked this pull request as ready for review June 16, 2026 00:25
Copilot AI review requested due to automatic review settings June 16, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR adds a well-designed read-only GET /daemon/status endpoint with summary and full detail levels. The API design is sound, with good separation between in-memory daemon state and live workspace diagnostics.

However, there is a critical bug that needs to be fixed before merge:

Critical Issue

visitStatusContainers at line 518 only visits 6 array keys (cells, errors, servers, budgets, skills, providers), but summarizeStatusData at line 435 counts 9 array keys (it also includes tools, hooks, and extensions). This inconsistency means:

  • collectStatuses() won't catch status issues in tools, hooks, or extensions sections
  • inspectBudgetContainers() won't detect budget problems in those sections
  • The summary counts will show these sections exist, but their status won't be validated

The visitStatusContainers function should visit all 9 array keys to be consistent with summarizeStatusData.

  for (const key of [
    'cells',
    'errors',
    'servers',
    'budgets',
    'skills',
    'tools',
    'providers',
    'hooks',
    'extensions',
  ]) {
    const value = data[key];
    if (!Array.isArray(value)) continue;
    for (const item of value) visitStatusContainers(item, visit);
  }

Other Observations

  • The API design is clean and well-documented
  • The separation of summary (in-memory only) vs full (live diagnostics) is a good design choice
  • Security considerations are properly addressed (no sensitive values exposed)
  • Test coverage is comprehensive

Verification

  • CI: Failing on review-pr check
  • Presubmit: Downgrade to COMMENT required due to CI failure

Assessment

The implementation is solid, but the visitStatusContainers inconsistency is a critical bug that will cause status validation to miss issues in tools, hooks, and extensions sections. This needs to be fixed before merge.

Verdict: COMMENT (downgraded from APPROVE due to critical bug + CI failure)

— claude-opus-4-6 via Qwen Code /review

Comment thread packages/cli/src/serve/daemonStatus.ts
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

wenshao
wenshao previously approved these changes Jun 16, 2026
@wenshao
wenshao dismissed their stale review June 16, 2026 02:41

dismiss

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

1 similar comment
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

qqqys
qqqys previously approved these changes Jun 16, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior critical daemon status rollup feedback is resolved in the latest head: tools, hooks, and extensions are now included in status traversal, with regression coverage. I did not find a remaining critical blocker in this pass.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao

wenshao commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — real qwen serve runtime (Linux)

Verified PR head 5efa94644 on Linux (the table marks Linux not tested). Built an isolated worktree, ran the full + targeted test suites, and drove a real qwen serve daemon end-to-end with curl. All claims in the test plan reproduce; no bugs found. This is a strong merge candidate.

Environment: Node v22.22.2, npm 10.9.7, Linux 6.12.63 x64. Merge-base a76183f9c; PR is 7 commits behind origin/main (3408c3211) but merges cleanly (0 conflicts) and none of the PR-touched source files drifted on main.


1. Static gates

Gate Result
npm install + full npm run build ✅ rc=0
typecheck@qwen-code/acp-bridge ✅ rc=0
typecheck@qwen-code/qwen-code (cli) ✅ rc=0
eslint on all 8 changed source files ✅ rc=0
merge-tree vs fresh origin/main ✅ 0 conflicts

2. Tests (vitest)

Suite Result
serve/daemonStatus.test.ts ✅ 4/4
serve/acpHttp/connectionRegistry.test.ts ✅ 2/2
server.test.ts -t "GET /daemon/status" ✅ 4/4
server.test.ts -t "serve capability registry" ✅ 9/9
full server.test.ts (regression for fakeBridge signature change) 433/433
full serve/acpHttp/ (regression for new kind discriminator) 127/127

3. Runtime E2E against a live daemon

Started real daemons (node packages/cli/dist/index.js serve …) in tmux and exercised the endpoint with curl. To prove the endpoint itself does not start the ACP child, preheat was disabled (VITEST_WORKER_ID=1) so the only thing that could spawn a child was the request.

3.1 detail=summary (default) — ✅ matches docs exactly

  • Top-level keys exactly {v, detail, generatedAt, status, issues, daemon, security, limits, capabilities, runtime}no full.
  • daemon omits logPath (as documented); v=1, detail="summary", status="ok", issues=[].
  • limits: maxSessions=20, listenerMaxConnections=256, acpConnectionCap=64 — both caps surfaced and distinct.
  • The ACP child was NOT spawnedpgrep -P <daemon-pid> returned 0 children before and after the request.

3.2 detail=full — ✅ all sections, fast, degrades independently

  • Adds full.{sessions, acpConnections, auth, workspace}; workspace has all 8 sections (mcp, skills, tools, providers, env, preflight, hooks, extensions).
  • Completed in 68 ms with each section well under the 1 000 ms timeout; on an idle daemon full also did not spawn the child (idle fallback).
  • daemon.logPath is present in full only (authenticated-operator view) — consistent with the docs.
  • Production-like run (preheat ON): the ACP child (node … --acp) spawned, runtime.channel.live became true in ~2 s, and full then returned status:"ok" with all 8 workspace sections initialized:true — confirming the live-child path, not just idle fallback.

3.3 Invalid detail400 invalid_detail — ✅ (and robust)

?detail=verbose                 → 400 {"code":"invalid_detail"}
?detail=                        → 400 (empty)
?detail=SUMMARY                 → 400 (case-sensitive, strict)
?detail=summary&detail=full     → 400 (repeated param / array — no crash)

3.4 Bearer auth on loopback — ✅ the headline security claim holds

With --token configured, /daemon/status is gated even on loopback (registered after bearerAuth), unlike /health’s loopback exemption:

Request (loopback, token set) Code
GET /daemon/status (no auth) 401
GET /daemon/status (Authorization: Bearer …) 200
GET /health (no auth) — contrast 200 (loopback-exempt)
GET /capabilities (no auth) 401

3.5 Sensitive-value redaction — ✅

  • Real ACP connection 4b29997c-1312-4df4-aaf7-d15608422f14 appeared in full.acpConnections[0] as connectionIdPrefix: "4b29997c" (8 chars). The full id is absent from the entire response.
  • With a token configured, the token value never appears in the response (only security.tokenConfigured: true).
  • The env section reports env-var presence only (present: true/false) — API-key values are never emitted.

3.6 Issue rollup with real data — ✅

  • Enabled --rate-limit --rate-limit-read 3, hammered a read endpoint (3×200, 5×429). /daemon/status then reported status:"warning", an issue {code:"rate_limit_hits", severity:"warning"}, and runtime.rateLimit.rejectedSinceStart.read: 5 — exactly matching the 5 rejections.

3.7 Capability-registry refactor is behavior-preserving — ✅

  • currentServeFeatures() was extracted and shared by /capabilities and /daemon/status. /capabilities.features is byte-identical to status.capabilities.features (61 features), and daemon_status is advertised.

Observations (non-blocking — for awareness, not change requests)

  1. /daemon/status is subject to the read rate-limit tier (120/min) when --rate-limit is on, whereas /health is exempt. This is deliberate and documented, but operators pointing a high-frequency dashboard at it should size --rate-limit-read accordingly (a 0.5 s poll = 120/min, exactly the default cap).
  2. acpConnectionCap (64, ACP registry) and listenerMaxConnections (256, TCP socket cap) are distinct limits — both are correctly surfaced under limits; just noting they are not the same number by design.
  3. The "summary does not start the ACP child" guarantee is endpoint-level. In a normal deployment the daemon preheats the child at startup independently of this route, so channel.live/workspace sections populate on their own; I isolated the endpoint by disabling preheat.

Verdict: builds, type-checks, lints, and the full serve test suites are green; every documented runtime behavior (summary/full shapes, 400 invalid_detail, loopback bearer gating, id/token redaction, issue rollup) reproduces on a real daemon. LGTM from a verification standpoint. ✅

🇨🇳 中文版本(点击展开)

✅ 本地验证报告 — 真实 qwen serve 运行时(Linux)

Linux 上验证了 PR head 5efa94644(PR 表格中 Linux 标记为 未测试)。我新建了独立 worktree,跑了全量 + 定向测试套件,并用 curl 端到端驱动了一个真实的 qwen serve 守护进程测试计划中的所有结论均可复现,未发现 bug,是一个可放心合并的候选。

环境: Node v22.22.2、npm 10.9.7Linux 6.12.63 x64。merge-base 为 a76183f9c;PR 落后 origin/main 7 个 commit3408c3211),但可干净合并(0 冲突),且 PR 改动的源文件在 main 上均无漂移。

1. 静态门禁

  • npm install + 全量 npm run build:✅ rc=0
  • typecheck(acp-bridge / cli):✅ rc=0 / rc=0
  • 对全部 8 个改动源文件 eslint:✅ rc=0
  • 对最新 origin/main 做 merge-tree:✅ 0 冲突

2. 测试(vitest)

  • daemonStatus.test.ts:✅ 4/4
  • acpHttp/connectionRegistry.test.ts:✅ 2/2
  • server.test.ts -t "GET /daemon/status":✅ 4/4
  • server.test.ts -t "serve capability registry":✅ 9/9
  • 全量 server.test.ts(回归 fakeBridge 签名改动):✅ 433/433
  • 全量 acpHttp/(回归新增 kind 判别字段):✅ 127/127

3. 真实守护进程端到端

tmux 启动真实 daemon,用 curl 驱动。为证明端点本身不会启动 ACP 子进程,关闭了 preheat(VITEST_WORKER_ID=1),使唯一可能拉起子进程的就是请求本身。

  • 3.1 detail=summary(默认)✅:顶层字段与文档完全一致,fulldaemon 不含 logPathstatus=okissues=[]limitsmaxSessions=20listenerMaxConnections=256acpConnectionCap=64。请求前后 pgrep -P <pid> 子进程数均为 0 → summary 没有启动 ACP 子进程
  • 3.2 detail=full:新增 full.{sessions, acpConnections, auth, workspace}workspace 含全部 8 个 section;68ms 完成,远低于 1000ms 超时;空闲时同样不拉起子进程(idle 降级);logPath 仅在 full 出现。preheat 开启的拟生产运行:ACP 子进程(node … --acp)成功拉起,~2s 后 channel.live=truefull 返回 status:ok 且 8 个 section 全部 initialized:true → 实时子进程路径也验证通过。
  • 3.3 非法 detail400 invalid_detailverbose / 空值 / SUMMARY(大小写敏感)/ 重复参数 全部返回 400,且不崩溃。
  • 3.4 loopback bearer 鉴权 ✅(核心安全点):配置 --token 后,即使在 loopback,/daemon/status 无 token → 401,带 token → 200;对照 /health 无 token → 200(loopback 豁免);/capabilities 无 token → 401。证明 status 路由注册在 bearerAuth 之后,不享受 /health 的豁免。
  • 3.5 敏感值脱敏 ✅:真实连接 4b29997c-… 在响应中只暴露 8 位前缀 connectionIdPrefix: "4b29997c"完整 id 不出现;配置 token 后响应中不含 token 值(仅 tokenConfigured:true);env section 仅报告变量是否存在,不输出值。
  • 3.6 issue 汇总(真实数据)✅:开启 --rate-limit --rate-limit-read 3 并打满(3×200、5×429)后,/daemon/status 返回 status:"warning"、issue rate_limit_hitsrejectedSinceStart.read:5,与 5 次 429 完全吻合。
  • 3.7 capability 重构等价 ✅:抽出的 currentServeFeatures()/capabilities/daemon/status 共用,两者 features(61 项)逐字节一致,且包含 daemon_status

观察项(非阻塞,仅供参考)

  1. 开启 --rate-limit 时,/daemon/statusread 限速(120/min)约束,而 /health 豁免——这是有意且已写入文档的设计,但若用高频 dashboard 轮询该端点,应相应调大 --rate-limit-read(0.5s 一次即 120/min,正好等于默认上限)。
  2. acpConnectionCap(64,ACP 注册表)与 listenerMaxConnections(256,TCP socket)是两个不同的上限,limits 中均正确暴露,二者本就不应相等。
  3. “summary 不启动子进程”是端点级保证;正常部署中 daemon 会在启动时独立 preheat 子进程,因此 channel.live/workspace section 会自行 populate,我通过关闭 preheat 来隔离验证端点行为。

结论: 构建 / 类型检查 / lint / 全量 serve 测试套件全绿;所有文档化的运行时行为(summary/full 结构、400 invalid_detail、loopback bearer 鉴权、连接 id / token 脱敏、issue 汇总)均在真实 daemon 上复现。从验证角度 LGTM ✅。

Verification methodology: isolated git worktree, own npm install/build, real qwen serve daemons driven over HTTP (no mocked transport); ACP connection minted via the real POST /acp initialize handshake.

@wenshao

wenshao commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @doudouOUC!

Template looks good ✓

On direction: This is well-aligned. Operators troubleshooting stuck sessions, pending permissions, or transport fan-out currently have to piece together state from multiple narrower endpoints or logs — a consolidated status API is a natural fit. Claude Code has claude daemon status and /doctor for similar reasons, so the area is clearly relevant for daemon-mode tooling.

On approach: The two-tier design (summary = cheap in-memory, full = workspace queries with independent degradation) is pragmatic and gets the 80/20 right — most troubleshooting starts with summary. The acpHandleRef pattern in server.ts is a clean way to let the status route see the ACP handle that's registered later. The kind addition on TransportStream is minimal and correct.

One thing worth splitting out: the EXPECTED_REGISTERED_FEATURES filter change in server.test.ts removes rate_limit and workspace_reload from the exclusion list. That's a test expectation change unrelated to the daemon status feature — it might belong in a separate PR unless there's a specific reason it's coupled here.

The daemonStatus.ts module is 602 lines, which is on the larger side for a single file, but the structure is clear: response builder → issue detection → workspace section aggregation → utility helpers. Nothing that screams "split me up."

Overall the scope feels right — 15 files, but each one is a focused change for the stated goal. Moving on to code review. 🔍

中文说明

感谢贡献,@doudouOUC

模板完整 ✓

方向: 对齐。排查 stuck session、pending permission、transport fan-out 时,operator 目前需要从多个窄接口或日志拼凑状态,统一的 status API 是自然选择。Claude Code 有 claude daemon status/doctor 做类似的事,这个方向对 daemon 模式工具显然相关。

方案: 两级设计(summary = 低成本内存读取,full = workspace 查询并独立降级)务实,80/20 分得合理——大部分排查从 summary 开始。server.ts 的 acpHandleRef 模式干净地解决了 status 路由访问后注册的 ACP handle 的问题。TransportStream 上加 kind 改动最小且正确。

有一点值得拆出来:server.test.tsEXPECTED_REGISTERED_FEATURES 的过滤变更移除了 rate_limitworkspace_reload,这是一个与 daemon status 无关的测试期望变更,除非有特定原因耦合在此,否则可能属于另一个 PR。

daemonStatus.ts 有 602 行,单文件偏大,但结构清晰:响应构建 → issue 检测 → workspace section 聚合 → 工具函数。没有需要拆分的信号。

整体范围合理——15 个文件,但每个都是聚焦于目标的改动。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: add a GET /daemon/status route in server.ts, create a dedicated daemonStatus.ts module to build the response shape, add snapshot methods to the bridge and connection registry, add the kind discriminator on TransportStream for SSE/WS counting, and write tests covering auth, detail levels, workspace degradation, and issue codes.

The PR's approach matches this almost exactly. A few things worth noting:

  • The acpHandleRef pattern in server.ts is the right call — the /daemon/status handler is registered before mountAcpHttp, but the ref is populated at app-setup time before any request arrives. Clean.
  • collectSection with withTimeout and independent degradation per workspace section is well-designed. Each section fails independently without taking down the whole response.
  • The visitStatusContainers recursive walk is a pragmatic approach to detect error/warning statuses in heterogeneous workspace data shapes. It relies on known array keys (cells, servers, tools, etc.) which matches the existing workspace status contract.
  • Security posture is correct: bearer tokens, client IDs, and full connection IDs are never exposed. logPath only appears in full mode for authenticated operators.
  • process.memoryUsage() in the runtime section is appropriate for an operator diagnostic endpoint.

No critical blockers. The EXPECTED_REGISTERED_FEATURES filter change (removing rate_limit and workspace_reload from the exclusion) is a pre-existing test expectation fix — these features were already advertised by default but the test was filtering them out. Not a blocker, but would have been cleaner as a separate commit.

Real-Scenario Testing

Started the daemon built from this PR's branch on port 4179 with a fresh workspace, then hit all three endpoint modes.

Server startup

$ node dist/cli.js serve --port 4179 --workspace /tmp/test-workspace 2>&1
qwen serve: daemon log → /home/runner/.qwen/debug/daemon/serve-6415-22847dd8.log
qwen serve: session reaper started (interval 60000ms, idle threshold 1800000ms)
qwen serve listening on http://127.0.0.1:4179 (mode=http-bridge, workspace=/tmp/test-workspace)
qwen serve: bound to workspace "/tmp/test-workspace"
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
qwen serve: /acp WebSocket transport enabled on /acp

GET /daemon/status (summary — default)

$ curl -s http://127.0.0.1:4179/daemon/status | python3 -m json.tool
{
    "v": 1,
    "detail": "summary",
    "generatedAt": "2026-06-16T04:56:25.998Z",
    "status": "ok",
    "issues": [],
    "daemon": {
        "pid": 6415,
        "uptimeMs": 10538,
        "mode": "http-bridge",
        "workspaceCwd": "/tmp/test-workspace",
        "qwenCodeVersion": "0.18.1",
        "daemonId": "serve-6415-22847dd8"
    },
    "security": {
        "tokenConfigured": false,
        "requireAuth": false,
        "loopbackBind": true,
        "allowOriginConfigured": false,
        "allowOriginMode": "none",
        "sessionShellCommandEnabled": false
    },
    "limits": { "maxSessions": 20, ... },
    "capabilities": { "protocolVersions": { "current": "v1", ... }, "features": ["health", "daemon_status", ...] },
    "runtime": {
        "sessions": { "active": 0 },
        "permissions": { "pending": 0, "policy": "first-responder" },
        "channel": { "live": true },
        "transport": { "restSseActive": 0, "acp": { "enabled": true, "connections": 0, ... } },
        "rateLimit": { "enabled": false, "rejectedSinceStart": { "prompt": 0, "mutation": 0, "read": 0 } },
        "process": { "rss": 214302720, "heapTotal": 128454656, "heapUsed": 106821632, ... }
    }
}

Summary correctly omits full section and logPath. daemon_status appears in capabilities features list. ✓

GET /daemon/status?detail=full

$ curl -s "http://127.0.0.1:4179/daemon/status?detail=full" | python3 -c "..."
{
  "sessions": [],
  "acpConnections": [],
  "workspace": {
    "mcp":      { "status": "ok", "durationMs": 14,  "summary": { "initialized": true, "clientCount": 0, ... } },
    "skills":   { "status": "ok", "durationMs": 14,  "summary": { "initialized": true, "skillsCount": 0 } },
    "tools":    { "status": "ok", "durationMs": 15,  "summary": { "initialized": true, "toolsCount": 59 } },
    "providers":{ "status": "ok", "durationMs": 15,  "summary": { "initialized": true, "providersCount": 2 } },
    "env":      { "status": "ok", "durationMs": 8,   "summary": { "initialized": true, "cellsCount": 24 } },
    "preflight":{ "status": "warning", "durationMs": 177, "summary": { "cellsCount": 12 } },
    "hooks":    { "status": "ok", "durationMs": 14,  "summary": { "hooksCount": 0 } },
    "extensions":{ "status": "ok", "durationMs": 14, "summary": { "extensionsCount": 0 } }
  },
  "auth": { "supportedDeviceFlowProviders": ["qwen-oauth"], "pendingDeviceFlowCount": 0 }
}

Full mode correctly includes logPath in the daemon section, all workspace sections with independent status/durationMs/summary, and auth device-flow counts. Preflight reports warning (missing ripgrep on this machine) without failing the whole response. ✓

GET /daemon/status?detail=verbose (invalid)

$ curl -s -w "\nHTTP_CODE:%{http_code}\n" "http://127.0.0.1:4179/daemon/status?detail=verbose"
{"error":"detail must be one of: summary, full","code":"invalid_detail"}
HTTP_CODE:400

Returns 400 with correct error code. ✓

Unit tests

 ✓ src/serve/daemonStatus.test.ts (4 tests)
 ✓ src/serve/acpHttp/connectionRegistry.test.ts (2 tests)
 ✓ src/serve/server.test.ts -t "GET /daemon/status" (4 tests)
 ✓ src/serve/server.test.ts -t "serve capability registry" (9 tests)
 ✓ src/serve/server.test.ts -t "GET /capabilities" (17 tests)

All 36 relevant tests pass.

中文说明

代码审查

独立方案:在 server.ts 添加路由,创建独立的 daemonStatus.ts 构建响应,给 bridge 和 connection registry 加 snapshot 方法,在 TransportStream 上加 kind 区分 SSE/WS,写测试覆盖鉴权、detail 级别、workspace 降级和 issue code。

PR 方案与独立方案基本一致。acpHandleRef 模式正确,collectSection + withTimeout 独立降级设计合理,安全姿态正确(不泄露 token/client ID/full connection ID)。无关键阻塞问题。

实际测试

从 PR 分支构建 daemon,在 4179 端口启动后测试三种模式:

  • GET /daemon/status:返回 summary,无 full section,无 logPath
  • GET /daemon/status?detail=full:返回完整诊断,包含所有 workspace section,preflight 独立降级为 warning ✓
  • GET /daemon/status?detail=verbose:返回 400 invalid_detail

36 个相关单测全部通过。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stepping back: this PR does one thing — gives operators a single endpoint to ask "how is the daemon doing?" — and does it well. The two-tier design (summary for dashboards, full for active troubleshooting) is the right call. The code is straightforward, the security posture is correct, and the real-scenario testing confirms everything the PR promised.

My independent proposal matched the PR's approach almost exactly, and I didn't find a simpler path it missed. The only minor reservation is the EXPECTED_REGISTERED_FEATURES filter change bundled into the test file — it's a valid fix but unrelated to this feature. Not worth blocking over.

Ship it. ✅

中文说明

这个 PR 做了一件事:给 operator 一个统一入口问"daemon 状态如何",做得很好。两级设计(summary 给 dashboard,full 给主动排查)是正确的选择。代码清晰,安全姿态正确,实际测试确认了 PR 的所有承诺。

唯一的小保留是测试文件中捆绑的 EXPECTED_REGISTERED_FEATURES 过滤变更——修复本身有效但与此功能无关。不值得阻塞。

可以合并。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao
wenshao merged commit 49c43cf into QwenLM:main Jun 16, 2026
30 of 33 checks passed
@doudouOUC
doudouOUC deleted the feat/daemon-status-api branch June 16, 2026 06:10
yiliang114 pushed a commit that referenced this pull request Jun 17, 2026
…n PRs (#5211)

* fix(e2e): add daemon_status to serve capabilities baseline; run E2E on PRs

The `qwen serve — capabilities envelope` integration test hard-codes the
expected advertised-feature list. PR #5174 added the `daemon_status`
capability to SERVE_CAPABILITY_REGISTRY (and the server.test.ts unit
baseline) but did not update this integration test, so the list drifted
by one entry and the E2E job began failing on main.

Root cause it slipped through: E2E only triggered on push to main /
merge_group, never on PRs, so this stale assertion was never exercised
before merge. Add a pull_request trigger (targeting main) so capability/
contract regressions surface at PR time, and key concurrency on the PR
number so superseded PR runs cancel.

* fix(e2e): guard fork PRs from secretless runs; dedupe push/PR triggers

Addresses review feedback on #5211:

- Skip the e2e matrix on fork PRs. Forks have no access to repository
  secrets (OPENAI_*, DOCKERHUB_*), so without a guard every fork PR
  produces 3 guaranteed red jobs and wastes CI minutes. A head-repo
  guard runs the jobs for same-repo PRs (and push / merge_group) but
  skips them for forks.

- Key concurrency on the head ref (github.head_ref || github.ref_name)
  so a push to a feat/e2e/** branch and a pull_request from that same
  branch share one group and cancel each other, instead of running the
  full matrix twice for the same change. Keeps the feat/e2e/** push
  trigger (the no-PR e2e iteration escape hatch) intact.

- Stop cancelling in-progress main runs so every main commit produces a
  complete e2e result (matching ci.yml's policy and the very signal that
  surfaced this bug); still cancel superseded PR / feature-branch runs.

---------

Co-authored-by: jinye <[email protected]>
liziwl pushed a commit to liziwl/qwen-code that referenced this pull request Jul 4, 2026
QwenLM#6272)

* feat(web-shell): add a daemon status page backed by GET /daemon/status

Surface the consolidated daemon status API (QwenLM#5174) in the Web Shell as a
dashboard dialog opened from a sidebar footer button.

- @qwen-code/sdk: DaemonClient.daemonStatus(detail) plus DaemonStatusReport*
  wire types for the /daemon/status envelope (summary and full detail).
- @qwen-code/webui: loadDaemonStatus workspace action and a
  useDaemonStatusReport hook (exported as useDaemonStatus from
  daemon-react-sdk).
- web-shell: DaemonStatusDialog rendering one dashboard — overall status
  badge, issues list, daemon/runtime/transport/security/limits/capabilities
  cards, plus per-session, workspace-diagnostics, and auth sections. The
  daemon's summary/full cost split is hidden from the operator rather than
  exposed as a toggle: the cheap summary rides a 5s auto-refresh while the
  expensive full report (which may spawn the ACP child and aggregate
  workspace diagnostics) is fetched only on open and on manual refresh, so
  parking the dialog open never rehits that path. Capabilities are sorted,
  counted, and height-capped; the long workspace path stays on one line,
  front-truncated so the tail remains visible. New pulse-icon sidebar entry;
  EN/zh-CN strings.
- vite dev proxy: forward /daemon to the daemon; without it the SPA fallback
  answered /daemon/status with index.html and the dialog failed JSON parsing
  under npm run dev:daemon.

* fix(web-shell): address review on the daemon status dashboard

- Drive the status badge and issues list off the full report when it is
  available, not the summary. The daemon only rolls workspace/preflight/MCP
  problems into status+issues for detail=full, so the summary can read "ok"
  with no issues while a loaded full report is degraded — the dashboard now
  reflects the full rollup (live counters still come from the summary).
- Guard the 5s poll with an in-flight ref so a slow/degraded daemon cannot
  accumulate overlapping status calls (useDaemonResource discards stale
  completions but does not abort; the client timeout is 30s).
- Fix the public DaemonStatusReport wire type: runtime.channelWorker.channels
  is string[] (ChannelWorkerSnapshot), not an array of objects; mirror the
  remaining optional snapshot fields.

* fix(web-shell): translate workspace section status badges

WorkspaceSectionRow rendered the raw wire status (`ok`/`warning`/`error`/
`unavailable`) while every other badge in the dialog goes through `t()`, so
under a Chinese UI these badges showed lowercase English. Route the badge
through `t('daemon.level.<status>')` and add the missing `daemon.level.unavailable`
key to both dictionaries.

* fix(web-shell): scope toolbar error to summary; broaden dashboard test coverage

- The toolbar "failed to load" banner now keys on the summary fetch only. A
  failed full fetch is already surfaced in the diagnostics section, so it no
  longer makes an otherwise-healthy summary (fresh cards + timestamp) read as
  broken.
- Use the ASCII "..." ellipsis in the diagnostics-loading string to match the
  rest of the i18n dictionary.
- Add tests: summary-healthy/full-failed degraded state, the ACP-disabled
  transport branch, uptime/memory/duration formatting across unit boundaries
  (day, GB, sub-second, fractional-second), and sidebar Daemon Status button
  click (expanded + collapsed) — the feature's only entry point.

* fix(web-shell): pause polling on hidden tab; scope dev proxy; fix test mock

- Skip the 5s status poll while document.hidden, matching the sidebar poll —
  a backgrounded tab no longer hits the daemon every 5s.
- Narrow the vite dev proxy to the exact /daemon/status route instead of a
  bare /daemon prefix, mirroring the scoped /voice/stream entry; verified the
  dashboard still proxies (summary + detail=full) in dev.
- Add a message field to the DaemonStatusReport issue mock in the webui
  provider test so it matches the required DaemonStatusReportIssue shape.

* feat(web-shell): surface runtime/channel-worker diagnostics; a11y + polish

Address the daemon-status review round:
- Render the runtime startup/failure state (runtime.loading / runtime.error)
  in the Runtime card so the plausible-looking zero counters during startup
  are not mistaken for a healthy idle daemon.
- Surface channel-worker diagnostics (state, exit code/signal, error, restart
  count) when the worker is enabled — these fields were fetched and typed but
  never shown, leaving a bare "down" with no context.
- Include full.error.message in the diagnostics-failure line (matching the
  summary error path) so a failed detail fetch is actionable.
- Show "N/A" instead of a literal "null" chip for null workspace summary
  values (the wire type allows null).
- Add role="status" + aria-label to the health badge for screen readers.
- Rename the public hook alias useDaemonStatus -> useStatusReport, matching
  the Daemon-prefix-stripping convention of the other re-exports.
- Add tests: runtime startup/failure, channel-worker diagnostics, and the
  empty/disabled placeholders (sessions, rate limit, capabilities, ACP),
  toolbar-banner-with-data, and pure-loading branches.

* fix(web-shell): contain daemon status crashes; workspace empty-state

- Wrap the dashboard in a local ErrorBoundary so a malformed/partial daemon
  response (e.g. an older daemon omitting an additive field like
  channelWorker) — most likely exactly when the daemon is sick and the
  dashboard is most needed — shows a contained fallback instead of throwing
  to the root boundary and white-screening the whole web shell.
- Add an empty-state to the Workspace Diagnostics card (parity with the
  Sessions card) for when full.workspace is empty.
- Tests: error-boundary containment on a malformed report, and the workspace
  empty-state.

* fix(web-shell): fix error-boundary recovery; contain detail crashes

Address the review round (one Critical):
- ErrorBoundary recovery was broken: the comment claimed resetKeys cleared the
  fallback, but none was passed. Switch to a function-form fallback that
  surfaces the actual render error (distinct from a network failure) and fix
  the comment — recovery happens on re-open, since the parent only mounts the
  dialog while open.
- Wrap FullDetail in its own ErrorBoundary so a malformed detail=full payload
  is contained to the detail region instead of taking down the healthy summary
  cards with it; add a catch-all branch so a fetch that resolves without a
  `full` section shows a failed state instead of hanging on "Loading...".
- Toolbar failure banner now shows only when the summary errored AND still has
  data on screen (`summary.error && summary.report`), so it no longer
  misrepresents a dashboard that is rendering from the full fallback.
- SDK type: drop `& Record<string, unknown>` on DaemonStatusReport.daemon and
  add the typed optional `startup` field, matching the DaemonCapabilities
  convention the interface JSDoc claims.
- Add a real useDaemonStatusReport hook test asserting the `report` alias maps
  from `data` — the dialog test mocks the whole hook, so nothing else guarded
  it.

* feat(web-shell): surface runtime.activity in the daemon status dashboard

PR QwenLM#6270 added a runtime.activity sub-object to GET /daemon/status
(activePrompts, lastActivityAt, idleSinceMs). Type it as an additive optional
on the SDK DaemonStatusReport and render it in the Runtime card: active-prompt
count and an idle duration ("no activity yet" when the daemon has seen none).
Gated on the field's presence so older daemons that omit it still render.
Verified end-to-end against a real qwen serve --web that emits the field.

* fix(web-shell): daemon status polish — i18n count, negative clamp, coverage

Address the review round (all minor):
- Move the capabilities count into the i18n string (daemon.capabilities.titleCount
  with a {count} placeholder) so locales can reorder it.
- Clamp negative durations in formatDurationMs (clock-skew defense).
- Re-export the hook options type as StatusReportOptions for consumers wrapping
  useStatusReport.
- Tests: use the real rate-limit tier keys (prompt/mutation/read) in the fixture,
  and cover the session-id display fallback, the channel-worker signal branch,
  and a healthy workspace section's chip/status rendering.

* feat(web-shell): name the failing checks behind a workspace section status

A "warning"/"error" workspace-diagnostics section only showed a rollup badge
plus count chips, so e.g. a warning preflight was opaque — the operator
couldn't tell it was the auth check without curling the API. Extract the
individual warning/error cells from the section's raw data (across cells /
servers / skills / tools / providers / hooks / extensions) and render each with
its label and message (e.g. "auth: No auth method configured."). OK and other
non-problem cells stay hidden. Verified end-to-end: a real daemon with no
credentials now shows the auth warning inline under preflight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants