Skip to content

feat(serve): Add remote LSP status route#5741

Merged
doudouOUC merged 8 commits into
QwenLM:mainfrom
doudouOUC:codex/lsp-status-route
Jun 23, 2026
Merged

feat(serve): Add remote LSP status route#5741
doudouOUC merged 8 commits into
QwenLM:mainfrom
doudouOUC:codex/lsp-status-route

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a structured, read-only remote LSP status surface for daemon and ACP clients. Sessions can now expose sanitized per-session LSP details through REST, ACP HTTP/WS, the ACP child status extension, and the TypeScript SDK without changing the existing slash-command /lsp Markdown output.

The new status response reports whether LSP is enabled, aggregate server counts, unavailable/initialization states, and a sanitized per-server list containing only stable client-facing fields.

Why it's needed

Issue #5677 tracks ACP gaps for remote status commands. /lsp already has local interactive and non-interactive behavior, but daemon/ACP clients only had coarse status from /status or /about, and adding ACP support directly to /lsp would expose presentation-oriented Markdown rather than a stable schema.

A structured route gives remote clients a typed capability, SDK helper, and versioned response shape while keeping LSP startup opt-in for spawned ACP children.

Reviewer Test Plan

Reviewers should confirm that remote clients can request LSP status for an owned session, that disabled or unavailable LSP is represented as a successful status payload rather than an HTTP error, and that the response does not expose debug-only LSP internals such as process ids, args, workspace folders, root URIs, or stderr tails.

How to verify

Run the focused bridge tests with cd packages/acp-bridge && npx vitest run src/bridge.test.ts src/spawnChannel.test.ts and expect the LSP status request to travel over the existing ACP channel and optional child args to be appended after --acp.

Run the focused CLI tests with cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts src/serve/server.test.ts src/serve/acp-http/transport.test.ts src/commands/serve.test.ts src/serve/run-qwen-serve.test.ts or the focused LSP test names and expect REST, ACP HTTP/WS, capability, disabled/unavailable, sanitization, and --experimental-lsp opt-in behavior to pass.

Run the focused SDK tests with cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts test/unit/DaemonSessionClient.test.ts test/unit/acpRouteTable.test.ts test/unit/daemon-public-surface.test.ts and expect the route mapping, helper forwarding, public exports, and client-id forwarding to pass.

Run npm run build and npm run typecheck from the repository root and expect both to complete successfully.

Evidence (Before & After)

N/A. This is a non-UI daemon/ACP/SDK API change.

Tested on

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

Environment (optional)

Node.js v22.22.3, npm 11.12.1, local macOS worktree.

Risk & Scope

  • Main risk or tradeoff: the LSP status schema mirrors the core snapshot shape, so future core LSP status changes need matching daemon and SDK type updates.
  • Not validated / out of scope: remote LSP restart, remote LSP configuration, changing slash-command /lsp output, and end-to-end tests against real language servers are intentionally out of scope.
  • Breaking changes / migration notes: no breaking changes; this only adds new read-only routes, ACP vendor methods, capability metadata, and SDK helpers.

Linked Issues

Refs #5677

中文说明

What this PR does

这个 PR 为 daemon 和 ACP 客户端新增结构化、只读的远端 LSP 状态面。Session 现在可以通过 REST、ACP HTTP/WS、ACP child status extension 和 TypeScript SDK 暴露经过清洗的 per-session LSP 详情,同时不改变现有 slash command /lsp 的 Markdown 输出。

新的状态响应会报告 LSP 是否启用、server 聚合计数、unavailable/initialization 状态,以及只包含稳定客户端字段的 per-server 清单。

Why it's needed

Issue #5677 跟踪远端 ACP 状态命令的缺口。/lsp 已经支持本地 interactive 和 non-interactive 行为,但 daemon/ACP 客户端之前只能从 /status/about 得到粗粒度信息;如果直接给 /lsp 增加 ACP 支持,会暴露面向展示的 Markdown,而不是稳定 schema。

结构化 route 可以给远端客户端提供 typed capability、SDK helper 和带版本的响应形状,同时保持 spawned ACP child 的 LSP 启动仍然是 opt-in。

Reviewer Test Plan

Reviewer 应确认远端客户端能为已拥有的 session 请求 LSP 状态,disabled 或 unavailable LSP 会以成功状态 payload 表示而不是 HTTP error,并且响应不会暴露 process id、args、workspace folder、root URI、stderr tail 等仅用于调试的 LSP 内部字段。

How to verify

运行 focused bridge 测试:cd packages/acp-bridge && npx vitest run src/bridge.test.ts src/spawnChannel.test.ts,预期 LSP status request 通过现有 ACP channel 传递,并且可选 child args 会追加在 --acp 后面。

运行 focused CLI 测试:cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts src/serve/server.test.ts src/serve/acp-http/transport.test.ts src/commands/serve.test.ts src/serve/run-qwen-serve.test.ts,或运行对应的 focused LSP test names,预期 REST、ACP HTTP/WS、capability、disabled/unavailable、sanitization 和 --experimental-lsp opt-in 行为都通过。

