Skip to content

refactor(cli): route every per-request runtime-root pin through runWithPinnedRuntimeBaseDir - #10988

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
tomsen02:refactor/session-mgmt-pinned-base-dir
Sep 4, 2026
Merged

refactor(cli): route every per-request runtime-root pin through runWithPinnedRuntimeBaseDir#10988
wenshao merged 5 commits into
QwenLM:mainfrom
tomsen02:refactor/session-mgmt-pinned-base-dir

Conversation

@tomsen02

@tomsen02 tomsen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Makes the agent's runtime-root pinning go through one documented seam instead of five hand-composed copies, and puts the "which settings pin this operation" decision inside that seam for per-request handlers. The existing private three-argument helper stays as the single place that composes the routing (every caller in the class goes through it; nothing else names the underlying context function). On top of it, a per-request variant takes only the request's cwd, resolves that cwd's settings itself, and hands them to the operation — so a handler serving a caller-supplied cwd has no settings argument it could fill with the process-wide cache. The six per-request sites (session listing, delete, rename, status transcript page, settled turn status, and the non-live branch of session loadUpdates) all use the variant; callers that hold deliberately scoped settings (workspace MCP discovery, live-session scope checks, session creation) keep the three-argument form. The helper bodies are one-line delegations, so runtime behavior is unchanged; the two wide call sites name their inline callbacks so the reroute does not re-indent two 60-line bodies.

Three tests pin this. A source-level test walks the file's TypeScript AST and asserts that the only mention of the underlying context function, other than the un-aliased import specifier, is the shared helper's own delegation — so comments, string literals and import formatting cannot false-positive, while an alias, an aliased import, or a direct call is reported with its line. Three behavioral tests cover the rerouted handlers that had none (settled turn status, status transcript page, non-live loadUpdates): each asserts the settings and cwd reaching the mocked context function are the request's, and the transcript test also checks the replay config built inside the pinned scope was seeded from the request's settings. The test describe block holding #10095's three regression tests is renamed to match what it actually covers.

Why it's needed

Follow-up to #10095, closing the two non-blocking items recorded there: the round-2 review's deferred D2-1 (the near-miss helper already existed, the fix hand-pasted the routing three more times) and the maintainer's nit that the three regression tests were reported under a renameSession routing describe. Hand-composed routing is exactly the shape that regressed in #10095 — each handler made its own "which settings pin this operation" decision and three of them made it wrong. Round 1 of this PR's own review showed that merely routing through a pass-through helper does not remove that decision from the call sites; the per-request variant does, and the mutation matrix below shows the seam is now load-bearing for every handler that uses it.

The choke-point invariant needs a source pin rather than a behavioral test because both spellings reach the same function. The pin follows the precedent in serve/fast-path.test.ts (TypeScript AST walk over a source file read with a package-relative path).

Reviewer Test Plan

