Repository navigation
fix(acp): disable LSP for MCP discovery config - #12256
Conversation
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
E2E verification report —
|
| file | bytes | sha1 | == GitHub files[].sha |
|---|---|---|---|
acp-integration/acpAgent.ts |
583,249 | 6cfe1443c862fe460700f9bba37860c076800822 |
✅ |
acp-integration/acpAgent.test.ts |
1,189,457 | 0273695ff3c7f07f0447b8e42bf23552a3bf06b7 |
✅ |
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, sofalsedisables.config/config.ts:2687–2716— insideif (lspEnabled),loadCliConfigconstructsNativeLspServiceand awaitsdiscoverAndPrepare()and thenstart()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-lspon 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-discoveryoccurs at exactly two places repo-wide in this file (:4151thesessionIdContext.runlabel,:4158the 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,clangdall ABSENT — sodiscoverAndPrepare()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 serveACP 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
left a comment
There was a problem hiding this comment.
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 APPROVED14:10:23Z(63 chars — the CI-green LGTM producer)qwen-code-ci-bot COMMENTED15:00:54Z— "Partially reviewed — gaps disclosed", its own ledger recordingfindings:[] posted:0, and the one Suggestion-level finding it confirmed as already reported here rather than repeatedCHANGES_REQUESTEDat 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.
What this PR does
When
qwen servelaunches 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 forcesexperimentalLsp: falseeven when the child argv enables it.Why it's needed
The discovery configuration exists only to discover MCP tools, but it inherited
--experimental-lspand 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=dotExpected result: the discovery configuration receives
experimentalLsp: falseeven when the ACP child argv hasexperimentalLsp: true.Local results:
acpAgent.test.ts: 764 passed, 2 skipped; one unrelated Windows symlink test failed withEPERM.npm run typecheck: 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
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
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。本地结果:
acpAgent.test.ts:764 passed、2 skipped;另有一个无关的 Windows symlink 测试因EPERM失败。npm run typecheck:通过。npm run build -- --cli-only:通过。Before & After 证据
不适用——这是内部 ACP 资源管理修复,不直接改变 UI。
测试平台
环境(可选)
Windows、Node.js v22.23.2。本地运行了单元测试和 CLI-only build;未运行 live ACP 进程树复现。
风险与范围
关联 Issue
Fixes #12216