运行 focused SDK 测试:cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts test/unit/DaemonSessionClient.test.ts test/unit/acpRouteTable.test.ts test/unit/daemon-public-surface.test.ts,预期 route mapping、helper forwarding、public exports 和 client-id forwarding 都通过。

在仓库根目录运行 npm run buildnpm run typecheck,预期两者都成功完成。

Evidence (Before & After)

N/A。这是非 UI 的 daemon/ACP/SDK API 变更。

Tested on

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

Environment (optional)

Node.js v22.22.3,npm 11.12.1,本地 macOS worktree。

Risk & Scope

  • Main risk or tradeoff: LSP status schema 会镜像 core snapshot 形状,因此未来 core LSP status 变化需要同步更新 daemon 和 SDK 类型。
  • Not validated / out of scope: 远端 LSP restart、远端 LSP 配置、修改 slash-command /lsp 输出,以及针对真实 language server 的端到端测试都明确不在范围内。
  • Breaking changes / migration notes: 无破坏性变更;这里只新增只读 routes、ACP vendor methods、capability metadata 和 SDK helpers。

Linked Issues

Refs #5677

@doudouOUC doudouOUC self-assigned this Jun 23, 2026
@doudouOUC
doudouOUC marked this pull request as ready for review June 23, 2026 03:47
Copilot AI review requested due to automatic review settings June 23, 2026 03:47

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

Thanks for the PR, @doudouOUC!

Template looks good ✓ — all required sections present, bilingual, N/A appropriately used for the evidence section on this non-UI change.

On direction: This directly addresses one of the five tracked ACP gaps in issue #5677 (/lsp). Adding a structured, read-only LSP status endpoint for daemon/ACP clients is clearly within scope — remote clients need typed status data, not Markdown from a slash command. The approach of keeping it separate from the existing /lsp interactive command is the right call.

On approach: The scope feels right. The change follows the established session-status pattern (REST route → ACP method → capability tag → SDK helper), mirrors existing routes like /session/:id/tasks and /session/:id/supported-commands, and the sanitization in buildSessionLspStatus correctly strips debug internals (pid, args, stderr, rootUri, workspaceFolder). The --experimental-lsp opt-in for spawned ACP children keeps LSP startup gated. The extraArgs mechanism on createSpawnChannelFactory is minimal and extensible without being speculative.

One thing worth noting: ServeLspServerStatus / ServeSessionLspStatus in packages/acp-bridge/src/status.ts and DaemonLspServerStatus / DaemonSessionLspStatus in packages/sdk-typescript/src/daemon/types.ts are near-identical type definitions. This mirrors the existing pattern for other status types (tasks, context, etc.) across the bridge/SDK boundary, so it's consistent — but if there's ever a consolidation effort for these duplicated shapes, this would be one more entry.

Moving on to code review. 🔍

中文说明

感谢贡献,@doudouOUC

模板完整 ✓ — 所有必填章节齐全,非 UI 变更合理使用 N/A。

方向: 直接对应 issue #5677 中跟踪的五个 ACP 缺口之一(/lsp)。为 daemon/ACP 客户端增加结构化、只读的 LSP 状态端点完全在项目范围内——远端客户端需要 typed 状态数据,而非 slash command 的 Markdown 输出。与现有 /lsp 交互命令保持分离的做法是正确的。

方案: 范围合理。变更遵循已有的 session-status 模式(REST route → ACP method → capability tag → SDK helper),与 /session/:id/tasks/session/:id/supported-commands 等现有路由保持一致,buildSessionLspStatus 中的清洗逻辑正确剥离了调试内部字段(pid、args、stderr、rootUri、workspaceFolder)。--experimental-lsp 对 spawned ACP child 的 opt-in 保持 LSP 启动受控。createSpawnChannelFactory 上的 extraArgs 机制最小且不做过度扩展。

值得注意的一点:packages/acp-bridge/src/status.ts 中的 ServeLspServerStatus / ServeSessionLspStatuspackages/sdk-typescript/src/daemon/types.ts 中的 DaemonLspServerStatus / DaemonSessionLspStatus 是近似重复的类型定义。这与现有的 tasks、context 等 status 类型在 bridge/SDK 边界两侧分别定义的模式一致,所以是合理的——但如果未来有整合这些重复类型的计划,这会多一个条目。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The implementation follows established patterns well — REST route, ACP method, capability tag, SDK helper, sanitization. The diff is clean and focused. One critical bug found:

🐛 --experimental-lsp flag is not registered in the yargs builder. The ServeArgs interface declares experimentalLsp?: boolean and the handler reads argv.experimentalLsp, but no .option('experimental-lsp', {...}) call exists in the builder chain (serve.ts:116-316). The production CLI uses .strict() (via config.ts:1057), so passing --experimental-lsp at runtime produces Unknown arguments: experimental-lsp, experimentalLsp and the daemon refuses to start. The unit test in serve.test.ts passes because its buildParser() uses yargs([]).exitProcess(false).fail(false) without .strict(), so the test never catches this gap. Fix: add .option('experimental-lsp', { type: 'boolean', default: false, description: '...' }) to the builder.