How to verify

  1. grep -n 'runWithAcpRuntimeOutputDir' packages/cli/src/acp-integration/acpAgent.ts — outside the canonical import line and comments, expect exactly one hit: the shared helper's delegation. On main there are six call sites. grep -c 'runWithPinnedRuntimeBaseDirForRequest(' … — expect 6 call sites (the definition line carries a type parameter and does not match).
  2. cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts — 605/605 (601 existing + the source pin + three behavioral pins). fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095's three regression tests report under QwenAgent session-management routing (rename / delete / list / branch / close) > ….
  3. Mutation matrix, one mutant at a time (each restored before the next), whole file each run:
    • deleteSession reverted to the three-argument helper with this.settings → 1 failed / 603 passed: only resolves deleteSession settings per request; the source pin stays green (it is not the test that guards this).
    • sessionTranscript handed this.settings → 3 failed / 601: the new transcript pin plus the two existing replay-config tests.
    • sessionTurnStatus handed this.settings → 1 failed / 603: only the new turn-status pin (this handler had no coverage before).
    • Non-live loadUpdates handed this.settings → 1 failed / 604: only the new loadUpdates pin (on the round-1 head this mutant was invisible, 605/605).
    • Alias probe (const pin = runWithAcpRuntimeOutputDir; return pin(…)) inside the helper → 1 failed / 603: the source pin, whose message lists the alias line next to the delegation line.
    • Aliased import (import { runWithAcpRuntimeOutputDir as pinDirect }) used by the helper with this.settings → 7 failed / 598: the source pin lists the import line, and the six handler pins catch the settings half.
    • Benign shapes the pin must ignore (multi-line reformat of the canonical import; a trailing comment or a string literal naming the function) → green each time, on the pin test alone.
    • The per-request helper itself reads this.settings → 7 failed / 597: all five per-request handler pins plus the two replay-config tests. This is the row that shows the seam is load-bearing.
  4. git diff -w --stat main equals git diff --stat main (+272 / −16 over the two files): no whitespace-only churn hides in the diff.

Evidence (Before & After)

N/A — refactor with no runtime behavior change. Evidence is the grep count in step 1 (six direct call sites → one) and the mutation matrix in step 3.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local unit tests only (vitest, tsc --noEmit on packages/cli with zero diagnostics under acp-integration/*, eslint and prettier --check on both files clean). Core and acp-bridge dist rebuilt from this branch's merge-base before running.

Risk & Scope

  • Main risk or tradeoff: none at runtime — both helpers are one-line delegations to the same function the sites called directly, and the per-request variant calls the same loadSettingsCached(cwd) the sites called themselves. The source-pin test parses the file with the TypeScript compiler API already used by fast-path.test.ts; if a second legitimate direct mention is ever introduced, the failure names its line and the assertion is a one-line update.
  • Not validated / out of scope: wenshao's other observation on fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 (nothing in the agent enforces that a directly-spawned qwen --acp client's cwd matches the workspace it asks about) is a separate topic and not touched here. Session load / resume keep resolving the request's settings at the call site on purpose (profiler-instrumented, and the settings are adopted for the session afterwards); they and the other three-argument callers are unchanged.
  • Breaking changes / migration notes: none. No public API, protocol, or settings change.

Linked Issues

Relates to #10095 (closes its deferred round-2 item D2-1 and the maintainer's describe-block nit; no standalone issue was filed since both findings live in that PR's review thread). Same follow-through shape as #10465 for #9930.

中文说明

本 PR 做了什么

把 agent 的运行时根目录钉定收拢到一个有注释的入口,取代五处手写的复制,并把"用哪份 settings 钉这次操作"的决定放进这个入口(针对按请求处理器)。原有的私有三参数辅助方法仍然是唯一组合路由的地方(类内所有调用方都经过它,此外没有任何地方直接引用底层的 context 函数)。在它之上新增一个按请求变体:只接收请求的 cwd,自己解析该 cwd 的 settings 并交给操作回调,这样服务于调用方指定 cwd 的处理器就没有可以被进程级缓存填错的 settings 参数。六处按请求调用点(会话列表、删除、重命名、status transcript 分页、settled turn status、以及 session loadUpdates 的非 live 分支)全部改用该变体;持有明确作用域 settings 的调用方(workspace MCP 发现、live session 作用域检查、会话创建)保留三参数形式。两个辅助方法的本体都是一行委托,运行时行为不变;两处较宽的调用点给内联回调起了名字,避免重排两段 60 行函数体。

三类测试钉住这些。一条源码级测试遍历该文件的 TypeScript AST,断言除未加别名的 import 说明符外,对底层 context 函数的唯一引用是共享辅助方法自己的委托——因此注释、字符串字面量和 import 的排版不会误报,而别名、别名 import 或直接调用都会连同行号被报出。三条行为测试覆盖此前没有覆盖的改道处理器(settled turn status、status transcript 分页、非 live 的 loadUpdates):各自断言到达被 mock 的 context 函数的 settings 与 cwd 是本次请求的;transcript 测试还检查在钉定作用域内构建的 replay config 是由请求的 settings 生成的。承载 #10095 三条回归测试的 describe 块改名为它实际覆盖的内容。

为什么需要

#10095 的跟进,关闭那里记录的两条非阻塞项:第 2 轮评审延后的 D2-1(近似的辅助方法已存在,修复却又手工粘贴了三遍路由),以及维护者指出三条回归测试被报在 renameSession routing 这个 describe 下。手写路由正是 #10095 里出问题的形态——每个处理器各自决定"用哪份 settings 钉这次操作",其中三个决定错了。本 PR 自己的第 1 轮评审表明:仅仅经过一个透传的辅助方法并不能把这个决定从调用点移走;按请求变体做到了,下面的变异矩阵显示这个入口对每个使用它的处理器都是承重的。

唯一入口这个不变量需要源码级钉住而不是行为测试,因为两种写法最终到达同一个函数。做法沿用 serve/fast-path.test.ts 的先例(用包内相对路径读源码后遍历 TypeScript AST)。

审阅者验证方案

如何验证

  1. grep -n 'runWithAcpRuntimeOutputDir' packages/cli/src/acp-integration/acpAgent.ts——除规范 import 行和注释外应恰好命中一处:共享辅助方法的委托。main 上有六处调用。grep -c 'runWithPinnedRuntimeBaseDirForRequest(' …——应为 6 处调用(定义行带类型参数,不匹配)。
  2. cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts——605/605(601 条原有 + 源码钉 + 三条行为钉)。fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 的三条回归测试报在 QwenAgent session-management routing (rename / delete / list / branch / close) > … 下。
  3. 逐点变异(每次恢复后再做下一个,每次跑整个文件):
    • **deleteSession 还原为三参数形式并传 this.settings**→1 失败 / 603 通过:只有 resolves deleteSession settings per request;源码钉保持绿色(它不是守这一点的测试)。
    • **sessionTranscriptthis.settings**→3 失败 / 601:新 transcript 钉加两条已有的 replay-config 测试。
    • **sessionTurnStatusthis.settings**→1 失败 / 603:只有新的 turn-status 钉(该处理器此前没有覆盖)。
    • **非 live 的 loadUpdatesthis.settings**→1 失败 / 604:只有新的 loadUpdates 钉(在第 1 轮的 head 上这个变异不可见,605/605)。
    • **在辅助方法里加别名探针(const pin = runWithAcpRuntimeOutputDir; return pin(…))**→1 失败 / 603:源码钉,报错信息把别名行与委托行并列列出。
    • **别名 import(import { runWithAcpRuntimeOutputDir as pinDirect })并在辅助方法里用它传 this.settings**→7 失败 / 598:源码钉列出该 import 行,六个处理器的钉抓住 settings 那一半。
    • 钉必须忽略的良性形状(规范 import 被拆成多行;行尾注释或字符串字面量里出现该函数名)→单跑钉测试,每次都绿。
    • **按请求辅助方法自己读 this.settings**→7 失败 / 597:五个按请求处理器的钉全部失败,加两条 replay-config 测试。这一行说明入口是承重的。
  4. git diff -w --stat maingit diff --stat main 一致(两个文件 +272 / −16):diff 里没有藏着纯空白改动。

前后对比证据

N/A——无运行时行为变化的重构。证据是第 1 步的 grep 计数(六处直接调用→一处)和第 3 步的变异矩阵。

测试平台

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

仅本地单元测试(vitestpackages/clitsc --noEmitacp-integration/* 下零报错;两个文件的 eslintprettier --check 干净)。运行前从本分支的 merge-base 重建了 core 与 acp-bridge 的 dist

风险与范围

  • 主要风险或权衡:运行时无——两个辅助方法都是对各调用点原本直接调用的同一函数的一行委托,按请求变体调用的也是各调用点原本自己调用的 loadSettingsCached(cwd)。源码钉测试用的是 fast-path.test.ts 已在用的 TypeScript 编译器 API 解析文件;若将来引入第二处合理的直接引用,失败信息会指出行号,改一行断言即可。
  • 未验证 / 超出范围:wenshao 在 fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 里的另一个观察(agent 里没有任何东西强制直接 spawn 的 qwen --acp 客户端的 cwd 与被查询的 workspace 一致)是独立话题,本 PR 不涉及。session load / resume 有意保留在调用点解析请求的 settings(有 profiler 埋点,且解析出的 settings 随后被会话采用);它们和其余三参数调用方均未改动。
  • 破坏性变更 / 迁移说明:无。不涉及公共 API、协议或 settings 变更。

关联 Issue

关联 #10095(关闭其第 2 轮延后项 D2-1 与维护者的 describe 块小建议;两条发现都在该 PR 的评审线程里,未另开 issue)。与 #10465 之于 #9930 是同一种跟进形态。

…thPinnedRuntimeBaseDir

Follow-up to QwenLM#10095 (review item D2-1 + maintainer nit). QwenAgent already
had a private runWithPinnedRuntimeBaseDir helper, but five handlers
composed the loadSettingsCached -> runWithAcpRuntimeOutputDir routing by
hand instead: unstable_listSessions, deleteSession, renameSession (the
three QwenLM#10095 fixed) plus the older qwen/status/session/transcript and
sessionTurnStatus readers. Hand-composed routing is exactly the shape
that picked up the stale this.settings cache in QwenLM#10095, so make the
helper the single choke point and document why it exists.

Pure delegation swap: the helper's body is
runWithAcpRuntimeOutputDir(settings, cwd, operation), so no runtime
behavior changes. The two wide sites name their inline callbacks
(readTranscriptPage / readSettledTurnResult) so the reroute does not
re-indent 60-line bodies.

Because the two spellings reach the same function, no behavioral test
can distinguish them; a source-level test pins the invariant instead
(the only remaining runWithAcpRuntimeOutputDir call is the helper's own
delegation), following the cli.test.ts source-pin precedent. Mutation
check, one site at a time: re-adding a direct call is killed only by
the new pin; routing this.settings through the helper at deleteSession
is killed only by the QwenLM#10095 deleteSession test; making the helper
itself use this.settings is killed by exactly the three QwenLM#10095 tests.

Also rename the describe block that holds those three tests -- it was
named "extMethod renameSession routing" but covers rename / delete /
list / branch / close, so a listSessions failure reported under
"renameSession routing" (wenshao's nit on QwenLM#10095).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGftREDUQDei396TrMLQHA
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up!

Template looks good ✓ — every section filled in, N/A used correctly for Before/After on a no-behavior-change refactor, and the paragraphs are single long lines rather than hard-wrapped.

Problem: this is not a bug fix, so "no reproduction" is the right answer here rather than a gap. More importantly the motivation checks out, and it checks out better than the description claims. I read back through #10095: it merged on 2026-09-03, and wenshao recorded both items this PR closes, in writing, on that thread — D2-1 ("folding the hand-composed loadSettingsCachedrunWithAcpRuntimeOutputDir routing into the pinned-base-dir helper … as a follow-up is the right call — I would not want it in this diff") and the describe-block nit ("two of them report as renameSession routing > resolves unstable_listSessions settings per request… … it will mislead the next person grepping a failure"). So this is sanctioned follow-up work on an observed bug class, not speculative hygiene. That distinction is the whole reason this passes the gate.

Direction: aligned. #10465 is a real merged precedent of the same shape (closing deferred items recorded in #9930's review rounds), so this is an established pattern here rather than a one-off. Nothing touches auth, model selection, telemetry, release, or a public contract — the helper is a pass-through to the same function these five sites already called, so route ownership and failure semantics are unchanged.

Size: not applicable for the core-module gate. packages/cli/src/acp-integration/** is not one of the protected core paths and this stays inside a single package. For the record: 33 production lines (26+/7− in acpAgent.ts) and 22 test lines (21+/1− in acpAgent.test.ts) — far under any threshold.

Approach: scope feels right and genuinely minimal — five call-site swaps, one doc comment explaining why the choke point exists, one invariant test, one describe rename. No drive-by edits and no formatting churn hiding in it. I also checked the obvious alternative before reading the diff: enforcing this with the repo's existing no-restricted-syntax selectors (the same mechanism that bans require()) instead of a test that reads its own source. It does not work here — flat-config overrides are per-file, and the helper's own legitimate delegation lives in the same file as the calls you want to ban, so a selector would forbid the one call that must remain. The source pin is the pragmatic choice, and the cli.test.ts precedent for reading source in a test is real. One spelling detail in the new test I would change — that is in the code review comment, not a gate concern.

Risk: Stage 1e matched. packages/cli/src/acp-integration/acpAgent.ts is on the high-risk path list from this repo's revert-history analysis (10 of 31 reverted PRs touched these paths vs 5 of 60 controls, p = 0.006). That is not a blocker and I am not reading anything into it for a 33-line delegation swap — it just means this gets full review depth and needs the PR's own CI evidence before anyone approves. Note also that the two checks which matter most for this diff, the ubuntu unit-test job and Lint & Static, are still running as of this pass. The author has read-only access, so the @qwen-code /tmux lane is unavailable; @qwen-code /verify is available as a sponsored run if a maintainer wants the invariant independently pinned.

Moving on to code review. 🔍

中文说明

感谢这个跟进 PR!

模板完整 ✓——各节都填了,无行为变化的重构在 Before/After 下正确写了 N/A,段落也是单行长文本而非硬换行。

问题: 这不是修 bug,所以"没有复现"在这里是正确答案,而不是缺口。更重要的是动机经得起核对,而且比 PR 描述里说的更硬。我回看了 #10095:它已于 2026-09-03 合并,wenshao 在那个线程里白纸黑字记录了本 PR 关闭的两条项——D2-1("把手写的 loadSettingsCachedrunWithAcpRuntimeOutputDir 路由折进 pinned-base-dir helper……留作后续是对的,我也不希望它进这个 diff")以及 describe 块的小建议("其中两个会以 renameSession routing > resolves unstable_listSessions settings per request… 的形式出现……会误导下一个 grep 失败信息的人")。所以这是针对已观测 bug 类别、经维护者认可的后续工作,不是投机性的代码整洁。这个区别正是它能过闸的原因。

方向: 对齐。#10465 是同一形态的真实已合并先例(关闭 #9930 评审轮次记录的延后项),说明这在本仓库是既有做法而不是一次性动作。不涉及 auth、模型选择、telemetry、发布或公共契约——辅助方法只是转发到这五个调用点原本就调用的同一个函数,路由归属与失败语义都没变。

规模: 核心模块闸门不适用。packages/cli/src/acp-integration/** 不属于受保护的核心路径,且改动只在单个 package 内。记录一下:生产代码 33 行(acpAgent.ts +26/−7),测试 22 行(acpAgent.test.ts +21/−1)——远低于任何阈值。

方案: 范围合理且确实最小——五处调用点替换、一段说明该入口为何存在的注释、一条不变量测试、一次 describe 改名。没有夹带无关改动,也没有藏着纯格式化改动。我在看 diff 之前也先核对了最明显的替代方案:用仓库已有的 no-restricted-syntax 选择器(就是禁 require() 的那套机制)来钉这个不变量,而不是写一个读自己源码的测试。这里行不通——flat config 的 override 是按文件粒度的,而辅助方法自己那次合法委托与要禁的调用在同一个文件里,选择器会把唯一必须保留的那次调用也禁掉。所以源码钉是务实选择,cli.test.ts 里"测试读源码"的先例也确实存在。新测试里有一处写法我会改——那放在代码审查评论里说,不属于闸门问题。

风险: Stage 1e 命中。packages/cli/src/acp-integration/acpAgent.ts 在本仓库 revert 历史分析的高风险路径清单上(31 个被 revert 的 PR 里有 10 个动了这些路径,对照组 60 个里只有 5 个,p = 0.006)。这不是阻塞项,对一个 33 行的委托替换我也不做过多解读——它只意味着本 PR 要走完整审查深度,并且在任何人 approve 之前需要 PR 自己的 CI 证据。另外注意:对这个 diff 最关键的两个检查——ubuntu 单元测试 job 与 Lint & Static——在本轮审查时仍在运行。作者只有读权限,所以 @qwen-code /tmux 通道不可用;如果维护者想独立钉住这个不变量,@qwen-code /verify 可以作为 sponsored run 使用。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4a372bb3901ad638e215164b61e566e4c88e2a29 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

I read the title and the "Why it's needed" section first and wrote down what I would have done before opening the diff: find every direct call to the context function, check which settings each one passes, route all of them through the existing private helper, then pin the invariant somehow. That is what this PR does, so we agree on the shape. The one place I would have gone differently — an ESLint no-restricted-syntax selector instead of a source-reading test — turns out not to work here, which I'll come back to.

The delegation swap is behavior-preserving. I checked this against main rather than taking the description's word for it. runWithPinnedRuntimeBaseDir<T>(settings, cwd, operation) is a one-line pass-through to runWithAcpRuntimeOutputDir(settings, cwd, operation) with an identical signature, so all five rerouted sites hand the same three values to the same function. this binds correctly at every one of them — each sits inside a class method, and the delete, rename, transcript and settled-turn scopes already dereference this.config, this.sessions or this.sessionOrThrow in the same scope. The two extracted callbacks (readTranscriptPage, readSettledTurnResult) are arrow consts declared in the same lexical scope as the inline arrows they replace, so this capture and evaluation order are unchanged and nothing executes at declaration. Naming them instead of re-indenting two 60-line bodies was the right call for reviewability.

The choke-point claim holds repo-wide, not just in this file. No production file outside acpAgent.ts imports or calls runWithAcpRuntimeOutputDir — the only other references are the context module's own definition and test mocks. Direct-call count goes 6 → 1, and the survivor is the helper's delegation, exactly as described. One self-inflicted trap avoided here worth naming: the new doc comment mentions the function without a trailing paren, so it doesn't trip the new test's own \brunWithAcpRuntimeOutputDir\( regex. That was easy to get wrong.

The existing #10095 pins really do survive the reroute. Those three regression tests mock ./runtimeOutputDirContext.js at module level, so they observe the same mock through the helper as they did through the direct call — the assertions on mock.calls[0][0] / [0][1] are unaffected.

The describe rename is accurate, not just cosmetic. The block spans lines 20206–21705 and contains rename (20525), delete (20666) and list (20741) tests plus branch tests (20871–21437) and close tests (21437–21621). So "session-management routing (rename / delete / list / branch / close)" describes what is actually in there, which is what wenshao asked for on #10095.

On the enforcement mechanism — I looked for the more idiomatic route before accepting the source pin. The repo already bans specific call expressions through generalRestrictedSyntaxSelectors in eslint.config.js (that's how require() is forbidden), so a lint selector is normally the right tool. It can't express this invariant: flat-config overrides are per-file, and the helper's own legitimate delegation lives in the same file as the calls you want to ban, so a selector would forbid the one call that must remain. Moving the helper out of the class would fix that, but it doesn't use this and relocating it is a larger change than the one under review. The source pin is the pragmatic choice and cli.test.ts / serve/fast-path.test.ts are real precedent for reading source in a test.

One thing I'd change before merge

The new test reads its source with readFileSync(new URL('./acpAgent.ts', import.meta.url), 'utf8'). That spelling appears nowhere else in this repo's tests. The house pattern for source-text assertions in packages/cli is a plain relative path resolved against vitest's package-root cwd — readFileSync('src/cli.ts', 'utf8') at cli.test.ts 1151/1173/1188/1195/1734, and readFileSync('src/serve/fast-path.ts', 'utf8') at fast-path.test.ts 397/406/419/439/454. Nine existing call sites, all the same shape.

That matters because cli.test.ts:1160 carries an explicit warning about the alternative: "Under vitest Vite rewrites new URL(…, import.meta.url) to a non-file URL." If that rewrite reaches a test file's own source, readFileSync is handed a non-file URL and the pin throws instead of asserting — which would make it a broken test rather than a weak one. I am not reporting that as a defect: I haven't verified it fails, the author reports 602/602 locally including mutation kills attributed to this pin, and the ubuntu unit job that would settle it is still running. But readFileSync('src/acp-integration/acpAgent.ts', 'utf8') is a one-line change that matches nine neighbours and deletes the question entirely. Cheap either way.

Nits (non-blocking)

  • expect(directCalls).toHaveLength(1) fails with an array of N identical strings and no line number, and the regex scans comments as well as code — so a future doc comment that writes the call with a paren breaks the pin with a message that doesn't say where. The description says "the test says so explicitly"; as written it doesn't. A short assertion message would make the failure self-explanatory.
  • The test file now imports node:fs twice — a value import for readFileSync alongside the existing import type { Stats }. That's normal under verbatimModuleSyntax and Lint & Static will confirm it; flagging only because that job hasn't finished.
sequenceDiagram
    participant P1 as session handlers 5 sites
    participant P2 as runWithPinnedRuntimeBaseDir
    participant P3 as runWithAcpRuntimeOutputDir
    participant P4 as Storage.runWithRuntimeBaseDir
    P1->>P2: pin this request runtime root
    P2->>P3: pass through unchanged
    P3->>P4: runtimeOutputDir plus cwd
    P4-->>P1: operation result
Loading

The single path above is the point of the PR: on main five of those six callers skipped the middle step and reached the context function directly, each making its own "which settings pin this operation" decision — three of them wrongly, which is #10095.

Test evidence

This was an unattended CI run, so I did not build or execute anything from this PR — the gate is static and the evidence below is this PR's own CI, read through the API for commit 4a372bb3901ad638e215164b61e566e4c88e2a29 (36 checks fetched once, no polling).

Check Conclusion
Test (ubuntu-latest, Node 22.x) in_progress
Lint & Static (ubuntu-latest, Node 22.x) in_progress
Integration Tests (no-AK, No Sandbox) success
Dependency CVE audit failure — pre-existing flake, see below
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
TUI parity snapshots (ink vs opentui) success
OpenTUI no-flicker gate success
Real daemon E2E / Java 11 success
Secret scan (TruffleHog) success
SDK Java matrix (ubuntu 11/17/21, windows 21, macos 21) success
precheck-pr / precheck success
Test (macos-latest, Node 22.x) skipped
Test (windows-latest, Node 22.x) skipped
Integration Tests (CLI, No Sandbox) skipped
review-pr, triage in_progress — bot orchestration, not PR CI
verify, tmux-testing, publish-verify, publish-tmux, publish-resolution, resolve-pr, review-config, ack-review-request skipped — maintainer-triggered lanes

The two checks that matter most for this diff are still running. Test (ubuntu-latest, Node 22.x) is the only job that executes acpAgent.test.ts — macOS and Windows unit jobs are skipped on this PR — so it is the sole oracle for whether the new source pin runs and passes, and for whether the five rerouted sites still satisfy #10095's three regression tests. Lint & Static (ubuntu-latest, Node 22.x) is the oracle for the duplicate node:fs import. Neither had a conclusion at fetch time; the finalize job rewrites the table above once they land.

The one red check is not this PR's. Dependency CVE audit failed, and I'd classify it as pre-existing infra noise on three independent grounds:

  1. The diff touches two .ts files and no dependency manifest — there is no mechanism by which it could introduce a CVE.
  2. The job's own output reports found 0 vulnerabilities (twice) immediately before ##[error]Process completed with exit code 1. The audit found nothing; the step exited nonzero anyway.
  3. The Security Checks workflow flapped across unrelated branches in the same half hour: failures at 03:01 (feat/output-style-files), 03:03 (feat/dws-message-prefix), 03:08 (codex/fix-opentui-live-slash-submit-10) and 03:15 (this PR), while feat/channel-background-agent-delivery (03:09) and opentui-live-slash-submit (03:07) succeeded. feat/dws-message-prefix succeeded at 02:57 and failed at 03:03 with no dependency change in between.

I'm classifying that from the diff and the check identity, not from anything the log asserts about itself.

Not verified: that the new source-pin test executes and passes, and that the rerouted sites keep #10095's three regression tests green. Both rest on the author's local macOS run (602/602, plus a mutation matrix), which is the author's claim and not evidence I re-ran — this gate never executes PR code. The ubuntu unit job settles it, and it is still in flight.

Sandboxed verification would close that gap now rather than waiting: @qwen-code /verify — specifically that the new pin actually fails when a direct runWithAcpRuntimeOutputDir( call is re-added at one of the five sites, which is the load-bearing claim of the whole PR and is not observable from the diff. The author has read-only access, so this would be a sponsored run: a maintainer's @qwen-code /verify comment approves the head it was written against, and that run carries a pre-execution risk screen plus a full workspace wipe before any PR code runs. Read the resulting report with the same skepticism as the fork's own CI logs — the code under verification is adversarial input, and a crafted PR can shape what a report says even though the sandbox bounds what it can do. @qwen-code /tmux is not available here (it executes the author's code and gates on write access), and there is no TUI surface in this diff anyway.

Real-scenario testing: N/A — unattended CI run, and a pure delegation refactor with no user-visible behavior to drive.

中文说明

代码审查

我先只读了标题和"为什么需要",在看 diff 之前写下自己会怎么做:找出所有对 context 函数的直接调用,确认每处传的 settings,全部改道经过已有的私有辅助方法,再想办法钉住这个不变量。这个 PR 做的正是这件事,所以整体形态我认同。唯一我会走不同路的地方——用 ESLint 的 no-restricted-syntax 选择器代替读源码的测试——在这里行不通,下面会说。

委托替换确实保持行为不变。 这一点我是对着 main 核的,没有只信描述。runWithPinnedRuntimeBaseDir<T>(settings, cwd, operation) 就是对 runWithAcpRuntimeOutputDir(settings, cwd, operation) 的一行转发,签名完全一致,所以五处改道的调用点交给同一个函数的是同样的三个值。每一处的 this 绑定都正确——五处都在类方法内部,且 delete、rename、transcript、settled-turn 这几个作用域本身就在同一层解引用 this.configthis.sessionsthis.sessionOrThrow。两个被提出来的回调(readTranscriptPagereadSettledTurnResult)是与原内联箭头函数同一词法作用域里的箭头 const,所以 this 捕获和求值顺序都没变,声明时也不会执行任何代码。给它们起名而不是重排两段 60 行函数体,对可审查性是对的选择。

"唯一入口"这个说法在整个仓库成立,不只是这一个文件。 acpAgent.ts 之外没有任何生产文件 import 或调用 runWithAcpRuntimeOutputDir——其余引用只有 context 模块自身的定义和测试 mock。直接调用数从 6 降到 1,剩下的那一处正是辅助方法自己的委托,与描述一致。这里有个容易自己踩的坑值得点出来:新加的注释提到了这个函数但没有跟左括号,所以不会触发新测试自己的 \brunWithAcpRuntimeOutputDir\( 正则。这一点很容易写错。

#10095 原有的三条钉确实不受改道影响。 那三条回归测试在模块级 mock 了 ./runtimeOutputDirContext.js,所以经辅助方法看到的仍是同一个 mock,对 mock.calls[0][0] / [0][1] 的断言不变。

describe 改名是准确的,不只是外观。 该块跨 20206–21705 行,里面有 rename(20525)、delete(20666)、list(20741),也有 branch(20871–21437)和 close(21437–21621)的测试。所以"session-management routing (rename / delete / list / branch / close)"描述的是里面真实存在的内容,这正是 wenshao 在 #10095 上提的要求。

关于约束机制——我在接受源码钉之前先找了更地道的路子。仓库已经通过 eslint.config.js 里的 generalRestrictedSyntaxSelectors 禁用特定调用表达式(require() 就是这么禁的),所以 lint 选择器通常是正确工具。但它表达不了这个不变量:flat config 的 override 是按文件粒度的,而辅助方法自己那次合法委托与要禁的调用在同一个文件里,选择器会把唯一必须保留的那次也禁掉。把辅助方法搬出类可以解决,但它并不使用 this,搬动它比本次改动更大。所以源码钉是务实选择,cli.test.tsserve/fast-path.test.ts 里"测试读源码"的先例也确实存在。

合并前我会改的一处

新测试用 readFileSync(new URL('./acpAgent.ts', import.meta.url), 'utf8') 读源码。这种写法在本仓库的测试里没有任何其他先例packages/cli 里源码文本断言的既有写法是相对于 vitest 的 package 根 cwd 的普通相对路径——cli.test.ts 的 1151/1173/1188/1195/1734 行用 readFileSync('src/cli.ts', 'utf8')fast-path.test.ts 的 397/406/419/439/454 行用 readFileSync('src/serve/fast-path.ts', 'utf8')。九处现存调用,形态一致。

之所以要紧,是因为 cli.test.ts:1160 对另一种写法有明确警告:"Under vitest Vite rewrites new URL(…, import.meta.url) to a non-file URL." 如果这个改写也作用到测试文件自身的源码,readFileSync 拿到的就是非 file URL,那么这条钉会抛异常而不是断言失败——那它就是一个坏掉的测试,而不只是一个弱测试。我没有把这当成缺陷上报:我并未验证它会失败,作者报告本地 602/602 通过并给出了归因于这条钉的变异结果,而能定论的 ubuntu 单元测试 job 仍在运行。但 readFileSync('src/acp-integration/acpAgent.ts', 'utf8') 是一行改动,与九个邻居一致,并且把这个问题彻底消掉。两种情况下成本都很低。

小问题(非阻塞)

  • expect(directCalls).toHaveLength(1) 失败时输出的是 N 个完全相同的字符串、没有行号,而且正则同时扫注释和代码——所以将来某个注释里带上左括号写了这个调用,钉会断,而报错信息不会告诉你断在哪。描述里说"测试会明确报出来",按现在的写法并没有。加一句断言消息就能让失败自解释。
  • 测试文件现在 import 了两次 node:fs——为 readFileSync 加的值导入与原有的 import type { Stats } 并存。在 verbatimModuleSyntax 下这是正常的,Lint & Static 会确认;之所以提一句,是因为那个 job 还没跑完。

上面那张时序图就是这个 PR 的意义所在:在 main 上,六个调用方里有五个跳过中间那步、直接到达 context 函数,各自决定"用哪份 settings 钉这次操作"——其中三个决定错了,那就是 #10095

测试证据

本轮是无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何代码——闸门是静态的,下面的证据来自 PR 自己的 CI,通过 API 读取 commit 4a372bb3901ad638e215164b61e566e4c88e2a29 的结果(一次性抓取 36 个检查,不轮询)。检查表在上方英文部分,用机器可读标记包裹,CI 落定后由 finalize 任务原地更新。

对这个 diff 最关键的两个检查仍在运行。 Test (ubuntu-latest, Node 22.x) 是唯一会执行 acpAgent.test.ts 的 job——本 PR 的 macOS 与 Windows 单元测试 job 都是 skipped——所以它是"新源码钉能否运行并通过"以及"五处改道后 #10095 三条回归测试是否仍绿"的唯一裁判。Lint & Static (ubuntu-latest, Node 22.x) 是重复 node:fs 导入的裁判。抓取时两者都还没有结论。

唯一的红检查不是本 PR 造成的。 Dependency CVE audit 失败了,我判定它是既有的基础设施噪声,有三条独立依据:一、diff 只动了两个 .ts 文件,没有动任何依赖清单,不存在引入 CVE 的途径;二、该 job 自己的输出在 ##[error]Process completed with exit code 1. 之前连续两次报告 found 0 vulnerabilities——审计没查出任何东西,是步骤本身以非零退出;三、同一半小时内 Security Checks 工作流在多个无关分支上抖动:03:01(feat/output-style-files)、03:03(feat/dws-message-prefix)、03:08(codex/fix-opentui-live-slash-submit-10)、03:15(本 PR)失败,而 03:09 的 feat/channel-background-agent-delivery 与 03:07 的 opentui-live-slash-submit 成功;feat/dws-message-prefix 更是在 02:57 成功、03:03 失败,中间没有任何依赖变更。这个判定依据的是 diff 与检查本身的身份,不是日志里对自己的说法。

未验证: 新源码钉是否真的执行并通过,以及改道后 #10095 的三条回归测试是否仍绿。这两点目前只有作者本地 macOS 的结果(602/602 加一份变异矩阵)支撑,那是作者的说法,不是我再跑一遍的证据——本闸门从不执行 PR 代码。ubuntu 单元测试 job 能定论,它还在跑。

沙箱验证可以现在就补上这个缺口而不必等待:@qwen-code /verify——具体要钉的是"在五个调用点之一重新加回一次直接的 runWithAcpRuntimeOutputDir( 调用时,新钉确实会失败",这是整个 PR 的承重主张,而且从 diff 上看不出来。作者只有读权限,所以这将是一次 sponsored run:维护者发一条 @qwen-code /verify 评论即批准其所针对的 head,该运行在执行任何 PR 代码之前还带有执行前风险筛查与完整工作区清除。读它产出的报告时,请保持与读 fork 自己 CI 日志同样的怀疑——被验证的代码是对抗性输入,精心构造的 PR 可以影响报告说什么,尽管沙箱限定了它能做什么。这里 @qwen-code /tmux 不可用(它执行作者代码并以写权限为门槛),而且本 diff 也没有 TUI 界面。

真实场景测试:N/A——无人值守 CI 运行,且本 PR 是纯委托重构,没有可驱动的用户可见行为。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4a372bb3901ad638e215164b61e566e4c88e2a29 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the refactor itself is clean and I would merge it; the 3 is the fork-refactor approval guardrail, not doubt about the code. Two small real reservations sit underneath it, named below.

Stepping back: this does the one thing worth doing after a bug like #10095. The original defect was three handlers each independently deciding "which settings pin this operation" and getting it wrong — the shape of the code was the bug's enabling condition. Collapsing six direct calls into one documented choke point removes that condition rather than patching its symptoms, and the doc comment tells the next person why the helper exists instead of leaving them to rediscover it from a git blame. If I picked this file up in six months I would thank whoever wrote this, which is the test I actually care about.

It also matches what I would have done independently, which is not nothing: I wrote my proposal down before reading the diff, and the only place I diverged — enforcing the invariant with an ESLint selector instead of a source-reading test — does not survive contact with the flat config, because the helper's own legitimate delegation lives in the same file as the calls a selector would ban. The author chose the workable option and cited real precedent for it. Scope is minimal, there is no drive-by churn, and every line serves the stated goal.

On the "does this problem exist" question that normally sinks refactors: it does, and I checked rather than accepted the framing. #10095 merged on 2026-09-03, and wenshao recorded both items this PR closes on that thread — the deferred D2-1 fold-into-helper item ("I would not want it in this diff") and the describe-block nit ("it will mislead the next person grepping a failure"). This is sanctioned follow-up work on an observed bug, not speculative tidying. On the volume question: the author has exactly one open PR and seven merged ones, so this is not a spray of plausible-looking diffs to wear a reviewer down — I'm judging it on its merits and it holds up.

Why I'm not approving. This is a cross-repository PR whose title is a refactor type, which trips a hard guardrail in this gate: fork refactors are never auto-approved, however clean the review looks, because they restructure shared code from an untrusted committer and need a human's eye. That is policy, not a finding — I found no correctness bug, no security issue and no regression, and I verified the behavior-preservation claim against main rather than trusting the description. So this is a defer, not a request for changes. I have also left no deferred-approval instruction for the finalize job, for the same reason: the guardrail blocks a deferred approval exactly as it blocks an immediate one, and the ubuntu unit job was still running regardless.

The two reservations a maintainer should weigh:

  1. The new source pin's file-read spelling is unproven in CI, and the one job that can prove it hadn't finished. It reads readFileSync(new URL('./acpAgent.ts', import.meta.url), 'utf8') — a pattern with no other instance in this repo's tests, where nine existing source-text assertions all use a plain readFileSync('src/…', 'utf8'). cli.test.ts:1160 explicitly warns that Vite rewrites new URL(…, import.meta.url) to a non-file URL under vitest. If that reaches the test file's own source, the pin throws instead of asserting — a broken test rather than a weak one. The author's local macOS run says otherwise, and I'm not calling it a defect because I haven't seen it fail. Switching to the house spelling is one line and ends the argument. Test (ubuntu-latest, Node 22.x) is the oracle; it was in progress at review time.
  2. The pin's failure message won't help whoever trips it. toHaveLength(1) on a regex over the whole file — comments included — reports N identical strings with no location, while the description promises the test "says so explicitly". Minor, and cheap to fix with an assertion message.

On the escalation itself: the guardrail wants this in a named maintainer's hands, and I could not resolve one deterministically. $QWEN_MAINTAINER_HANDLE is not set in this run; the owner map in .github/issue-owners.json matches areas by label only (matchArea never reads paths for label matching) and this PR carries no labels, so no area matched; and the fallback — the most recent human reviewer — is empty because nobody has reviewed it yet. Rather than guess a login and ping someone the resolver declined to name, I'm leaving this unassigned and unmentioned. A maintainer picking this up can add an area label or set the handle, and a re-run with @qwen-code /triage will then resolve and assign an owner. For context on who is closest to this work: wenshao recorded both items on #10095 and approved it.

If a maintainer wants the load-bearing claim pinned before deciding rather than after, @qwen-code /verify would do it as a sponsored run — details and the caveat about reading a fork's verification report are in the Stage 2 comment.

⏸️ Deferring to a maintainer — clean refactor, blocked only by the fork-refactor auto-approval guardrail, with the source-pin spelling above as the one thing I'd want settled before merge. Needs a human call on this one.

中文说明

Confidence: 3/5——重构本身是干净的,我会合并它;这个 3 分来自 fork refactor 的审批护栏,而不是对代码的怀疑。底下有两条小的真实保留意见,列在下面。

退一步看:这个 PR 做的正是 #10095 那类 bug 之后唯一值得做的事。原缺陷是三个处理器各自独立决定"用哪份 settings 钉这次操作"并且都决定错了——代码的形态本身就是 bug 的成因。把六处直接调用收拢成一个带注释的唯一入口,是消除这个成因,而不是补症状;注释也告诉下一个人这个辅助方法为何存在,而不必靠 git blame 重新发现。如果六个月后我接手这个文件,我会感谢写这段的人——这才是我真正在意的检验。

它也与我独立想到的做法一致,这点并非无关紧要:我在看 diff 之前先写下了自己的方案,唯一分歧处——用 ESLint 选择器而不是读源码的测试来钉不变量——在 flat config 面前站不住,因为辅助方法自己那次合法委托与选择器要禁的调用在同一个文件里。作者选了可行的那条路,并且引用了真实先例。范围最小,没有夹带无关改动,每一行都服务于既定目标。

关于通常会让重构沉掉的"这个问题真的存在吗":存在,而且我是去核过的,没有接受现成叙事。#10095 已于 2026-09-03 合并,wenshao 在那个线程里记录了本 PR 关闭的两条项——延后的 D2-1 折进辅助方法("我也不希望它进这个 diff")和 describe 块的小建议("会误导下一个 grep 失败信息的人")。这是针对已观测 bug、经认可的后续工作,不是投机性整洁。关于数量的疑问:作者当前只有这一个 open PR,另有七个已合并,所以这不是一堆看着合理、意在磨软审查者的 diff——我是就其本身判断的,它站得住。

为什么我不 approve。 这是一个跨仓库(fork)PR,标题是 refactor 类型,触发了本闸门的硬护栏:fork 重构无论审查看起来多干净都不自动 approve,因为它以不可信提交者的身份改动共享结构,需要人的眼光。这是策略,不是发现——我没有找到正确性 bug、安全问题或回归,而且行为不变这一点我是对着 main 核实的,没有只信描述。所以这是延后(defer),不是要求修改(request changes)。我也没有为 finalize 任务留下任何延后审批指令,理由相同:护栏挡住延后审批与挡住立即审批是一样的,何况 ubuntu 单元测试 job 当时仍在运行。

维护者需要权衡的两条保留意见:

  1. 新源码钉的读文件写法在 CI 上尚未被证明,而唯一能证明它的那个 job 当时还没跑完。 它用 readFileSync(new URL('./acpAgent.ts', import.meta.url), 'utf8')——这种写法在本仓库测试里没有第二处,而现存九处源码文本断言全部使用普通的 readFileSync('src/…', 'utf8')cli.test.ts:1160 明确警告:在 vitest 下 Vite 会把 new URL(…, import.meta.url) 改写成非 file URL。如果这个改写也作用到测试文件自身的源码,这条钉就会抛异常而不是断言失败——那是坏掉的测试,而不只是弱测试。作者本地 macOS 的结果说明不是这样,我也没有把它定为缺陷,因为我并未见它失败。改成仓库既有写法只需一行,并且能终结这个争论。裁判是 Test (ubuntu-latest, Node 22.x),审查时它仍在进行。
  2. 这条钉失败时的报错信息帮不上踩到它的人。 toHaveLength(1) 作用于扫全文件(含注释)的正则,失败时输出 N 个完全相同的字符串且没有位置,而描述里承诺测试"会明确报出来"。很小,加一句断言消息就能解决。

关于上报本身: 护栏希望它落到一位具名维护者手里,而我无法确定性地解析出这个人。本轮 $QWEN_MAINTAINER_HANDLE 未设置;.github/issue-owners.json 的 area 只按 label 匹配(matchArea 在 label 匹配时不读 paths),而本 PR 没有任何 label,所以没有 area 命中;兜底路径——最近一位人类审查者——也是空的,因为还没有人审过。与其猜一个 login、去 ping 一个解析器拒绝点名的人,我选择不指派、不 mention。接手的维护者可以加一个 area label 或设置该 handle,之后用 @qwen-code /triage 重跑就能解析并指派负责人。作为"谁最贴近这块工作"的背景:两条项都是 wenshao 在 #10095 上记录的,并且他 approve 了那个 PR。

如果维护者想在决定之前而不是之后把承重主张钉住,@qwen-code /verify 可以作为 sponsored run 完成——细节以及"读 fork 验证报告应有的怀疑"在 Stage 2 评论里。

⏸️ 转交维护者——重构干净,只被 fork refactor 自动审批护栏挡住,另有上面那条源码钉写法是我希望合并前先落定的唯一一项。这一件需要人来拍板。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 4a372bb3901ad638e215164b61e566e4c88e2a29 · re-run with @qwen-code /triage

…e its failures (round-1)

Triage round 1 on QwenLM#10988 asked for two things on the new source pin:

- Read the source with the spelling every other source-text assertion in
  packages/cli uses (readFileSync('src/...', 'utf8') against vitest's
  package-root cwd) instead of new URL(..., import.meta.url), which
  cli.test.ts warns Vite may rewrite to a non-file URL under vitest.
- Make the failure self-locating. The pin now scans line by line, skips
  comment lines, and puts "<line>: <text>" for every direct call into the
  assertion message, so re-adding a direct call reports e.g.
  "12099: const success = await runWithAcpRuntimeOutputDir(" next to the
  helper's own delegation line. A doc comment that spells the call with a
  paren no longer trips it.

The surviving call is asserted to be the helper's delegation body, which
subsumes the previous "helper still exists" check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGftREDUQDei396TrMLQHA
@tomsen02

tomsen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Round-1 items addressed in 51b54ca721 (test file only; production diff unchanged):

  • Source-read spellingreadFileSync('src/acp-integration/acpAgent.ts', 'utf8'), the same shape as the nine existing source-text assertions in cli.test.ts / fast-path.test.ts. (For the record the new URL form did work under vitest here — the local 602/602 and the mutation kill were real — but matching the house pattern ends the question, agreed.)

  • Self-locating failure → the pin now scans line by line, skips comment lines, and puts every direct call as <line>: <text> into the assertion message. Re-running the "re-add a direct call at deleteSession" mutant now prints:

    AssertionError: acpAgent.ts must not call runWithAcpRuntimeOutputDir directly; route the operation through this.runWithPinnedRuntimeBaseDir (see #10095). Direct calls at:
    4572: return runWithAcpRuntimeOutputDir(settings, cwd, operation);
    12099: const success = await runWithAcpRuntimeOutputDir(
    

    The surviving call is additionally asserted to be the helper's delegation body (subsumes the old "helper still exists" check), and a doc comment that spells the call with a paren no longer trips the pin — verified with that mutant too (passes).

Re-verified on 51b54ca721: 602/602, tsc --noEmit zero diagnostics under acp-integration/*, eslint + prettier clean; the direct-call mutant is still killed by exactly this one test.

Agree with the CVE-audit classification: the job printed found 0 vulnerabilities and then exited 1 on audit endpoint returned an error / Service Unavailable, and #10963 / #10979 / #10987 went red in the same window with no dependency changes. This push will retrigger it; if the registry is still flaky a maintainer rerun is the only fix I can't do from a fork.

@wenshao this is the D2-1 + describe-rename follow-up you recorded on #10095; the gate deferred it to a human on the fork-refactor guardrail only. No rush — whenever convenient. 谢谢!

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): 601 passed — this review observed 28138 passed; 599 passed — this review observed 28138 passed.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan(非阻断):601 passed — this review observed 28138 passed; 599 passed — this review observed 28138 passed

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

* operation to the settings loaded for THAT request's cwd. Every handler
* that resolves session storage for a caller-supplied cwd goes through
* here rather than calling `runWithAcpRuntimeOutputDir` directly, so the
* "which settings pin this operation" decision lives in one place — the

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] R1-1: The new doc comment claims the "which settings pin this operation" decision "lives in one place", but runWithPinnedRuntimeBaseDir is a pure pass-through that still receives settings from every caller — that decision remains in the call sites, and the new source-pin test pins only the call shape, not the decision. If a future session-management handler is written as this.runWithPinnedRuntimeBaseDir(this.settings, cwd, ...), it compiles and keeps the new pin green, yet in a multi-workspace daemon whose this.settings cache holds another workspace's advanced.runtimeOutputDir it pins the wrong runtime root — unstable_listSessions returns an empty/foreign list, and deleteSession reports success:false for an existing session or deletes a stale same-id copy under the wrong root. The #10095 bug class regresses while the guard created to prevent it reports safe.

Witness:

scratch-tree probe: added probeStaleSettingsPin(cwd) { return this.runWithPinnedRuntimeBaseDir(this.settings, cwd, () => null); }
plus one direct-call probe. The pin test failed listing ONLY the direct call:
  Direct calls at:
  4572: return runWithAcpRuntimeOutputDir(settings, cwd, operation);
  4589: return runWithAcpRuntimeOutputDir(settings, cwd, operation);
(the stale-settings call appears nowhere in the list)
baseline on unmodified PR: Tests 1 passed | 601 skipped

Put the decision where the comment says it lives: add a per-request variant that calls loadSettingsCached(cwd) itself, and migrate the five per-request sites (acpAgent.ts:5786, 8926, 11877, 12099, 12150):

private runWithPinnedRuntimeBaseDirForRequest<T>(cwd: string, operation: () => T): T {
  return this.runWithPinnedRuntimeBaseDir(loadSettingsCached(cwd), cwd, operation);
}

Minimal fallback if the shape stays: rewrite the doc comment and the test comment to claim only what the shape guarantees — "the wrapper call lives in one place" — not that the settings decision does.

The settings parameter cannot be removed from the shared helper: createWorkspaceMcpDiscoveryConfig(settings) at acpAgent.ts:3887 is fed deliberately workspace-scoped settings (loadSettings(this.config.getTargetDir()), acpAgent.ts:3948) and assertLiveSessionScope at acpAgent.ts:4613 receives caller-scoped settings — the fix must be an added per-request variant, not a signature change. If you add the variant, please seed this.settings with a foreign workspace's settings, invoke one per-request pin for cwd, and assert the settings argument reaching the mocked runWithAcpRuntimeOutputDir equals loadSettingsCached(cwd) (mirroring acpAgent.test.ts:20682-20768) — reverting any converted handler to pass this.settings must turn that test red while the source pin stays green; please confirm by running that mutation.

中文说明

新增的 doc 注释声称 "which settings pin this operation"(用哪份 settings 钉住本次操作)的决定 "lives in one place"(只在一处做出),但 runWithPinnedRuntimeBaseDir 是一个纯透传,仍然从每个调用方接收 settings —— 这个决定实际上留在各调用点,新加的源码级 pin 测试也只钉住了调用形态,没钉住这个决定。如果将来某个会话管理处理器写成 this.runWithPinnedRuntimeBaseDir(this.settings, cwd, ...),它能编译、新 pin 测试也保持绿色,但在多 workspace 守护进程里(this.settings 缓存装着另一个 workspace 的 advanced.runtimeOutputDir)会钉错运行时根目录 —— unstable_listSessions 返回空/别的 workspace 的列表,deleteSession 对存在的会话报 success:false,或者删掉错误根目录下同 id 的陈旧副本。#10095 那类 bug 回归了,而为防止它而建的守卫却报告安全。

建议:把决定放到注释所说的那个位置 —— 新增一个自己调用 loadSettingsCached(cwd) 的按请求变体,并把五个按请求调用点(acpAgent.ts:5786、8926、11877、12099、12150)迁移过去。若保持现有形态的最小替代方案:改写 doc 注释和测试注释,只声称该形态真正保证的内容 —— "wrapper 调用集中在一处" —— 而不是 settings 的决定也集中了。

settings 参数不能从共享辅助方法里移除:acpAgent.ts:3887 的 createWorkspaceMcpDiscoveryConfig(settings) 传入的是刻意的 workspace 级 settings(loadSettings(this.config.getTargetDir()),acpAgent.ts:3948),acpAgent.ts:4613 的 assertLiveSessionScope 接收调用方作用域的 settings —— 所以修复必须是新增按请求变体,而不是改签名。若新增变体,请用另一个 workspace 的 settings 塞进 this.settings,对 cwd 发起一次按请求 pin,并断言到达被 mock 的 runWithAcpRuntimeOutputDir 的 settings 参数等于 loadSettingsCached(cwd)(参照 acpAgent.test.ts:20682-20768)—— 把任一已转换的处理器还原为传 this.settings 必须让该测试变红,而源码 pin 保持绿色;请跑这个变异确认。

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

} as Record<string, unknown>;
});
};
return await this.runWithPinnedRuntimeBaseDir(

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] R1-2: Of the five handlers this PR re-routes, sessionTranscript (call at acpAgent.ts:8926) and sessionTurnStatus (call at acpAgent.ts:11877) have no behavioral test pinning which settings/cwd reach the runtime-root pin — every runWithAcpRuntimeOutputDir mock-call-argument assertion lives in the deleteSession / renameSession / unstable_listSessions tests, and the added source-level test pins call shape only. If a future edit resolves settings for either of these two handlers from this.settings instead of loadSettingsCached(cwd) — the exact stale-cache regression #10095 fixed — every test stays green, and in a multi-workspace daemon the transcript page / settled turn result is read under the wrong runtime root, returning an empty or foreign-workspace result for the request's cwd.

Witness:

sweep of acpAgent.test.ts — runWithAcpRuntimeOutputDir references:
  854 (module mock factory), 1084 (import),
  20682-20768 (mock.calls[0]![0]/[1] assertions — deleteSession ~20690,
  renameSession ~20730, unstable_listSessions ~20764),
  29920-29951 (the new pin test — pins call shape only)
pin-argument assertions for sessionTranscript/sessionTurnStatus: 0
pattern runs green: Tests 45 passed | 557 skipped

Add two behavioral tests mirroring the existing per-request pattern (acpAgent.test.ts:20667-20770): mock loadSettings to return per-request settings, invoke agent.extMethod(SERVE_STATUS_EXT_METHODS.sessionTranscript, { cwd: '/tmp/workspace-a', sessionId, ... }) and agent.extMethod(SERVE_CONTROL_EXT_METHODS.sessionTurnStatus, { cwd: '/tmp/workspace-a', ... }), then assert vi.mocked(runWithAcpRuntimeOutputDir).mock.calls[0]![0] is the per-request settings and mock.calls[0]![1] is '/tmp/workspace-a'. The new tests must go red if loadSettingsCached(cwd) in the sessionTranscript handler (acpAgent.ts:8871) or the sessionTurnStatus handler (acpAgent.ts:11802) is replaced with this.settings — please confirm by running that mutation.

中文说明

本 PR 改道的五个处理器中,sessionTranscript(调用在 acpAgent.ts:8926)和 sessionTurnStatus(调用在 acpAgent.ts:11877)没有行为测试钉住到达运行时根 pin 的 settings/cwd —— 所有对 runWithAcpRuntimeOutputDir mock 调用参数的断言都在 deleteSession / renameSession / unstable_listSessions 的测试里,而新增的源码级测试只钉调用形态。如果将来某次修改把这两个处理器之一的 settings 来源从 loadSettingsCached(cwd) 换成 this.settings —— 正是 #10095 修复的陈旧缓存回归 —— 所有测试都保持绿色,而在多 workspace 守护进程里,transcript 分页 / settled turn 结果会在错误的运行时根下读取,对该请求的 cwd 返回空或别的 workspace 的结果。

建议:仿照现有按请求模式(acpAgent.test.ts:20667-20770)补两条行为测试:mock loadSettings 返回按请求的 settings,调用 agent.extMethod(SERVE_STATUS_EXT_METHODS.sessionTranscript, { cwd: '/tmp/workspace-a', sessionId, ... })agent.extMethod(SERVE_CONTROL_EXT_METHODS.sessionTurnStatus, { cwd: '/tmp/workspace-a', ... }),然后断言 vi.mocked(runWithAcpRuntimeOutputDir).mock.calls[0]![0] 是按请求的 settings、mock.calls[0]![1]'/tmp/workspace-a'。若把 sessionTranscript 处理器(acpAgent.ts:8871)或 sessionTurnStatus 处理器(acpAgent.ts:11802)里的 loadSettingsCached(cwd) 换成 this.settings,新测试必须变红 —— 请跑这个变异确认。

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

.map((line, index) => ({ line: index + 1, text: line.trim() }))
.filter(
({ text }) =>
/\brunWithAcpRuntimeOutputDir\(/.test(text) &&

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] R1-3: The choke-point pin only matches the literal identifier immediately followed by ( on one line, so an aliased or indirect call to runWithAcpRuntimeOutputDir is invisible to it and the property it advertises — every per-request pin goes through runWithPinnedRuntimeBaseDir — is not enforced. A future handler that binds const pin = runWithAcpRuntimeOutputDir; and calls pin(this.settings, cwd, ...) resurrects the stale-cache bug class of #10095 (delete/rename/list of a caller-supplied cwd runs against another workspace's advanced.runtimeOutputDir) while this pin test stays green.

Witness:

scratch-tree mutations against the real vitest harness:
baseline (unmodified PR):            Tests 1 passed | 601 skipped
alias-only mutant:                   test stays GREEN (alias invisible to the regex)
alias + direct-call mutants:         red, listing only the direct call
                                     (the `const pin = ...` line appears nowhere)
strengthened regex (\b) + import skip: RED on alias mutant
                                     ("4580: const pin = runWithAcpRuntimeOutputDir;"),
                                     green on intact source

Match identifier occurrences rather than same-line calls only, so const pin = runWithAcpRuntimeOutputDir; itself becomes a flagged occurrence:

Suggested change
/\brunWithAcpRuntimeOutputDir\(/.test(text) &&
/\brunWithAcpRuntimeOutputDir\b/.test(text) &&
!text.startsWith('import ') &&

Loosening the regex makes the import line at acpAgent.ts:320 (import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';) newly match, so the import skip is part of the fix; the helper's delegation at acpAgent.ts:4572 remains the sole expected element. Please re-apply the alias probe (const pin = runWithAcpRuntimeOutputDir; return pin(...)) and confirm the strengthened test goes red naming that line, then remove the probe and confirm it is green again.

中文说明

这个 choke-point pin 只匹配同一行里紧跟 ( 的字面标识符,因此对 runWithAcpRuntimeOutputDir 的别名或间接调用是不可见的,它所宣称的性质 —— 每个按请求的 pin 都经过 runWithPinnedRuntimeBaseDir —— 实际上没有被强制。将来某个处理器若绑定 const pin = runWithAcpRuntimeOutputDir; 再调用 pin(this.settings, cwd, ...),会让 #10095 的陈旧缓存 bug 类复活(对调用方给定 cwd 的 delete/rename/list 会跑在另一个 workspace 的 advanced.runtimeOutputDir 下),而这个 pin 测试保持绿色。

建议:匹配标识符的出现而不是仅匹配同行调用,让 const pin = runWithAcpRuntimeOutputDir; 本身也成为被标记的出现。注意:放宽正则后,acpAgent.ts:320 的 import 行(import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';)会新匹配上,所以跳过 import 行是修复的一部分;辅助方法在 acpAgent.ts:4572 的委托仍是唯一期望的元素。请重新加上别名探针(const pin = runWithAcpRuntimeOutputDir; return pin(...))确认加强后的测试变红并指名该行,移除探针后确认恢复绿色。

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

…in (round-1 R1-1/R1-2/R1-3)

review-pr round 1 on QwenLM#10988 showed the shared helper still let a handler
hand it `this.settings`, so the doc comment's "decision lives in one
place" was not true and the source pin only checked call shape.

- R1-1: add runWithPinnedRuntimeBaseDirForRequest(cwd, operation): it
  loads the settings for the request's cwd itself and hands them to the
  operation, so a per-request handler has no settings parameter to get
  wrong. All five per-request sites (unstable_listSessions,
  deleteSession, renameSession, sessionTranscript, sessionTurnStatus)
  now use it; the three-argument helper stays for callers that hold
  deliberately scoped settings (workspace MCP discovery, live-session
  scope checks, session creation). Both doc comments now claim only
  what each helper guarantees.
- R1-2: add behavioral pins for the two handlers that had none:
  sessionTurnStatus and qwen/status/session/transcript each assert the
  settings/cwd reaching the mocked context function are the request's
  (the transcript test also checks the replay config was seeded from
  the request's settings, distinguished by outputLanguage).
- R1-3: the source pin matches identifier occurrences, not only
  same-line calls, and skips import lines, so an alias
  (`const pin = runWithAcpRuntimeOutputDir;`) is flagged with its line.

Mutation matrix, one mutant at a time, whole file each run (604 tests):
  deleteSession -> this.settings via 3-arg helper: 1 failed (its pin)
  sessionTranscript -> this.settings:              3 failed (new pin +
                                                   2 replay-config tests)
  sessionTurnStatus -> this.settings:              1 failed (new pin)
  alias probe in the helper:                       1 failed (source pin,
                                                   names the alias line)
  ForRequest helper itself uses this.settings:     7 failed (all five
                                                   handlers' pins + 2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGftREDUQDei396TrMLQHA
@tomsen02

tomsen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Round-1 review items (R1-1 / R1-2 / R1-3) addressed in 81df6e5b64. All three were right, and R1-1 in particular: the comment claimed more than the shape guaranteed.

R1-1 — the decision now lives where the comment says. Added runWithPinnedRuntimeBaseDirForRequest(cwd, operation): it loads the settings for the request's cwd itself and passes them to the operation, so a per-request handler has no settings argument to get wrong. All five per-request sites use it (unstable_listSessions, deleteSession, renameSession, sessionTranscript, sessionTurnStatus). The three-argument helper stays, as you noted it must, for callers holding deliberately scoped settings (createWorkspaceMcpDiscoveryConfig, assertLiveSessionScope, session creation); both doc comments now state only what each one guarantees. Your requested check — seed a foreign workspace's settings, pin for cwd, assert the settings reaching the mock equal loadSettingsCached(cwd) — is what the five handler tests do; the mutation you asked for is row 1 below.

R1-2 — the two uncovered handlers now have behavioral pins. sessionTurnStatus and qwen/status/session/transcript each assert mock.calls[0][0] is the request's settings and [0][1] is '/tmp/workspace-a', mirroring the existing three. The transcript test additionally seeds the request settings with a different outputLanguage and asserts the replay config built inside the pinned scope was created from those settings, since that is the one site that consumes the settings inside the operation. Rows 2–3 below are the mutations you asked to see.

R1-3 — the source pin matches identifier occurrences. Applied your regex (\b…\b + import skip). Row 4 is the alias probe.

Mutation matrix on 81df6e5b64, one mutant at a time, whole file each run (604 tests), all restored between runs:

# Mutant Result
1 deleteSession → 3-arg helper with this.settings 1 failed / 603 passed — only resolves deleteSession settings per request; source pin stays green
2 sessionTranscriptthis.settings 3 failed / 601 — the new transcript pin + disposes a pending transcript config… + coalesces concurrent transcript config…
3 sessionTurnStatusthis.settings 1 failed / 603 — only the new sessionTurnStatus pin (this handler had no coverage before, as you said)
4 const pin = runWithAcpRuntimeOutputDir; return pin(…) inside the helper 1 failed / 603 — source pin, message lists 4592: const pin = runWithAcpRuntimeOutputDir; next to the delegation line
5 …ForRequest helper itself reads this.settings 7 failed / 597 — all five per-request handler pins + the two replay-config tests

Row 5 is the one I'd point a reader at: the choke point is now load-bearing for every handler that goes through it, which is the property the first push only claimed.

Also re-verified on 81df6e5b64: 604/604 green, tsc --noEmit zero diagnostics under acp-integration/*, eslint + prettier clean. Production diff is still confined to acpAgent.ts; the PR body's "What this PR does" and "How to verify" are updated to describe the per-request variant.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

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

Comment on lines +4565 to +4567
* session creation) pass them in. Handlers that serve a caller-supplied cwd
* must not make that decision themselves — they use
* `runWithPinnedRuntimeBaseDirForRequest` below.

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] R1-1: (fix-induced) The replacement doc comment written by the R1-1 fix overclaims in the same shape R1-1 condemned. It says handlers serving a caller-supplied cwd "must not make that decision themselves — they use runWithPinnedRuntimeBaseDirForRequest", but the qwen/session/loadUpdates non-live branch (acpAgent.ts:12396-12405) still hand-composes loadSettingsCached(cwd) + runWithPinnedRuntimeBaseDir, and loadSession restore (5240-5244) / unstable_resumeSession (5629-5634) resolve settings at the call site under profiler.timeSync('settings_load') instrumentation. The decision the comment claims is made in one place is still made at three call sites, and neither the source-pin test nor any behavioral test covers those shapes. Behavior is correct today, but swapping loadSettingsCached(cwd) for this.settings at acpAgent.ts:12397 — the exact #10095 bug shape — compiles and keeps all 604 tests green, so the guard reports safe while the bug class regresses for a foreign-cwd loadUpdates; conversely, a maintainer enforcing the documented contract migrates load/resume into the variant and breaks the profiler instrumentation (5240, 5629) plus their downstream this.settings adoption.

Witness:

C1 mutant (acpAgent.ts:12397 const settings = this.settings;):
full acpAgent.test.ts run → "Test Files 1 passed (1) / Tests 604 passed (604)"
baseline unmodified: 604/604 — the bug-shape swap is invisible to every test.
Suggested change
* session creation) pass them in. Handlers that serve a caller-supplied cwd
* must not make that decision themselves they use
* `runWithPinnedRuntimeBaseDirForRequest` below.
* session creation) pass them in. Per-request session-management handlers
* (list, delete, rename, transcript page, settled turn status) must not make
* that decision themselves they use `runWithPinnedRuntimeBaseDirForRequest`
* below. (`qwen/session/loadUpdates`' non-live branch still composes at the
* call site; session load/resume resolve there deliberately, under profiler
* instrumentation, before adopting the settings.)

The fix must not disturb profiler.timeSync('settings_load', () => loadSettingsCached(params.cwd)) at acpAgent.ts:5240-5242 and 5629-5631 — deliberate instrumentation whose resolved settings are adopted downstream (this.settings = settings at 5258/5645). If consolidation is chosen instead of the comment rewrite, each new behavioral pin must go red when loadSettingsCached at acpAgent.ts:5241 / 5630 / 12397 is replaced with this.settings — please confirm by running those mutations.

中文说明

R1-1 修复写入的替代 doc 注释又以 R1-1 所批评的形态过度声称。它说服用调用方给定 cwd 的处理器"不得自己做这个决定——它们使用 runWithPinnedRuntimeBaseDirForRequest",但 qwen/session/loadUpdates 的非 live 分支(acpAgent.ts:12396-12405)仍然手写 loadSettingsCached(cwd) + runWithPinnedRuntimeBaseDir,且 loadSession 恢复(5240-5244)/ unstable_resumeSession(5629-5634)在 profiler.timeSync('settings_load') 埋点下于调用点解析 settings。注释声称只在一处做出的决定实际上仍在三个调用点做出,且源码级 pin 测试与行为测试都不覆盖这些形态。当前行为是正确的,但在 acpAgent.ts:12397 把 loadSettingsCached(cwd) 换成 this.settings——正是 #10095 的 bug 形态——能编译且 604 条测试全部保持绿色,即守卫报告安全而该 bug 类已在外部 cwd 的 loadUpdates 上回归;反过来,若维护者按文档契约强制把 load/resume 迁入该变体,会破坏 5240、5629 处的 profiler 埋点及其后续的 this.settings 承接。修复不得改动 acpAgent.ts:5240-5242 与 5629-5631 的 profiler.timeSync('settings_load', () => loadSettingsCached(params.cwd))——那是刻意的埋点,解析出的 settings 还会在下游被承接(5258/5645 的 this.settings = settings)。若选择收拢而不是改注释,每条新的行为钉必须在 acpAgent.ts:5241 / 5630 / 12397 的 loadSettingsCached 被替换为 this.settings 时变红——请跑这些变异确认。

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

Comment on lines +30062 to +30063
/\brunWithAcpRuntimeOutputDir\b/.test(text) &&
!text.startsWith('import ') &&

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] R2-1: The choke-point pin's !text.startsWith('import ') exclusion makes an aliased import invisible. With import { runWithAcpRuntimeOutputDir as pinDirect }, the import line is skipped by the exclusion and the alias call site names no identifier, so a direct pin via the alias re-ships the stale-this.settings bug class while the test — whose own comment claims to catch direct naming "as a call or via an alias" — stays green. A future change adding that import and calling pinDirect(this.settings, cwd, op) in a per-request handler keeps located equal to exactly the helper's delegation line, and the #10095 bug class re-ships through the one guard meant to catch it generically.

Witness:

intact filter + alias-import mutant (pinDirect(this.settings, '/tmp/probe-alias', () => 1))
  → "Tests 1 passed | 603 skipped"
canonical-only exclusion + same mutant
  → AssertionError "Direct calls at:
    321: import { runWithAcpRuntimeOutputDir as pinDirect } from './runtimeOutputDirContext.js';
    4575: return runWithAcpRuntimeOutputDir(settings, cwd, operation);"
Suggested change
/\brunWithAcpRuntimeOutputDir\b/.test(text) &&
!text.startsWith('import ') &&
/\brunWithAcpRuntimeOutputDir\b/.test(text) &&
!text.startsWith("import { runWithAcpRuntimeOutputDir } from") &&

The exclusion must keep matching the canonical import import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js'; at packages/cli/src/acp-integration/acpAgent.ts:320.

中文说明

choke-point pin 的 !text.startsWith('import ') 排除条件对别名 import 不可见。若写成 import { runWithAcpRuntimeOutputDir as pinDirect },import 行会被该排除条件跳过,而别名调用点不含该标识符,因此经由别名直接钉定会重新引入陈旧 this.settings 的 bug 类,而这个测试——其自身注释声称能捕获"直接调用或经由别名"的直接引用——仍保持绿色。将来若添加该 import 并在某个按请求处理器里调用 pinDirect(this.settings, cwd, op)located 仍恰好等于辅助方法的委托行,#10095 的 bug 类就会穿过这个本应通用地捕获它的守卫重新出现。注意:修复必须继续匹配 acpAgent.ts:320 的规范 import import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';

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

const located = directCalls.map(({ line, text }) => `${line}: ${text}`);
expect(
located,
`acpAgent.ts must not call runWithAcpRuntimeOutputDir directly; route the operation through this.runWithPinnedRuntimeBaseDir (see #10095). Direct calls at:\n${located.join('\n')}`,

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] R2-2: The source-pin test's failure message directs fixers to route through this.runWithPinnedRuntimeBaseDir — the variant whose caller still chooses the settings — instead of runWithPinnedRuntimeBaseDirForRequest, contradicting the base helper's own new doc comment (acpAgent.ts:4558-4567). A fixer following the message literally writes this.runWithPinnedRuntimeBaseDir(this.settings, cwd, op), which compiles and turns the pin green because the test only bans direct token mentions — and in a multi-workspace daemon whose this.settings cache holds another workspace's advanced.runtimeOutputDir the handler then pins the wrong runtime root (empty/foreign session list; deleteSession deleting a stale same-id copy under the wrong root): the exact #10095 bug class, shipped with a green guard.

Witness:

deleteSession mutant (this.runWithPinnedRuntimeBaseDir(this.settings, cwd, …) at acpAgent.ts:12116):
  ✓ QwenAgent runtime-root pinning choke point > routes every per-request runtime-root pin…
  × resolves deleteSession settings per request
    → AssertionError: expected { merged: { mcpServers: {} }, … } to be … // Object.is equality
    at acpAgent.test.ts:20812
Suggested change
`acpAgent.ts must not call runWithAcpRuntimeOutputDir directly; route the operation through this.runWithPinnedRuntimeBaseDir (see #10095). Direct calls at:\n${located.join('\n')}`,
`acpAgent.ts must not call runWithAcpRuntimeOutputDir directly. Handlers serving a caller-supplied cwd must route through this.runWithPinnedRuntimeBaseDirForRequest; only callers holding deliberately scoped settings may use this.runWithPinnedRuntimeBaseDir (see #10095). Direct calls at:\n${located.join('\n')}`,
中文说明

源码级 pin 测试的失败信息把修复者指向 this.runWithPinnedRuntimeBaseDir——仍由调用方选择 settings 的那个变体——而不是 runWithPinnedRuntimeBaseDirForRequest,与基础辅助方法自身的新 doc 注释(acpAgent.ts:4558-4567)相矛盾。照字面执行该信息的修复者会写出 this.runWithPinnedRuntimeBaseDir(this.settings, cwd, op):它能编译、能让 pin 变绿(因为测试只禁止直接出现该标识符)——而在 this.settings 缓存装着另一个 workspace 的 advanced.runtimeOutputDir 的多 workspace 守护进程里,该处理器会钉错运行时根目录(空/别的 workspace 的会话列表;deleteSession 删掉错误根目录下同 id 的陈旧副本):正是 #10095 的 bug 类,且在守卫全绿的情况下被放出。

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

…lper (round-2 R1-1/R2-1/R2-2)

review-pr round 2 on QwenLM#10988:

- R1-1 (fix-induced): the shared helper's comment claimed every
  caller-supplied-cwd handler already used the per-request variant, but
  qwen/session/loadUpdates' non-live branch still composed
  loadSettingsCached + the three-argument helper by hand, and session
  load/resume resolve settings at the call site on purpose. Migrate the
  loadUpdates branch (settings are not needed outside the pin), add a
  behavioral pin for it, and make the comment say exactly which handlers
  use the variant and why load/resume do not (profiler-instrumented
  resolution whose settings are adopted for the session afterwards).
- R2-1: the source pin skipped every import line, so an aliased import
  (`import { runWithAcpRuntimeOutputDir as pinDirect }`) was invisible.
  It now skips only the canonical import line.
- R2-2: the pin's failure message pointed fixers at the three-argument
  helper; it now names runWithPinnedRuntimeBaseDirForRequest for
  caller-supplied-cwd handlers.

Mutation matrix, one mutant at a time, whole file (605 tests):
  loadUpdates non-live -> this.settings:      1 failed (its new pin)
  aliased import + helper pins this.settings: 7 failed (source pin names
                                              line 321 + six handler pins)
  deleteSession -> this.settings:             1 failed (its pin), source
                                              pin green, message now
                                              names the per-request helper

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGftREDUQDei396TrMLQHA
@tomsen02

tomsen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Round-2 items addressed in e95b83c289.

R1-1 (fix-induced) — took the consolidation route for the one site that fits, and the comment-rewrite route for the two that don't. qwen/session/loadUpdates' non-live branch does not need the settings outside the pin, so it now uses runWithPinnedRuntimeBaseDirForRequest (six per-request sites in total), with a behavioral pin mirroring the other five. Session load / resume are left exactly as they are: their profiler.timeSync('settings_load', …) resolution is deliberate and the resolved settings are adopted for the session afterwards (this.settings = settings), so they are not the shape the variant is for. The shared helper's comment now lists precisely which handlers use the variant and states why load/resume resolve at the call site — no more "every caller-supplied cwd" claim.

R2-1 — the pin now skips only the canonical import line (import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';), so an aliased import is a flagged mention.

R2-2 — applied your message wording: fixers are pointed at runWithPinnedRuntimeBaseDirForRequest for caller-supplied-cwd handlers, and told the three-argument helper is only for callers holding deliberately scoped settings.

Mutation matrix on e95b83c289, one mutant at a time, whole file each run (605 tests), restored between runs:

# Mutant Result
1 loadUpdates non-live → 3-arg helper with this.settings (your C1 shape at the old 12397) 1 failed / 604 — only the new resolves non-live qwen/session/loadUpdates settings per request pin. On the previous head this was 605/605, as you showed.
2 import { runWithAcpRuntimeOutputDir as pinDirect } + helper pins through pinDirect(this.settings, …) 7 failed / 598 — source pin lists 321: import { runWithAcpRuntimeOutputDir as pinDirect } … next to the delegation line; the six handler pins catch the this.settings half
3 deleteSession → 3-arg helper with this.settings 1 failed / 604 — its behavioral pin; source pin stays green, and its message (when it does fire) now names the per-request helper

Re-verified on e95b83c289: 605/605, tsc --noEmit zero diagnostics under acp-integration/*, eslint + prettier clean, git diff -w --stat equals git diff --stat. PR body updated to six per-request sites and the two matrices.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): 603 passed — this review observed 28141 passed.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/acpAgent.test.ts:30107 — [probe] D3-1 the source-pin test's comment exclusion only handles whole-line comments — a trailing comment or string literal mentioning runWithAcpRuntimeOutputDir false-positives as …

Convergence: round 3 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/acpAgent.test.ts (findings in round 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan(非阻断):603 passed — this review observed 28141 passed

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 3 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/acpAgent.test.ts(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +30104 to +30106
!text.startsWith(
"import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';",
) &&

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] R2-1: (fix-induced) The R2-1 fix replaced the choke-point pin's !text.startsWith('import ') exclusion with an exact match against today's single-line canonical import. That closed the aliased-import blind spot, but made the exemption format-sensitive: any benign reformat of that import now trips the tripwire. A future change that adds a second specifier to the ./runtimeOutputDirContext.js import pushes the line past Prettier's 80-column width; Prettier splits it, the continuation line no longer starts with the exact exempted string, so the filter counts it and the test goes RED with the "must not name runWithAcpRuntimeOutputDir directly" message — actively accusing the change of re-introducing the #10095 routing regression when it merely reformatted an import. The failure is loud rather than silent, so this is maintenance tax, not a hole — but the tripwire's stated purpose is to fire only on the regression shape.

Witness:

PR tree (benign multi-line import, scratch probe):
× routes every per-request runtime-root pin through runWithPinnedRuntimeBaseDir
 Direct mentions at:
 321: runWithAcpRuntimeOutputDir,
 4581: return runWithAcpRuntimeOutputDir(settings, cwd, operation);: expected [ …(2) ] to deeply equal [ StringMatching{…} ]
RESET (unmodified tree, same test): Tests 1 passed | 604 skipped (605)

Exempt the import statement structurally instead of one exact spelling — additionally filter the multi-line continuation shapes, or strip the import statement before scanning. Alias detection stays intact: an aliased import plus renamed delegation still fails the expected-delegation assertion, and an aliased import with an unchanged delegation is a compile error. For example, after the exact-match line:

          !/^runWithAcpRuntimeOutputDir( as \w+)?,$/.test(text) &&
中文说明

[Suggestion] R2-1:(修复引入)R2-1 的修复把钉点测试的 !text.startsWith('import ') 排除换成了对当前单行规范 import 的精确匹配。这关闭了别名 import 的盲区,但让豁免变得依赖格式:对该 import 的任何无害重排现在都会触发这个绊线。将来若给 ./runtimeOutputDirContext.js 的 import 增加第二个符号,该行会超过 Prettier 的 80 列宽度而被拆成多行;续行不再以精确豁免串开头,过滤器把它计入,测试变红,报错信息指控该改动重新引入了 #10095 的路由回归——而它只是重排了一个 import。失败是显式的而非静默的,所以这是维护成本而不是漏洞——但该绊线的既定目的是只在回归形态上触发。

(Witness 见英文部分:探针实测显示多行 import 使测试变红,还原后恢复绿色。)

建议改为按结构豁免 import 语句,而不是只认一种精确拼写——额外过滤多行 import 的续行形态,或在扫描前把 import 语句剥掉。别名检测不受影响:别名 import 加上改名的委托仍会使期望的委托断言失败;别名 import 而委托未改名则是编译错误。示例见英文部分代码块。

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

…1 root cause)

review-pr rounds 2 and 3 kept finding false positives in the line-based
source pin (exact-match import exemption trips on a Prettier reformat;
whole-line comment exclusion misses trailing comments and string
literals). The root cause is scanning text lines, so scan the AST
instead, following the fast-path.test.ts precedent: every Identifier
named runWithAcpRuntimeOutputDir is a mention unless it is the name of
an un-aliased import specifier. Comments, string literals and import
formatting cannot false-positive; an aliased import keeps the identifier
under propertyName and is reported with its line.

Probe matrix on the pin alone (restored between runs):
  benign multi-line import reformat     green
  trailing comment naming the function  green
  string literal naming the function    green
  aliased import used by the helper     red, names the import line
  direct call at deleteSession          red, names the call line
  const pin = runWithAcpRuntimeOutputDir red, names the alias line

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGftREDUQDei396TrMLQHA
@tomsen02

tomsen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 (R2-1 fix-induced) and the deferred D3-1 addressed together in 2c4d313eca, by going after the root cause your convergence note pointed at: the pin scanned text lines, so every exemption was a heuristic with its own edge (exact-match import spelling, whole-line comments). It now walks the TypeScript AST, following the fast-path.test.ts precedent: every Identifier named runWithAcpRuntimeOutputDir is a mention unless it is the name of an un-aliased import specifier (propertyName unset). Comments, string literals and import formatting cannot false-positive by construction; an aliased import keeps the identifier under propertyName and is reported with its line. No production change in this push.

Probe matrix on 2c4d313eca (pin test alone, tree restored between runs):

Probe Result
Benign multi-line reformat of the canonical import (your R2-1 witness) green
Trailing // … runWithAcpRuntimeOutputDir(…) comment on a code line (D3-1) green
String literal 'runWithAcpRuntimeOutputDir(' (D3-1) green
import { runWithAcpRuntimeOutputDir as pinDirect } used by the helper red — 321: import { runWithAcpRuntimeOutputDir as pinDirect } …
Direct call at deleteSession red — 12120: const success = await runWithAcpRuntimeOutputDir(
const pin = runWithAcpRuntimeOutputDir; inside the helper red — 4596: const pin = runWithAcpRuntimeOutputDir;

Re-verified: 605/605, tsc --noEmit zero diagnostics under acp-integration/*, eslint + prettier clean. The only red check remains Dependency CVE audit, which is still the registry's audit endpoint returned an error after printing found 0 vulnerabilities.

@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 issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

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

@qqqys

qqqys commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

tmux E2E report — real qwen --acp agent at the head ci-bot approved, plus a mutant arm that proves the measurement is sensitive

I built this PR's head, drove a real ACP agent process through the converted handlers in tmux, and then re-ran the identical measurement against a deliberately broken build. No Critical found. From my side this is merge-ready. I am not approving in the same breath — per my own process an approve follows a prior report that already concluded mergeable, so that is owed next round.

Head verified 2c4d313eca40f27964eb362f1a221a91d6fa5765 — the head qwen-code-ci-bot APPROVED at 11:16:36Z, and the head CI is green on (27 success / 26 skipped / 0 failure / 0 pending / 0 cancelled across all 53 check-runs)
Merge base d4e3e4fc87
Build git archive 2c4d313eca extracted outside the repo → npm ci && npm run bundleBUNDLE_OK (dist 155 M)
Environment Linux 5.10.134-19.3.1.al8.x86_64 (Alibaba Cloud Linux 3), Node v24.18.1, tmux 2.7, isolated HOME=/tmp/r31_e2e/home
Model calls none possibleOPENAI_API_KEY is a dummy and OPENAI_BASE_URL=http://127.0.0.1:9/v1 (discard port, nothing listening). Every prompt fails locally; that failure is what makes the product write a genuine transcript
QWEN_RUNTIME_DIR deliberately unset. Storage.getRuntimeBaseDir() lets that env var override a non-pinned contextual dir, so setting it would have made the whole A/B vacuous
Base arm not rebuilt (disk budget). Behavioural equivalence to base is argued from the diff below instead

Build witness — the measurements are against this PR, not a stale tree

dist/chunks/acpAgent-2IT3RGWT.js (sha256 888440697d329f9187168cfa9b86de3f79db035d5aa0231786fbc480a2274bd3) contains the compiled new helper with its body intact:

runWithPinnedRuntimeBaseDirForRequest(cwd, operation) {
    const settings = loadSettingsCached(cwd);
    return this.runWithPinnedRuntimeBaseDir(
      settings, cwd, () => operation(settings)
    );
}
  • 6 call sites of the new helper in the shipped chunk (lines 18332, 20780, 23216, 23396, 23434, 23624) — exactly the six converted handlers.
  • runWithAcpRuntimeOutputDir has exactly ONE call site in the whole bundle (line 17342, the shared helper's own delegation; the other textual hits are the function definition, its __name registration and a doc comment). That independently confirms in production wiring the invariant the PR's new AST test asserts about the source.

Code review — five hypotheses measured dead, please don't re-raise them

  1. "A per-request handler was missed and still pins with the process-wide this.settings"DEAD. At head runWithAcpRuntimeOutputDir is called in exactly one place, inside runWithPinnedRuntimeBaseDir. Base had 5 direct call sites (5777, 8864, 11789, 12080, 12131); all 5 are converted (5811, 8950, 11899, 12120, 12170). The 5 remaining runWithPinnedRuntimeBaseDir callers all hold deliberately scoped settings, matching the new doc comment: createWorkspaceMcpDiscoveryConfig(settings) (3887), assertLiveSessionScope(config, settings, cwd) (4639), the non-live loadSession (5248) and resumeSession (5638) branches — both resolving loadSettingsCached(params.cwd) at the call site under profiler.timeSync('settings_load') and only then adopting it via this.settings = settings — and newSessionConfig(cwd, mcpServers, settings, …) (13417).
  2. "A converted operation no longer receives settings but silently resolves the name from an enclosing scope" — the exact fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 shape surviving the refactor, and it would compile → DEAD. Only one converted body references settings at all: the transcript page, at 8919 (this.getTranscriptReplayConfig(cwd, settings)), and that operation takes it as its parameter at 8897. The other five bodies reference no settings identifier (only comments).
  3. "The helper changes when/how settings are resolved"DEAD. loadSettingsCached(cwd) is evaluated before the pin is entered in both the old and the new shape, and runWithPinnedRuntimeBaseDir is a pure pass-through to runWithAcpRuntimeOutputDir. loadSettingsCached is a per-workspace LRU keyed by path.resolve(workspaceDir) with freshness fingerprints (settings-cache.ts:161), not a last-caller cache, so it really returns the request workspace's settings.
  4. "The new AST source-pin test's relative readFileSync('src/acp-integration/acpAgent.ts') breaks under a different cwd"not a defect. Established convention in this package (cli.test.ts:1151, cli.test.ts:1188, serve/fast-path.test.ts:397, …) and vitest runs per package.
  5. "Workspace settings could be trust-gated, making the pin untestable/vacuous"DEAD. workspaceSettingsActive = !opts.skipWorkspaceSettings && realWorkspaceDir !== realHomeDir (settings.ts:1089) — no folder-trust gate on advanced.runtimeOutputDir.

E2E design — a decoy makes "read rootB" distinguishable from "read rootA" by content

Two workspaces whose own settings point at different runtime roots:

wsA/.qwen/settings.json  {"advanced":{"runtimeOutputDir":"/tmp/r31_e2e/rootA"}}
wsB/.qwen/settings.json  {"advanced":{"runtimeOutputDir":"/tmp/r31_e2e/rootB"}}

Process 1 boots in wsB and creates a real session (sidB=9719936c-…); the product writes its own transcript to rootB/projects/-tmp-r31-e2e-wsB/chats/<sidB>.jsonl (1413 b). I then plant a decoy — a copy of that genuine transcript with its marker rewritten — at exactly the path a wrong-root pin would read:

rootA/projects/-tmp-r31-e2e-wsB/chats/<sidB>.jsonl   1418 b   "DECOY-UNDER-ROOTA"

(The project segment is sanitizeCwd(wsB) in both roots, so a pin carrying wsA's runtimeOutputDir while the SessionService is constructed with cwd=wsB lands precisely on the decoy.)

Process 2 boots in wsA and creates a real session there (sidA=c1a87e68-…, transcript written under rootA/projects/-tmp-r31-e2e-wsA/chats/), so this.settings genuinely holds wsA's settings — the multi-workspace shape the pin exists for. Process 2 then serves requests naming cwd=wsB. Every result is read back from raw disk bytes, not from handler-reported success.

Pristine head — all five reachable handlers resolve workspace B's root

# Handler (head line) Result Which root
M0 unstable_listSessions(cwd=wsA) — control sidA / title RECORD-FOR-A control marker rootA ✓
M1 unstable_listSessions(cwd=wsB) (5811) sidB / title RECORD-FOR-B e2e marker rootB
M2 qwen/status/session/transcript (8950) RECORD-FOR-B=true, DECOY=false rootB
M3 qwen/control/session/turn_status (11899) Session not found — see "not verified" unreachable
M4 qwen/session/loadUpdates non-live (12401) 1 update, RECORD-FOR-B=true, DECOY=false rootB
M5 renameSession non-live (12170) success:true; rootB 1413→1765 b, rename marker present; decoy byte- and mtime-identical (1418 b, 14:30:15.581Z) rootB only
M6 deleteSession (12120) success:true; rootB original ABSENT; decoy intact (1418 b, same mtime) rootB only
M7 unstable_listSessions(cwd=wsB) after delete sessions: [] rootB

Mutant arm — the same harness, one line changed, every measurement flips

I patched the shipped chunk to re-introduce the bug class (const settings = this.settings; instead of loadSettingsCached(cwd)), re-ran the whole flow from scratch, then restored the chunk and verified the sha256 matches the pristine value byte-for-byte.

# Pristine head Mutant (this.settings)
M0 control RECORD-FOR-A control marker RECORD-FOR-A control marker (unchanged, as a control should be)
M1 list cwd=wsB RECORD-FOR-B e2e marker DECOY-UNDER-ROOTA e2e marker
M2 transcript RECORD-FOR-B=true, DECOY=false RECORD-FOR-B=false, DECOY=true
M4 loadUpdates RECORD-FOR-B=true, DECOY=false RECORD-FOR-B=false, DECOY=true
M5 rename rootB 1413→1765 b, marker set; decoy untouched rootB untouched (1413 b, mtime unchanged); decoy 1418→1770 b, marker set
M6 delete rootB ABSENT; decoy intact rootB INTACT; decoy ABSENT

M6 is the one that matters most: the mutant still returns {"success":true} while the user's real session survives and a foreign workspace's file is destroyed. So the harness does not merely observe "non-empty output" — it discriminates the two behaviours on read, on write and on delete. The pristine head is on the correct side of every row.

The patch was also narrow, which is what makes the comparison fair: the mutant arm's own setup step (session creation for wsB, which goes through runWithPinnedRuntimeBaseDir with caller-scoped settings, not through the per-request helper) still wrote its transcript to rootB/projects/-tmp-r31-e2e-wsB/chats/ exactly as the pristine arm did. Only the six handlers under test changed behaviour.

What I did NOT verify (disclosed, not glossed)

  • turn_status (M3) is unverified and unverifiable in this shape. this.sessionOrThrow(sessionId) at 11824 runs before the pin at 11899, so a non-live session throws Session not found and the pinned block is never entered; both arms failed identically. Reaching it needs a session that is live in the same process and a request naming a different cwd, which this two-process design cannot produce. Its correctness here rests on the code review (hypotheses 1–3), not on measurement.
  • Base arm not rebuilt. Equivalence is argued from the diff: each converted site previously did loadSettingsCached(cwd)runWithAcpRuntimeOutputDir(settings, cwd, op), which is literally what the helper composes.
  • A harness bug of mine, which does not change any conclusion: for M2 I extracted the record count from value.updates ?? value.records, but the response key is events — hence the reported recordCount: 0 in both arms. The root attribution for M2 comes from the marker scan over the whole response body (RECORD-FOR-B vs DECOY-UNDER-ROOTA), which is unaffected.
  • Not exercised: sandbox/seatbelt relaunch paths, the archive-state variants of delete/rename, and concurrent requests from two workspaces.

Two observations that are NOT Criticals

I am not requesting changes on either; both are recorded so the merge decision is made with them visible.

  • (a) turn_status mixes a live session with a request-supplied cwd. Inside the pinned block it flushes the live session's recording (session.getConfig().getChatRecordingService()?.flush()) but builds new SessionTranscriptReader(cwd) from the request's cwd, and unlike loadSession/resumeSession it does not call assertLiveSessionScope. A client naming a different cwd for a live session would flush one workspace and read another. Pre-existing and untouched by this diff — base resolved the same cwd and the same loadSettingsCached(cwd) — so it is a question for a follow-up, not a blocker here.
  • (b) A foreign-cwd request's debug log lands under the boot workspace's root. After process 2 served cwd=wsB requests, rootA/debug/<sidB>.txt (432 b) appeared alongside rootB/debug/<sidB>.txt. Debug-log placement follows the process's ambient runtime root, not the per-request pin. Also not touched by this diff (which changes only the six call sites and adds two helpers); noted only so a reader who spots the file does not mistake it for a leak the pin was supposed to prevent.

Conclusion

Merge-ready from my side. The refactor is semantics-preserving at every one of the six converted sites, no per-request pin was missed, the choke point the new AST test asserts really holds in the shipped bundle, and on a kernel and Node version nobody else has reported here the handlers resolve the request workspace's runtime root for reads, writes and deletes — with a mutant arm proving the measurement can tell the difference. Observations (a) and (b) are accuracy and follow-up items, not blockers. This is my first report on this PR, so the approve follows next round rather than landing in the same breath.


tmux E2E 报告 —— 在 ci-bot 已 approve 的 head 上驱动真实 qwen --acp agent,并附带一个证明测量灵敏度的变异臂

我用本 PR 的 head 构建产物,在 tmux 中驱动真实的 ACP agent 进程穿过被改造的各个 handler,随后对同一个测量流程又跑了一个故意改坏的构建未发现 Critical。从我这边看可以合入。 我不会在同一口气里 approve —— 按我自己的流程,approve 要跟在一份已结论"可以合入"的既有报告之后,所以留到下一轮。

验证的 head 2c4d313eca40f27964eb362f1a221a91d6fa5765 —— 即 qwen-code-ci-bot 于 11:16:36Z APPROVED 的 head,也是 CI 全绿的 head(53 个 check-run 中 27 success / 26 skipped / 0 failure / 0 pending / 0 cancelled
Merge base d4e3e4fc87
构建 git archive 2c4d313eca 在仓库外解包 → npm ci && npm run bundleBUNDLE_OK(dist 155 M)
环境 Linux 5.10.134-19.3.1.al8.x86_64(Alibaba Cloud Linux 3)、Node v24.18.1、tmux 2.7、隔离 HOME=/tmp/r31_e2e/home
模型调用 不可能发生 —— OPENAI_API_KEY 是假值,OPENAI_BASE_URL=http://127.0.0.1:9/v1(discard 端口,无人监听)。每次 prompt 都在本地失败,而正是这个失败让产品写出了真实的 transcript
QWEN_RUNTIME_DIR 刻意不设置。 Storage.getRuntimeBaseDir() 允许该环境变量覆盖非 pinned 的上下文目录,一旦设置整个 A/B 就失去意义
Base 臂 未重新构建(磁盘预算)。与 base 的行为等价性改由下面的 diff 论证

构建见证 —— 测量对象确实是本 PR,不是旧树

dist/chunks/acpAgent-2IT3RGWT.js(sha256 888440697d329f9187168cfa9b86de3f79db035d5aa0231786fbc480a2274bd3)中包含编译后的新 helper,函数体完整:

runWithPinnedRuntimeBaseDirForRequest(cwd, operation) {
    const settings = loadSettingsCached(cwd);
    return this.runWithPinnedRuntimeBaseDir(
      settings, cwd, () => operation(settings)
    );
}
  • 出货 chunk 中新 helper 的调用点为 6 处(18332、20780、23216、23396、23434、23624),正好对应六个被改造的 handler。
  • 整个 bundle 中 runWithAcpRuntimeOutputDir 只有 1 个调用点(17342,即共享 helper 自己的委托;其余文本命中是函数定义、__name 注册和文档注释)。这在生产接线中独立证实了本 PR 新增 AST 测试对源码所断言的那条不变量。

代码审查 —— 五个假设已被测死,请勿重复提出

  1. "某个 per-request handler 被漏掉,仍用进程级 this.settings 去 pin"已死。 head 上 runWithAcpRuntimeOutputDir 只有一处调用,就在 runWithPinnedRuntimeBaseDir 内部。base 有 5 个直接调用点(5777、8864、11789、12080、12131),全部被改造(5811、8950、11899、12120、12170)。剩下 5 个 runWithPinnedRuntimeBaseDir 调用方都持有刻意限定作用域的 settings,与新文档注释一致:createWorkspaceMcpDiscoveryConfig(settings)(3887)、assertLiveSessionScope(config, settings, cwd)(4639)、非 live 的 loadSession(5248)与 resumeSession(5638)分支 —— 两者都在调用点于 profiler.timeSync('settings_load') 下解析 loadSettingsCached(params.cwd),之后才通过 this.settings = settings 采纳 —— 以及 newSessionConfig(cwd, mcpServers, settings, …)(13417)。
  2. "某个被改造的 operation 不再接收 settings,却从外层作用域静默解析到同名变量" —— 这正是 fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 的形态在重构后残留,而且能编译通过 → 已死。 只有一个被改造的函数体引用了 settings:transcript page 的 8919 行(this.getTranscriptReplayConfig(cwd, settings)),而它在 8897 行以参数形式接收。其余五个函数体完全没有引用 settings 标识符(只有注释)。
  3. "新 helper 改变了 settings 的解析时机或方式"已死。 新旧两种写法中 loadSettingsCached(cwd) 都在进入 pin 之前求值,且 runWithPinnedRuntimeBaseDir 是对 runWithAcpRuntimeOutputDir 的纯转发。loadSettingsCached 是以 path.resolve(workspaceDir) 为键、带新鲜度指纹的按工作区 LRU(settings-cache.ts:161),不是"最后一个调用者"缓存,因此确实返回请求工作区自己的 settings。
  4. "新增的 AST 源码钉测试用相对路径 readFileSync('src/acp-integration/acpAgent.ts'),换个 cwd 就会坏"不是缺陷。 这是本包既有惯例(cli.test.ts:1151cli.test.ts:1188serve/fast-path.test.ts:397 等),且 vitest 按包运行。
  5. "工作区 settings 可能被 trust 门禁挡住,导致 pin 不可测/无意义"已死。 workspaceSettingsActive = !opts.skipWorkspaceSettings && realWorkspaceDir !== realHomeDirsettings.ts:1089)—— advanced.runtimeOutputDir 上没有 folder-trust 门禁。

E2E 设计 —— 用诱饵让"读 rootB"与"读 rootA"在内容上可区分

两个工作区,各自的 settings 指向不同的运行时根:

wsA/.qwen/settings.json  {"advanced":{"runtimeOutputDir":"/tmp/r31_e2e/rootA"}}
wsB/.qwen/settings.json  {"advanced":{"runtimeOutputDir":"/tmp/r31_e2e/rootB"}}

进程 1 在 wsB 启动并创建真实会话(sidB=9719936c-…),产品自己把 transcript 写到 rootB/projects/-tmp-r31-e2e-wsB/chats/<sidB>.jsonl(1413 b)。随后我植入诱饵 —— 复制这份真实 transcript 并改写其标记 —— 放在"错误根 pin"恰好会读到的路径上:

rootA/projects/-tmp-r31-e2e-wsB/chats/<sidB>.jsonl   1418 b   "DECOY-UNDER-ROOTA"

(两个根下的项目段都是 sanitizeCwd(wsB),所以一个携带 wsA 的 runtimeOutputDir、却用 cwd=wsB 构造 SessionService 的 pin,会精确落在诱饵上。)

进程 2 在 wsA 启动并在当地创建真实会话(sidA=c1a87e68-…,transcript 写在 rootA/projects/-tmp-r31-e2e-wsA/chats/),因此 this.settings 确实持有 wsA 的 settings —— 正是 pin 为之存在的多工作区形态。随后进程 2 处理指名 cwd=wsB 的请求。所有结论都从磁盘原始字节读回,而不是采信 handler 自报的成功。

原始 head —— 五个可达的 handler 全部解析到工作区 B 的根

# Handler(head 行号) 结果 落在哪个根
M0 unstable_listSessions(cwd=wsA) —— 对照 sidA / 标题 RECORD-FOR-A control marker rootA ✓
M1 unstable_listSessions(cwd=wsB)(5811) sidB / 标题 RECORD-FOR-B e2e marker rootB
M2 qwen/status/session/transcript(8950) RECORD-FOR-B=true、DECOY=false rootB
M3 qwen/control/session/turn_status(11899) Session not found —— 见"未验证" 不可达
M4 qwen/session/loadUpdates 非 live(12401) 1 条 update,RECORD-FOR-B=true、DECOY=false rootB
M5 renameSession 非 live(12170) success:true;rootB 1413→1765 b,含重命名标记;诱饵字节与 mtime 均未变(1418 b,14:30:15.581Z) 仅 rootB
M6 deleteSession(12120) success:true;rootB 原文件 ABSENT;诱饵完好(1418 b,mtime 不变) 仅 rootB
M7 删除后 unstable_listSessions(cwd=wsB) sessions: [] rootB

变异臂 —— 同一套装置,只改一行,每项测量都翻转

我把出货 chunk 改成重新引入该 bug 类(用 const settings = this.settings; 取代 loadSettingsCached(cwd)),从零重跑整个流程,随后还原该 chunk 并验证 sha256 与原始值逐字节一致。

# 原始 head 变异体(this.settings
M0 对照 RECORD-FOR-A control marker RECORD-FOR-A control marker(不变,正符合对照组应有的表现)
M1 list cwd=wsB RECORD-FOR-B e2e marker DECOY-UNDER-ROOTA e2e marker
M2 transcript RECORD-FOR-B=true、DECOY=false RECORD-FOR-B=false、DECOY=true
M4 loadUpdates RECORD-FOR-B=true、DECOY=false RECORD-FOR-B=false、DECOY=true
M5 rename rootB 1413→1765 b,标记写入;诱饵未动 rootB 未动(1413 b,mtime 不变);诱饵 1418→1770 b,标记写入
M6 delete rootB ABSENT;诱饵完好 rootB 完好;诱饵 ABSENT

M6 最关键:变异体依然返回 {"success":true},而用户真实的会话幸存下来,被销毁的是另一个工作区的文件。所以这套装置并非只观察"输出非空" —— 它在读、写、删三个维度上都能区分两种行为。原始 head 在每一行上都站在正确的一侧。

没有验证的部分(如实披露,不粉饰)

  • turn_status(M3)未验证,且在该形态下无法验证。 this.sessionOrThrow(sessionId)11824 行,先于 11899 行的 pin 执行,因此非 live 会话会抛 Session not found,pinned 代码块根本不会进入;两臂表现完全相同。要触达它需要"同一进程内 live 的会话"+"指名另一个 cwd 的请求",我的双进程设计造不出这种组合。它的正确性在这里只由代码审查(假设 1–3)支撑,而非测量。
  • 未重建 base 臂。 等价性由 diff 论证:每个被改造的点此前都是 loadSettingsCached(cwd)runWithAcpRuntimeOutputDir(settings, cwd, op),而这正是 helper 所组合的内容。
  • 我自己的一个装置 bug,不影响任何结论: M2 我从 value.updates ?? value.records 取记录数,但响应的键是 events —— 这就是两臂都报 recordCount: 0 的原因。M2 的根归属判断来自对整个响应体的标记扫描(RECORD-FOR-B vs DECOY-UNDER-ROOTA),不受影响。
  • 未触及:sandbox/seatbelt 重启路径、delete/rename 的 archive-state 变体、以及来自两个工作区的并发请求。

两条不是 Critical 的观察

两条我都没有据此请求变更;记录下来是为了让合入决策在可见这些信息的前提下做出。

  • (a)turn_status 把 live 会话与请求提供的 cwd 混用。 在 pinned 代码块内,它 flush 的是 live 会话的录制(session.getConfig().getChatRecordingService()?.flush()),却用请求的 cwd 构造 new SessionTranscriptReader(cwd);而且与 loadSession/resumeSession 不同,它没有调用 assertLiveSessionScope。若客户端为一个 live 会话指名另一个 cwd,就会 flush 一个工作区、读取另一个工作区。属既有问题,本 diff 未触碰 —— base 解析的是同一个 cwd 和同一个 loadSettingsCached(cwd) —— 所以这是后续 PR 的问题,不是此处的阻塞项。
  • (b)外部 cwd 请求的 debug 日志落在启动工作区的根下。 进程 2 处理完 cwd=wsB 的请求后,rootA/debug/<sidB>.txt(432 b)与 rootB/debug/<sidB>.txt 同时出现。debug 日志的落点跟随进程的环境运行时根,而非 per-request 的 pin。同样未被本 diff 触碰(diff 只改了六个调用点并新增两个 helper);记录此条只是为了让看到该文件的人不会误以为这是 pin 本该防住的泄漏。

结论

从我这边看可以合入。 六个被改造的调用点每一处都保持语义不变,没有漏掉任何 per-request pin,新 AST 测试所断言的收敛点在出货 bundle 中确实成立;并且在别人尚未报告过的内核与 Node 版本上,这些 handler 在读、写、删三类操作中都解析到了请求工作区的运行时根 —— 同时变异臂证明了这套测量能够分辨二者之差。观察(a)与(b)属于措辞准确性与后续事项,不是阻塞项。这是我在本 PR 上的第一份报告,所以 approve 留到下一轮,不在同一口气里给出。

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

Approving on the strength of my tmux E2E report above, which concluded this is merge-ready.

Gate re-verified at approve time (2026-09-04T15:00Z):

Condition Value
Head 2c4d313eca40f27964eb362f1a221a91d6fa5765 — unchanged since the report
Latest review qwen-code-ci-bot APPROVED at this head, 2026-09-04T11:16:36Z (5112259581); no review after it
CI at head 53 check-runs = 27 success / 26 skipped / 0 failure / 0 pending / 0 cancelled
Activity since the report none — no new commit, review or comment

No Critical found. The five hypotheses I measured against this head are recorded as dead in the report. Both observations I raised there are pre-existing and non-blocking, so neither should hold this merge:

  • turn_status builds its SessionTranscriptReader from the request's cwd while flushing the live session's recording, and unlike loadSession/resumeSession never calls assertLiveSessionScope. Base resolved the same cwd, so this is a follow-up question rather than a regression introduced here.
  • A foreign-cwd request's debug log lands under the boot workspace's runtime root rather than the per-request pin (I measured rootA/debug/<sidB>.txt appearing alongside rootB/debug/<sidB>.txt). This diff touches only the 6 converted call sites and the 2 helpers, so debug-log placement is out of its scope.

Still unverified by anyone, carried over from the report so the merge decision owns it: turn_status end-to-end (unreachable in my harness shape — sessionOrThrow runs before the pin, so a non-live session throws Session not found), a base arm (equivalence argued from the diff instead), sandbox relaunch, and two-workspace concurrency.

@wenshao
wenshao added this pull request to the merge queue Sep 4, 2026
Merged via the queue into QwenLM:main with commit 9bb2f85 Sep 4, 2026
72 checks passed
@wenshao

wenshao commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — recommend merge

Verified 2c4d313eca against merge-base d4e3e4fc87 in a purpose-built local environment: two workspaces with different advanced.runtimeOutputDir, a real qwen --acp child, and a raw newline-delimited JSON-RPC ACP client. Every claim in the Reviewer Test Plan reproduces. Three non-blocking observations at the bottom, none of which should hold the merge.

Linux, Node 22.22.2, npm ci in a clean worktree per side.

1. A live E2E the plan doesn't have: does the pin actually work on a running agent?

The plan is a grep count plus a mutation matrix over mocks. Since the whole point is that both spellings reach the same function, I wanted the process to answer which runtime root each handler opens. Setup:

  • ws-boot pins advanced.runtimeOutputDir = lab/root-boot; ws-req pins lab/root-req.
  • The ACP child boots in ws-boot, opens a live session in ws-req, then opens a second live session in ws-boot — which is what repoints this.settings at the boot workspace (newSession: this.settings = loadSettingsCached(cwd)). Every call after that names cwd = ws-req while the cache holds ws-boot's settings: the multi-workspace skew fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 was about.
  • Every session id is seeded under both runtime roots at the same project-relative path with different content, so a wrong pin returns or mutates the decoy.
  • The child runs under strace, so the root each handler opened is read off the openat() lines rather than inferred.

All six rerouted handlers pin the request workspace's root, and base and PR are byte-for-byte the same verdict — the behavior-preservation claim, measured on a running agent rather than argued from the diff:

base vs PR

The interesting half is non-vacuity. I rebuilt the PR head with one line changed inside the new seam — const settings = loadSettingsCached(cwd)const settings = this.settings — and re-ran the identical probe. All six flip to the boot workspace's root, and deleteSession deletes lab/root-boot/.../11111111.jsonl: another workspace's session file. That is the #10095 bug class reproduced live, from a single line inside the seam this PR introduces. The choke point is load-bearing, not cosmetic:

non-vacuity

2. The Reviewer Test Plan, step by step

grep counts, suites, mutation matrix

Step 1 — grep. On d4e3e4fc87, runWithAcpRuntimeOutputDir( has 6 direct call sites in acpAgent.ts. On the PR head there is exactly 1 (line 4578, the shared helper's delegation); the only other mentions are the canonical import (320) and the doc comment (4561, deliberately written without a paren). this.runWithPinnedRuntimeBaseDirForRequest( = 6 call sites (5811 / 8950 / 11899 / 12120 / 12170 / 12401). Repo-wide, no production file outside acpAgent.ts and runtimeOutputDirContext.ts names the function.

Step 2 — suites. acpAgent.test.ts is 605/605 on the PR head and 601/601 on the merge-base, so the PR adds exactly the 4 tests it says it does. The whole src/acp-integration/ directory (39 files) is 2013/2013. tsc --noEmit -p packages/cli exits 0; eslint and prettier --check on both files are clean. The three #10095 regression tests do report under QwenAgent session-management routing (rename / delete / list / branch / close).

Step 3 — mutation matrix. Ran 13 mutants one at a time, whole file each run, tree restored between runs. Every row lands where the PR says, and I added the two handler mutants the body doesn't list (renameSession, unstable_listSessions) — each is caught by its own pin alone. The aliased import row (7 failed) and the seam reads this.settings row (8 failed) are the two that show the source pin and the behavioral pins covering different halves of the same invariant.

Step 4 — whitespace. git diff -w --stat equals git diff --stat: +272 / −16 over the two files. No whitespace-only churn.

Beyond the plan — is the taxonomy in the doc comment actually true? I audited all six remaining three-argument call sites, since a per-request handler hiding among them would defeat the seam. createWorkspaceMcpDiscoveryConfig (3887), assertLiveSessionScope (4639) and newSessionConfig (13417) all take settings as a parameter from a caller that scoped it; loadSession (5248) and unstable_resumeSession (5638) call loadSettingsCached(params.cwd) themselves — the same value the variant would resolve — under profiler.timeSync('settings_load', …), and adopt it as this.settings afterwards. The remaining loadSettingsCached(cwd) sites all feed assertLiveSessionScope, which pins internally. So the split the comment describes is the split the code has, and nothing per-request was left behind.

CI is now fully green on the head (23 pass, 30 skipping, 0 fail) — the Dependency CVE audit red you flagged has cleared on its own.

3. Non-blocking observations

source pin observations

(a) The source pin asserts the surviving mention's raw line text, so a formatting-only reflow of the delegation fails with a message that misdiagnoses it. The AST walk is correct — a trailing comment on another line, a multi-line canonical import and a string literal naming the function are all green, exactly as round 3 claims. But the assertion is toEqual([expect.stringMatching(/^\d+: return runWithAcpRuntimeOutputDir\(settings, cwd, operation\);$/)]), so if that call ever exceeds 80 columns and prettier wraps it across five lines, the mention count is still exactly 1 and the test fails with "acpAgent.ts must not name runWithAcpRuntimeOutputDir directly. Direct mentions at: 4578: return runWithAcpRuntimeOutputDir(". Reproduced. The pin is really two assertions — "exactly one direct mention" and "that mention is the delegation" — sharing one message that only explains the first. If you ever touch this, anchoring the second half on the AST (the identifier's enclosing method is runWithPinnedRuntimeBaseDir) instead of the line text removes the coupling to formatting. Low probability today: the line is 64 columns.

(b) readFileSync('src/acp-integration/acpAgent.ts') is cwd-relative, so the pin ENOENTs when vitest is driven from the repo root — which the root vitest.config.ts projects shape supports. This is not a PR defect: cli.test.ts's own long-standing source assertions fail identically from the root (ENOENT … open 'src/cli.ts'), and the author moved to this spelling in round 1 precisely to match that house pattern. npm run test (--workspaces, cwd = packages/cli) and CI are unaffected. Recording it so the house pattern's limitation is on the record rather than mistaken for something this PR introduced.

(c) The PR body's step-3 mutation matrix wasn't refreshed after the loadUpdates site was added. Four of its rows quote pass counts summing to 604 while the head has 605, and its last row reads "7 failed / 597: all five per-request handler pins" where there are now six — measured, that mutant is 8 failed / 597. The matrices in the round-2 and round-3 comments are the accurate ones. Purely cosmetic, but the last row is the one you'd point a reader at, so it's worth a one-line fix if you touch the body again.

None of these change the recommendation. The refactor does the thing worth doing after #10095 — it removes the "which settings pin this operation" decision from six call sites instead of re-auditing them — and the E2E above shows the seam holds the property on a real process, not just under mocks.

中文说明

维护者验证 —— 建议合并

在本地专门搭建的真实环境中验证了 2c4d313eca(对比 merge-base d4e3e4fc87):两个 workspace 配置不同的 advanced.runtimeOutputDir,跑真实的 qwen --acp 子进程,用一个裸的按行分隔 JSON-RPC 的 ACP 客户端驱动。Reviewer Test Plan 中的每一条都能复现。文末有三条非阻断的观察,都不影响合并。

环境:Linux、Node 22.22.2,两侧各在干净 worktree 中 npm ci

1. 计划里没有的一项:在真正跑起来的 agent 上,钉定到底生效了吗

原计划是 grep 计数加一套跑在 mock 上的变异矩阵。既然整件事的前提就是"两种写法最终到达同一个函数",我希望由进程本身回答每个处理器实际打开的是哪个 runtime root。搭法:

  • ws-bootadvanced.runtimeOutputDir = lab/root-bootws-reqlab/root-req
  • ACP 子进程在 ws-boot 启动,先在 ws-req 开一个 live session,再在 ws-boot 开第二个——后者正是把 this.settings 重新指向 boot workspace 的动作(newSessionthis.settings = loadSettingsCached(cwd))。此后每次调用都用 cwd = ws-req,而缓存里是 ws-boot 的 settings:这正是 fix(cli): resolve session-management settings per request, not from the stale this.settings cache #10095 说的多 workspace 偏移。
  • 每个 session id 都在两个 runtime root 下同一个项目相对路径上以不同内容播种,因此钉错就会返回或改动诱饵。
  • 子进程跑在 strace 下,每个处理器实际打开的 root 直接从 openat() 行读出,而不是推断。

改道的六个处理器全部钉在请求 workspace 的 root 上,且 base 与 PR 的判定完全一致——"行为不变"这一点是在跑起来的 agent 上量出来的,不是从 diff 推出来的(见上方第一张图)。

更关键的是非空洞性。我把 PR head 里新入口内的一行改掉——const settings = loadSettingsCached(cwd)const settings = this.settings——重新 bundle 后跑完全相同的探针。六个全部翻到 boot workspace 的 root,其中 deleteSession 删掉了 lab/root-boot/.../11111111.jsonl另一个 workspace 的 session 文件。这就是 #10095 的问题形态被真实复现,而触发它的只是这个新入口里的一行。也就是说这个唯一入口是承重的,不是装饰(见第二张图)。

2. 逐条复核 Reviewer Test Plan

第 1 步 grep。d4e3e4fc87acpAgent.tsrunWithAcpRuntimeOutputDir( 有 6 处直接调用;PR head 上恰好 1 处(4578 行,共享辅助方法的委托),其余两处引用是规范 import(320)和刻意不带括号的文档注释(4561)。this.runWithPinnedRuntimeBaseDirForRequest( 为 6 处调用(5811 / 8950 / 11899 / 12120 / 12170 / 12401)。全仓范围内,除 acpAgent.tsruntimeOutputDirContext.ts 外没有生产文件提到该函数。

第 2 步 测试。 PR head 上 acpAgent.test.ts 605/605,merge-base 上 601/601,即 PR 恰好新增它声称的 4 条。整个 src/acp-integration/ 目录(39 个文件)2013/2013。tsc --noEmit -p packages/cli 退出码 0;两个文件的 eslintprettier --check 干净。#10095 的三条回归测试确实报在 QwenAgent session-management routing (rename / delete / list / branch / close) 下。

第 3 步 变异矩阵。 逐个跑了 13 个变异,每次跑整个文件、每次恢复。每一行都落在 PR 所述位置;我另补了 body 未列出的两个处理器变异(renameSessionunstable_listSessions),各自只被自己的钉抓住。别名 import 一行(7 失败)与 入口读 this.settings 一行(8 失败)说明源码钉与行为钉各自覆盖同一不变量的不同一半。

第 4 步 空白。 git diff -w --statgit diff --stat 一致:两个文件 +272 / −16,没有纯空白改动。

计划之外——文档注释里的分类是否属实? 我核了剩下全部六处三参数调用点,因为若有按请求处理器混在其中就会架空这个入口。createWorkspaceMcpDiscoveryConfig(3887)、assertLiveSessionScope(4639)、newSessionConfig(13417)的 settings 都由已确定作用域的调用方作为参数传入;loadSession(5248)与 unstable_resumeSession(5638)在 profiler.timeSync('settings_load', …) 下自己调用 loadSettingsCached(params.cwd)——与变体解析出的值相同——随后将其采纳为 this.settings。其余 loadSettingsCached(cwd) 调用点全部喂给 assertLiveSessionScope,而后者内部会钉。所以注释描述的划分与代码实际划分一致,没有遗漏任何按请求的处理器。

当前 head 的 CI 已全绿(23 通过、30 跳过、0 失败)——你提到的 Dependency CVE audit 红色已自行恢复。

3. 非阻断观察

(a) 源码钉断言的是存活引用所在行的原始文本,因此对委托行做纯格式化重排会以一条误导性的信息失败。 AST 遍历本身是对的——另一行上的行尾注释、被拆成多行的规范 import、字符串字面量,第 3 轮声称的三种良性形状实测全绿。但断言是 toEqual([expect.stringMatching(/^\d+: return runWithAcpRuntimeOutputDir\(settings, cwd, operation\);$/)]),所以一旦这次调用超过 80 列被 prettier 拆成五行,引用计数仍然恰好是 1,测试却会以 "acpAgent.ts must not name runWithAcpRuntimeOutputDir directly. Direct mentions at: 4578: return runWithAcpRuntimeOutputDir(" 失败。已复现。这个钉其实是两条断言——"恰好一处直接引用"和"该处就是委托"——共用一条只解释前者的信息。将来若要动它,把后半条也锚在 AST 上(该标识符的外层方法是 runWithPinnedRuntimeBaseDir)而不是行文本,就能解开与排版的耦合。当前概率不高:该行 64 列。

(b) readFileSync('src/acp-integration/acpAgent.ts') 相对于进程 cwd,因此从仓库根目录驱动 vitest 时该钉会 ENOENT——而根 vitest.config.tsprojects 形态是支持这种跑法的。这不是本 PR 的缺陷:cli.test.ts 自己长期存在的源码断言从根目录跑同样失败(ENOENT … open 'src/cli.ts'),而作者在第 1 轮改成这个写法正是为了对齐这个既有惯例。npm run test--workspaces,cwd = packages/cli)与 CI 不受影响。在此记录,是为了让这个惯例的局限留档,而不至于被误认为本 PR 引入。

(c) PR body 第 3 步的变异矩阵在新增 loadUpdates 调用点之后没有同步更新。 其中四行引用的通过数合计为 604,而当前 head 是 605;最后一行写的是 "7 failed / 597:五个按请求处理器的钉",而现在是六个——实测该变异是 8 失败 / 597。第 2 轮与第 3 轮评论里的矩阵才是准确的。纯属表述问题,但最后一行恰是最值得让读者看的一行,下次若再改 body 顺手修一行即可。

以上都不改变结论。这次重构做的正是 #10095 之后值得做的事——把"用哪份 settings 钉这次操作"的决定从六个调用点移走,而不是逐个复查它们——而上面的 E2E 表明这个入口在真实进程上、而不仅在 mock 下,确实承载着这个性质。

wenshao added a commit to zhuyuy/qwen-code that referenced this pull request Sep 5, 2026
…ke point (QwenLM#11047)

* fix(cli): route the transcript turn-index handler through the pin choke point

The QwenLM#10988 guard fails the build on any direct mention of the runtime-root
context runner outside the single choke point; the session turn-index page
handler (from QwenLM#10751) still composed the routing by hand with
loadSettingsCached, the exact decision runWithPinnedRuntimeBaseDirForRequest
exists to make in one place (transcript pages are listed in the choke
point's own doc). Behaviour is identical: same cached settings for the same
cwd, same pin; the operation never used the local settings object.

* test(cli): pin the turn-index handler's per-request routing and roster

The choke-point guard permits the scoped-settings shape, so an edit back to
this.runWithPinnedRuntimeBaseDir(this.settings, cwd, ...) would stay green
without a behavioural test; mirror the six sibling routing tests. Also add
the turn-index handler to the choke point's normative handler roster.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
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.

4 participants