Skip to content

fix(acp): disable LSP for MCP discovery config - #12256

Merged
yiliang114 merged 1 commit into
QwenLM:mainfrom
dvd233:codex/fix/disable-lsp-mcp-discovery-12216
Sep 28, 2026
Merged

yiliang114 merged 1 commit into
QwenLM:mainfrom
dvd233:codex/fix/disable-lsp-mcp-discovery-12216

Conversation

@dvd233

@dvd233 dvd233 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

What this PR does

When qwen serve launches an ACP child with --experimental-lsp, the workspace MCP discovery pass now disables LSP for its discovery-only configuration. The discovery pass still loads and refreshes MCP servers, while regular ACP session configs retain the requested LSP behavior. A regression test verifies the discovery request forces experimentalLsp: false even when the child argv enables it.

Why it's needed

The discovery configuration exists only to discover MCP tools, but it inherited --experimental-lsp and eagerly started a second complete LSP server set. Those servers were cached for the ACP process lifetime and were not used by MCP discovery, doubling language-server processes and memory. This addresses #12216.

Reviewer Test Plan

How to verify

Run the focused regression from packages/cli:

npm test -- src/acp-integration/acpAgent.test.ts -t "manages the workspace MCP discovery config used by status" --reporter=dot

Expected result: the discovery configuration receives experimentalLsp: false even when the ACP child argv has experimentalLsp: true.

Local results:

  • Focused regression: 1 passed.
  • Full acpAgent.test.ts: 764 passed, 2 skipped; one unrelated Windows symlink test failed with EPERM.
  • npm run typecheck: passed.
  • Targeted ESLint and Prettier checks: passed.
  • npm run build -- --cli-only: passed.

Evidence (Before & After)

N/A — this is an internal ACP resource-management fix with no direct UI change.

Tested on

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

Environment (optional)

Windows, Node.js v22.23.2. Unit tests and CLI-only build were run locally; a live ACP process-tree reproduction was not run.

Risk & Scope

  • Main risk or tradeoff: the discovery-only configuration no longer starts LSP servers. It does not change the LSP flag for ordinary ACP session configurations, and MCP discovery continues through the same configuration.
  • Not validated / out of scope: live multi-process measurements on macOS/Linux and the separate question of sharing LSP services across sessions.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #12216

中文说明

本 PR 做了什么

当 qwen serve 使用 --experimental-lsp 启动 ACP 子进程时,workspace MCP discovery 流程现在会为仅用于 discovery 的配置关闭 LSP。discovery 仍会加载并刷新 MCP server,普通 ACP session 配置仍保留请求的 LSP 行为。新增回归测试,验证即使子进程 argv 开启 experimentalLsp,discovery 请求也会强制使用 experimentalLsp: false。

为什么需要

该配置只用于发现 MCP tool,却继承了 --experimental-lsp,并主动启动了第二套完整的 LSP server。由于这套 server 会在 ACP 进程生命周期内缓存,且不会被 MCP discovery 使用,它会使语言服务器进程数和内存开销翻倍。本改动修复 #12216 所报告的问题。

Reviewer Test Plan

如何验证

在 packages/cli 目录运行聚焦回归:

npm test -- src/acp-integration/acpAgent.test.ts -t "manages the workspace MCP discovery config used by status" --reporter=dot

预期结果:即使 ACP 子进程 argv 中 experimentalLsp 为 true,discovery 配置仍收到 experimentalLsp: false。

本地结果:

  • 聚焦回归:1 passed。
  • 完整 acpAgent.test.ts:764 passed、2 skipped;另有一个无关的 Windows symlink 测试因 EPERM 失败。
  • npm run typecheck:通过。
  • 目标 ESLint 与 Prettier 检查:通过。
  • npm run build -- --cli-only:通过。

Before & After 证据

不适用——这是内部 ACP 资源管理修复,不直接改变 UI。

测试平台

OS 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ✅
🐧 Linux ⚠️ 未测试

环境(可选)

Windows、Node.js v22.23.2。本地运行了单元测试和 CLI-only build;未运行 live ACP 进程树复现。

风险与范围

  • 主要风险或权衡:仅用于 discovery 的配置不再启动 LSP server。普通 ACP session 配置的 LSP flag 不变,MCP discovery 仍通过同一配置工作。
  • 未验证 / 范围外:macOS/Linux 上的 live 多进程测量,以及跨 session 共享 LSP service 的独立设计问题。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #12216

The discovery-only Config starts MCP tools and never serves LSP requests, so keep the inherited ACP LSP flag from starting a second LSP stack.\n\nFixes QwenLM#12216
@qqqys

qqqys commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

E2E verification report — fix(acp): disable LSP for MCP discovery config