Everything else looks correct: sanitization strips pid/args/stderr/rootUri/workspaceFolder, session ownership is enforced, disabled/unavailable states return HTTP 200 with proper payload shape.

Real-Scenario Testing

Attempt 1: --experimental-lsp flag (BROKEN)

$ QWEN_SERVER_TOKEN=testtoken npm run dev -- serve --port 18899 --experimental-lsp
...
Unknown arguments: experimental-lsp, experimentalLsp

The daemon refuses to start with --experimental-lsp because the flag is not registered in the yargs builder (see code review above).

Attempt 2: Default (no flag) — LSP status route works

$ QWEN_SERVER_TOKEN=testtoken npm run dev -- serve --port 18899
...
qwen serve listening on http://127.0.0.1:18899 (mode=http-bridge, workspace=...)
qwen serve: bound to workspace "..."
qwen serve: /acp WebSocket transport enabled on /acp

Capability check confirms session_lsp_status is advertised:

$ curl -s http://127.0.0.1:18899/capabilities -H "Authorization: Bearer testtoken" | jq '.features[] | select(contains("lsp"))'
"session_lsp_status"

Non-existent session returns proper 404:

$ curl -s http://127.0.0.1:18899/session/fake-session-id/lsp -H "Authorization: Bearer testtoken"
{"error":"No session with id \"fake-session-id\"","sessionId":"fake-session-id"}

Real session returns valid disabled-LSP payload:

$ curl -s http://127.0.0.1:18899/session/fc7ed8f4.../lsp -H "Authorization: Bearer testtoken" -H "X-Qwen-Client-Id: client_..."
{
  "v": 1,
  "sessionId": "fc7ed8f4-291b-47e2-b1ae-4679ebf00a10",
  "workspaceCwd": "/home/runner/work/qwen-code/qwen-code/.qwen/worktrees/triage",
  "enabled": false,
  "configuredServers": 0,
  "readyServers": 0,
  "failedServers": 0,
  "inProgressServers": 0,
  "notStartedServers": 0,
  "servers": []
}

Unit Tests

All focused tests pass:

  • packages/acp-bridge: 320 passed (bridge.test.ts, spawnChannel.test.ts)
  • packages/cli: 828 passed (acpAgent.test.ts, server.test.ts, transport.test.ts, serve.test.ts, run-qwen-serve.test.ts)
  • packages/sdk-typescript: 273 passed (DaemonClient.test.ts, DaemonSessionClient.test.ts, acpRouteTable.test.ts, daemon-public-surface.test.ts)
  • npm run typecheck: ✅ passed
  • npm run build: ✅ passed
中文说明

代码审查

实现很好地遵循了已有模式——REST route、ACP method、capability tag、SDK helper、清洗逻辑。Diff 干净且聚焦。发现一个关键 bug:

🐛 --experimental-lsp 标志未在 yargs builder 中注册。 ServeArgs 接口声明了 experimentalLsp?: boolean,handler 也读取了 argv.experimentalLsp,但 builder 链中没有 .option('experimental-lsp', {...}) 调用。生产 CLI 使用 .strict()(通过 config.ts:1057),因此运行时传入 --experimental-lsp 会产生 Unknown arguments: experimental-lsp, experimentalLsp,daemon 拒绝启动。serve.test.ts 中的单元测试通过是因为其 buildParser() 使用 yargs([]).exitProcess(false).fail(false) 但没有 .strict(),所以测试未能捕获此问题。修复方法:在 builder 中添加 .option('experimental-lsp', { type: 'boolean', default: false, description: '...' })

其他部分均正确:清洗剥离了 pid/args/stderr/rootUri/workspaceFolder,session 所有权被强制执行,disabled/unavailable 状态返回 HTTP 200 和正确的 payload 形状。

真实场景测试

尝试 1:--experimental-lsp 标志(已损坏)

daemon 拒绝启动,报告 Unknown arguments: experimental-lsp, experimentalLsp

尝试 2:默认(无标志)— LSP 状态路由正常工作

  • session_lsp_status capability 已声明 ✓
  • 不存在的 session 返回正确的 404 ✓
  • 真实 session 返回有效的 disabled-LSP payload ✓
  • 所有单元测试通过(1421 个测试)✓
  • typecheck 和 build 均通过 ✓

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a well-structured PR that follows the project's established patterns for session-status endpoints. The REST route, ACP methods, capability tag, SDK helpers, and sanitization are all implemented consistently with existing routes like /session/:id/tasks. The documentation updates are thorough.

