feat(core): declare create_sub_session only under qwen serve - #9425
feat(core): declare create_sub_session only under qwen serve#9425DragonnZhang wants to merge 1 commit into
Conversation
create_sub_session needs the daemon bridge, which only exists under `qwen serve`, yet it was declared in every session. Interactive TUI and headless runs therefore carried a tool that can never succeed, polluting the model's action space and ToolSearch results. The tool is now registered by the ACP session at the same point it wires the sub-session spawner, so it exists exactly where it can work and nowhere else.
|
Thanks for the PR! Template: complete ✓ Problem: real and verifiable, not theoretical. Direction: aligned. Keeping the model's action space honest — a declared tool should be a callable tool — is the right direction, and deferred-tool availability is an actively maintained area upstream (Claude Code's CHANGELOG carries several recent fixes around deferred tools being unavailable in the wrong contexts). No direct reference to this exact change, but the area is clearly relevant. Size: core paths are touched ( Approach: focused and minimal — moving the registration from the always-on built-in set to the exact point where the spawner is wired is the natural spot, the runtime guard is kept as defense, and no drive-by changes. One implementation question worth checking in code review: the old path went through Risk: Moving on to code review. 🔍 中文说明感谢贡献! **模板:**完整 ✓ **问题:**真实且可验证,不是理论性问题。 **方向:**对齐。保持模型 action space 的诚实性(声明的工具就应该是可调用的)是正确方向;deferred-tool 的可用性在上游也是活跃维护的领域(Claude Code 的 CHANGELOG 中有多条关于 deferred tools 在错误上下文中不可用的修复)。没有直接对应的条目,但该领域明显相关。 **规模:**触及核心路径( **方案:**聚焦且最小化——把注册从"总是开启"的内置工具集移到 spawner 接线处是最自然的位置,运行时保护作为防御保留,没有夹带无关改动。有一个实现问题需要在代码审查中核实:旧路径经过 **风险:** 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal before reading the diff was exactly what this PR does: drop the unconditional One real finding — the registration bypasses the PermissionManager gate. The old path went through Everything else verified clean:
Test evidence (PR's own CI — unattended run, PR code never executed here)The PR's unit suite pins both sides of the change (config.test.ts asserts the built-in registry no longer registers the tool; the Session suites assert the ACP session registers it) and would fail if reverted. At review time the main suite was still running — table below is a snapshot and the finalize job will refresh it once CI settles: Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: real rendered tool surface. The unit tests assert registration against mocked registries, which pins the mechanism but doesn't exercise what the model actually sees. Sandboxed verification would settle this: 中文说明代码审查:方案与我独立设想的一致(从内置集移除无条件注册、在 spawner 接线处注册、保留运行时保护),方向正确。但发现一个实际问题:新注册直接调用 其余均已核实: 测试证据:无人值守 CI 运行,未在此执行任何 PR 代码。单元测试从两侧钉住了改动(config 侧断言不再注册、Session 侧断言注册),revert 后必然失败。审查时主套件仍在运行,表格为快照,CI 结束后由 finalize 任务刷新。未验证项:真实渲染的工具面——单元测试基于 mock 注册表断言,未实际执行模型看到的界面;可用 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — everything is clean except one bypass of a documented permission gate; that needs resolving before approval. Stepping back: this is a well-motivated, well-built PR. The approach matches what I'd have proposed independently, the diff is minimal, every edit earns its place, and the tests pin both sides of the behavior change (a revert fails them). The motivation is real, not theoretical — a dead tool in the action space costs tokens and invites wasted model calls. What stops me short of approving is the finding above: the new registration path skips the Holding approval until then. @DragonnZhang — if you agree the gate should be restored, the comment above sketches the wrinkle (sync constructor vs async check); if you think restricted-daemon sessions should deliberately keep the tool declared, say so and let's make that an explicit decision. @wenshao — flagging for a maintainer's eye on the call either way, since this touches core tool-registration semantics. CI note: the main unit suite was still running at review time; no approval is being issued this run, so nothing is deferred on CI — the finding above is the open item. 中文说明置信度:3/5 —— 除了一处绕过已记录的权限门控外全部干净;该问题解决前不予批准。 整体来看:这是一个动机充分、实现良好的 PR。方案与我独立设想的一致,diff 最小化,每处改动都有必要,测试从两侧钉住了行为变化(revert 后必然失败)。动机真实而非理论性——action space 中的死工具浪费 token 并诱导无效调用。 阻止我批准的是上面的发现:新的注册路径跳过了 因此暂缓批准。@DragonnZhang —— 如果你认同应恢复门控,上面的评论指出了关键难点(同步构造函数 vs 异步检查);如果你认为受限 daemon 会话就应该刻意保留该工具的声明,请说明,让我们把它变成一个明确的决定。@wenshao —— 提请维护者关注,因为这触及核心工具注册语义。 CI 说明:审查时主单元测试套件仍在运行;本次运行不发布任何批准,因此没有基于 CI 的延迟批准——上述发现是唯一的未决项。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
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 6 by the review time budget.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——评审时间预算不足,未能开始第 6 轮。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| // create_sub_session is daemon-only: it is registered by the ACP Session | ||
| // when it wires the sub-session spawner (see acp-integration's Session), |
There was a problem hiding this comment.
[Critical] R1-1: Removing the unconditional registerLazy from createToolRegistry silently strips create_sub_session from every rebuilt sub-agent/override registry — rebuildToolRegistryOnOverride (agent.ts), InProcessBackend.ts, subagent-manager.ts, workflow-orchestrator.ts — all built via createToolRegistry(undefined, { forSubAgent: true }) + copyDiscoveredToolsFrom, which copies only DiscoveredTool/DiscoveredMCPTool. Pre-PR the lazy factory had no forSubAgent guard (the adjacent structured_output registration demonstrably has one), so daemon sub-agents had the tool, and the spawner is still reachable from those configs (override configs delegate to the base Config via Object.create). EXCLUDED_TOOLS_FOR_SUBAGENTS — the canonical list of tools sub-agents must not have — was not updated. This contradicts the PR's own "No behavior change under qwen serve" claim.
Failure scenario: under qwen serve, a daemon session delegates work via the Agent tool and the spawned sub-agent tries to fan out via create_sub_session (a flow that worked pre-PR) → the tool is simply absent from the sub-agent's declarations and ToolSearch; no error is raised, the capability silently disappears, and the PR description never mentions the removal.
Witness (merge-base vs HEAD, A/B):
BASE f0dcdfc157, config.ts:8728:
await registerLazy(ToolNames.CREATE_SUB_SESSION, ...)
// unconditional, shared section, no forSubAgent guard
PR HEAD: createToolRegistry carries no registration (comment only);
sole site is Session.ts:2890-2894; copyDiscoveredToolsFrom copies only
DiscoveredTool/DiscoveredMCPTool — rebuilt forSubAgent registries
never receive the built-in
(not run — registration presence/absence is a static fact quoted from both trees, and the declaration-resolution path is deterministic given it)
Suggested fix: decide and record intent — if daemon sub-agents should keep the capability, register the tool in the forSubAgent rebuild path too (or condition a core-side registration on a wired spawner); if the removal is deliberate, add ToolNames.CREATE_SUB_SESSION to EXCLUDED_TOOLS_FOR_SUBAGENTS and say so in the PR description and in the comment beside the new registration site ("every daemon session that can spawn sub-sessions declares the tool" is currently false for sub-agent registries).
中文说明
[Critical] 从 createToolRegistry 移除无条件的 registerLazy 后,create_sub_session 被悄悄从所有重建的子代理/覆盖注册表中移除——rebuildToolRegistryOnOverride(agent.ts)、InProcessBackend.ts、subagent-manager.ts、workflow-orchestrator.ts——这些注册表都通过 createToolRegistry(undefined, { forSubAgent: true }) + copyDiscoveredToolsFrom 构建,而后者只复制 DiscoveredTool/DiscoveredMCPTool。改动前该懒加载工厂没有 forSubAgent 守卫(相邻的 structured_output 注册明确有该守卫),因此 daemon 子代理原本拥有该工具,且 spawner 仍可从这些配置触达(覆盖配置通过 Object.create 委托到基础 Config)。EXCLUDED_TOOLS_FOR_SUBAGENTS(子代理不应拥有的工具的规范清单)并未更新。这与 PR 自己声称的"qwen serve 下无行为变化"相矛盾。
失败场景:在 qwen serve 下,daemon 会话通过 Agent 工具派发任务, spawned 出的子代理尝试用 create_sub_session 继续 fan-out(改动前该流程可用)→ 该工具在子代理的声明列表和 ToolSearch 中直接缺席;不报错,能力静默消失,PR 描述未提及这一移除。
证据(merge-base 与 HEAD 的 A/B 对比,见上方代码块):改动前 config.ts:8728 存在无条件的 registerLazy 注册;改动后 createToolRegistry 中无任何注册,唯一注册点是 Session.ts:2890-2894,而 copyDiscoveredToolsFrom 只复制 DiscoveredTool/DiscoveredMCPTool,重建的 forSubAgent 注册表永远拿不到该内置工具。(未实际运行——注册与否是可从两棵树直接引用的静态事实,声明解析路径在此前提下是确定的。)
建议修复:明确并记录意图——如果 daemon 子代理应保留该能力,请在 forSubAgent 重建路径中也注册该工具(或在 core 侧按 spawner 是否接线做条件注册);如果是有意移除,请将 ToolNames.CREATE_SUB_SESSION 加入 EXCLUDED_TOOLS_FOR_SUBAGENTS,并在 PR 描述和新注册点旁的注释中说明("每个能派生子会话的 daemon 会话都声明该工具"这一说法对子代理注册表目前不成立)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| this.config | ||
| .getToolRegistry() | ||
| .registerTool(new CreateSubSessionTool(this.config)); |
There was a problem hiding this comment.
[Suggestion] R1-2: The direct registry.registerTool() bypasses the PermissionManager.isToolEnabled() gate (coreTools allowlist + whole-tool deny rules) that the removed registerLazy path applied — and create_sub_session is explicitly listed in PermissionManager.CORE_TOOLS. Execution remains blocked at runtime (verified: Session.runTool's L1 check at Session.ts:9763-9784 and CoreToolScheduler at coreToolScheduler.ts:2409-2437 both re-check the same isToolEnabled), so this is declaration-level: in operator-restricted daemon configurations the tool is advertised (function declarations, deferred-tools reminder, ToolSearch) but every call ends in EXECUTION_DENIED — reintroducing exactly the dead-tool pollution this PR exists to remove, violating its own stated invariant ("if the model can see create_sub_session, calling it will work"), and dropping defense-in-depth from two blocking layers to one. The codebase documents this exact hazard at config.ts:8773-8781 ("would bypass coreTools allowlist + whole-tool deny rules"). This corroborates the mechanism already raised in the triage stage-2 comment and settles its open runtime-enforcement question.
Failure scenario: qwen serve with settings tools.core: ["read_file", "edit"] (or --core-tools, or a specifier-less permissions.deny rule for the tool) → pre-PR isToolEnabled() returned false and the tool was never registered in daemon sessions; post-PR every Session constructor registers it unconditionally, and the model spends a call on it only to receive EXECUTION_DENIED.
Witness: not run — the claim reduces to "is pm.isToolEnabled called on the execution path", settled by direct quotation of the two call sites above.
Suggested fix: apply the same gate registerLazy used before registering:
// isToolEnabled is async while #registerSubSessionSpawner() runs in the
// constructor — move this registration into the session's async init path
// (e.g. createAndStoreSession, after config.initialize()):
const pm = this.config.getPermissionManager();
if (!pm || (await pm.isToolEnabled(ToolNames.CREATE_SUB_SESSION))) {
this.config.getToolRegistry().registerTool(new CreateSubSessionTool(this.config));
}中文说明
[Suggestion] 直接调用 registry.registerTool() 绕过了被移除的 registerLazy 路径所应用的 PermissionManager.isToolEnabled() 门控(coreTools 白名单 + 整工具 deny 规则)——而 create_sub_session 明确列于 PermissionManager.CORE_TOOLS。运行时执行仍会被拦截(已验证:Session.ts:9763-9784 的 L1 检查与 coreToolScheduler.ts:2409-2437 都会重新检查同一个 isToolEnabled),因此这是声明层面的问题:在运维受限的 daemon 配置下,工具被声明出来(函数声明、deferred-tools 启动提示、ToolSearch),但每次调用都以 EXECUTION_DENIED 结束——重新引入了本 PR 要消除的"死工具污染",违反其自身不变量("模型只要能看到 create_sub_session,调用它就一定可用"),并把纵深防御从两层拦截削弱为一层。代码库在 config.ts:8773-8781 明确警告过这一模式("会绕过 coreTools 白名单和整工具 deny 规则")。此发现与 triage stage-2 评论已提出的机制一致,并解决了其中关于运行时是否仍强制执行的疑问。
失败场景:qwen serve 配置 tools.core: ["read_file", "edit"](或 --core-tools、或针对该工具的无 specifier permissions.deny 规则)→ 改动前 isToolEnabled() 返回 false,daemon 会话从不注册该工具;改动后每个 Session 构造函数无条件注册,模型会花一次调用去尝试并收到 EXECUTION_DENIED。
证据:未实际运行——该 claim 归结为"执行路径上是否调用了 pm.isToolEnabled",上述两处调用点的直接引用即可定论。
建议修复:注册前补上与 registerLazy 相同的门控(代码示例见上)。由于 isToolEnabled 是异步的而 #registerSubSessionSpawner() 在构造函数中同步执行,可将注册移到会话的异步初始化路径(如 createAndStoreSession,在 config.initialize() 之后)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| expect( | ||
| (registerToolMock as Mock).mock.calls.map((call) => call[0]), | ||
| ).not.toContain(ToolNames.CREATE_SUB_SESSION); |
There was a problem hiding this comment.
[Suggestion] R1-3: The new negative test asserts only on ToolRegistry.prototype.registerFactory mock calls (the local variable is misleadingly named registerToolMock). A future regression that re-adds the tool via a direct eager registry.registerTool(new CreateSubSessionTool(this)) inside createToolRegistry — the exact pattern this PR legitimizes in Session.ts — never touches registerFactory, so this test stays green while the action-space pollution it guards against returns in interactive/headless runs. Probe-verified in this worktree:
original oracle + simulated eager regression: Tests 1 passed | 533 skipped
widened oracle + same regression: Tests 1 failed
AssertionError: expected [ 'create_sub_session' ] to not include 'create_sub_session'
Suggested fix: also capture and assert on ToolRegistry.prototype.registerTool calls (assert neither registerFactory nor registerTool was called with ToolNames.CREATE_SUB_SESSION). Note: asserting on config.getToolRegistry().getAllToolNames() is not viable in this file — its mock hardcodes getAllToolNames = vi.fn(() => []).
中文说明
[Suggestion] 新增的负向测试只断言了 ToolRegistry.prototype.registerFactory 的 mock 调用(局部变量名 registerToolMock 有误导性)。未来若通过直接的 eager registry.registerTool(new CreateSubSessionTool(this)) 把该工具加回 createToolRegistry——正是本 PR 在 Session.ts 中合法化的模式——完全不会触碰 registerFactory,该测试仍为绿色,而它本要防范的 action-space 污染将在交互/headless 运行中复现。已在本 worktree 用探针验证(见上方输出:原始 oracle + 模拟的 eager 回归仍通过;扩展 oracle 后同一回归使其失败)。
建议修复:同时捕获并断言 ToolRegistry.prototype.registerTool 的调用(断言 registerFactory 与 registerTool 都未以 ToolNames.CREATE_SUB_SESSION 被调用)。注意:断言 config.getToolRegistry().getAllToolNames() 在此文件不可行——该文件的 mock 将 getAllToolNames 硬编码为 vi.fn(() => [])。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| // Register the tool exactly where the spawner is wired: the registry is | ||
| // session-scoped, so every daemon session that can spawn sub-sessions | ||
| // declares the tool, and no non-daemon session ever does. |
There was a problem hiding this comment.
[Suggestion] R1-4: The daemon's workspace-tools inventory (buildWorkspaceToolsStatus in acpAgent.ts, served via SERVE_STATUS_EXT_METHODS.workspaceTools and consumed by SDK DaemonClient.workspaceTools(), the WebUI tools dialog, serve/routes/workspace-status.ts, daemon-status.ts) reads the workspace/bootstrap Config registry via getAllToolNames(), which includes pending factories and applies only an MCP serverName filter (no shouldDefer filter). Pre-PR the bootstrap registry carried the tool's lazy factory, so the tool was listed in the operator panel with displayName/description and its enable toggle. Post-PR only per-session registries carry the tool, so it vanishes from the inventory on every qwen serve workspace, unconditionally: operators get no indication that a privileged session-spawning tool exists, and the UI-driven disable flow (POST /workspace/tools/:name/enable, only offered for listed tools) has no row to toggle — though the route itself accepts any name, so a direct-API disable still works. The diff's rationale argues only the non-daemon pollution case; this daemon-surface state is unargued, and the PR claims "no behavior change under qwen serve".
Failure scenario: an operator opens the daemon workspace tools panel (WebUI tools dialog / SDK status) on any qwen serve workspace → create_sub_session is not listed; it cannot be inspected or disabled from the UI even though every daemon session still declares it to the model.
Witness: not run — every link is a static, deterministic fact in the worktree (status builder, filter set, route validation and consumer wiring read in full); a live A/B would add no discriminating power.
Suggested fix: keep the workspace-tools status aware of the daemon-only tool — e.g. union getAllToolNames() with ToolNames.CREATE_SUB_SESSION in buildWorkspaceToolsStatus when the ACP channel is live, or source that cell from a live session registry; alternatively decide deliberately and document that the panel lists only workspace-registry tools.
中文说明
[Suggestion] daemon 的 workspace-tools 清单(acpAgent.ts 中的 buildWorkspaceToolsStatus,经 SERVE_STATUS_EXT_METHODS.workspaceTools 提供,消费方包括 SDK DaemonClient.workspaceTools()、WebUI 工具对话框、serve/routes/workspace-status.ts、daemon-status.ts)通过 getAllToolNames() 读取 workspace/引导 Config 的注册表——该方法包含待执行的工厂,且只应用 MCP 的 serverName 过滤(没有 shouldDefer 过滤)。改动前引导注册表携带该工具的懒加载工厂,因此工具会出现在运维面板中,带 displayName/description 和启用开关。改动后只有每个会话各自的注册表携带该工具,于是它在所有 qwen serve 工作区的清单中无条件消失:运维人员看不到这个可派生会话的特权工具的存在,UI 侧的禁用入口(POST /workspace/tools/:name/enable 只对列出的工具提供)也没有可切换的行——不过路由本身接受任意名称,直接走 API 禁用仍然有效。diff 的理由只论证了非 daemon 污染这一种状态;该 daemon 面板状态未被论证,而 PR 声称"qwen serve 下无行为变化"。
失败场景:运维人员在任意 qwen serve 工作区打开 daemon 工具面板(WebUI 工具对话框 / SDK 状态)→ create_sub_session 不再列出;即使每个 daemon 会话仍向模型声明它,也无法在 UI 中查看或禁用。
证据:未实际运行——每一环都是 worktree 中静态且确定的事实(状态构建器、过滤集、路由校验与消费方接线均已完整阅读);实际跑 A/B 不会提供更多区分力。
建议修复:让 workspace-tools 状态感知该 daemon-only 工具——例如在 ACP 通道存活时在 buildWorkspaceToolsStatus 中将 getAllToolNames() 与 ToolNames.CREATE_SUB_SESSION 取并集,或从某个存活会话的注册表取数;或者有意地决策并记录"面板只列出 workspace 注册表中的工具"。
— qwen3.8-max via Qwen Code /review (v0.21.13)
What this PR does
create_sub_sessiononly works underqwen serve, where the daemon bridge exists — but until now it was declared in every session regardless of mode. This PR moves the tool's registration out of the always-on built-in tool set and into the exact place where the daemon ACP session wires its sub-session spawner: the tool now exists only in sessions that can actually spawn sub-sessions. In interactive TUI and headless runs it is simply not declared — it disappears from the function declarations, from ToolSearch results, and from the deferred-tools startup reminder. The tool's runtime "daemon-only" error path is kept as a defensive guard, and its docs/description now state that it is not declared at all outside daemon mode.Why it's needed
In non-daemon sessions the tool can never succeed: there is no session bridge to spawn through. Declaring it there anyway pollutes the model's action space with a dead tool — it consumes prompt tokens, shows up in ToolSearch keyword matches, and invites the model to spend a call discovering an error it could never avoid. Registering it only where its spawner is wired keeps the tool surface honest: if the model can see
create_sub_session, calling it will work.Reviewer Test Plan
How to verify
qwen(TUI orqwen -p ...) and inspect the tool surface (e.g./toolsor ToolSearch) —create_sub_sessionshould be absent. Before this change it was present (as a deferred tool) in both modes.qwen serveand open a session — the tool is declared as before and spawning sub-sessions works unchanged (registration happens at session construction, right after the spawner is wired).cd packages/core && npx vitest run src/config/config.test.ts src/tools/create-sub-session.test.ts(asserts the built-in registry no longer registers the tool, and the tool's own behavior is unchanged) andcd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/session/Session.worktree.test.ts src/acp-integration/session/Session.review-lease.test.ts(asserts the ACP session registers the tool alongside the spawner).Observed locally: core 548/548 and the three ACP Session suites 676/676 pass, and
tsc --buildfor core + cli succeeds.Evidence (Before & After)
N/A (non-UI change; verified via the unit tests above — before: the tool appeared in the built-in registry for every session; after: it appears only in daemon ACP sessions).
Tested on
Environment (optional)
Local runtime: per-package
npx vitest rununit tests and incrementaltsc --buildover core and cli on top of a fullnpm installbuild.Risk & Scope
create_sub_sessionis always present in the registry (e.g. checks that look the name up unconditionally) now sees it only under the daemon — which is the intended semantic; the runtime guard means even a stale direct call degrades to the same clear daemon-only error as before.qwen serve; scheduled tasks, nested sub-sessions, and workspace sessions all run as daemon ACP sessions and register the tool through the same path.Linked Issues
None — standalone improvement.
中文说明
这个 PR 做了什么
create_sub_session只在qwen serve下可用(那里才有 daemon 桥接),但此前它在所有模式的会话中都会被声明。本 PR 将该工具的注册从"总是注册"的内置工具集移出,放到 daemon ACP 会话接线子会话 spawner 的同一位置:工具只存在于真正能派生子会话的会话中。在交互式 TUI 和 headless 运行里它不再被声明——从函数声明列表、ToolSearch 结果和延迟工具启动提示中彻底消失。工具运行时的 "daemon-only" 错误路径作为防御性保护保留,其文档/描述也更新为"非 daemon 模式下完全不声明"。为什么需要
在非 daemon 会话中该工具永远不可能成功:没有可用于派生的会话桥接。即便如此仍声明它,会用一个死工具污染模型的 action space——浪费提示词 token、出现在 ToolSearch 关键词匹配中,并诱导模型花一次调用去发现一个本可避免的错误。只在 spawner 接线处注册,可以让工具面保持诚实:模型只要能看到
create_sub_session,调用它就一定可用。审阅者测试计划
如何验证
qwen(TUI 或qwen -p ...),查看工具面(如/tools或 ToolSearch)——create_sub_session应不存在。改动前它在两种模式下都存在(作为延迟工具)。qwen serve并打开会话——工具像以前一样被声明,派生子会话行为不变(注册发生在会话构造时、spawner 接线之后)。cd packages/core && npx vitest run src/config/config.test.ts src/tools/create-sub-session.test.ts(断言内置注册表不再注册该工具、工具自身行为不变),以及cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/session/Session.worktree.test.ts src/acp-integration/session/Session.review-lease.test.ts(断言 ACP 会话在接线 spawner 的同时注册该工具)。本地观测:core 548/548 与三个 ACP Session 套件 676/676 通过,core + cli 的
tsc --build成功。前后对比证据
N/A(非 UI 变更;通过上述单元测试验证——改动前:该工具出现在每个会话的内置注册表中;改动后:仅出现在 daemon ACP 会话中)。
测试平台
环境(可选)
本地运行时:在完整
npm install构建基础上,使用各包内npx vitest run单元测试与 core、cli 的增量tsc --build。风险与范围
create_sub_session始终存在于注册表中的逻辑(例如无条件按名查找)现在只在 daemon 下能看到它——这正是预期语义;即使有过时的直接调用,运行时保护也会退化为与之前相同的清晰 daemon-only 错误。qwen serve下行为无变化;定时任务、嵌套子会话与工作区会话都作为 daemon ACP 会话运行,经由同一路径注册该工具。关联 Issue
无——独立改进。