Skip to content

fix(cli): use target dir for ACP core settings - #9897

Closed
AaronZ345 wants to merge 3 commits into
QwenLM:mainfrom
AaronZ345:fix-worktree-settings-cwd-target
Closed

fix(cli): use target dir for ACP core settings#9897
AaronZ345 wants to merge 3 commits into
QwenLM:mainfrom
AaronZ345:fix-worktree-settings-cwd-target

Conversation

@AaronZ345

Copy link
Copy Markdown
Contributor

What this PR does

  • Uses the active Config target directory when ACP core settings methods are called without an explicit cwd.
  • Adds regression coverage for qwen/settings/getCore and qwen/settings/setCoreValue so omitted cwd resolves through config.getTargetDir().

Why it's needed

Worktree-backed ACP sessions can run from a daemon process root that differs from the active workspace. Falling back to process.cwd() makes core settings reads and writes target the wrong .qwen/settings.json; using the target directory keeps workspace settings scoped to the active worktree.

Fixes #8138.

Reviewer Test Plan

  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts
  • cd packages/cli && npm run typecheck
中文说明

本 PR 做了什么

  • ACP core settings 方法没有传入 cwd 时,改用当前 Config 的 target directory。
  • 增加 qwen/settings/getCoreqwen/settings/setCoreValue 的回归测试,覆盖省略 cwd 时走 config.getTargetDir() 的路径。

为什么需要

worktree 场景下,ACP daemon 的进程根目录可能不是当前工作区。继续 fallback 到 process.cwd() 会把 core settings 读写到错误的 .qwen/settings.json;使用 target directory 后,workspace 设置会落在当前 worktree。

修复 #8138

验证

  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts
  • cd packages/cli && npm run typecheck

zhangyu.34 added 2 commits August 24, 2026 17:30
Load ACP core settings from the active target directory when clients omit cwd so worktree sessions write workspace settings into the worktree instead of the daemon process root.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 38ccd72 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 38ccd72 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Gate check below.