The one issue is real though: --experimental-lsp doesn't work at runtime because it's missing from the yargs builder. The unit test masks the problem by using a non-strict parser. This means the LSP opt-in for spawned ACP children — a key part of the feature — is non-functional. The fix is trivial (one .option() call), but it needs to be there before this ships.

Requesting changes for that one fix. Everything else looks ready.

中文说明

这是一个结构良好的 PR,遵循了项目中 session-status 端点的既有模式。REST route、ACP methods、capability tag、SDK helpers 和清洗逻辑都与 /session/:id/tasks 等现有路由保持一致。文档更新也很完善。

唯一的问题是 --experimental-lsp 在运行时不工作,因为它缺少 yargs builder 注册。单元测试使用了非 strict 解析器,掩盖了这个问题。这意味着 spawned ACP child 的 LSP opt-in——该功能的关键部分——完全不可用。修复很简单(一个 .option() 调用),但在合入前需要补上。

请求修改这一处。其他部分都已就绪。

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.

Missing yargs builder registration for --experimental-lsp — the flag is rejected as unknown at runtime due to .strict() in the production CLI. The unit test doesn't catch this because buildParser() lacks .strict(). Fix: add .option('experimental-lsp', { type: 'boolean', default: false, description: '...' }) to the builder chain in serve.ts. Everything else looks good. 🙏

@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.

Summary

This PR adds a well-structured, read-only remote LSP status surface across REST, ACP HTTP/WS, and the TypeScript SDK. The sanitization boundary correctly strips debug internals via an explicit allowlist, tests cover the key paths (enabled, disabled, unavailable), and the implementation closely follows established patterns from existing status routes.

2 findings reported, 2 confirmed after verification (1 high-confidence, 1 low-confidence).

Findings

Suggestion

  • File: packages/cli/src/serve/acp-http/dispatch.ts:99
  • Source: [review]
  • Issue: The JSON-RPC method is named _qwen/session/lsp_status, but the REST path is /session/:id/lsp and the ext method is qwen/status/session/lsp. Every other session-status route uses matching suffixes across all three layers (e.g., tasks/tasks/tasks, context/context/context). LSP is the only route where the JSON-RPC suffix (lsp_status) diverges from both REST (lsp) and ext (lsp).
  • Impact: Future developers adding a new session status route will see two contradictory precedents. SDK clients using the JSON-RPC transport directly must call _qwen/session/lsp_status while REST clients call /session/:id/lsp — the asymmetry is unique to this route.
  • Suggested fix: Rename _qwen/session/lsp_status_qwen/session/lsp in ALL_QWEN_VENDOR_METHODS, the dispatch case arm, WS_READ_METHODS, and acpRouteTable.ts. Update corresponding test assertions.

Needs Human Review

Possibly:

  • File: packages/cli/src/serve/server.test.ts — The existing "maps missing sessions on read-only session routes to 404" test covers context, supported-commands, and tasks but was not extended to include the new /session/:id/lsp route. Adding a 404 assertion for the LSP route would close the gap.

Verdict

Comment — One naming inconsistency worth aligning before merge; no blockers.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/acp-http/dispatch.ts Outdated

@wenshao wenshao 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.

Independent review of the remote LSP status route. The implementation is clean and consistent with the sibling session-status routes: per-server sanitization strips debug internals (args, pid, stderrTail, rootUri, workspaceFolder) behind an explicit allowlist, the core LspStatusSnapshot ↔ bridge ↔ SDK types line up, and the REST / ACP HTTP+WS / SDK wiring plus tests are thorough.

No new issues surfaced beyond the threads already open on this PR. I did independently reproduce and confirm the existing --experimental-lsp registration blocker is real (the strict serve parser rejects the flag, and CI stays green only because the unit test's parser isn't strict), so that remains the one item to address before merge — deferring to the existing thread for the fix.

— claude-opus-4-8[1m] via Qwen Code /qreview

@wenshao wenshao 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.

I did not find any additional issues beyond the existing requested change. The --experimental-lsp blocker is still reproducible on the built CLI: node packages/cli/dist/index.js serve --experimental-lsp --no-web --port 0 exits with Unknown arguments: experimental-lsp, experimentalLsp, so the serve command still needs to register the flag before this can merge.

— GPT-5 Codex via Qwen Code /review

Comment thread packages/cli/src/acp-integration/acpAgent.ts

@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.

No review findings. Downgraded from Approve to Comment: CI failing: Test (windows-latest, Node 22.x).

Implementation is clean and well-structured. The sanitization allowlist correctly strips debug internals, all 1421 tests pass, and the new routes follow established patterns for session-status endpoints.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@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.

No review findings. The implementation is clean and well-structured — REST route, ACP HTTP/WS methods, capability tag, SDK helpers, sanitization, and documentation all follow established patterns consistently across 35 files. Downgraded from Approve to Comment: CI failing (Test (windows-latest, Node 22.x)).

— qwen3.7-max via Qwen Code /review

wenshao
wenshao previously approved these changes Jun 23, 2026
chiga0
chiga0 previously approved these changes Jun 23, 2026