Verified at head 44bc36e8cd05cc1bb66b1afae42b55af58c28d95 (read 2026-09-19T14:3xZ). No Critical found. Recommendation: mergeable.

Why not a tmux arm

A tmux/TUI run cannot reach this code. createWorkspaceMcpDiscoveryConfig() only executes inside a qwen serve ACP child, so the executed arm here is a build + focused-regression + mutation A/B in a worktree at the PR head, with the multi-process claim reported as NOT MEASURED and the reason given below.

Arm 1 — compile (r179's mandatory gate: a red build voids every test argument)

Worktree at head, node_modules hardlinked (17 package-level dirs, zero symlinks, @qwen-code/qwen-code-core resolving inside the worktree):

  • npm run generate → GEN_rc=0
  • npm run build → BUILD_rc=0, and grep -c 'error TS' over the build log = 0

Blob fidelity was certified structurally, not by grep — sha1('blob ' + len + '\0' + bytes) against the API's own files[].sha:

file bytes sha1 == GitHub files[].sha
acp-integration/acpAgent.ts 583,249 6cfe1443c862fe460700f9bba37860c076800822 ✅
acp-integration/acpAgent.test.ts 1,189,457 0273695ff3c7f07f0447b8e42bf23552a3bf06b7 ✅

⚠️ Worth noting for anyone re-verifying: acpAgent.test.ts is 1,189,457 bytes, i.e. over the ~1 MB inline cap on GET contents/…, which returns HTTP 200 with encoding: "none" and an empty content field. Reading it that way decodes to one empty line and fabricates a build break. The git fetch upstream pull/12256/head route used here is byte-faithful.

Arm 2 — focused regression at head, and its non-vacuity oracle

npx vitest run src/acp-integration/acpAgent.test.ts \
  -t "manages the workspace MCP discovery config used by status" --reporter=dot

Test Files  1 passed (1)
     Tests  1 passed | 766 skipped (767)

Exactly one test was selected, so the green cannot be an empty selection.

Arm 3 — mutation witness (delete the single production line)

Removing experimentalLsp: false, from acpAgent.ts (assert count == 1 before removing; Python len 583,019 → 582,983 characters, −36, the line being pure ASCII):

Test Files  1 failed (1)
     Tests  1 failed | 766 skipped (767)

AssertionError: expected { experimentalLsp: undefined, …(4) } to deeply equal ObjectContaining{…}
-   "experimentalLsp": false,
+   "experimentalLsp": undefined,
   30771|       expect.objectContaining({ experimentalLsp: false }),

Restoration witnesses: marker census back to 1, and git diff --exit-code <head-ref> -- packages/cli/src/acp-integration/ → rc=0.

Mechanism, precedence and completeness — read in the head blob, not in the patch

  • config/config.ts:1740 — lspEnabled = !provisionalWorkspace && !bareMode && argv.experimentalLsp === true, a strict === true, so false disables.
  • config/config.ts:2687–2716 — inside if (lspEnabled), loadCliConfig constructs NativeLspService and awaits discoverAndPrepare() and then start() before returning. So the cached discovery config (if (this.workspaceMcpDiscoveryConfig) return;) really did own a second started stack for the ACP process lifetime — the mechanism in bug(lsp): MCP workspace-discovery config starts a second, unused LSP server set per ACP process (inherits --experimental-lsp) #12216 is confirmed, not assumed.
  • Precedence: the override sits after ...this.argv (acpAgent.ts:4159), so object-literal semantics guarantee it wins over a truthy --experimental-lsp on the child argv. (Had it been placed before the spread it would have been a silent no-op — that is the one defect shape available here, and it is not present.)
  • Completeness: workspace-mcp-discovery occurs at exactly two places repo-wide in this file (:4151 the sessionIdContext.run label, :4158 the argv override), i.e. there is only one discovery-config construction site, so no second site is left inheriting the flag.

Deref-safety enumeration (does anything need the client the discovery config no longer has?)

getLspClient() has exactly 4 non-test call sites, and every one is guarded:

site guard
core/src/tools/lsp.ts:148 if (!client || !this.config.isLspEnabled()) → returns a graceful "unavailable" ToolResult
core/src/config/config.ts:11351 if (this.isLspEnabled() && this.getLspClient())
core/src/config/config.ts:7767 the getter itself, typed LspClient | undefined
cli/src/ui/commands/lspCommand.ts:66 preceded by if (!config.isLspEnabled()) at :56; and BuiltinCommandLoader.ts:188 registers the command only when LSP is enabled

⇒ A config with no LSP client cannot throw on this path.

Base arm (r194 discipline — is the hole the diff's?): acpAgent.ts:14900 already ships ...(provisionalWorkspace ? { experimentalLsp: false } : {}) for ACP session configs. "An ACP-loaded config with LSP disabled" is therefore a pre-existing, already-exercised state, and this PR extends an existing pattern rather than introducing a new one.

NOT MEASURED, with the precondition that makes it unmeasurable here

  • The multi-process claim (doubled language-server processes and memory). This host has no LSP server binary at all — typescript-language-server, vscode-langservers-extracted, pylsp, gopls, rust-analyzer, clangd all ABSENT — so discoverAndPrepare() would find zero servers on both arms and a process-count A/B would be vacuous by construction, painting both arms green. Not attempted rather than reported as a null result.
  • A live qwen serve ACP child process tree. Not driven.

Corroboration of existing feedback — no new finding filed

The bot's non-blocking note that the regression test pins undefined → false rather than the true → false override the description advertises (the mockArgv.experimentalLsp = true line sits in a different it block, and the beforeEach this PR touched resets it first) is independently confirmed by Arm 3's own output: the received value on the mutated arm is literally experimentalLsp: undefined. That agrees with the bot's classification — the test is load-bearing, not decorative, and the gap is a Suggestion. Nothing is added to it here, and it is not treated as blocking.

Gate state at post time (re-read inside the pre-write guard)

state=open merged=False draft=False; qwen-code-ci-bot APPROVED at head 14:10:23Z is the newest and only verdict row; CHANGES_REQUESTED at any commit = 0; [Critical] at head by original_commit_id = 0; check-runs total_count=54 fetched=54 MATCH=True, raw {success 24, skipped 29, in_progress 1} over 45 distinct names, product lanes 40, non-green 0, in-flight 0, and the single unsettled run identified by name as the bot's own review-pr lane (in_progress since 13:40:01Z) rather than assumed to be a bot lane; check-suites total_count=11 fetched=11 MATCH=True at suite level {success 7, in_progress 1, skipped 3} (latest_check_run is null in the list route, so the suite-level pair is the sound field); ms=blocked, autoMergeRequest=None.

As of the state read immediately before posting, this comment carries no approval.

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

Approve — our E2E verification report at this head concluded mergeable, and the gate state re-read immediately before this approval still holds.

Head 44bc36e8cd05cc1bb66b1afae42b55af58c28d95 — unmoved since that report (5742726174, 14:38:07Z), whose verdict line is "No Critical found. Recommendation: mergeable."

Verdict rows at head (pulls/12256/reviews, paginated, 2 rows total, no page cap):

  • qwen-code-ci-bot APPROVED 14:10:23Z (63 chars — the CI-green LGTM producer)
  • qwen-code-ci-bot COMMENTED 15:00:54Z — "Partially reviewed — gaps disclosed", its own ledger recording findings:[] posted:0, and the one Suggestion-level finding it confirmed as already reported here rather than repeated
  • CHANGES_REQUESTED at any commit = 0; newest verdict row at head = APPROVED

Criticals: [Critical] at head by original_commit_id = 0 (inline comment total = 0).

CI — check-runs total_count=73 fetched=73 MATCH=True, raw {success 25, skipped 47, in_progress 1} over 45 distinct lane names, product lanes non-green = 0. Product lanes that actually executed and passed: Test (ubuntu-latest, Node 22.x), Lint & Static (ubuntu-latest, Node 22.x), web-shell E2E Smoke, Desktop Shell (ubuntu-22.04), Desktop Shell (windows-2022), Integration Tests (no-AK, No Sandbox), OpenTUI no-flicker gate, TUI parity snapshots (ink vs opentui), Real daemon E2E / Java 11, and the Java 11/17/21 ubuntu + macos + windows matrix.

The single unsettled run is named, not assumed: the bot's own review-pr lane instance started 13:40:01Z, superseded by completed review-pr instances at 14:10:27Z and 15:00:59Z and by the bot's posted review output at 15:00:54Z. It is a bot lane, not a product lane.

Not a phantom green: check-suites total_count=15 fetched=15 MATCH=True, suite-level {success 8, skipped 6, in_progress 1}, action_required = 0 — so no fork first-time-contributor gate is parking these workflows, and the product lanes above were genuinely permitted to run. (latest_check_run is null in the list route, so the suite-level (status, conclusion) pair is the field read here.)

ms=blocked, auto_merge=None, draft=False, merged=False.

The bot's Suggestion-level note — that the discovery-config regression test pins undefined → false rather than the true → false override the description advertises — is non-blocking, was independently confirmed by Arm 3 of our report (the mutated arm literally received experimentalLsp: undefined), and nothing is added to it here.

This approval reflects the state read immediately before posting.

@yiliang114
yiliang114 added this pull request to the merge queue Sep 28, 2026
Merged via the queue into QwenLM:main with commit d439660 Sep 28, 2026
93 checks passed
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.

bug(lsp): MCP workspace-discovery config starts a second, unused LSP server set per ACP process (inherits --experimental-lsp)

3 participants