Template — the three main sections are present (same shape as your recently merged #9888). The ## Risk & Scope and ## Linked Issues headings are missing, but both are covered in the prose ("Fixes #8138"), so this passes.

Problem — real, not theoretical. Linked issue #8138 is open, triaged (type/bug / priority/P2 / welcome-pr), and names the root cause: the qwen/settings/getCore / setCoreValue handlers resolve an omitted cwd to process.cwd(), which in a daemon-backed worktree session is the project root, not the active worktree — so workspace-scoped settings land in <projectRoot>/.qwen/settings.json instead of the worktree's own copy.

Direction — aligned. Workspace settings should follow the active workspace, the same way file operations are already isolated inside worktrees.

Size — 10 production lines (+6/−4 in acpAgent.ts) plus 41 lines of tests. Not a core-infrastructure path, so Stage 0 doesn't apply.

Approach — minimal, and consistent with the file: six sibling call sites already do loadSettings(config.getTargetDir()), and Config.relocateWorkingDirectory() is what keeps targetDir pinned to the session workspace (including worktrees) — so this picks the right source of truth, and an explicit cwd parameter still wins. One follow-up question, not for this PR: roughly eight sibling qwen/settings/* handlers in the same switch still carry the identical process.cwd() fallback and presumably the same latent problem.

Riskacp-integration is on this repo's revert-correlated path list, so review depth stays full and CI evidence is required before approval.

Moving on to code review. 🔍

中文说明

感谢贡献!准入检查如下。

模板 —— 三个主要小节齐全(和你刚合并的 #9888 结构一致)。缺少 ## Risk & Scope## Linked Issues 标题,但正文里都已覆盖("Fixes #8138"),予以通过。

问题 —— 真实存在,不是理论加固。关联 issue #8138 处于 open 状态、已分诊(type/bug / priority/P2 / welcome-pr),并给出了根因:qwen/settings/getCore / setCoreValue 在缺省 cwd 时 fallback 到 process.cwd(),而 daemon 支撑的 worktree 会话里进程根目录是项目根而非当前 worktree——于是 workspace 作用域的设置写进了 <项目根>/.qwen/settings.json,而不是 worktree 自己的那份。

方向 —— 对齐。workspace 设置理应跟随当前工作区,正如文件操作已经在 worktree 内做了隔离。

规模 —— 生产代码 10 行(acpAgent.ts +6/−4),测试 41 行。不是核心基础设施路径,Stage 0 不适用。

方案 —— 克制,且与本文件现有写法一致:同文件已有 6 处调用 loadSettings(config.getTargetDir()),而 Config.relocateWorkingDirectory() 正是把 targetDir 钉在会话工作区(含 worktree)上的机制——选对了真值来源;显式传入的 cwd 参数仍然优先。一个后续问题(不属于本 PR):同一个 switch 里还有约 8 个 qwen/settings/* 处理器带着同样的 process.cwd() fallback,大概率有同样的隐患。

风险 —— acp-integration 在本仓库的"易回滚路径"名单上,因此保持完整 review 深度,且批准前必须有 CI 证据。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 38ccd723c6e0b63f715ad1a55fab776b24a98d96 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal, written before reading the diff: swap the process.cwd() fallback for config.getTargetDir() in exactly the two handlers the issue names, keep an explicit cwd parameter authoritative, and add wiring regression tests. The diff does precisely that, and nothing else — no drive-by changes.

What I verified in the code:

  • requestedCwd is the raw params['cwd'] (undefined unless a string), computed once at the top of extMethodInternal. The two core-settings cases now fall back to this.config.getTargetDir(). Config.relocateWorkingDirectory() — called by the session-move handler further up in the same file — mutates targetDir in place to the session workspace, so in a worktree-backed session this resolves to the worktree while process.cwd() stays at the daemon root. The fix's semantics check out.
  • coreSettingsCwd is threaded consistently into both loadSettings(...) and buildCoreSettings(...). The latter also feeds ExtensionManager({ workspaceDir }), so workspace extensions resolve from the same corrected directory — consistent, not a side effect to worry about.
  • The pattern is established, not invented: six sibling call sites in this file already use loadSettings(config.getTargetDir()).
  • The tests pin the change: with getTargetDir mocked to /worktree/.qwen, omitting cwd must produce loadSettings('/worktree/.qwen'); reverting the fix makes those assertions fail. The shared mock config and bootCoreSettingsAgent both get a getTargetDir stub so the pre-existing handler tests keep running.

No critical findings. Non-blocking, carried from Stage 1: the ~eight sibling qwen/settings/* handlers in the same switch still on the process.cwd() fallback likely share the same latent problem — worth a follow-up issue rather than scope creep here.

Files changed (2 of 2 shown)
File What changed
packages/cli/src/acp-integration/acpAgent.ts omitted-cwd fallback switched from process.cwd() to config.getTargetDir() in the getCore and setCoreValue handlers
packages/cli/src/acp-integration/acpAgent.test.ts regression tests for both handlers, plus a getTargetDir stub on the shared mock config and the boot helper

Testing

What this pass carries: the PR's own CI checks, quoted below. Per triage rules I don't execute PR code, and there is no local tmux run here — the changed surface is the ACP qwen/settings/* ext methods (the editor-integration path), not the interactive TUI, and live execution of fork code belongs to the isolated sandboxed jobs, not the triage bot.

The unit suite and the Real daemon E2E job are still running; the windows/macos/integration jobs are skipped pending fork-CI approval, so the table below is a mid-flight snapshot. Not verified: end-to-end persistence in a real daemon + worktree ("the file actually lands in the worktree's .qwen/settings.json"). The unit tests pin the cwd-resolution wiring, and the persistence side is established loadSettings/Storage behavior used across this file, but nothing in this PR exercises a live daemon. Sandboxed verification would settle that: @qwen-code /verify — that a real worktree session writes workspace settings into the worktree's .qwen/settings.json instead of the project root. This is a sponsored run (author has read-only access): a maintainer-triggered @qwen-code /verify approves the head it was written against and runs with a pre-execution risk screen and a full workspace wipe — read the resulting report with the same skepticism as the fork's own CI logs.

Final CI results for 38ccd72 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The Qwen Triage Finalize workflow rewrites the table region above once CI settles.

中文说明

代码审查

先说我不看 diff 时的独立方案:恰好就是在 issue 点名的两个处理器里,把 process.cwd() fallback 换成 config.getTargetDir(),显式 cwd 参数保持优先,并补上接线回归测试。diff 与之一致,且没有任何顺手改动。

核实过的要点:

  • requestedCwd 是原始 params['cwd'](非字符串时为 undefined),在 extMethodInternal 顶部只算一次;两个 core settings 分支现在 fallback 到 this.config.getTargetDir()。同文件里会话切换处理器调用的 Config.relocateWorkingDirectory() 会把 targetDir 原地改为会话工作区——所以 worktree 会话下它解析到 worktree,而 process.cwd() 仍停在 daemon 根目录。修复语义成立。
  • coreSettingsCwd 一致地传入 loadSettings(...)buildCoreSettings(...);后者的 cwd 还会喂给 ExtensionManager({ workspaceDir }),workspace 扩展也从同一修正后的目录解析——一致,不是副作用。
  • 该模式是本文件既有写法:已有 6 处调用 loadSettings(config.getTargetDir())
  • 测试钉住了改动:getTargetDir mock 为 /worktree/.qwen 时,省略 cwd 必须得到 loadSettings('/worktree/.qwen');回退修复断言即失败。共享 mock 和 bootCoreSettingsAgent 都补了 getTargetDir,旧测试不受影响。

无阻断性问题。非阻断、承接 Stage 1:同一 switch 里约 8 个 qwen/settings/* 处理器仍带同样的 process.cwd() fallback,大概率同样中招——建议另开 issue,不在本 PR 扩scope。

测试

本次证据:PR 自身 CI 检查(下表)。按分诊规则不执行 PR 代码;也没有本地 tmux 实测——改动面是 ACP qwen/settings/* ext 方法(编辑器集成路径),不是交互式 TUI,fork 代码的实际运行由隔离的沙箱任务负责。

单测套件与 Real daemon E2E 仍在运行;windows/macos/集成测试因 fork CI 需审批而 skipped,下表是进行中的快照。未验证:真实 daemon + worktree 下的端到端落盘("文件确实写进 worktree 的 .qwen/settings.json")。单测钉住了 cwd 解析接线,落盘侧是本文件广泛使用的既有 loadSettings/Storage 行为,但本 PR 没有驱动真实 daemon。沙箱验证可以补齐:@qwen-code /verify —— 验证真实 worktree 会话把 workspace 设置写进 worktree 的 .qwen/settings.json 而非项目根。这是赞助运行(作者仅有只读权限):由维护者触发,运行前有风险筛查与工作区清理——请对其报告保持与 fork CI 日志同等的审慎。

CI 表格区域将由 Qwen Triage Finalize 在 CI 结束后原地更新。

Qwen Code · qwen3.8-max

Reviewed at 38ccd723c6e0b63f715ad1a55fab776b24a98d96 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — small, well-aimed fix that does exactly what the maintainer-triaged issue asks, matches the file's own established pattern, and ships tests that pin the wiring; the remaining reservations are the unfixed sibling handlers and an end-to-end daemon claim that rests on those wiring tests.

Reflection: my independent proposal and the diff are the same change — I found no simpler path, and no reason one exists. The problem is real and maintainer-accepted (#8138, welcome-pr), the implementation is 10 production lines with every edit load-bearing (both handlers need the fallback; both loadSettings and buildCoreSettings consume it), and there are no drive-by changes. In six months this reads as an obvious consistency fix, not something to curse. The volume note: the author runs a high-throughput stream of small PRs (20+ open this week); this one stands on its own merits — focused, tested, and scoped to the issue.

Two things keep this from a 5: roughly eight sibling qwen/settings/* handlers in the same switch still carry the identical process.cwd() fallback (the same latent bug likely applies to them — a follow-up issue is the right next step, not widening this PR), and the end-to-end claim ("a real worktree session writes into the worktree's .qwen/settings.json") is substantiated by unit tests pinning the cwd wiring plus established loadSettings persistence semantics, not by a live daemon run — @qwen-code /verify (maintainer-sponsored) is the lane for anyone who wants that proof before merge.

CI on the reviewed commit is still running (unit suite and Real daemon E2E), so approval is deferred until CI lands green on 38ccd723c6e0b63f715ad1a55fab776b24a98d96.

中文说明

置信度:4/5 —— 小而准的修复,完全对应维护者已分诊的 issue,与本文件既有模式一致,并附带钉住接线的测试;剩余保留意见是尚未修复的兄弟处理器,以及端到端 daemon 结论目前由接线测试承载。

复盘:我的独立方案与 diff 完全一致——没找到更简单的路径,也不认为存在。问题真实且已被维护者认可(#8138welcome-pr);实现仅 10 行生产代码,每一处改动都必要(两个处理器都需要该 fallback;loadSettingsbuildCoreSettings 都要消费它);无顺手改动。六个月后再看,这是一处显而易见的对齐修复。备注:该作者近期有大量小型 PR(本周 20+ 个 open),但本 PR 凭自身质量成立——聚焦、有测试、范围与 issue 一致。

没给到 5 分的两点:同一 switch 里约 8 个 qwen/settings/* 处理器仍带同样的 process.cwd() fallback(大概率同样中招——正确做法是另开 issue 跟进,而不是扩大本 PR);端到端结论("真实 worktree 会话写入 worktree 的 .qwen/settings.json")由钉住 cwd 接线的单测加既有的 loadSettings 落盘语义支撑,而非真实 daemon 运行——如需合并前拿到该证据,@qwen-code /verify(维护者赞助运行)是对应通道。

被审提交的 CI 仍在运行(单测套件与 Real daemon E2E),因此批准推迟到 CI 在该提交上全绿之后。

Qwen Code · qwen3.8-max

Reviewed at 38ccd723c6e0b63f715ad1a55fab776b24a98d96 · re-run with @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 — CI landed green after the review. ✅

}
case 'qwen/settings/getCore': {
const settings = loadSettings(cwd);
const coreSettingsCwd = requestedCwd || this.config.getTargetDir();

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.

[Critical] This fallback swap is observably a no-op in every reachable ACP topology, so the PR does not fix #8138. this.config is the bootstrap Config built from process.cwd() (gemini.tsx:853 → targetDir: cwd) and is never relocated: the only relocateWorkingDirectory call inside the agent mutates session.getConfig() (acpAgent.ts:9514 → 9718) with skipProcessChdir: true, sessions get fresh Configs via loadCliConfig, --worktree + --acp is rejected at startup, and nothing in the ACP process calls process.chdir. So this.config.getTargetDir() === process.cwd() for the agent process's whole life, and settings I/O still lands in <launch-dir>/.qwen/settings.json — the exact symptom the issue reports. The endorsement of getTargetDir() in the issue thread rested on a static-analysis-only claim that it tracks the worktree, which this trace disproves.

The new tests pass only because they stub getTargetDir to /worktree/.qwen — a value production never produces. A/B probe in a scratch tree (PR arm with production wiring vs reverted base):

PR arm : PROBE-RESULT process.cwd()=.../packages/cli getCoreCalls=[".../packages/cli"] redirectedToWorktree=false
BASE   : identical PROBE-RESULT; the two new tests flip to FAILED (expected spy called with '/worktree/.qwen')

Resolve the settings directory from the session that owns the request instead — e.g. thread sessionId through the qwen/settings/* methods and use session.getConfig().getTargetDir() (the object ACP worktree relocation actually mutates) — and add a test that simulates a relocated session config and shows getCore/setCoreValue actually follow it.

中文说明

这个 fallback 替换在所有可达的 ACP 拓扑中都没有可观察的行为变化(no-op),因此本 PR 并没有修复 #8138this.config 是 bootstrap Config,由 process.cwd() 构建(gemini.tsx:853 → targetDir: cwd),且从不被重定位:agent 内唯一的 relocateWorkingDirectory 调用只修改 session.getConfig()(acpAgent.ts:9514 → 9718,且 skipProcessChdir: true),session 通过 loadCliConfig 获得全新的 Config,--worktree + --acp 在启动时被拒绝,ACP 进程中没有任何地方调用 process.chdir。因此 this.config.getTargetDir() === process.cwd() 在整个 agent 进程生命周期内恒成立,settings 读写仍然落在 <启动目录>/.qwen/settings.json —— 正是 issue 报告的症状。issue 讨论中对 getTargetDir() 的认可基于"它会跟随 worktree"的纯静态分析结论,而上述代码追踪证明该结论不成立。

新增的两个测试之所以通过,仅仅是因为它们把 getTargetDir 打桩为 /worktree/.qwen —— 生产环境从不产生这个值。在 scratch tree 中做的 A/B 探针(PR 侧按生产接线 vs 回退 base):

PR arm : PROBE-RESULT process.cwd()=.../packages/cli getCoreCalls=[".../packages/cli"] redirectedToWorktree=false
BASE   : PROBE-RESULT 完全相同;两个新测试变为 FAILED(期望 spy 被以 '/worktree/.qwen' 调用)

建议改为从拥有该请求的 session 解析 settings 目录 —— 例如给 qwen/settings/* 方法透传 sessionId,使用 session.getConfig().getTargetDir()(ACP worktree 重定位实际修改的对象)—— 并新增一个模拟已重定位 session config 的测试,证明 getCore/setCoreValue 确实跟随它。

— qwen3.8-max via Qwen Code /review (v0.22.0)

const settings = makeCoreSettings();
const { agent, agentPromise } = await bootCoreSettingsAgent(settings);

await agent.extMethod('qwen/settings/getCore', {});

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.

[Suggestion] The requestedCwd || half of this fallback is pinned by zero tests: all 11 getCore/setCoreValue call sites in the suite omit cwd, and loadSettings is mocked to return the same object regardless of argument. The mutation requestedCwd || this.config.getTargetDir()this.config.getTargetDir() survives the entire settings suite (probe-verified: Tests 25 passed | 440 skipped). Since the desktop client always sends an explicit cwd, that regression would silently redirect desktop settings calls to the config target dir while the suite stays green. Add a variant per method in which the explicit cwd must win:

it('qwen/settings/getCore prefers explicit cwd over config target dir', async () => {
  mockConfig.getTargetDir = vi.fn().mockReturnValue('/worktree/project');
  const settings = makeCoreSettings();
  const { agent, agentPromise } = await bootCoreSettingsAgent(settings);

  await agent.extMethod('qwen/settings/getCore', { cwd: '/explicit/dir' });

  expect(loadSettings).toHaveBeenCalledWith('/explicit/dir');

  mockConnectionState.resolve();
  await agentPromise;
});
中文说明

这个 fallback 中 requestedCwd || 的一半没有任何测试覆盖:套件中全部 11 处 getCore/setCoreValue 调用都省略了 cwd,且 loadSettings 被 mock 为无论参数如何都返回同一对象。变异 requestedCwd || this.config.getTargetDir()this.config.getTargetDir() 在整个 settings 套件中存活(已通过探针验证:Tests 25 passed | 440 skipped)。由于 desktop 客户端总是显式传入 cwd,这种回归会在套件全绿的情况下悄悄把 desktop 的 settings 调用重定向到 config target dir。建议为每个方法补一个"显式 cwd 必须优先"的变体测试(代码见上)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

const settings = loadSettings(coreSettingsCwd);
this.settings = settings;
return this.buildCoreSettings(settings, cwd);
return this.buildCoreSettings(settings, coreSettingsCwd);

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.

[Suggestion] The diff changes four sites (loadSettings + buildCoreSettings in each handler), but these new tests assert only the loadSettings calls. buildCoreSettings feeds its second argument to new ExtensionManager({ workspaceDir: cwd, ... }), and the module-level ExtensionManager mock ignores constructor arguments while mockExtensionManagerState.extensions resets to [] — so the mutation buildCoreSettings(settings, coreSettingsCwd)buildCoreSettings(settings, cwd) compiles (cwd is still in scope from extMethodInternal) and survives all tests (probe-verified). A partial revert would load settings from the worktree while resolving workspace extensions against process.cwd(), undetected. Assert the second argument as well:

expect(vi.mocked(ExtensionManager)).toHaveBeenCalledWith(
  expect.objectContaining({ workspaceDir: targetDir }),
);
中文说明

本次改动修改了四处(每个 handler 中的 loadSettings + buildCoreSettings),但这些新测试只断言了 loadSettings 的调用。buildCoreSettings 会把第二个参数传给 new ExtensionManager({ workspaceDir: cwd, ... }),而模块级 ExtensionManager mock 忽略构造参数,mockExtensionManagerState.extensions 又被重置为 [] —— 因此变异 buildCoreSettings(settings, coreSettingsCwd)buildCoreSettings(settings, cwd) 可以编译(cwdextMethodInternal 作用域内仍然可见)且在所有测试中存活(已通过探针验证)。部分回退时会从 worktree 加载 settings、却按 process.cwd() 解析 workspace 扩展,且不会被察觉。建议同时断言第二个参数(代码见上)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Use the live session config when ACP core settings requests omit cwd, while preserving explicit cwd priority for desktop callers.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
@yiliang114

Copy link
Copy Markdown
Collaborator

Thanks for the fix! This overlaps with #8152, which was opened earlier (Jul 30) for the same issue #8138 and covers worktree-aware settings/context-file resolution. To keep the discussion in one place, I'll close this in favor of that one — thanks again for the effort, and you're very welcome to pick up any other unclaimed issues. 🙏

@yiliang114 yiliang114 closed this Aug 24, 2026
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: worktree settings.json writes to project root .qwen instead of worktree's .qwen

3 participants