@chiga0 chiga0 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.

Overview

Final Verdict: Approve — Clean, well-structured addition of a read-only LSP status surface across REST, ACP HTTP/WS, bridge, and SDK. Follows the established pattern of existing status routes (tasks, stats, context). Sanitization is correctly scoped to stable client-facing fields.

Cross-Validation

# Finding Reviewer My Assessment
C1 Missing yargs builder registration for --experimental-lsp (strict parser rejects) qwen-code-ci-bot, wenshao Fixed at HEADserve.ts now has .option('experimental-lsp', ...) in the builder chain
C2 Naming inconsistency _qwen/session/lsp_status vs REST /lsp qwen-code-ci-bot Fixed at HEAD — commit 1596d06 renamed to session_lsp / _qwen/session/lsp consistently
C3 Missing error-state sanitization test wenshao Fixed at HEAD — commit abf9992 added 'includes error field for FAILED server while stripping debug internals' test

No unique Critical or Major findings from my independent review.

Additional Audit Coverage

Areas I independently verified beyond existing findings:

  • Sanitization completeness: buildSessionLspStatus in acpAgent.ts maps snapshot → response, only including name, status, languages, transport, command, error. Debug internals (pids, args, workspace folders, root URIs, stderr tails) are correctly excluded. Conditional spreads (...(server.error ? { error } : {})) correctly omit fields when absent.
  • Route consistency: REST GET /session/:id/lsp, ACP vendor _qwen/session/lsp, WS read method, status ext method qwen/status/session/lsp — all consistently named and wired. requireOwned check present in ACP dispatcher before processing.
  • Type parity: DaemonSessionLspStatus (SDK) and ServeSessionLspStatus (bridge) are structurally identical with matching field names and types. Separate definitions follow existing codebase convention.
  • extraArgs generically threaded: spawnChannel.ts spreads extraArgs after --acp, correctly forwarding --experimental-lsp to child processes. The mechanism is generic and reusable.
  • Capability versioning: session_lsp: { since: 'v1' } correctly registered in SERVE_CAPABILITY_REGISTRY.

Minor Observations

  • Type literal difference: ServeSessionLspStatus.v uses typeof STATUS_SCHEMA_VERSION (literal type) while DaemonSessionLspStatus.v uses 1 (numeric literal). Functionally equivalent but stylistically inconsistent. Non-blocking.
  • Trailing semicolon: buildSessionLspStatus method has }; (semicolon after method definition) — valid TS but inconsistent with other method definitions in the class. Non-blocking.

This review was generated by QoderWork AI

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @doudouOUC!

Template looks good ✓ — all required sections present, bilingual, N/A correctly used for the evidence section since this is a non-UI API change.

On direction: well-aligned. Issue #5677 tracks the ACP gap for remote status commands, and /lsp already has local interactive behavior. Daemon/ACP clients need structured data, not Markdown — this PR fills that gap cleanly by extending the existing session-status surface pattern (context, tasks, stats). No direct CHANGELOG reference, but session-status endpoints are a well-established area.

On approach: scope feels right. REST route, ACP HTTP/WS methods, capability tag, SDK helpers, sanitization boundary, docs — each file in the diff serves the stated goal. The --experimental-lsp flag for child opt-in is appropriate since LSP is still experimental. The minor whitespace normalization in 00-index.md is cosmetic noise but not worth splitting out.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必需部分齐全,双语,非 UI 变更的 Evidence 部分正确使用 N/A。

方向:对齐。Issue #5677 跟踪远端状态命令的 ACP 缺口,/lsp 已有本地交互行为。Daemon/ACP 客户端需要结构化数据而非 Markdown — 本 PR 通过扩展现有 session-status 面(context、tasks、stats)来填补这个缺口。CHANGELOG 无直接引用,但 session-status 端点是成熟领域。

方案:范围合理。REST route、ACP HTTP/WS methods、capability tag、SDK helpers、sanitization 边界、文档 — diff 中每个文件都服务于目标。--experimental-lsp 作为子进程 opt-in 合理。00-index.md 的空白规范化是微小噪音,不值得拆分。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Code Review

The implementation is clean and consistent across all surfaces. The buildSessionLspStatus sanitization in acpAgent.ts is the critical piece — it uses an explicit allowlist (name, status, languages, transport, command, error) that strips debug internals (args, pid, stderrTail, rootUri, workspaceFolder, exitCode). This is correctly tested with assertions like expect(JSON.stringify(lsp)).not.toContain('pid') etc.

The new types (ServeSessionLspStatus in status.ts, DaemonSessionLspStatus in SDK types.ts) mirror each other and match the core LspStatusSnapshot shape. The REST route, ACP dispatch, WS method registration, capability tag, and SDK route-table entry all follow the established patterns of sibling routes (/tasks, /stats, /context).

No correctness bugs, security holes, or structural violations found. The --experimental-lsp flag is properly threaded through ServeArgsServeOptionscreateSpawnChannelFactory → child args after --acp, with a test verifying it's absent by default.

Test Results

All focused test suites pass:

Package Tests Status
acp-bridge (bridge + spawnChannel) 320 ✅ pass
cli (acpAgent) 136 ✅ pass
cli (server + transport + serve + run-qwen-serve) 693 ✅ pass
sdk-typescript (DaemonClient + DaemonSessionClient + acpRouteTable + public-surface) 273 ✅ pass
Total 1422 ✅ all pass

Build: ✅ 0 errors (15 pre-existing warnings). Typecheck: ✅ clean. CI: ✅ all green (macOS, Linux, Windows, lint, integration tests).

Real-Scenario Testing (tmux)

Booted the daemon from PR code via npm run dev -- serve --no-web --port 19876:

$ npm run dev -- serve --no-web --port 19876

> @qwen-code/[email protected] dev
> node scripts/dev.js serve --no-web --port 19876

qwen serve: daemon log → /home/runner/.qwen/debug/daemon/serve-5135-918fec44.log
qwen serve: session reaper started (interval 60000ms, idle threshold 1800000ms)
qwen serve listening on http://127.0.0.1:19876 (mode=http-bridge, workspace=...)
qwen serve: bound to workspace "..."
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
qwen serve: /acp WebSocket transport enabled on /acp

Capabilities endpoint confirms session_lsp is advertised (verified via curl /capabilities).

LSP endpoint returns correct 404 for non-existent sessions:

$ curl -s http://127.0.0.1:19876/session/nonexistent/lsp
{"error":"No session with id \"nonexistent\"","sessionId":"nonexistent"}
HTTP_STATUS:404

Daemon log confirms the route is handled:

2026-06-23T10:11:15.714Z [WARN] [DAEMON] route=GET /session/nonexistent/lsp sessionId=nonexistent durationMs=4 status=404 request completed

(Note: a full session-attached LSP status test requires an active ACP session with model configuration, which is not feasible in a sandboxed CI runner without API keys. The unit tests cover the enabled/disabled/unavailable/sanitization paths comprehensively.)

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

This is a clean, well-tested PR that fills a real gap (issue #5677) — daemon/ACP clients can now get structured LSP status instead of relying on Markdown from the local /lsp command.

The implementation follows the established session-status pattern exactly: REST route, ACP methods (HTTP + WS), capability tag, SDK types + helpers, sanitization boundary. The buildSessionLspStatus allowlist correctly strips debug internals. All 1422 focused tests pass, build and typecheck are clean, CI is green across all platforms, and the daemon boots and serves the new endpoint correctly in real-scenario testing.

The scope is proportional to the goal — no drive-by refactors, no speculative features. The --experimental-lsp child opt-in is appropriately scoped.

Approval guardrail check: this is a feat PR (not refactor), so the cross-repository guardrail does not block. All stages are clean.

Approving. ✅

中文说明

这是一个干净、测试充分的 PR,填补了真实缺口(issue #5677)— daemon/ACP 客户端现在可以获取结构化 LSP 状态,而无需依赖本地 /lsp 命令的 Markdown 输出。

实现完全遵循已有的 session-status 模式:REST route、ACP methods(HTTP + WS)、capability tag、SDK types + helpers、sanitization 边界。buildSessionLspStatus 白名单正确地剔除了调试内部字段。所有 1422 个测试通过,build 和 typecheck 干净,CI 全平台绿色,实际场景测试中 daemon 正常启动并提供新端点。

范围与目标相称 — 无附带重构,无投机功能。--experimental-lsp 子进程 opt-in 作用域恰当。

审批护栏检查:这是 feat PR(非 refactor),跨仓库护栏不阻止。所有阶段干净。

批准 ✅

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.

One doc fix needed: capability tag is session_lsp in code but session_lsp_status in 5 doc places (08-session-lifecycle.md, 11-capabilities-versioning.md, qwen-serve-protocol.md). Quick find-and-replace, then this is ready. See review comments above for details. 🙏

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local real-binary + tmux E2E ✅

I built this PR from a clean worktree at the PR head (f6453e4a9), ran the full focused test plan, and exercised the new route end-to-end against a real qwen serve daemon running in tmux with a real typescript-language-server. The feature works as designed. One docs-only inconsistency is worth fixing before merge.

Environment

  • macOS (darwin), Node v22.22.2, npm 10.9.7
  • Worktree at PR head f6453e4a9, fresh npm ci + npm run build
  • Real LSP server: [email protected] (--stdio), in a throwaway TS workspace with a .lsp.json

1. Build / typecheck / tests — all green

Check Result
npm run build (root) ✅ exit 0 (only pre-existing vscode-ide-companion lint warnings, 0 errors)
npm run typecheck (root) ✅ exit 0
acp-bridge: bridge.test.ts + spawnChannel.test.ts ✅ 320 passed
sdk-typescript: 4 unit files from the plan ✅ 273 passed
cli: 5 files from the plan ✅ 828 passed, 1 flake*

*The single failure was server.test.ts › bearer auth › is open by default with Error: socket hang up — a transient supertest socket flake unrelated to this PR (it touches no auth code). It passes in isolation on re-run. All LSP-specific cases pass, verified by name:

  • acpAgent.test.ts: disabled / unavailable / READY-with-sanitization / FAILED server while stripping debug internals
  • transport.test.ts: initialize advertises _qwen/session/lsp, _qwen/session/lsp returns status
  • serve.test.ts / run-qwen-serve.test.ts: --experimental-lsp parse + spawn opt-in ✅

2. Real tmux E2E

Daemon A — qwen serve --experimental-lsp + real typescript-language-server:

// GET /session/:id/lsp  → HTTP 200
{"v":1,"enabled":true,"configuredServers":1,"readyServers":1,
 "failedServers":0,"inProgressServers":0,"notStartedServers":0,
 "servers":[{"name":"typescript-language-server","status":"READY",
   "languages":["typescript"],"transport":"stdio",
   "command":"typescript-language-server"}]}

Sanitization proven against a live server: the actual process exists with a real PID and --stdio args —

52826 node .../typescript-language-server --stdio

— yet the response leaks none of pid / args / stderrTail / rootUri / workspaceFolder / exitCode / exitSignal / warmedUp / restartAttempts. Server entry keys are exactly name, status, languages, transport, command. The whitelist mapping in buildSessionLspStatus makes this structural (future core debug fields can't leak).

Daemon B — qwen serve without --experimental-lsp:

// GET /session/:id/lsp  → HTTP 200  (NOT an error)
{"v":1,"enabled":false,"configuredServers":0,"readyServers":0,"servers":[]}

Unknown session → HTTP 404 {"error":"No session with id ...","sessionId":"..."}. ✅

End-to-end opt-in chain confirmed working: serve --experimental-lspextraArgs:['--experimental-lsp'] → child spawned as qwen --acp --experimental-lsp → main CLI config.ts (lspEnabled = !bareMode && argv.experimentalLsp) → getLspStatusSnapshot() returns the real snapshot. getLspStatusSnapshot() already exists in core on main, so the PR correctly leaves packages/core untouched.

3. ⚠️ Finding (docs only): capability tag name mismatch — session_lsp_status vs session_lsp

Code, tests, and the running daemon all use the tag session_lsp (the last commit f6453e4a9 deliberately renamed the test baseline to session_lsp). But the docs still say session_lsp_status in 5 places. Runtime proof from GET /capabilities:

has session_lsp        : true
has session_lsp_status : false

A client that follows the docs and pre-flights session_lsp_status (as the protocol doc explicitly instructs: "pre-flight this tag before exposing remote LSP status") will never detect the capability, because the daemon advertises session_lsp. Stale locations:

  • docs/developers/daemon/08-session-lifecycle.md:227 (heading) and :259 (capability-tags list)
  • docs/developers/daemon/11-capabilities-versioning.md:92 (the "67 tags" Sessions list)
  • docs/developers/qwen-serve-protocol.md:132 (the features array literal) and :162 (prose)

Recommendation: rename those 5 session_lsp_statussession_lsp to match the registry/route convention (/taskssession_tasks, /statssession_stats, so /lspsession_lsp). Docs-only, non-blocking for runtime, but it breaks the documented capability-discovery contract for SDK consumers, so worth folding into this PR.

Verdict

Functionally correct, well-tested, and the sanitization holds against a real language server. The only action item is the 5-line docs tag-name fix above. LGTM once that's addressed. 👍

🇨🇳 中文版(完整对应)

维护者验证 —— 本地真实二进制 + tmux 端到端 ✅

我在 PR head(f6453e4a9)的干净 worktree 上完整构建了本 PR,跑完了 focused 测试计划,并在 tmux 中运行的真实 qwen serve daemon + 真实 typescript-language-server 上端到端验证了新路由。功能符合设计预期。有一处纯文档不一致建议合并前修掉。

环境

  • macOS(darwin),Node v22.22.2,npm 10.9.7
  • worktree 位于 PR head f6453e4a9,全新 npm ci + npm run build
  • 真实 LSP server:[email protected](--stdio),临时 TS 工作区配 .lsp.json

1. 构建 / 类型检查 / 测试 —— 全绿

检查项 结果
npm run build(根) ✅ exit 0(仅 vscode-ide-companion 既有 lint warning,0 error)
npm run typecheck(根) ✅ exit 0
acp-bridge:bridge.test.ts + spawnChannel.test.ts ✅ 320 passed
sdk-typescript:计划中的 4 个单测文件 ✅ 273 passed
cli:计划中的 5 个文件 ✅ 828 passed,1 个 flake*

*唯一失败的是 server.test.ts › bearer auth › is open by default,报 Error: socket hang up —— 这是 supertest 的瞬时 socket flake,与本 PR 无关(它不碰 auth 代码),单独重跑即通过。所有 LSP 相关用例均通过(按名字逐一核实):

  • acpAgent.test.ts:disabled / unavailable / READY-带脱敏 / FAILED server while stripping debug internals
  • transport.test.ts:initialize advertises _qwen/session/lsp_qwen/session/lsp returns status
  • serve.test.ts / run-qwen-serve.test.ts:--experimental-lsp 解析 + spawn opt-in ✅

2. 真实 tmux 端到端

Daemon A —— qwen serve --experimental-lsp + 真实 typescript-language-server:

// GET /session/:id/lsp  → HTTP 200
{"v":1,"enabled":true,"configuredServers":1,"readyServers":1,
 "failedServers":0,"inProgressServers":0,"notStartedServers":0,
 "servers":[{"name":"typescript-language-server","status":"READY",
   "languages":["typescript"],"transport":"stdio",
   "command":"typescript-language-server"}]}

脱敏对照活的 server 得到证明:真实进程确实存在,有真实 PID 和 --stdio 参数 ——

52826 node .../typescript-language-server --stdio

—— 但响应没有泄漏任何 pid / args / stderrTail / rootUri / workspaceFolder / exitCode / exitSignal / warmedUp / restartAttempts。server 条目的字段恰好是 name, status, languages, transport, commandbuildSessionLspStatus 用的是白名单映射,所以脱敏是结构性的(未来 core 新增的 debug 字段也不会泄漏)。

Daemon B —— qwen serve(不带 --experimental-lsp):

// GET /session/:id/lsp  → HTTP 200(不是 error)
{"v":1,"enabled":false,"configuredServers":0,"readyServers":0,"servers":[]}

未知 session → HTTP 404 {"error":"No session with id ...","sessionId":"..."}。✅

端到端 opt-in 链路确认可用:serve --experimental-lspextraArgs:['--experimental-lsp'] → child 以 qwen --acp --experimental-lsp 启动 → 主 CLI config.ts(lspEnabled = !bareMode && argv.experimentalLsp) → getLspStatusSnapshot() 返回真实快照。getLspStatusSnapshot()maincore 中已存在,所以本 PR 不动 packages/core 是正确的。

3. ⚠️ 发现项(纯文档):capability tag 名称不一致 —— session_lsp_status vs session_lsp

代码、测试、以及运行中的 daemon 用的都是 session_lsp(最后一个 commit f6453e4a9 特意把测试基线改成了 session_lsp)。但文档里仍有 5 处写成 session_lsp_status。来自 GET /capabilities 的 runtime 证据:

has session_lsp        : true
has session_lsp_status : false

按文档去 pre-flight session_lsp_status 的客户端(协议文档明确要求:"pre-flight this tag before exposing remote LSP status")将永远发现不了这个能力,因为 daemon 广告的是 session_lsp。滞后位置:

  • docs/developers/daemon/08-session-lifecycle.md:227(标题)和 :259(capability-tags 列表)
  • docs/developers/daemon/11-capabilities-versioning.md:92("67 tags" 的 Sessions 列表)
  • docs/developers/qwen-serve-protocol.md:132(features 数组字面量)和 :162(正文)

建议: 把这 5 处 session_lsp_status 改成 session_lsp,以符合注册表/路由命名惯例(/taskssession_tasks/statssession_stats,故 /lspsession_lsp)。纯文档、不影响 runtime,但它破坏了面向 SDK 使用者的能力发现契约,建议并入本 PR 一起修。

结论

功能正确、测试充分、脱敏在真实 language server 下成立。唯一待办是上面这 5 行文档 tag 名称修正。修掉即可合并。👍

@doudouOUC
doudouOUC dismissed stale reviews from chiga0 and wenshao via 5cb3a9d June 23, 2026 08:59
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[qwen] Fixed in 5cb3a9d: renamed all 5 session_lsp_status references to session_lsp across the three doc files (08-session-lifecycle.md, 11-capabilities-versioning.md, qwen-serve-protocol.md). Capability tag name now matches code/tests/runtime consistently.

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@chiga0 chiga0 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.

Re-Review at HEAD 5cb3a9d

One new commit since last review — docs-only follow-up.

Change Assessment
5cb3a9d — rename session_lsp_statussession_lsp in docs ✓ Clean string replacement across 3 doc files, matching the code rename from 1596d06

No new issues. All previous findings remain resolved.

Verdict: LGTM


This review was generated by QoderWork AI

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@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. ✅

@doudouOUC
doudouOUC merged commit c9b5c99 into QwenLM:main Jun 23, 2026
30 checks passed
@doudouOUC
doudouOUC deleted the codex/lsp-status-route branch June 23, 2026 10:13

@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.

No review findings. Downgraded from Approve to Comment: CI still running.

Implementation is clean and well-structured — sanitization allowlist correctly strips debug internals, all 1,422 focused tests pass, and the new REST/ACP/WS/SDK routes follow established patterns consistently across 35 files.

— qwen3.7-max via Qwen Code /review

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.

5 participants