Skip to content

feat(serve): scope extensions to workspace runtimes - #11086

Open
ytahdn wants to merge 44 commits into
mainfrom
codex/daemon-workspace-runtime-extensions-main
Open

feat(serve): scope extensions to workspace runtimes#11086
ytahdn wants to merge 44 commits into
mainfrom
codex/daemon-workspace-runtime-extensions-main

Conversation

@ytahdn

@ytahdn ytahdn commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes the global extension catalog available through the runtime selected for each workspace. It reconciles extension state into live workspace runtimes, exposes workspace-qualified daemon and SDK access, and updates extension management, the composer add menu, and @ mentions to use the selected workspace runtime. It also advertises the complete capability surface and documents the protocol behavior.

Why it's needed

Extensions are globally installed, but their skills and commands must be projected into the runtime that owns the active workspace. Without workspace-qualified routing, extension management and task composition can read or update the primary runtime instead of the selected runtime, so secondary workspaces may miss extension-provided skills or show stale state.

Reviewer Test Plan

How to verify

Start the daemon with at least two workspace runtimes, select a non-primary workspace, and confirm that the extension management page lists the global catalog while enablement changes are reconciled into that workspace's live runtime. In a new task for the same workspace, confirm that the add menu and @ menu expose extension-provided entries. Switch workspaces and confirm each UI surface follows the selected runtime without falling back to the primary runtime. Automated coverage verifies coordinator reconciliation, workspace-qualified routes, ACP propagation, SDK behavior, capability documentation, and Web Shell consumers.

Evidence (Before & After)

Before: extension UI and composer discovery could resolve through the primary runtime, so a selected secondary workspace could not reliably access extension-provided skills.

After: extension management and composer discovery resolve through the selected workspace runtime, with global extension metadata reconciled into each live runtime.

Tested on

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

Environment (optional)

Node.js 22 workspace build and package-scoped Vitest runs against the daemon, SDK, and Web Shell.

Risk & Scope

  • Main risk or tradeoff: Runtime reconciliation is asynchronous, so generation and epoch checks guard against applying stale extension state after a runtime replacement.
  • Not validated / out of scope: Browser-driven E2E and Windows/Linux local runs were not performed; CI provides cross-platform coverage.
  • Breaking changes / migration notes: None. The new protocol fields and SDK options are additive.

Linked Issues

Related to #10593.

中文说明

本 PR 做了什么

本 PR 让全局扩展目录能够通过每个工作区所选的运行时使用。它会把扩展状态协调到存活的工作区运行时中,提供工作区限定的 daemon 和 SDK 访问,并让扩展管理、输入区加号菜单和 @ 提及都使用当前选中的工作区运行时。同时补全 capability 声明并记录协议行为。

为什么需要

扩展是全局安装的,但扩展提供的技能和命令必须投射到当前工作区所属的运行时中。如果没有工作区限定路由,扩展管理和新建任务输入区可能读取或更新主运行时,而不是选中的运行时,导致次级工作区缺少扩展技能或显示过期状态。

Reviewer 测试计划

如何验证

启动至少包含两个工作区运行时的 daemon,选择一个非主工作区,确认扩展管理页展示全局扩展目录,并且启用状态变更会协调到该工作区的存活运行时。在同一工作区新建任务,确认加号菜单和 @ 菜单能够展示扩展提供的条目。切换工作区,确认所有界面都跟随选中的运行时,并且不会回退到主运行时。自动化测试覆盖协调器同步、工作区限定路由、ACP 传播、SDK 行为、capability 文档和 Web Shell 消费端。

前后对比证据

改造前:扩展界面和输入区发现逻辑可能通过主运行时解析,因此选中的次级工作区无法稳定获取扩展提供的技能。

改造后:扩展管理和输入区发现逻辑通过选中的工作区运行时解析,并将全局扩展元数据协调到每个存活运行时。

测试平台

系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

Node.js 22 工作区构建,以及针对 daemon、SDK 和 Web Shell 的包级 Vitest 测试。

风险与范围

  • 主要风险或取舍:运行时协调是异步的,因此使用 generation 和 epoch 校验,避免运行时替换后应用过期扩展状态。
  • 未验证或范围外:按用户要求未执行浏览器驱动 E2E,也未在 Windows/Linux 本地运行;跨平台覆盖交由 CI 验证。
  • 破坏性变更或迁移说明:无。新增协议字段和 SDK 选项均为增量兼容变更。

关联事项

关联 #10593

@ytahdn

ytahdn commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

Verification report

Browser-driven E2E was not run, per request not to control the browser.

  • npm run build && npm run typecheck: passed.
  • SDK daemon client tests: 414 passed.
  • Web Shell extension management, task composer, add-menu, @ mention, and scheduled-task tests: 240 passed.
  • Daemon coordinator, routes, server, and ACP tests: 1,949 passed when package-scoped. One pre-existing malformed-batch test flaked only in a high-concurrency combined invocation; its complete owning test file passed 52/52 in isolation.
  • Capability documentation contract: 2 passed; documented counts match the registry at 159 total and 47 conditional capabilities.
  • Two post-merge full-diff review rounds completed with no actionable findings.

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 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 PR!

Template looks good ✓ — every required heading is filled in, and the 中文说明 mirrors the English body.

Problem: Real, and verifiable in the tree rather than theoretical. This is the second layer of the contract landed in #10593, which explicitly deferred "capability convergence and catalog projections" as follow-up work. I checked the base: workspace-skills.ts already serves GET /workspace/runtime/skills and GET /workspaces/:workspace/runtime/skills, and there is no runtime/extensions counterpart — so extension reads still fall through to the primary runtime. That is an observed gap, not a hypothesis. Worth saying plainly though: the Evidence section is prose ("could resolve through the primary runtime"), not a captured before/after. For a feature completion that is fine, but a two-workspace capture would make the reviewer test plan checkable by someone who did not write it.

Direction: Aligned. It finishes an architecture this repo already merged, and it does so by extending existing shapes rather than adding parallel ones — the new routes mirror the skills runtime routes, the capability goes on the coordinator that #10593 established, and the SDK methods it calls (extensionCatalog, installUserExtension, updateUserExtension, uninstallUserExtension, checkUserExtensionUpdates) already exist on main. Reference CHANGELOG: no direct hit, but this is daemon internals so that is expected.

Size: Core paths are touched — this spans four packages (acp-bridge, cli, sdk-typescript, web-shell). Breakdown of the 2276 changed lines:

  • 1335 production logic lines (14 files)
  • 698 test lines (9 files)
  • 243 docs lines (5 files, including a new design doc)

Title is feat, so no size block applies. But 1335 production lines is over both the 500-line maintainer-awareness threshold and the 1000-line large-PR advisory, so flagging for maintainer awareness and noting the advisory: if any part of this is independently shippable — the ACP workspaceExtensionsReconcile control method, or the composer @/+ wiring behind workspace_extension_mentions — splitting would make the coordinator change reviewable on its own. Informational only, not a block.

Approach: Scope feels right for the stated goal, and the backward-compat handling is careful — mergeExtensionCatalog degrades to catalog-plus-projection when the runtime snapshot is not certified current, and older daemons keep the legacy primary-workspace flow behind the feature flag. Two things I would think about before the code review, neither a blocker:

  • Three call sites now hand-roll the same ladder (check the feature flag → ensureRuntimeworkspaceRuntimeExtensions): useComposerCore.ts, ScheduledTasksDialog.tsx, and ExtensionsManagerPage.tsx. The first uses workspaceByCwd(cwd) unconditionally; the second branches on formWorkspace?.primary === false to reach the primary client instead. Since the qualified client resolves the primary cwd too, that branch looks like avoidable complexity — and at three sites it is arguably already a shared helper.
  • GET /workspaces/:workspace/extensions now calls observeExtensionGeneration, so a read route mutates coordinator state (it invalidates skills and MCP readiness). The design doc says that is deliberate convergence, and I agree it is defensible — just calling it out so it is not a surprise to a maintainer reading the route.

Risk: Stage 1e matched — packages/cli/src/acp-integration/acpAgent.ts (and its test) sit on the acp-integration high-risk path. Review depth is full, and I will name a sandboxed lane in the code review comment. The new workspaceExtensionsReconcile ext-method handler is the piece to focus on: it refreshes every active session's config in parallel and treats a skipped session as a hard reconciliation failure, so one persistently broken session config keeps that runtime's extensions capability out of ready.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 所有必需小标题都填写了,中文说明与英文正文对应。

问题: 真实存在,而且可以在代码树里验证,不是理论性加固。这是 #10593 所确立契约的第二层,那个 PR 明确把「capability 收敛与 catalog 投射」列为后续工作。我核对了基线:workspace-skills.ts 已经提供 GET /workspace/runtime/skillsGET /workspaces/:workspace/runtime/skills,但没有 runtime/extensions 对应路由 —— 所以扩展读取仍然会落到主运行时。这是已观测到的缺口,不是假设。不过要直说:Evidence 部分是文字描述(「可能通过主运行时解析」),不是实际抓取的前后对比。对功能补全来说可以接受,但一份双工作区的抓取记录能让 Reviewer 测试计划对非作者也可核验。

方向: 对齐。它完成的是本仓库已经合并的架构,而且是扩展既有形态而不是另起一套 —— 新路由与 skills runtime 路由对称,capability 挂在 #10593 建立的协调器上,调用的 SDK 方法(extensionCataloginstallUserExtensionupdateUserExtensionuninstallUserExtensioncheckUserExtensionUpdates)在 main 上已存在。参考 CHANGELOG:无直接命中,但这属于 daemon 内部实现,属预期。

规模: 触及核心路径 —— 跨四个包(acp-bridgeclisdk-typescriptweb-shell)。2276 行改动构成:

  • 生产逻辑 1335 行(14 个文件)
  • 测试 698 行(9 个文件)
  • 文档 243 行(5 个文件,含新增设计文档)

标题是 feat,因此不触发规模硬阻断。但 1335 行生产逻辑同时超过 500 行的维护者关注阈值和 1000 行的大 PR 建议阈值,所以提请维护者关注,并附上建议:如果其中有可独立发布的部分 —— 例如 ACP 的 workspaceExtensionsReconcile 控制方法,或 workspace_extension_mentions 背后的输入区 @/+ 接线 —— 拆分出来能让协调器改动被单独审查。仅供参考,不构成阻断。

方案: 就目标而言范围合理,向后兼容处理也很谨慎 —— 当运行时快照未被认证为最新时,mergeExtensionCatalog 会退化为「目录 + 投射」;旧版 daemon 在 feature flag 下保留原有主工作区流程。进入代码审查前有两点建议思考,均非阻断项:

  • 现在有三处各自手写同一套阶梯逻辑(检查 feature flag → ensureRuntimeworkspaceRuntimeExtensions):useComposerCore.tsScheduledTasksDialog.tsxExtensionsManagerPage.tsx。第一处无条件使用 workspaceByCwd(cwd);第二处则按 formWorkspace?.primary === false 分支去取主客户端。既然限定客户端同样能解析主工作区 cwd,这个分支看起来是可以省掉的复杂度 —— 而且到了三处,其实已经够格抽成共享 helper。
  • GET /workspaces/:workspace/extensions 现在会调用 observeExtensionGeneration,于是一个读路由会改动协调器状态(使 skills 与 MCP 就绪态失效)。设计文档说明这是有意的收敛行为,我也认为站得住脚 —— 只是提出来,免得维护者读该路由时感到意外。

风险: Stage 1e 命中 —— packages/cli/src/acp-integration/acpAgent.ts(及其测试)位于 acp-integration 高风险路径。审查深度为完整审查,并会在代码审查评论中点名沙箱验证通道。需要重点关注新增的 workspaceExtensionsReconcile ext-method 处理逻辑:它并行刷新每个活跃会话的 config,并把被跳过的会话视为协调硬失败,因此一个持续损坏的会话 config 会让该运行时的 extensions capability 一直无法进入 ready

进入代码审查 🔍

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I wrote my own proposal before opening the diff: put an extensions capability on the coordinator that #10593 landed, carry a desired/applied generation plus the runtime epoch, reconcile through an ACP control method, add workspace-qualified routes mirroring the skills runtime routes, and have the Web Shell talk to the selected workspace client. That is essentially what this PR does, and it does it by extending existing shapes — getWorkspaceRuntimeCoordinatorIfSupported, the V2 SDK methods already on main, the bridge's epoch stamping, and the workspace_skills_config_runtime feature-flag pattern. No parallel machinery. The certification chain is the part I most expected to be hand-wavy and it is not: the bridge stamps runtimeEpoch at request time (bridge.ts:6184), the coordinator re-reads the lifecycle snapshot after the catalog fetch and rejects on epoch change, drain, or a not-initialized catalog, and only then advances appliedExtensionGeneration. hasActiveWork() also picks up extensionsQueuedWork, so the foundation PR's lifecycle accounting covers the new async work rather than leaking around it.

Two things worth fixing, neither blocking on its own:

1. buildLocalExtensionsStatus(currentManager) writes the controller's shared cache with an externally-built manager. The new parameter skips the cache read and skips refreshCache(), but the function still writes extensionsStatusCache at the end, keyed on resolveExtensionLocale(boundWorkspace). The new caller is GET /extensions, which builds its manager as primaryController.createExtensionManager(boundWorkspace, /* trustedOverride */ true) — trust forced on. The existing no-arg caller (GET {base} at workspace-extensions.ts:847) builds its manager through deps.isWorkspaceTrusted?.(), so for an untrusted primary workspace it resolves a different locale (resolveExtensionLocale(dir, false) sets skipWorkspaceSettings: true) and a different isWorkspaceTrusted. The cache key does not distinguish those, so for up to 2s the trust-respecting route can be served entries built by the trust-forced manager. This is a consistency wart rather than an escalation — GET /extensions already hands the trusted catalog to any daemon client — but a caller asking for "use my manager, don't refresh" should not silently repopulate the controller's own cache. One line: only assign extensionsStatusCache when currentManager is undefined.

2. A config-only refresh failure reports failed: 0. In reconcileExtensionGeneration's catch path, failed is sessionsFailed + sessionsSkipped (or 1 when there is no refresh result). configsFailed is not counted. That is reachable: if the config that fails is the bootstrap config or workspaceMcpDiscoveryConfig and no session holds that object, attemptedSessions is the full set, so sessionsSkipped is 0 and every send succeeds — yielding { state: 'failed', refreshed: N, failed: 0, error: ... }. The controller then broadcasts refreshed: N, failed: 0 through broadcastExtensionsChanged for a reconciliation that actually failed. The warning path is unaffected (it keys off reconciliationError, which is always populated when state is failed), so this only misreports the event payload — but failed: 0 on a failed reconcile is the kind of thing that misleads someone debugging it later. Count configsFailed, or drop the counts from the failed path.

Things I checked and am not raising: the ?? false added in extensionIsActive is type-driven, not behavioural — ExtensionCatalogEntry.defaultActivation is required, so catalog entries never reach that fallback, and on the legacy path isActive was already required. The 2s retry timer for a starting/stale capability is cleared in the effect cleanup alongside loadRef and the request counter, so it cannot outlive the page or stack up across re-renders. uninstallUserExtension(selectedExtension.id, ...) is safe inside that dialog because the dialog only opens when uninstallName === selectedExtension.name. And the stale-request guard (loadRequestRef) correctly wraps every apply and the finally setLoading.

The three hand-rolled feature-flag ladders from my Stage 1 note stand as a question, not a finding — ScheduledTasksDialog's primary/non-primary branch is the one I would collapse.

sequenceDiagram
    participant P1 as Web Shell Extensions page
    participant P2 as WorkspaceDaemonClient
    participant P3 as serve extensions route
    participant P4 as WorkspaceRuntimeCoordinator
    participant P5 as AcpSessionBridge
    participant P6 as ACP agent runtime
    P1->>P2: extensionCatalog and workspaceExtensions (no ACP start)
    P2-->>P1: catalog generation plus activation projection
    P1->>P2: ensureRuntime
    P2->>P3: POST workspace runtime ensure
    P3->>P4: ensure
    P4->>P5: preheat when cold
    P4->>P5: invoke workspaceExtensionsReconcile
    P5->>P6: ext method control call
    P6-->>P5: config and session refresh counts
    P4->>P5: getWorkspaceExtensionsStatus
    P5-->>P4: live catalog stamped with runtimeEpoch
    P4->>P4: certify epoch and generation then mark ready
    P4-->>P1: runtime status carrying extensions capability
    P1->>P2: workspaceRuntimeExtensions
    P2-->>P1: live details merged only when certified current
Loading
Files changed (20 of 28 shown)
File What changed
packages/cli/src/serve/workspace-runtime-coordinator.ts The core of the PR. Adds desired/applied extension generation, an extensions capability status, a serialized extensions work queue counted into hasActiveWork, observeExtensionGeneration, reconcileExtensionGeneration, and invalidateDerivedCapabilities which also stales skills and MCP.
packages/cli/src/serve/routes/workspace-extensions-controller.ts Mutation path now prefers the coordinator over the legacy bridge refresh, reports reconciliation errors as warnings, and buildLocalExtensionsStatus accepts an injected manager (finding 1). The rest is that function being un-nested, which inflates the line count without changing logic.
packages/cli/src/serve/routes/workspace-extensions.ts Splits applied-generation tracking into legacy vs coordinator-owned, teaches the generation poller to read coordinator state, adds GET /workspace/runtime/extensions and GET /workspaces/:workspace/runtime/extensions, and sources the V2 catalog response from buildLocalExtensionsStatus.
packages/cli/src/acp-integration/acpAgent.ts New workspaceExtensionsReconcile handler — high-risk path. Dedupes configs across bootstrap, MCP discovery and active sessions, refreshes each in parallel, skips sessions whose config failed, and reports per-stage counts.
packages/cli/src/serve/capabilities.ts Registers workspace_extensions_config_runtime and workspace_extension_mentions, both conditional on workspaceRuntimeAvailable.
packages/acp-bridge/src/status.ts Adds the extensions capability status type, the reconcile control method name, runtimeEpoch on the extensions status, and the refresh-result shape.
packages/acp-bridge/src/bridge.ts One line, and load-bearing — adds workspaceExtensions to the methods whose responses get stamped with the request-time runtimeEpoch.
packages/sdk-typescript/src/daemon/DaemonClient.ts workspaceRuntimeExtensions() on both the primary and workspace-qualified clients.
packages/sdk-typescript/src/daemon/types.ts Mirrors the new capability and runtimeEpoch fields.
packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx Largest UI change. Splits load into catalog-plus-projection first, then runtime merge only when certified, with stale-request and retry-timer handling; switches install/update/uninstall/update-check to the V2 global routes; threads workspaceCwd and workspaceControl props.
packages/web-shell/client/components/extensions/extensions-manager-logic.ts New extensionSnapshotsCurrent certification predicate, mergeExtensionCatalog, and the widened ManagedExtensionEntry type — extracted as pure functions, which is why they are testable.
packages/web-shell/client/components/plugins/PluginManagerPage.tsx Shows the workspace selector on the extensions tab and keys the page on the selected cwd so switching workspaces remounts.
packages/web-shell/client/hooks/useComposerCore.ts Overrides loadExtensionsStatus for the @ menu to use the selected workspace runtime when the feature is advertised.
packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx Same ladder for the scheduled-task reference picker, with a primary/non-primary branch (see the simplification note).
workspace-runtime-coordinator.test.ts + 8 more test files 698 test lines. The coordinator tests are the substantive ones — they pin reconcile-without-a-session, defer-until-ensured, epoch-change staleness, uninitialized-catalog failure, drain-interruption replay, and skills/MCP continuing after an extension failure. workspace-qualified-extensions.test.ts additionally asserts the secondary runtime was read and the primary was not, which is the no-fallback property that matters most here.
docs/design/daemon-workspace-runtime-extensions.md + 4 more docs files 243 lines. New design doc stating ownership, reconciliation and the Web Shell contract; protocol and SDK docs updated for the new routes and fields.

Testing

This is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI, read through the API for f9311625af64f960010db2f68d3aaedaad83e2fd. No check has failed (0 failure conclusions), but the suite is nowhere near settled: 16 success, 13 skipped, 8 still in progress. The ones that matter most for this diff are all still running — Test (ubuntu-latest, Node 22.x) (the unit suite that would actually execute the new coordinator and Web Shell tests), Lint & Static, Serve A/B, and Integration Tests (no-AK, No Sandbox). So there is no green unit-suite result to cite yet, and I am not going to guess one. Test (macos-latest) and Test (windows-latest) are skipped, which is worth a maintainer's eye given the PR itself only verified on macOS.

Not verified: the runtime behaviour. Nothing in the diff or the CI state so far demonstrates a live two-workspace daemon actually serving extension skills from the secondary runtime. The author's own report says browser-driven E2E was not run and Windows/Linux were not exercised locally — that is the author's claim about what they did, not evidence about what the code does, and I am not treating it as either.

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

Check Conclusion
Integration Tests (no-AK, No Sandbox) ❌ failure
Serve A/B (ubuntu-latest, Node 22.x) 🚫 cancelled
Test (ubuntu-latest, Node 22.x) 🚫 cancelled
web-shell E2E Smoke (ubuntu-latest, Node 22.x) 🚫 cancelled
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Live Host (macos-latest) ✅ success
macos-latest / Java 21 ✅ success
OpenTUI no-flicker gate ✅ success
Real daemon E2E / Java 11 ✅ success
TUI parity snapshots (ink vs opentui) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success

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

Sandboxed verification would settle this: @qwen-code /verify — that a selected secondary workspace actually serves extension-provided skills and commands, and that neither the new runtime/extensions routes nor the composer loaders fall back to the primary runtime, is not observable from the diff. The no-fallback property is asserted against mocks in workspace-qualified-extensions.test.ts, which pins the route's intent but not the daemon's behaviour end to end, and the unit suite has not finished. @qwen-code /tmux is the better lane for the Web Shell half — the Extensions page now renders from the global catalog before the runtime merge lands, so the intermediate state (missing path, empty capability lists) and the workspace-selector behaviour on tab switch are exactly what a real terminal capture would show and what no unit test here covers. This PR touches the acp-integration high-risk path, so naming a lane is least optional here.

中文说明

代码审查

我在看 diff 之前先写了自己的方案:把 extensions capability 挂在 #10593 落地的协调器上,携带 desired/applied generation 与 runtime epoch,通过 ACP 控制方法协调,新增与 skills runtime 路由对称的工作区限定路由,Web Shell 改为与选中的工作区客户端通信。这个 PR 基本就是这么做的,而且是扩展既有形态 —— 复用了 getWorkspaceRuntimeCoordinatorIfSupportedmain 上已有的 V2 SDK 方法、bridge 的 epoch 打标,以及 workspace_skills_config_runtime 的 feature-flag 模式,没有另起一套机制。我原以为最容易含糊的认证链条并不含糊:bridge 在请求时打上 runtimeEpochbridge.ts:6184),协调器在取回 catalog 之后重新读取生命周期快照,遇到 epoch 变化、drain 或 catalog 未初始化就拒绝,只有全部通过才推进 appliedExtensionGenerationhasActiveWork() 也纳入了 extensionsQueuedWork,所以基础 PR 的生命周期计账覆盖了新的异步工作,而不是从旁边漏掉。

有两处值得修,单独看都不构成阻断:

1. buildLocalExtensionsStatus(currentManager) 会用外部传入的 manager 写入控制器共享缓存。 新参数跳过了缓存读取refreshCache(),但函数末尾仍然写入 extensionsStatusCache,键为 resolveExtensionLocale(boundWorkspace)。新的调用方是 GET /extensions,它以 primaryController.createExtensionManager(boundWorkspace, /* trustedOverride */ true) 构造 manager —— 强制信任开启。而既有的无参调用方(workspace-extensions.ts:847GET {base})走 deps.isWorkspaceTrusted?.(),因此当主工作区不受信任时,它解析出的 locale 不同(resolveExtensionLocale(dir, false) 会设置 skipWorkspaceSettings: true),isWorkspaceTrusted 也不同。缓存键并不区分这两种情况,于是在最长 2 秒内,尊重信任状态的路由可能拿到由强制信任的 manager 构造出的条目。这是一致性瑕疵而非权限提升 —— GET /extensions 本来就会把受信任目录交给任何 daemon 客户端 —— 但一个只想「用我的 manager、不要刷新」的调用方不应该悄悄重填控制器自己的缓存。一行即可:仅当 currentManager 为 undefined 时才写 extensionsStatusCache

2. 仅有 config 刷新失败时会上报 failed: 0 reconcileExtensionGeneration 的 catch 分支里,failedsessionsFailed + sessionsSkipped(无刷新结果时取 1),没有计入 configsFailed。这是可达的:如果失败的是 bootstrap config 或 workspaceMcpDiscoveryConfig,且没有会话持有该对象,那么 attemptedSessions 就是全集,sessionsSkipped 为 0 且所有发送都成功 —— 结果是 { state: 'failed', refreshed: N, failed: 0, error: ... }。控制器随后会通过 broadcastExtensionsChanged 广播 refreshed: N, failed: 0,而这次协调其实是失败的。告警路径不受影响(它依据 reconciliationError,而 state 为 failed 时该字段必然有值),所以这只是事件负载报错 —— 但一次失败的协调上报 failed: 0,正是日后排查时会误导人的那种东西。建议计入 configsFailed,或在失败路径上去掉这些计数。

以下是我核查过但不提为问题的:extensionIsActive 中新增的 ?? false 是类型驱动的,不是行为变更 —— ExtensionCatalogEntry.defaultActivation 是必填,目录条目永远走不到该回退;旧路径上 isActive 本来就是必填。针对 starting/stale 能力的 2 秒重试定时器,在 effect 清理中与 loadRef 和请求计数器一起被清除,因此不会比页面存活更久,也不会在重渲染间累积。uninstallUserExtension(selectedExtension.id, ...) 在该对话框内是安全的,因为对话框仅在 uninstallName === selectedExtension.name 时打开。陈旧请求防护(loadRequestRef)也正确包裹了每次 apply 以及 finally 中的 setLoading

Stage 1 提到的三处手写 feature-flag 阶梯仍然是疑问而非结论 —— 我会优先收敛 ScheduledTasksDialog 里的主/非主分支。

测试

这是无人值守的 CI 运行,因此我没有构建或执行本 PR 的任何代码 —— 下面的证据是 PR 自身的 CI,通过 API 针对 f9311625af64f960010db2f68d3aaedaad83e2fd 读取。没有任何检查失败failure 结论为 0),但整套远未跑完:16 项成功、13 项跳过、8 项仍在进行。对这个 diff 最关键的几项都还在跑 —— Test (ubuntu-latest, Node 22.x)(真正执行新增协调器与 Web Shell 测试的单元测试套件)、Lint & StaticServe A/BIntegration Tests (no-AK, No Sandbox)。所以目前没有一个可以引用的绿色单元测试结果,我也不会去猜。Test (macos-latest)Test (windows-latest)skipped,考虑到 PR 自身仅在 macOS 上验证过,这一点值得维护者留意。

未验证:运行时行为。diff 和目前的 CI 状态都没有证明一个真实的双工作区 daemon 确实能从次级运行时提供扩展技能。作者自己的报告说明未执行浏览器驱动 E2E,也未在 Windows/Linux 本地运行 —— 那是作者关于自己做了什么的陈述,不是关于代码实际行为的证据,我两者都不当作证据。

沙箱验证可以定论:@qwen-code /verify —— 选中的次级工作区是否真的能提供扩展带来的技能与命令,以及新的 runtime/extensions 路由和输入区加载器是否都不会回退到主运行时,从 diff 上看不出来。无回退这一属性在 workspace-qualified-extensions.test.ts 中是针对 mock 断言的,它固定了路由意图但没有端到端固定 daemon 行为,而单元测试套件尚未跑完。Web Shell 那一半更适合 @qwen-code /tmux —— Extensions 页面现在会在运行时合并完成之前先按全局目录渲染,因此中间状态(缺失 path、能力列表为空)以及切换标签页时工作区选择器的行为,正是真实终端截取能展示、而此处没有任何单元测试覆盖的部分。本 PR 触及 acp-integration 高风险路径,所以点名验证通道在这里最不该省略。

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 4913ac9. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 5 render-shaping files:

  • packages/web-shell/client/components/AtMentionPanel.tsx
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx
  • packages/web-shell/client/components/plugins/PluginManagerPage.tsx
  • packages/web-shell/client/i18n.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the review itself came back clean apart from two small fixes, but a 1335-production-line change spanning four packages is over the size threshold where this gate hands the call to a maintainer rather than approving it, and the unit suite has not finished.

Going back to the proposal I wrote before opening the diff: this PR matches it, and beats it on the part I would most likely have got wrong. I said "carry a generation and an epoch and reject stale results" — the implementation actually does that in two independent places, stamping the epoch at bridge request time and then re-reading the lifecycle snapshot after the catalog comes back, so a runtime replaced mid-reconcile cannot advance readiness either way. It also folds the new async work into the lifecycle accounting that #10593 built instead of running around it. That is the difference between a change that looks right and one that fails closed.

Does it solve something users care about? Yes, and it is concrete rather than architectural self-indulgence: today a selected secondary workspace can miss extension-provided skills entirely, because there is no runtime/extensions route at all on main while runtime/skills has existed for a while. Is the code straightforward? Mostly. The coordinator additions follow the established skills/MCP shape closely enough that reading one tells you how the other works, which is the strongest argument for merging it. Is every change necessary? I went looking for drive-by churn and did not find any — the large line counts in workspace-extensions-controller.ts and ExtensionsManagerPage.tsx are mostly a function being un-nested and a backward-compat branch being added, not new surface. In six months I would thank the author, not curse them: the merge and certification logic is extracted as pure functions with their own tests, and there is a design doc that states the contract.

So why not approve? Three reasons, in order of weight:

  1. Policy. Stage 0 escalated this for maintainer awareness — 1335 production lines across acp-bridge, cli, sdk-typescript and web-shell, over both the 500-line escalation threshold and the 1000-line large-PR advisory. That cap is about breadth of core surface, not about doubt in this diff, and an escalated PR does not carry a deferred-approval marker. So there is no approval pending on CI here; a human has to make this call.
  2. CI is unfinished. Test (ubuntu-latest, Node 22.x), Lint & Static, Serve A/B, Integration Tests (no-AK, No Sandbox), Real daemon E2E and Capture web-shell visuals were all still in progress at the time of writing, with 0 failures so far. Approving now would attest to a unit-suite result that does not exist yet. Note also that Test (macos-latest) and Test (windows-latest) are skipped on this run while the PR was only verified locally on macOS.
  3. The central claim is not demonstrated end to end. That a secondary workspace serves extension skills without falling back to the primary runtime is asserted against mocks. That is a good test — it pins the route's intent, including an explicit assertion that the primary was never consulted — but it is not the daemon behaving that way. @qwen-code /verify and @qwen-code /tmux are both named in my previous comment with the specific gap each would close.

The two findings from the review are real but small, and I would not hold the PR on either alone: the injected-manager path repopulating the controller's own 2-second cache (one-line fix), and a config-only refresh failure reporting failed: 0 in the broadcast event. Neither is a correctness hole in the reconciliation itself.

One process note: I could not resolve an owner to hand this to. The deterministic resolver found no matching area because this PR carries no labels, and there is no human review to fall back on, so I am not going to guess a login or assign it to someone who did not opt in. A maintainer picking this up — or adding the relevant area label and re-running /triage — is what unblocks it.

⏸️ Deferring to a maintainer. Nothing here looks wrong to me; it is too broad for this gate to sign off on its own, and CI has not landed.

中文说明

Confidence: 3/5 —— 审查本身除了两处小修改之外是干净的,但一个跨四个包、1335 行生产逻辑的改动超过了本关卡应交回维护者判断的规模阈值,而且单元测试套件尚未跑完。

回到我在看 diff 之前写的方案:这个 PR 与之一致,并且在我最可能做错的那部分做得比我更好。我当时说的是「携带 generation 与 epoch,并拒绝陈旧结果」—— 实现实际上在两个相互独立的位置做到了这一点:在 bridge 请求时打上 epoch,并在 catalog 返回之后重新读取生命周期快照,因此协调过程中被替换的运行时无论走哪条路径都无法推进就绪态。它还把新的异步工作纳入了 #10593 建立的生命周期计账,而不是绕开它。这正是「看起来对」与「失败即封闭」之间的区别。

它是否解决了用户在意的问题?是,而且很具体,不是架构上的自我满足:今天一个被选中的次级工作区可能完全拿不到扩展提供的技能,因为 main 上根本没有 runtime/extensions 路由,而 runtime/skills 早已存在。代码是否直白?大体是。协调器的新增部分与既有的 skills/MCP 形态高度一致,读懂其中一个就知道另一个怎么运作,这是支持合并的最有力理由。每处改动是否必要?我专门找过顺手夹带的改动,没有找到 —— workspace-extensions-controller.tsExtensionsManagerPage.tsx 的大量行数主要来自一个函数被拆出嵌套,以及新增了一条向后兼容分支,而不是新增表面积。半年后我会感谢作者而不是埋怨:合并与认证逻辑被抽成带独立测试的纯函数,而且有一份说明契约的设计文档。

那为什么不批准?三个理由,按权重排列:

  1. 策略。 Stage 0 已将本 PR 提请维护者关注 —— 跨 acp-bridgeclisdk-typescriptweb-shell 的 1335 行生产逻辑,同时超过 500 行的升级阈值和 1000 行的大 PR 建议阈值。这个上限针对的是核心表面积的广度,而不是对本 diff 的怀疑,而且被升级的 PR 不携带延迟批准标记。所以这里没有一个在等 CI 的批准;这个判断必须由人来做。
  2. CI 未完成。 撰写时 Test (ubuntu-latest, Node 22.x)Lint & StaticServe A/BIntegration Tests (no-AK, No Sandbox)Real daemon E2ECapture web-shell visuals 均仍在进行,目前 0 失败。此刻批准等于为一个尚不存在的单元测试结果背书。另请注意本次运行中 Test (macos-latest)Test (windows-latest) 被跳过,而 PR 仅在 macOS 上做过本地验证。
  3. 核心主张未被端到端证明。 「次级工作区能提供扩展技能且不回退到主运行时」是针对 mock 断言的。那是个好测试 —— 它固定了路由意图,包括一条明确断言主运行时从未被查询 —— 但那不等于 daemon 真的这样运行。上一条评论中已点名 @qwen-code /verify@qwen-code /tmux,并各自说明了能补上的具体缺口。

审查中的两处发现是真实的但都很小,任何一处单独都不足以压住这个 PR:注入 manager 的路径会重填控制器自己的 2 秒缓存(一行即可修复),以及仅有 config 刷新失败时在广播事件里上报 failed: 0。两者都不是协调逻辑本身的正确性漏洞。

一个流程说明:我无法解析出接手人。确定性解析器没有找到匹配的区域,因为本 PR 没有任何标签,也没有可回退的人工 review,所以我不会去猜一个登录名、也不会把 PR 指派给没有主动参与的人。需要一位维护者接手 —— 或者补上相应区域标签后重跑 /triage —— 才能解开这个阻塞。

⏸️ 转交维护者处理。在我看来这里没有错的东西;只是范围太广,本关卡不宜独自签署,而且 CI 尚未落地。

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

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

钉萁 added 2 commits September 5, 2026 20:21
…e-runtime-extensions-main

# Conflicts:
#	docs/developers/qwen-serve-protocol.md
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 4913ac9, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

capabilities

field PR base (before) this PR (after)
features[] "workspace_extensions_config_runtime"
features[] "workspace_extension_mentions"

Qwen Code · serve A/B

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

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • the shared extensions status cache is written from a trust-forced manager, so GET /workspace/extensions can serve workspace-localized entries for an untrusted primary — already reported (issue comment 5550758390, item 1; still stands at 949…
  • the reconcile failed count omits configsFailed, reporting failed: 0 on a config-only refresh failure — already reported (issue comment 5550758390, item 2; still stands at 9493fa6, probe-confirmed)

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh predates 2.72.0, so closingIssuesReferences is unavailable); the set is UNKNOWN, not empty. Agent 0 fetched the PR-named #10593 and replayed the narrated incident instead, and root-cause ownership was ruled from the PR own narrative..

Not explored to full depth (tool budget reached): chunk 2: design-doc claim daemon-workspace-runtime-extensions.md item 4 ("shows the workspace selector on the list page and the disabled selector in detail view") — I …; "agent reverse-audit (round 2)": what buildLocalExtensionsStatus() actually returns for an untrusted primary (i.e. whether the pre-diff trust-free GET /workspace/extensions listed any isAc…; "agent reverse-audit (round 2)": whether the ACP child can answer SERVE_STATUS_EXT_METHODS.workspaceExtensions with initialized: false while its channel is live (which would widen Finding 2…; "agent reverse-audit (round 1)": did not read diff lines 387-429 line by line — the re-flowed conditional-serve-features table tail of qwen-serve-protocol.md ; I verified the two added rows …; "agent reverse-audit (round 1)": did not independently count SERVE_CAPABILITY_REGISTRY keys / CONDITIONAL_SERVE_FEATURES entries to confirm the literal numbers 159 and 47; I verified only t…, and 16 more.

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

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh predates 2.72.0, so closingIssuesReferences is unavailable); the set is UNKNOWN, not empty. Agent 0 fetched the PR-named #10593 and replayed the narrated incident instead, and root-cause ownership was ruled from the PR own narrative..

未探索到全部深度(达到工具调用预算):chunk 2:design-doc claim daemon-workspace-runtime-extensions.md item 4 ("shows the workspace selector on the list page and the disabled selector in detail view") — I …"agent reverse-audit (round 2)"what buildLocalExtensionsStatus() actually returns for an untrusted primary (i.e. whether the pre-diff trust-free GET /workspace/extensions listed any isAc…"agent reverse-audit (round 2)"whether the ACP child can answer SERVE_STATUS_EXT_METHODS.workspaceExtensions with initialized: false while its channel is live (which would widen Finding 2…"agent reverse-audit (round 1)"did not read diff lines 387-429 line by line — the re-flowed conditional-serve-features table tail of qwen-serve-protocol.md ; I verified the two added rows …"agent reverse-audit (round 1)"did not independently count SERVE_CAPABILITY_REGISTRY keys / CONDITIONAL_SERVE_FEATURES entries to confirm the literal numbers 159 and 47; I verified only t…,另有 16 条。

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

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

Comment thread packages/cli/src/acp-integration/acpAgent.ts
Comment thread packages/cli/src/serve/routes/workspace-extensions-controller.ts Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts
Comment thread packages/web-shell/client/components/extensions/extensions-manager-logic.ts Outdated
...configuredEntry,
...live,
updateState: configuredEntry.updateState,
isActive:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The merge computes the selected workspace's effective activation into isActive, but the only reader short-circuits on defaultActivation — which in split mode is always defined — so the newly fetched authoritative per-workspace value is dead data on new surface.

extensionIsActive (ExtensionsManagerPage.tsx:156-166) reads workspaceActivation (when present and not 'inherit'), then defaultActivation, then isActive. ExtensionCatalogEntry.defaultActivation is a required field and GET /extensions always sets it (policy?.defaultActivation ?? 'enabled'), so in split mode the third branch is unreachable and the merged isActive is never consulted; every badge goes through extensionIsActive. For an extension disabled by a legacy path rule the projection reports workspaceActivation: null with effectiveActivation: 'disabled', so the badge reads the user-scope default. Two arms genuinely regress against the merge base: when the projection was unavailable pre-diff (.catch(() => null)) or had no matching entry, DaemonExtensionEntry carried no defaultActivation and the badge fell through to the real isActive — correct — whereas the catalog now always supplies one; and when activationCurrent is false the projection fields are dropped entirely, so a real workspaceActivation override disappears from the badge too. For a secondary workspace the page has no pre-diff comparison at all, so the wrong badge there is new surface.

Witness:
probe with the real exported mergeExtensionCatalog and a transcription of the nine-line display rule evaluated beside the merge-base transcription (git show 82612e3584), using the finding's own fixture (user default enabled, effectiveActivation: 'disabled', activationSource: 'legacy_path_rule', live runtime isActive: false):

groundTruth_effectiveActivation: "disabled"
converged:                 {"isActive":false,"defaultActivation":"enabled","workspaceActivation":"inherit","badge":"Enabled"}
baseWithProjectionEntry:   {"isActive":false,"defaultActivation":"enabled","badgeAtBase":"Enabled"}
baseWithoutProjectionEntry:{"isActive":false,"defaultActivation":"(absent)","badgeAtBase":"Disabled"}

The third line is why the filed Critical/regression framing was downgraded: in the normal pre-diff case the badge already read "Enabled" over a disabled ground truth, so that arm is pre-existing behaviour in unchanged code.

Make the third branch reachable in split mode so the new runtime read earns its keep: prefer the merged isActive when the runtime leg converged — reorder to workspaceActivation -> (converged) isActive -> defaultActivation — or have mergeExtensionCatalog omit defaultActivation from a merged row whose runtime leg was certified.

The fix must not violate an existing fact: isActive: boolean; is required on DaemonExtensionEntry (packages/sdk-typescript/src/daemon/types.ts:4750) while ManagedExtensionEntry widened it to optional, so the ?? false arm at ExtensionsManagerPage.tsx:165 must stay for catalog-only rows; and the merge must not start preferring a stale runtime isActive, which is what runtimeCurrent certification exists to prevent.

Acceptance criterion: A case in extensions-manager-logic.test.ts plus a page-level assertion that a converged merge with effectiveActivation: 'disabled' and a user defaultActivation: 'enabled' renders Disabled; it must go red when the precedence is restored to defaultActivation first. Please apply the fix, then remove it again and confirm that test goes red.

中文说明

合并逻辑把所选工作区的有效激活计算进 isActive,但唯一的读取方会在 defaultActivation 上短路——而 split 模式下它总是有值——于是新获取的、权威的单工作区值成了新表面上的死数据。extensionIsActive 依次读取 workspaceActivation(存在且不为 inherit 时)、defaultActivation、最后才是 isActiveExtensionCatalogEntry.defaultActivation 是必填字段,GET /extensions 也总会设置它,所以 split 模式下第三个分支不可达,合并出的 isActive 永不被查阅,而所有徽标都经过 extensionIsActive。对一个被 legacy path rule 禁用的扩展,投影上报 workspaceActivation: nulleffectiveActivation: 'disabled',徽标于是读用户级默认值。相较合并基线确实退化的有两处:改动前当投影不可用(.catch(() => null))或没有匹配条目时,DaemonExtensionEntry 不带 defaultActivation,徽标会落到真实的 isActive——那是正确的——而现在目录总会提供它;以及当 activationCurrent 为假时投影字段被整体丢弃,一个真实的 workspaceActivation 覆盖也会从徽标上消失。对次级工作区而言页面根本没有改动前的可比状态,所以那里的错误徽标属于新表面。建议让第三个分支在 split 模式下可达,使新的运行时读取真正发挥作用:把顺序改为 workspaceActivation →(已收敛时的)isActivedefaultActivation,或让 mergeExtensionCatalog 在运行时那一腿已认证时从合并行中省略 defaultActivation

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

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.

Valid — the isActive computation is dead data (no component reads it; the page recomputes from defaultActivation/workspaceActivation). Deferred to the next round; the behavior will be pinned by tests.

中文说明

成立——isActive 的计算是死数据(没有组件读取;页面用 defaultActivation/workspaceActivation 重新计算)。顺延至下一轮;届时将用测试固定该行为。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in 0e3dbea201. This remains Suggestion-level and is deferred to a follow-up: this PR has passed the review-round convergence limit, so the latest pass was limited to correctness and regression fixes. Leaving this thread open.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against pushed head c80017edd0 (repairs in c031c91474).

Partially fixed: when the projection is missing/non-current, extensionIsActive now reads live isActive before the global default. The inherited/legacy-path-rule badge precedence described in the review remains pre-existing behavior and has not been redesigned. Keeping this thread open for that remaining presentation issue rather than treating the whole suggestion as fixed.

expect(
container
.querySelector('[data-testid="extensions-page"]')
?.getAttribute('data-workspace'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new Extensions test's fixture makes the default selection equal the primary workspace, so it cannot distinguish selectedWorkspaceCwd from workspace.workspaceCwd and the retargeting plus the cwd-keyed remount this PR adds for that tab are unpinned.

The fixture sets workspaceState.workspaceCwd = '/work/a' with a trusted, so defaultWorkspaceCwd (PluginManagerPage.tsx:49-55) resolves selectedWorkspaceCwd to /work/a — the same value workspace.workspaceCwd already has. Replacing the wiring workspaceCwd={splitExtensionsRuntimeAvailable ? selectedWorkspaceCwd : workspace.workspaceCwd} with plain workspace.workspaceCwd, or dropping selectedWorkspaceCwd from the key, keeps the test green: a regression that leaves the extensions page on the primary workspace while the selector shows B ships undetected. The Skills suite avoids this by marking a untrusted so the default resolves to /work/b (:139-141, asserting /work/b at :180-183), and the MCP suite switches the selector to Secondary and re-asserts (:302-323); the extensions tab — the tab this PR is about — does neither.

Witness:
mutation with a positive control in the same tree:

INTACT: Test Files 1 passed | Tests 5 passed (5)
MUTANT (both selectedWorkspaceCwd uses removed): Test Files 1 passed | Tests 5 passed (5)   <- still green
CONTROL (workspaceCwd={'/MUTATED-PRIMARY'}):      x expected '/MUTATED-PRIMARY' to be '/work/a'

Either mark /work/a untrusted in this describe's fixture (so the default becomes /work/b and the assertion differs from workspace.workspaceCwd), or add a step that opens the combobox, selects /work/b, and asserts data-workspace becomes /work/b — mirroring the MCP test at :302-323.

The fix must not violate an existing fact: defaultWorkspaceCwd prefers the entry matching workspace.workspaceCwd when trusted — packages/web-shell/client/components/plugins/PluginManagerPage.tsx:49-55 — so a discriminating fixture must either mark that entry untrusted or drive the selector explicitly.

Acceptance criterion: The strengthened assertion must go red when workspaceCwd={splitExtensionsRuntimeAvailable ? selectedWorkspaceCwd : workspace.workspaceCwd} collapses to workspace.workspaceCwd, and when the key drops -${selectedWorkspaceCwd ?? ''}. Please apply the fix, then remove it again and confirm that test goes red.

中文说明

新的扩展标签页测试的 fixture 让默认选中项等于主工作区,因此它无法区分 selectedWorkspaceCwdworkspace.workspaceCwd,本 PR 为该标签页加入的重定向与按 cwd 键的重挂载都未被固定。fixture 设置 workspaceState.workspaceCwd = '/work/a'a 受信任,于是 defaultWorkspaceCwdselectedWorkspaceCwd 解析为 /work/a——与 workspace.workspaceCwd 同值。把接线 workspaceCwd={splitExtensionsRuntimeAvailable ? selectedWorkspaceCwd : workspace.workspaceCwd} 换成裸的 workspace.workspaceCwd,或从 key 中去掉 selectedWorkspaceCwd,测试仍然全绿:一个让扩展页停留在主工作区、而选择器显示 B 的回归会不被察觉地进入仓库。Skills 套件通过把 a 标为未受信任来避免这一点(默认解析为 /work/b),MCP 套件通过切换选择器再断言来避免;而本 PR 真正涉及的扩展标签页两者都没有做。建议要么在该 describe 的 fixture 中把 /work/a 标为未受信任(使默认值变成 /work/b,断言因此不同于 workspace.workspaceCwd),要么补一步打开下拉、选择 /work/b 并断言 data-workspace 变为 /work/b,照 MCP 测试的写法。

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

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.

Valid — the PluginManagerPage fixture equals the primary workspace, so the test cannot catch a regression that ignores workspaceCwd. Deferred to the next round; the fixture will move off the primary.

中文说明

成立——PluginManagerPage 的 fixture 与主工作区相同,无法捕获忽略 workspaceCwd 的回归。顺延至下一轮;fixture 将改为非主工作区。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in 0e3dbea201. This remains Suggestion-level and is deferred to a follow-up: this PR has passed the review-round convergence limit, so the latest pass was limited to correctness and regression fixes. Leaving this thread open.

const workspaceRuntimeExtensions = vi.fn(async () => ({
extensions: [] as never[],
}));
const workspaceByCwd = vi.fn(() => ({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The only tests for the new composer Extension loader cannot fail for the two properties they exist to pin — that the catalog comes from the selected workspace's client, and that the runtime is ensured before the catalog is read.

workspaceByCwd returns the same stub for every cwd, and useComposerCore.ts:1577 already calls workspace.client.workspaceByCwd(atWorkspaceCwd) in the render body for globWorkspace/listDirectory, so expect(workspaceByCwd).toHaveBeenCalledWith('/secondary') is satisfied by pre-existing code and proves nothing about the new loader. Nothing asserts ordering or the returned value (both stubs resolve { extensions: [] }). Rewriting the override to resolve the client from workspace.workspaceCwd ?? atWorkspaceCwd and to drop the await before the read keeps both new tests green, while in production the composer would read the live catalog before the runtime is prepared (an empty list, cached for the whole menu session) and resolve the client from the wrong workspace — exactly the primary/selected confusion this change exists to remove.

Witness:
mutation with a discriminating probe (per-cwd clients, workspaceCwd: '/primary' on the workspace object, ensure held behind a gate):

BASELINE: 66/66 green; under the mutant 66/66 STILL green (the 1 failure is the probe)
INTACT probe: order while ensure pending ["ensure:/secondary"] -> after release ["ensure:/secondary","catalog:/secondary"]
MUTANT probe: order while ensure pending ["ensure:/primary","catalog:/primary"]
              -> expected [ 'ensure:/primary', ...(1) ] to deeply equal [ 'ensure:/secondary' ]

Key the stub by cwd (vi.fn((cwd) => ({ cwd, ensureRuntime, workspaceRuntimeExtensions }))), assert the resolved catalog is the one tagged /secondary, assert sequencing with expect(ensureRuntime.mock.invocationCallOrder[0]).toBeLessThan(workspaceRuntimeExtensions.mock.invocationCallOrder[0]), and give the two stubs distinguishable payloads so the pass-through is pinned.

The fix must not violate an existing fact: useComposerCore.ts:1577 calls workspace.client.workspaceByCwd(atWorkspaceCwd) in the render body for the glob/dirList actions, so a toHaveBeenCalledWith assertion on that mock is satisfied by pre-existing code — the new assertion must key on the returned object's identity, not the call argument.

Acceptance criterion: The strengthened test must go red under the mutation named above (wrong-cwd client, or the read fired without awaiting ensure); today it does not. Please apply the fix, then remove it again and confirm that test goes red.

中文说明

新的输入区扩展加载器只有这两个测试,而它们对各自要固定的两个性质都无法失败——目录来自所选工作区的客户端,以及运行时在读取目录之前被 ensure。workspaceByCwd 对每个 cwd 返回同一个 stub,而 useComposerCore.ts:1577 在渲染体中就已经为 globWorkspace/listDirectory 调用了 workspace.client.workspaceByCwd(atWorkspaceCwd),所以 expect(workspaceByCwd).toHaveBeenCalledWith('/secondary') 由既有代码就能满足,对新加载器毫无证明力。也没有任何断言检查顺序或返回值(两个 stub 都解析为 { extensions: [] })。具体地:把该覆写改成从 workspace.workspaceCwd ?? atWorkspaceCwd 解析客户端、并在读取前不 await ensure,两个新测试仍然全绿,而生产中输入区会在运行时准备好之前读取 live 目录(得到空列表,并在整个菜单会话中被缓存)、并从错误的工作区解析客户端——正是本次改动要消除的主/选中混淆。建议按 cwd 键化 stub(vi.fn((cwd) => ({ cwd, ensureRuntime, workspaceRuntimeExtensions }))),断言解析到的目录是标记为 /secondary 的那一个,用 invocationCallOrder 断言先后顺序,并让两个 stub 返回可区分的内容以固定透传。

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

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.

Valid — the composer extension loader tests register capabilities that short-circuit before the code under test. Deferred to the next round; positive witnesses will be added.

中文说明

成立——composer 扩展加载器测试注册的能力会在被测代码之前短路。顺延至下一轮;届时将补充正向见证。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in 0e3dbea201. This remains Suggestion-level and is deferred to a follow-up: this PR has passed the review-round convergence limit, so the latest pass was limited to correctness and regression fixes. Leaving this thread open.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against pushed head c80017edd0 (repairs in c031c91474).

Additional readiness/error/trust/retry cases exist, but the original happy-path fixture still does not prove every requested per-client identity and pending-ensure ordering property. No claim that the mutation witness is fully covered. Leaving this test-strengthening suggestion open.

) === true
? {
async loadExtensionsStatus() {
await client.ensureRuntime();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The composer's @ext: / + Extension loader awaits a mutating POST .../runtime/ensure on a keystroke-driven read path, drops the withActionTimeout bound every sibling workspace action uses, propagates no abort, is re-issued on every menu open, and turns a rejection into a silently empty list.

Typing @ resets the provider cache (AddMenu.tsx:321-324; useAtMentionMenu.close() at :503; the workspaceKey effect at :476), so each open re-issues the ensure. On a workspace whose runtime is not live, the Extensions pane sits at loading: true with zero items for the ensure round trip — the client budget is 62 s (WORKSPACE_RUNTIME_ENSURE_TIMEOUT_MS = 60_000 + 2_000, DaemonClient.ts:419-423) against the legacy path's bounded 30 s withActionTimeout(client.workspaceExtensions(), ...) (client/daemon/workspace/actions.ts:538-544) — and the menu's abort.signal cannot cancel it because neither SDK method takes a signal (useAtMentionSources.ts:496 checks signal.aborted only after the await). When ensure rejects, createExtensionProvider.search rethrows and loadItems's catch sets items: [] with only a console.warn (useAtMentionMenu.ts:680-698); because the .catch at useAtMentionSources.ts:492-495 clears the cached promise on rejection, every subsequent debounced keystroke fires another ensure POST. The Skills equivalent in this same composer does the opposite: it reads the config catalog first so the menu is never empty and void-prefixes the ensure as fire-and-forget (App.tsx:7411-7448). The harm is bounded to the cold or non-converged runtime — on a live converged runtime ensure() short-circuits.

Witness:
probe, blocking and re-issue arms with a positive control:

order while ensure pending: ["ensure:/secondary"]            <- nothing reaches the menu until ensure resolves
loader call count after 4 keystrokes = 4
  outcomes = ["query=\"\" threw=workspace_draining","query=\"d\" threw=...","query=\"de\" threw=...","query=\"dem\" threw=..."]
loader call count on success path = 1 (three searches, one call)   <- the instrument reports 1 when caching works

Declared stub: the rejection was an injected new Error('workspace_draining'); the probe settles the provider's retry/caching behaviour given any rejection.

Do not bootstrap a runtime from autocomplete: read the runtime status (a GET) first and call ensureRuntime() only when the runtime is not live, or void-prefix the ensure and return the config-side catalog immediately as the Skills loader does. Give the action an options?: { signal?: AbortSignal } so the menu's abort reaches both legs as it does for globWorkspace and listDirectory, and on ensure failure still attempt the read for the same workspace rather than rejecting the whole provider.

The fix must not violate an existing fact: const WORKSPACE_RUNTIME_ENSURE_TIMEOUT_MS = WORKSPACE_RUNTIME_ENSURE_SERVER_DEADLINE_MS + WORKSPACE_RUNTIME_ENSURE_CLIENT_HEADROOM_MS; = 60 000 + 2 000 (packages/sdk-typescript/src/daemon/DaemonClient.ts:419-424, commented "Keep in sync with DEFAULT_ENSURE_TIMEOUT_MS"), so a withActionTimeout wrapper at its DEFAULT_ACTION_TIMEOUT_MS = 30_000 default (packages/web-shell/client/daemon/timing.ts:9) would abort a legitimate in-progress ensure — any bound must exceed 62 s, or the fix must avoid calling ensure at all. docs/developers/qwen-serve-protocol.md:286 requires the selected workspace's live runtime as the source, so no primary fallback.

Acceptance criterion: Add cases to packages/web-shell/client/hooks/useComposerCore.dom.test.tsx where ensureRuntime rejects and where it never settles, asserting loadExtensionsStatus() still resolves with the workspace catalog or settles within a bound, plus a case passing an already-aborted signal asserting neither call is made; removing any bound must turn them red. Please apply the fix, then remove it again and confirm that test goes red.

中文说明

输入区的 @ext: / + 扩展加载器现在会在一次由按键驱动的读取路径上等待一个会产生副作用的 POST .../runtime/ensure:它丢掉了所有同类工作区动作都在用的 withActionTimeout 约束、不传播 abort、每次菜单打开都重新发起,并且把一次拒绝变成一个静默的空列表。输入 @ 会重置 provider 缓存(打开时、close() 时、以及 workspaceKey 变化时),所以每次打开都重新发起 ensure。在运行时不 live 的工作区上,扩展面板会以零条目停留在 loading: true 直到 ensure 往返结束——客户端预算是 62 秒,而被替换掉的旧路径是有 30 秒约束的 withActionTimeout(client.workspaceExtensions(), ...)——并且菜单的 abort.signal 无法取消它,因为两个 SDK 方法都不接受 signal(provider 只在 await 之后检查 signal.aborted)。ensure 被拒绝时,createExtensionProvider.search 重抛,loadItems 的 catch 只设置 items: []console.warn;由于 .catch 会在拒绝时清掉缓存的 promise,之后每一次去抖按键都会再发一次 ensure POST。同一输入区里的 Skills 等价实现做法相反:先读 config 目录以保证菜单永不为空,并把 ensure 以 void 前缀作为 fire-and-forget。危害仅限于冷启动或未收敛的运行时——在 live 且已收敛的运行时上 ensure() 会短路。建议不要从自动补全里引导运行时:先读运行时状态(GET),只在运行时不 live 时才调用 ensureRuntime();或像 Skills 加载器那样 void 前缀 ensure 并立即返回 config 侧目录。同时给该动作加上 options?: { signal?: AbortSignal },让菜单的 abort 像对 globWorkspacelistDirectory 那样传到两条腿,并在 ensure 失败时仍尝试对同一工作区读取,而不是让整个 provider 拒绝。

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

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.

Valid — the composer loader awaits a mutating POST on the render path, so keystrokes can serialize behind a mutation. Deferred to the next round; the call will be serialized or short-circuited.

中文说明

成立——composer 加载器在渲染路径上等待一个会修改状态的 POST,按键可能被串行在变更之后。顺延至下一轮;该调用将被串行化或短路。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in 0e3dbea201. This remains Suggestion-level and is deferred to a follow-up: this PR has passed the review-round convergence limit, so the latest pass was limited to correctness and regression fixes. Leaving this thread open.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against pushed head c80017edd0 (repairs in c031c91474).

The silent-empty failure is fixed by the mention menu's error state/alert, and startup retry is bounded to three attempts. Awaiting selected-runtime ensure remains intentional: the durable global Extension inventory is not evidence of the selected runtime's available contributions, and no primary fallback is acceptable. SDK ensure already has its 62-second budget. Cancellation and a different cold-menu UX remain follow-up work; not adopting the proposed unconditional config fallback.

@wenshao

wenshao commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 5, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 2 ended without publishing a reportview run.

中文说明

⚠️ AutoFix 第 2 轮结束但未发布报告 —— 查看运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 1/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 在完成前耗尽了时间(timeout (7200000ms))(第 1/100 次尝试)—— 将在下次扫描时重试。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/33999036192


🧠 Handled by Qwen Code · model/模型 kimi-k3

…iness (#11086)

- Surface per-session command-update failures from
  workspaceExtensionsReconcile instead of swallowing them, so a stale
  command set can no longer be certified as reconciled.
- Sanitize Extension refresh errors once at the coordinator (URL
  credentials, ANSI/control sequences, 500-char bound) covering both the
  persisted capability status and the extensions_changed broadcast.
- Give the Extensions capability the Skills failed-revision guard so a
  failed prepare is terminal for the ensure path until an observed
  generation move or an explicit reconcile retries it.
- Keep read-side generation observation pure: derived Skills/MCP
  capabilities are invalidated only on the reconcile path, which owns
  rescheduling them, so a queued reload can no longer be silently
  discarded by a GET.
- Advertise the Extension runtime capability tags only when the primary
  workspace is trusted, since the routes they gate onto require trust.
- Close the stale Extension reference picker when the scheduled-task
  form's target workspace changes.
- Treat the split-mode uninstall 204 as a completed no-op instead of a
  queued operation that will never be polled.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Round summary — review feedback addressed

Commit: fbec518897fix(serve): address round-1 review findings on Extension runtime readiness (#11086)

This round implements the 9 Critical findings (10 inline threads, two pairs
share one fix). All 32 [Suggestion] threads are deferred to the next
round per the round-size bound (~8 findings/round) and the budget warning;
each deferred thread has a reply posted on its own conversation explaining
the disposition, so nothing is dropped silently.

Resolved in code (10 threads)

Thread Finding Fix
rc:3941458963 (R1-1) workspaceExtensionsReconcile derived sessionsFailed from the error-swallowing sendAvailableCommandsUpdate(), so a failed command update was certified as reconciled. The reconcile handler now drives sendAvailableCommandsUpdateOrThrow() (made public on Session; the swallowing wrapper stays for its fire-and-forget callers), so rejections surface as sessionsFailed/sessionErrors and the coordinator's existing sessionsFailed > 0 promotion marks the capability error.
rc:3941458969 + rc:3941458971 (R1-2/R1-3) Extension runtime refresh failed: ${details[0]} reached the extensions_changed broadcast and the persisted status unsanitized (credentials, ANSI, unbounded length). Sanitized once at the producer: a sanitizeExtensionsErrorMessage helper (redactUrlCredentials + stripAnsiAndControl + 500-char slice) now covers both coordinator sinks — the reconcile result error the controller broadcasts and the persisted capabilities.extensions.error.
rc:3941458974 + rc:3941458978 (R1-4/R1-5) prepareExtensionsRevision had no already-failed guard, so the Extensions page's 2s ensureRuntime() retry re-ran the full reconcile forever after a terminal failure. Mirrored the Skills guard: extensionsRefreshFailedRevision is recorded when the error cell is written and cleared when an observed generation moves; prepareExtensions() (the ensure path) returns early for an already-converged generation or an already-failed revision, while the explicit reconcileExtensionGeneration path bypasses the guard and always retries.
rc:3941458982 (R1-6) observeExtensionGeneration invalidated derived Skills/MCP capabilities from read-only paths (GET routes, poller pre-pass), aborting their queued closures with nothing queueing a replacement. Observation is no longer mutation: invalidateDerivedCapabilities() moved out of observeExtensionGeneration into reconcileExtensionGeneration after the admission check, next to the success gate that already owns the reschedule pair. A read-side observe now only does the desired-generation bookkeeping and the extensions status downgrade.
rc:3941458985 (R1-7) The scheduled-task workspace <select> never reset the reference picker, so an open Extension picker kept the previous workspace's candidates. The picker onChange now calls resetReferenceState() alongside setFormWorkspaceId.
rc:3941458988 + rc:3941458990 (R1-8/R1-9) workspace_extensions_config_runtime / workspace_extension_mentions were advertised on workspaceRuntimeAvailable alone, but the routes they gate clients onto are trust-gated — an untrusted primary broke the announced entrances. Both predicates now also require primaryWorkspaceTrusted === true; createServeFeatures feeds the toggle from the existing isPrimaryWorkspaceTrusted() evaluation (defaulting to trusted when primary trust is not tracked, matching the legacy single-workspace behavior). Clients of an untrusted primary keep the legacy trust-free loader.
rc:3941458992 (R1-10) Split-mode uninstall of an entry with no removable user-store policy returned 204 → undefined, and runMutation fell through to the "queued" notice for an operation that will never exist. runMutation now treats an undefined result with operation === 'uninstall' as a completed no-op (success tone + a new extensions.manage.uninstallNothingToRemove message, en/zh); the reload in finally is unchanged. The decision lives in a pure isUninstallNoOpResult helper in extensions-manager-logic.ts.

Deferred to the next round (32 threads)

All [Suggestion] threads: rc:3941458994, rc:3941458997, rc:3941459000,
rc:3941459003, rc:3941459008, rc:3941459013 (partially addressed this
round — the reconcile test now covers session-failure surfacing),
rc:3941459018, rc:3941459025, rc:3941459031, rc:3941459036,
rc:3941459041, rc:3941459045, rc:3941459048, rc:3941459052,
rc:3941459057, rc:3941459064, rc:3941459069, rc:3941459074,
rc:3941459078, rc:3941459081, rc:3941459086, rc:3941459089,
rc:3941459091, rc:3941459093, rc:3941459094, rc:3941459098,
rc:3941459109, rc:3941459115, rc:3941459119, rc:3941459122,
rc:3941459126, rc:3941459130.

Reason: round-size bound after the budget warning — Critical findings first;
each thread got an individual reply with its disposition and will be picked
up next round.

Notes

  • The four cancelled CI checks (Test/Lint/Integration/web-shell E2E) were
    cancellations, not failures; the required checks were re-run locally
    (below).
  • --conflict false: no base merge was needed.
  • The witness for R1-10 lives at the logic level (extensions-manager-logic)
    rather than a page-level render because no harness renders
    ExtensionsManagerPage for real today; building that harness is exactly
    the deferred R1-35 work.
  • The pre-existing unsanitized capabilities.skills.error /
    capabilities.mcp.error pattern noted in R1-2/R1-3 exists at the merge
    base and is unchanged by this PR's footprint; it was left alone to keep
    the round scoped.

Verification

Commands actually run, with results:

  • npm run build --workspace=packages/core (+ channels/* ×10, acp-bridge, sdk-typescript, web-templates) — passed
  • npm run build --workspace=packages/cli — passed
  • npm run build --workspace=packages/web-shell — passed
  • npx tsc --noEmit (packages/cli) — passed
  • npx tsc -p tsconfig.json --noEmit (packages/web-shell) — passed
  • npx eslint <all 14 changed files> — passed
  • npx prettier --check <all 14 changed files> — passed
  • vitest packages/cli src/serve/workspace-runtime-coordinator.test.ts — 53 passed (incl. 3 new)
  • vitest packages/cli src/serve/server.test.ts — 1227 passed (incl. 1 new)
  • vitest packages/cli src/acp-integration/acpAgent.test.ts — 621 passed
  • vitest packages/cli src/acp-integration/session/Session.test.ts -t sendAvailableCommandsUpdate — 20 passed (full file exceeds the 120s tool limit locally; the changed method's describe block ran)
  • vitest packages/cli src/serve/routes/workspace-extensions-controller.test.ts — 14 passed
  • vitest packages/cli src/serve/routes/workspace-qualified-extensions.test.ts — 52 passed
  • vitest packages/cli src/serve/routes/workspace-runtime.test.ts src/serve/workspace-registry.test.ts — 33 passed
  • vitest packages/cli src/serve/routes/workspace-skills.test.ts — 25 passed
  • vitest packages/web-shell client/components/dialogs/ScheduledTasksDialog.test.tsx client/components/extensions/extensions-manager-logic.test.ts — 65 passed (incl. 2 new)
  • vitest packages/web-shell client/i18n.test.ts — 1 passed
  • Integration tests — not run: the touched behaviors are exercised by the unit suites above (not only through the bundled CLI).

Mutation probes (guard removed/negated → focused test red → restored → green):

  • R1-7: dropped resetReferenceState() from the picker onChangecloses the extension reference picker when the target workspace changes failed → restored, green.
  • R1-2/R1-3: persisted the raw message in recordExtensionsErrorsanitizes Extension reconcile failures before broadcasting and persisting them failed → restored, green.
  • R1-4: deleted the failed-revision guard from prepareExtensionsdoes not re-run a failed Extension revision from the ensure path failed → restored, green.
  • R1-6: re-added invalidateDerivedCapabilities() to observeExtensionGenerationkeeps a queued MCP reload alive across a read-side Extension observation failed → restored, green.
  • R1-1: switched the handler back to sendAvailableCommandsUpdate()coalesces bootstrap extension refreshes without directly refreshing the session skills failed → restored, green.
  • R1-8/R1-9: dropped the trust term from the workspace_extension_mentions predicate → honors every entry in CONDITIONAL_SERVE_FEATURES failed → restored, green.
  • R1-10: negated isUninstallNoOpResulttreats only an undefined uninstall result as a completed no-op failed → restored, green.
中文说明

本轮总结 — 评审反馈处理

提交:fbec518897fix(serve): address round-1 review findings on Extension runtime readiness (#11086)

本轮实现了全部 9 项 Critical 发现(10 条内联讨论,其中两对共用一个修复)。
全部 32 条 [Suggestion] 讨论按单轮处理规模上限(约 8 项/轮)及时间预算警告
顺延至下一轮;每条顺延的讨论都在其各自会话下回复了处理决定,不会有任何一项被静默丢弃。

已在代码中解决(10 条讨论)

讨论 发现 修复
rc:3941458963 (R1-1) workspaceExtensionsReconcile 从吞掉错误的 sendAvailableCommandsUpdate() 推导 sessionsFailed,失败的命令更新会被认证为已调和。 调和处理器改为调用 sendAvailableCommandsUpdateOrThrow()(在 Session 上改为 public;原吞错包装保留给既有的 fire-and-forget 调用点),拒绝会体现为 sessionsFailed/sessionErrors,coordinator 既有的 sessionsFailed > 0 逻辑随之把能力标记为 error
rc:3941458969 + rc:3941458971 (R1-2/R1-3) Extension runtime refresh failed: ${details[0]} 未经消毒就进入 extensions_changed 广播与持久化状态(凭据、ANSI、长度无界)。 在生产侧统一消毒:新增 sanitizeExtensionsErrorMessageredactUrlCredentials + stripAnsiAndControl + 500 字符截断),覆盖 coordinator 的两个出口——控制器广播用的调和结果错误与持久化的 capabilities.extensions.error
rc:3941458974 + rc:3941458978 (R1-4/R1-5) prepareExtensionsRevision 没有“已失败”守卫,扩展页面每 2 秒的 ensureRuntime() 重试会在终态失败后永远重跑完整调和。 对齐 Skills 守卫:写入错误单元格时记录 extensionsRefreshFailedRevision,观察到 generation 前移时清除;prepareExtensions()(ensure 路径)对已收敛的 generation 或已失败的修订号提前返回,而显式的 reconcileExtensionGeneration 路径绕过守卫、始终重试。
rc:3941458982 (R1-6) observeExtensionGeneration 在只读路径(GET 路由、轮询预扫描)上作废派生的 Skills/MCP 能力,中止其排队闭包却没有任何一方补排。 观察不再是变更:invalidateDerivedCapabilities()observeExtensionGeneration 移到 reconcileExtensionGeneration 的准入检查之后,紧邻本已负责重调度的成功门。只读观察现在只做期望 generation 记账与 extensions 状态降级。
rc:3941458985 (R1-7) 计划任务的工作区 <select> 从不重置引用选择器,打开的 Extension 选择器仍显示上一个工作区的候选。 选择器 onChange 现在一并调用 resetReferenceState()
rc:3941458988 + rc:3941458990 (R1-8/R1-9) workspace_extensions_config_runtime / workspace_extension_mentions 仅凭 workspaceRuntimeAvailable 就对外宣告,但它们把客户端引向的路由有信任门控——主工作区不受信时会破坏已宣告的入口。 两个谓词现在都要求 primaryWorkspaceTrusted === truecreateServeFeatures 用既有 isPrimaryWorkspaceTrusted() 求值填充该开关(未跟踪主信任时默认受信,与旧的单工作区行为一致)。主工作区不受信时客户端保留旧的免信任加载器。
rc:3941458992 (R1-10) split 模式卸载没有可移除用户存储策略的条目时返回 204 → undefinedrunMutation 落入“已排队”提示,而该操作永远不会存在。 runMutation 现在把 undefined 结果且 operation === 'uninstall' 视为已完成的无操作(success 语气 + 新的 extensions.manage.uninstallNothingToRemove 文案,中英文);finally 中的重载不变。判定收敛到 extensions-manager-logic.ts 的纯函数 isUninstallNoOpResult

顺延至下一轮(32 条讨论)

全部 [Suggestion] 讨论:rc:3941458994rc:3941458997rc:3941459000
rc:3941459003rc:3941459008rc:3941459013(本轮已部分处理——reconcile 测试
现已覆盖会话失败上报)、rc:3941459018rc:3941459025rc:3941459031
rc:3941459036rc:3941459041rc:3941459045rc:3941459048rc:3941459052
rc:3941459057rc:3941459064rc:3941459069rc:3941459074rc:3941459078
rc:3941459081rc:3941459086rc:3941459089rc:3941459091rc:3941459093
rc:3941459094rc:3941459098rc:3941459109rc:3941459115rc:3941459119
rc:3941459122rc:3941459126rc:3941459130

原因:时间预算警告后的单轮规模约束——Critical 优先;每条讨论均已单独回复处理
决定,下一轮继续处理。

备注

  • 四个被取消的 CI 检查(Test/Lint/Integration/web-shell E2E)是取消而非失败;
    必需的检查已在本地重新执行(见下)。
  • --conflict false:无需合并基线。
  • R1-10 的见证落在逻辑层(extensions-manager-logic)而非页面级渲染,因为目前
    没有真正渲染 ExtensionsManagerPage 的测试设施;搭建该设施正是顺延的 R1-35 工作。
  • R1-2/R1-3 提到的、在合并基线就存在的 capabilities.skills.error /
    capabilities.mcp.error 未消毒模式不在本 PR 范围内,为控制本轮范围未做改动。

验证

实际执行过的命令及结果:

  • npm run build --workspace=packages/core(以及 channels/* ×10、acp-bridgesdk-typescriptweb-templates)— 通过
  • npm run build --workspace=packages/cli — 通过
  • npm run build --workspace=packages/web-shell — 通过
  • npx tsc --noEmit(packages/cli)— 通过
  • npx tsc -p tsconfig.json --noEmit(packages/web-shell)— 通过
  • npx eslint <全部 14 个改动文件> — 通过
  • npx prettier --check <全部 14 个改动文件> — 通过
  • vitest packages/cli src/serve/workspace-runtime-coordinator.test.ts — 53 通过(含 3 个新增)
  • vitest packages/cli src/serve/server.test.ts — 1227 通过(含 1 个新增)
  • vitest packages/cli src/acp-integration/acpAgent.test.ts — 621 通过
  • vitest packages/cli src/acp-integration/session/Session.test.ts -t sendAvailableCommandsUpdate — 20 通过(完整文件超出本地 120 秒工具时限;已运行被改方法所在的 describe 块)
  • vitest packages/cli src/serve/routes/workspace-extensions-controller.test.ts — 14 通过
  • vitest packages/cli src/serve/routes/workspace-qualified-extensions.test.ts — 52 通过
  • vitest packages/cli src/serve/routes/workspace-runtime.test.ts src/serve/workspace-registry.test.ts — 33 通过
  • vitest packages/cli src/serve/routes/workspace-skills.test.ts — 25 通过
  • vitest packages/web-shell client/components/dialogs/ScheduledTasksDialog.test.tsx client/components/extensions/extensions-manager-logic.test.ts — 65 通过(含 2 个新增)
  • vitest packages/web-shell client/i18n.test.ts — 1 通过
  • 集成测试 — 未运行:本轮触动的行为已由上述单元套件覆盖(并非只有打包 CLI 才能触达)。

变异探针(删除/取反守卫 → 聚焦测试转红 → 还原 → 转绿):

  • R1-7:从选择器 onChange 删除 resetReferenceState()closes the extension reference picker when the target workspace changes 失败 → 还原后转绿。
  • R1-2/R1-3:在 recordExtensionsError 中持久化原始消息 → sanitizes Extension reconcile failures before broadcasting and persisting them 失败 → 还原后转绿。
  • R1-4:从 prepareExtensions 删除失败修订守卫 → does not re-run a failed Extension revision from the ensure path 失败 → 还原后转绿。
  • R1-6:在 observeExtensionGeneration 重新加入 invalidateDerivedCapabilities()keeps a queued MCP reload alive across a read-side Extension observation 失败 → 还原后转绿。
  • R1-1:处理器改回 sendAvailableCommandsUpdate()coalesces bootstrap extension refreshes without directly refreshing the session skills 失败 → 还原后转绿。
  • R1-8/R1-9:从 workspace_extension_mentions 谓词删除信任项 → honors every entry in CONDITIONAL_SERVE_FEATURES 失败 → 还原后转绿。
  • R1-10:取反 isUninstallNoOpResulttreats only an undefined uninstall result as a completed no-op 失败 → 还原后转绿。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

🧵 Resolved all 10 selected review thread(s). · 已关闭全部选中的 10 条评审线程。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Integration Tests (no-AK, No Sandbox)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Integration Tests (no-AK, No Sandbox)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

…velope test (#11086)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #11086

Feedback addressed

1. Failed check: Integration Tests (no-AK, No Sandbox) — FIXED

Reproduction (evidence first). The gate runs npm run typecheck:integration then npm run test:integration:no-ak:sandbox:none with a fresh, credential-less HOME. Reproduced locally with the same env: cli/qwen-serve-routes.test.ts > qwen serve — capabilities envelope > advertises all baseline capabilities fails deterministically — the daemon advertises 132 feature tags where the test (updated by this PR) expects 134, the delta being exactly the PR's two new tags workspace_extensions_config_runtime and workspace_extension_mentions. Same failure signature as the red CI check.

Root cause. The PR registered the two tags in SERVE_CAPABILITY_REGISTRY, gated them in CONDITIONAL_SERVE_FEATURES behind workspaceRuntimeAvailable && primaryWorkspaceTrusted, wired primaryWorkspaceTrusted into the runtime envelope (serve-features.ts), and added both tags to the integration test's exact-equality expectation. It did not — and correctly could not — add them to the bootstrap envelope (currentServeFeaturesForRunQwenServe in run-qwen-serve.ts), which is built synchronously before the runtime app exists and therefore cannot evaluate primary trust. A real qwen serve builds the runtime app asynchronously after listen; until it mounts, /capabilities (a bootstrap route) is answered with the bootstrap envelope. The test issued its assertion inside that startup window and compared the transient bootstrap envelope against the full runtime expectation. Verified against the live daemon: the bootstrap envelope lacks both tags and has no workspaces array; after the runtime app mounts, the same /capabilities returns the runtime envelope carrying both tags with workspaces: [{…, trusted: true }] — the trust gating itself works as the PR intended.

Fix. Test-side, minimal: advertises all baseline capabilities now polls client.capabilities() (vitest expect.poll, 10s, the idiom already used in this file) until the served envelope contains workspaces — a field only the runtime envelope has — before running the unchanged exact-equality assertion. Advertising the tags in the bootstrap envelope was rejected: it would over-report trust-gated routes in untrusted workspaces, defeating the PR's gating during exactly the preflight window, and the bootstrap handler cannot evaluate trust synchronously. Waiting on workspaces (not on the two tags) keeps the assertion able to catch a real trust-gating regression in trusted workspaces.

2. Cancelled check: web-shell E2E Smoke (ubuntu-latest, Node 22.x) — no independent failure

The job was CANCELLED in the same workflow run after the no-AK gate failed; it is not a separate defect and carries no failure log. Fixing the gate lets the run complete; nothing to change for this item.

Changes

  • integration-tests/cli/qwen-serve-routes.test.ts (+15/−1): wait for the runtime capabilities envelope (marker: workspaces !== undefined) before the baseline feature assertion.

No inline review comments existed this round, so resolved-comments.txt and comment-replies.json are omitted. No deferred findings. No conflicts (--conflict false; no merge performed).

Verification

  • npm run typecheck:integration — passed.
  • Pre-fix reproduction (CI env: fresh HOME, all model keys emptied, QWEN_SANDBOX=false): vitest run cli/qwen-serve-routes.test.ts — 1 failed / 37 passed; assertion diff shows exactly the two new tags missing (132 vs 134).
  • Post-fix same command — 38/38 passed.
  • Mutation probe: git checkout -- the fix → capabilities test fails again with the identical assertion; git apply restore → 38/38 passed. (The changed test fails on the pre-round tree and passes post-round.)
  • Full no-AK gate (npm run test:integration:no-ak:sandbox:none file set, CI env, run in chunks because this sandbox caps commands at 120s): all 22 files green — fake-openai-server + test-helper + chat-transcript-contract (25), qwen-live-m4-acp-call/permission (3), qwen-live-m4-acp-steering/multibackend (3), qwen-live-m1-call + m2-inject (8), qwen-live-m2-permission/steering (3), cli/_prompt-latency-policy + daemon-invocation-context + list_directory (14), cli/qwen-serve-routes (38), cli/qwen-serve-streaming (11), sdk-typescript abort-and-lifecycle + permission-control (37), sdk-mcp-server + subagents (7), system-control + tool-control (36). 185 tests, 0 failures.
  • npm run typecheck (workspaces + integration) — passed.
  • npx eslint integration-tests/ — passed; npx eslint integration-tests/cli/qwen-serve-routes.test.ts — passed; npx prettier --check integration-tests/cli/qwen-serve-routes.test.ts — passed.
  • npm run build — per-package builds passed (core, cli, sdk-typescript, acp-bridge, web-shell, web-templates, vscode-ide-companion, qwen-live); npm run bundle — passed (the single-command build exceeds this sandbox's 120s cap, so packages were built individually; the deterministic gate re-runs the canonical command).
  • npm run lint — exceeded the sandbox's 120s cap with 0 errors/warnings emitted before termination; covered by the directory-scoped eslint run above.
  • Live-daemon probes against the freshly bundled dist/cli.js (fresh HOME): bootstrap /capabilities lacks the two tags and workspaces; runtime /capabilities includes both tags and workspaces: [{ primary: true, trusted: true }]; /workspace/trust reports folderTrustEnabled: false, state: trusted.
中文说明

Autofix 本轮总结 — PR #11086

已处理的反馈

1. 失败检查:Integration Tests (no-AK, No Sandbox) —— 已修复

复现(先取证)。 该门禁在无凭据的全新 HOME 下依次运行 npm run typecheck:integrationnpm run test:integration:no-ak:sandbox:none。本地用相同环境复现:cli/qwen-serve-routes.test.ts > qwen serve — capabilities envelope > advertises all baseline capabilities 确定性失败 —— daemon 实际通告 132 个特性标签,而(被本 PR 更新的)测试期望 134 个,差值恰好是 PR 新增的两个标签 workspace_extensions_config_runtimeworkspace_extension_mentions。与 CI 红色检查的失败特征一致。

根因。 该 PR 在 SERVE_CAPABILITY_REGISTRY 注册了这两个标签,在 CONDITIONAL_SERVE_FEATURES 中以 workspaceRuntimeAvailable && primaryWorkspaceTrusted 进行门控,把 primaryWorkspaceTrusted 接入了运行时信封(serve-features.ts),并把两个标签加进了集成测试的全等期望里。它没有 —— 也确实无法 —— 把它们加进 bootstrap 信封(run-qwen-serve.tscurrentServeFeaturesForRunQwenServe):bootstrap 信封在运行时应用就绪之前同步构建,无法求值主工作区信任状态。真实的 qwen serve 在监听之后异步构建运行时应用;在其挂载前,/capabilities(bootstrap 路由)由 bootstrap 信封应答。测试恰好在该启动窗口内发起断言,拿瞬态的 bootstrap 信封去比对完整的运行时期望。已对存活 daemon 验证:bootstrap 信封缺少这两个标签且没有 workspaces 数组;运行时应用挂载后,同一 /capabilities 返回的运行时信封携带这两个标签且 workspaces: [{…, trusted: true }] —— 信任门控本身按 PR 意图工作正常。

修复。 测试侧、最小改动:advertises all baseline capabilities 现在用 vitest 的 expect.poll(本文件已有的惯用法,10 秒超时)轮询 client.capabilities(),直到应答的信封包含只有运行时信封才有的 workspaces 字段,再执行保持不变的精确全等断言。否定了"在 bootstrap 信封中也通告这两个标签"的方案:那会在工作区不受信任时过度通告信任门控路由,恰好在本 PR 设防的预检窗口期破坏门控,且 bootstrap 处理器无法同步求值信任。以 workspaces(而非这两个标签本身)作为等待标记,使断言仍能捕获受信任工作区中真实的门控回归。

2. 被取消的检查:web-shell E2E Smoke (ubuntu-latest, Node 22.x) —— 无独立缺陷

该 job 在同一工作流运行中、no-AK 门禁失败后被取消(CANCELLED),并非独立缺陷,也没有失败日志。修复门禁后运行即可走完;此项无需任何改动。

变更

  • integration-tests/cli/qwen-serve-routes.test.ts(+15/−1):在基线特性断言前,先等待运行时 capabilities 信封(标记:workspaces !== undefined)。

本轮没有行内评审评论,故省略 resolved-comments.txtcomment-replies.json。无延期处理的发现。无冲突(--conflict false;未执行合并)。

验证

  • npm run typecheck:integration —— 通过。
  • 修复前复现(CI 环境:全新 HOME、清空全部模型密钥、QWEN_SANDBOX=false):vitest run cli/qwen-serve-routes.test.ts —— 1 失败 / 37 通过;断言差异恰好显示两个新标签缺失(132 对 134)。
  • 修复后同一命令 —— 38/38 全部通过
  • 变异探测:git checkout -- 撤掉修复 → capabilities 测试再次以完全相同的断言失败;git apply 恢复 → 38/38 通过。(被改动的测试在修复前的树上失败、修复后通过。)
  • 完整 no-AK 门禁(npm run test:integration:no-ak:sandbox:none 的全部文件集,CI 环境;因本沙箱 120 秒命令上限而分组运行):22 个文件全绿 —— fake-openai-server + test-helper + chat-transcript-contract(25)、qwen-live-m4-acp-call/permission(3)、qwen-live-m4-acp-steering/multibackend(3)、qwen-live-m1-call + m2-inject(8)、qwen-live-m2-permission/steering(3)、cli/_prompt-latency-policy + daemon-invocation-context + list_directory(14)、cli/qwen-serve-routes(38)、cli/qwen-serve-streaming(11)、sdk-typescript abort-and-lifecycle + permission-control(37)、sdk-mcp-server + subagents(7)、system-control + tool-control(36)。共 185 个测试,0 失败。
  • npm run typecheck(workspaces + integration)—— 通过。
  • npx eslint integration-tests/ —— 通过;npx eslint integration-tests/cli/qwen-serve-routes.test.ts —— 通过;npx prettier --check integration-tests/cli/qwen-serve-routes.test.ts —— 通过。
  • npm run build —— 逐包构建全部通过(core、cli、sdk-typescript、acp-bridge、web-shell、web-templates、vscode-ide-companion、qwen-live);npm run bundle —— 通过(单条全量 build 命令超出本沙箱 120 秒上限,故改为逐包构建;确定性门禁会重新运行标准命令)。
  • npm run lint —— 超出沙箱 120 秒上限,终止前输出 0 错误 0 警告;已由上面的目录范围 eslint 运行覆盖。
  • 针对新打包的 dist/cli.js 的存活 daemon 探测(全新 HOME):bootstrap /capabilities 缺少两个标签与 workspaces;运行时 /capabilities 包含两个标签且 workspaces: [{ primary: true, trusted: true }];/workspace/trust 报告 folderTrustEnabled: false, state: trusted

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3

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

22 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-12 SDK recipe names ensureRuntime() with no receiver — already reported (round-1 inline comment on docs/developers/daemon/13-sdk-daemon-client.md:188)
  • R1-13 documented merge condition weaker than the shipped client — already reported (round-1 inline comment on docs/developers/qwen-serve-protocol.md:284)
  • R1-15 runtimeEpoch stamp on the Extension catalog untested — already reported (round-1 inline comment on packages/acp-bridge/src/bridge.ts:6202)
  • R1-16 reconcile test pins only the returned counters — already reported (round-1 inline comment on packages/cli/src/acp-integration/acpAgent.test.ts:29762)
  • R1-17 added refreshCache duplicates the refresh inside refreshTools — already reported (round-1 inline comment on packages/cli/src/acp-integration/acpAgent.ts:13543)
  • R1-19 coordinator branch unreachable from the route tests — already reported (round-1 inline comment on packages/cli/src/serve/routes/workspace-extensions-controller.ts:716)
  • R1-20 coordinator branch unreachable from the route tests — already reported (round-1 inline comment on packages/cli/src/serve/routes/workspace-extensions.ts:2455)
  • R1-21 coordinator branch never reads options.skillsOnly — already reported (round-1 inline comment on packages/cli/src/serve/routes/workspace-extensions-controller.ts:718)
  • R1-22 runtime-catalog route trust gate untested — already reported (round-1 inline comment on packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:435)
  • R1-23 test pins a lower generation moving desired and applied — already reported (round-1 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.test.ts:186)
  • R1-25 drain-rollback replay test counts only the catalog read — already reported (round-1 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.test.ts:302)
  • R1-26 cancelDrain replay double-schedules the derived reconciliations — already reported (round-1 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.ts:157)
  • R1-29 only the secondary branch of the dialog loader is tested — already reported (round-1 inline comment on packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx:1427)
  • R1-30 failed workspace activation projection swallowed to null — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:569)
  • R1-31 split path's first apply() replaces detailed rows — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:588)
  • R1-32 refetch when the certification gate is false — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:592)
  • R1-34 two new user-facing strings bypass the i18n table — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:620)
  • R1-35 no test executes the split-runtime arm of ExtensionsManagerPage — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:626)
  • R1-36 fixture uses an activationSource outside the SDK union — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/extensions-manager-logic.test.ts:186)
  • R1-38 merge overwrites the live runtime entry's updateState — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/extensions-manager-logic.ts:75)
  • …and 2 more (see the run report)

Not reviewed: build-and-test — the test phase never started (the build set exhausted build-test's 600s budget), so no suite ran and no test behaviour is certified; the test-efficacy harness was never validated (harnessValidated null) and every probe was inconclusive on a missing generated prerequisite.

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

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 predates 2.72.0, so closingIssuesReferences is unavailable); the set is UNKNOWN, not empty.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": did not trace that a prompt session created from a composer mention is bound to the same atWorkspaceCwd whose runtime supplied the catalog — the "resolve in t…; "agent reverse-audit (round 1)": did not open ExtensionsManagerPage 's render, so the design doc's step 4 ("shows the workspace selector on the list page and the disabled selector in detail vi…; "agent reverse-audit (round 1)": the documented-behaviour-nothing-tests layer was traced by grep reference for the mentions gate ( useComposerCore.dom.test.tsx:267–298 ) instead of by reading i…; "agent reverse-audit (round 1)": whether the Web Shell refreshes workspace.capabilities after the runtime app replaces the bootstrap app — that settles whether the under-report above is a bri…; "agent reverse-audit (round 1)": whether refreshCacheWithSnapshot() can return a different generation for a manager built as (runtime.workspaceCwd, runtime.trusted) than for the poller's …, and 6 more.

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

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

  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:728 — [probe] coordinator branch broadcasts extensions_changed unconditionally, dropping the bridge's no-op suppression gate
  • packages/web-shell/client/hooks/useComposerCore.ts:1583 (+1 locations) — [probe] the new runtime Extension loaders discard the ensure result and never check catalog currency
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:187 — [probe] the new extensionsQueuedWork term feeding the removal busy gate is pinned by no test
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:878 — [probe] the dialog's workspace_extension_mentions gate has no negative-direction witness
  • packages/cli/src/serve/routes/workspace-extensions.ts:768 — [probe] the poller permanently disables ensure()'s new boot fast path, so every cold start pays an inline reconcile
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:1015 — [review] both deferral assignments in invalidateDerivedCapabilities are unreachable-true no-ops
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:994 — [review] the mutation-side half of the invalidation asymmetry has no test
  • packages/cli/src/serve/routes/workspace-extensions.ts:2403 — [probe] the primary runtime-catalog alias has no server-side test
  • integration-tests/cli/qwen-serve-routes.test.ts:389 — [test] the new end-to-end capability coverage sits outside every npm workspace, so the standard gate never collects it
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:604 — [review] the one caller that awaits load() clobbers the runtime error banner with a success message

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 22 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):build-and-test — the test phase never started (the build set exhausted build-test's 600s budget), so no suite ran and no test behaviour is certified; the test-efficacy harness was never validated (harnessValidated null) and every probe was inconclusive on a missing generated prerequisite.

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

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 predates 2.72.0, so closingIssuesReferences is unavailable); the set is UNKNOWN, not empty.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"did not trace that a prompt session created from a composer mention is bound to the same atWorkspaceCwd whose runtime supplied the catalog — the "resolve in t…"agent reverse-audit (round 1)"did not open ExtensionsManagerPage 's render, so the design doc's step 4 ("shows the workspace selector on the list page and the disabled selector in detail vi…"agent reverse-audit (round 1)"the documented-behaviour-nothing-tests layer was traced by grep reference for the mentions gate ( useComposerCore.dom.test.tsx:267–298 ) instead of by reading i…"agent reverse-audit (round 1)"whether the Web Shell refreshes workspace.capabilities after the runtime app replaces the bootstrap app — that settles whether the under-report above is a bri…"agent reverse-audit (round 1)"whether refreshCacheWithSnapshot() can return a different generation for a manager built as (runtime.workspaceCwd, runtime.trusted) than for the poller's …,另有 6 条。

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

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/cli/src/serve/server/serve-features.ts Outdated
// error can carry git credentials, ANSI/control sequences, or unbounded
// output. Sanitize once at the producer so both sinks stay safe.
const sanitizeExtensionsErrorMessage = (message: string): string =>
redactUrlCredentials(stripAnsiAndControl(message)).slice(0, 500);

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-7: This adds a second copy of the pre-existing sanitizeDaemonMessage in the same subsystem, so the daemon's credential/ANSI scrubbing rule now lives in two places that already disagree on where the 500-char cap is applied.

The two copies are applied to the same string on the failure path: the coordinator caps inside the helper, while the controller re-sanitizes the already-sanitized reconciliationError and caps at each call site (workspace-extensions-controller.ts:785, 800, 807, 812, all sanitizeDaemonMessage(...).slice(0, 500)); a third composition exists at packages/core/src/telemetry/session-tracing.ts:473. When the next secret shape is added — an Authorization: Bearer header or a ?token= query parameter — a fix landed in one copy leaves the other a stale near-duplicate, which is exactly the drift stripAnsiAndControl's own docstring warns against ("Centralised here so the rule can't drift between call sites … instead of leaving a stale near-duplicate vulnerable"). The extensions_changed broadcast and the persisted capabilities status are the sinks that would keep leaking.

Witness:

witness: not run — the nearest capability was a probe on both helpers with a new secret shape, which would only
demonstrate the drift after a hypothetical future edit. The duplication and the divergent cap placement are read
from source: sanitizeDaemonMessage at routes/workspace-extensions-controller.ts:41-43 (no cap inside) against
sanitizeExtensionsErrorMessage at workspace-runtime-coordinator.ts:47-48 (cap inside), with four call sites
re-capping the controller's output.

Hoist one helper — for example serve/sanitize-daemon-message.ts exporting sanitizeDaemonMessage(message: string, maxLength = 500) — and have both workspace-runtime-coordinator.ts and workspace-extensions-controller.ts import it, deleting the local copies and the per-call-site .slice(0, 500) calls.

The fix must not violate an existing fact: the coordinator cannot import the existing helper from packages/cli/src/serve/routes/workspace-extensions-controller.ts — that module already imports the coordinator at :34, so the hoist must land in a third module to avoid a cycle.

Acceptance criterion: packages/cli/src/serve/workspace-runtime-coordinator.test.tssanitizes Extension reconcile failures before broadcasting and persisting them, which pins both sinks (reconciliation.error and status().capabilities.extensions.error.message) for tok3n, ANSI escapes and length <= 500. It must stay green through the hoist, and go red if the shared helper drops either the cap or the redaction.

中文说明

这在同一子系统内新增了既有 sanitizeDaemonMessage 的第二份副本,于是 daemon 的凭据/ANSI 清理规则现在存在于两处,而且它们对 500 字符截断的应用位置已经不一致。

两份副本在失败路径上被应用于同一个字符串:coordinator 在辅助函数内部截断,而 controller 会对已经清理过的 reconciliationError 再清理一次,并在每个调用点各自截断(workspace-extensions-controller.ts:785800807812,都是 sanitizeDaemonMessage(...).slice(0, 500));第三处组合存在于 packages/core/src/telemetry/session-tracing.ts:473。当下一种密钥形态被加入时——例如 Authorization: Bearer 头或 ?token= 查询参数——落在其中一份副本里的修复会让另一份成为过期的近似重复,而这正是 stripAnsiAndControl 自身文档注释所警告的漂移(“集中于此,规则就不会在调用点之间漂移……而不是留下一个易受攻击的过期近似副本”)。extensions_changed 广播与持久化的能力状态就是会继续泄漏的两个出口。

建议提取一个辅助函数——例如 serve/sanitize-daemon-message.ts,导出 sanitizeDaemonMessage(message: string, maxLength = 500)——让 workspace-runtime-coordinator.tsworkspace-extensions-controller.ts 都从它导入,删除各自的本地副本以及调用点上的 .slice(0, 500)

修复不得违反的既有事实:coordinator 不能从 packages/cli/src/serve/routes/workspace-extensions-controller.ts 导入既有辅助函数——该模块在 :34 已经导入了 coordinator,因此提取必须落在第三个模块中以避免循环依赖。

验收标准:packages/cli/src/serve/workspace-runtime-coordinator.test.ts 中的 sanitizes Extension reconcile failures before broadcasting and persisting them,它为两个出口(reconciliation.errorstatus().capabilities.extensions.error.message)固定了 tok3n、ANSI 转义和 length <= 500。它在提取过程中必须保持绿色,而在共享辅助函数丢掉截断或脱敏任一项时必须变红。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not addressed in 0e3dbea201. This remains Suggestion-level and is deferred to a follow-up: this PR has passed the review-round convergence limit, so the latest pass was limited to correctness and regression fixes. Leaving this thread open.

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.

Still deferred as of this round. The duplicate sanitizeDaemonMessage hoist remains Suggestion-level, and the thread already carries the author's deferral under the review-round convergence posture — this round was limited to the correctness regression (untrusted-primary 403s after the predicate revert, R2-10) and the unanswered coverage gap (R2-8). Leaving the thread open for the follow-up.

中文说明

本轮仍然暂缓。重复的 sanitizeDaemonMessage 抽取仍是 Suggestion 级,且该讨论串已有作者按评审轮次收敛策略做出的推迟回复——本轮只处理正确性回归(谓词回退后未信任主工作区的 403,R2-10)和唯一无人回复的覆盖缺口(R2-8)。讨论串保持开放,留待后续处理。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against pushed head c80017edd0 (repairs in c031c91474).

Not extracting another shared helper in this PR. Both current daemon sinks apply the existing core credential/control-character sanitizers and a 500-character cap; the suggested drift is a future-maintenance concern rather than a reproduced current leak. Consolidation remains open for follow-up, without introducing the controller/coordinator import cycle.

Comment thread packages/cli/src/serve/workspace-runtime-coordinator.test.ts
Comment thread packages/cli/src/serve/capabilities.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Lint & Static (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Lint & Static (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

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

21 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-6 new toggle wired into only one of the two producers of the advertised feature set — still stands, already reported (round-2 inline comment on packages/cli/src/serve/server/serve-features.ts:164)
  • R2-7 second copy of sanitizeDaemonMessage in the same subsystem — still stands, already reported (round-2 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.ts:48)
  • R2-8 the only ExtensionRuntimeRefreshError test asserts nothing about refreshed/failed — still stands, already reported (round-2 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.test.ts:288)
  • R2-9 resetReferenceState clears only the picker's candidate list — still stands, already reported (round-2 inline comment on packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:1348)
  • R2-10 primaryWorkspaceTrusted gates both tags on the primary while the routes are trust-gated per target — still stands, already reported (round-2 inline comment on packages/cli/src/serve/capabilities.ts:719)
  • shared extensions status cache written from a trust-forced manager — already reported (issue comment 5550758390 item 1, packages/cli/src/serve/routes/workspace-extensions-controller.ts:1063)
  • reconcile failed count omits configsFailed — already reported (issue comment 5550758390 item 2, packages/cli/src/serve/workspace-runtime-coordinator.ts:407)
  • both new coordinator branches unreachable from the unit suite — already reported (round-1 inline comments on packages/cli/src/serve/routes/workspace-extensions-controller.ts:716 and packages/cli/src/serve/routes/workspace-extensions.ts:2455…
  • cancelDrain replay double-schedules the derived reconciliations — already reported (round-1 inline comment on packages/cli/src/serve/workspace-runtime-coordinator.ts:170)
  • unreachable drain-deferral assignments in invalidateDerivedCapabilities — already reported (round-2 deferral on packages/cli/src/serve/workspace-runtime-coordinator.ts:1015)
  • two new user-facing strings bypass the i18n table — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:621)
  • merge overwrites the live runtime entry's updateState — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/extensions-manager-logic.ts:75)
  • PluginManagerPage Extensions fixture equals the primary — already reported (round-1 inline comment on packages/web-shell/client/components/plugins/PluginManagerPage.test.tsx:239)
  • runtimeEpoch stamp on the Extension catalog untested — already reported (round-1 inline comment on packages/acp-bridge/src/bridge.ts:6306)
  • extensionsQueuedWork term in hasActiveWork() pinned by no test — already reported (round-2 deferral on packages/cli/src/serve/workspace-runtime-coordinator.ts:187)
  • runtime-catalog route trust gate untested — already reported (round-1 inline comment on packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:435)
  • primary runtime-catalog alias has no server-side test — already reported (round-2 deferral on packages/cli/src/serve/routes/workspace-extensions.ts:2403)
  • no test executes the split-runtime arm of ExtensionsManagerPage — already reported (round-1 inline comment on packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:627)
  • new end-to-end capability coverage sits outside every npm workspace — already reported (round-2 deferral on integration-tests/cli/qwen-serve-routes.test.ts:389)
  • poller reconciles an already-applied generation a second time — folded into R1-4 as its measured trigger (packages/cli/src/serve/routes/workspace-extensions.ts:799)
  • …and 1 more (see the run report)

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 predates 2.72.0), so the set is UNKNOWN rather than empty; the motivating-incident replay ran against the PR's own narrative instead.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally, so the assertions this PR adds in integration-tests/cli/qwen-serve-routes.test.ts were never executed anywhere.

Not reviewed: build-and-test — the test-efficacy probe could not validate its harness (harnessValidated null: the positive control never ran, no probe file was green in the unmutated baseline) and ran no mutants or hunks (24+3 mutants and 70+6 hunks skipped for cap/baseline), so no coverage claim rests on it.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": did not compare the scope of the legacy DELETE /workspace/extensions/:name (used by actions.uninstallExtension ) against the new user-store DELETE /extensio…; "agent reverse-audit (round 2)": did not confirm that Extension.id always matches /^[a-f0-9]{64}$/ ( parseExtensionId , workspace-extensions.ts:1785-1795 ), so I could not rule out a 400 …; "agent 1c": did not execute the new/changed unit suites ( packages/cli/src/serve/workspace-runtime-coordinator.test.ts , packages/cli/src/serve/routes/workspace-qualified-…; "agent reverse-audit (round 1)": design-doc claim "Source installs use the V2 global route. Archive uploads remain on the legacy workspace route until a V2 archive endpoint exists, so they reta…; "agent reverse-audit (round 1)": design-doc fan-out claim "Global mutations invalidate every managed runtime. Workspace activation and resource-state mutations invalidate only the selected runt…, and 8 more.

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

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

  • docs/developers/qwen-serve-protocol.md:375 — [probe] the new wire-contract row carries no owning-capability annotation
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:623 — [probe] the retry condition enumerates two of the five capability states
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:589 — [probe] the split branch never reads runtime.errors
  • packages/web-shell/client/components/extensions/extensions-manager-logic.test.ts:170 — [probe] the updateState precedence assertion cannot fail
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:613 — [probe] no drain re-check after the preheat await, so a drain mid-flight still fires the mutating reconcile
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:619 — [probe] any configsFailed makes the 30s poller re-drive the full refresh forever with no bound
  • packages/cli/src/serve/workspace-runtime-coordinator.test.ts:168 — [probe] no test drives a non-zero sessionsRefreshed, so the broadcast refreshed count is unpinned
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:627 — [probe] the 2s retry has no cap or backoff and re-fetches twice per cycle plus a mutating preheat
  • packages/web-shell/client/components/plugins/PluginManagerPage.test.tsx:212 — [probe] no test pins the feature-absent Extensions branch
  • docs/design/workspace-runtime-architecture.md:983 (+3 locations) — [probe] the docs claim an Extensions config/runtime Catalog pair; only the runtime leg exists
  • packages/cli/src/serve/routes/workspace-extensions.ts:781 — [probe] the poller's state !== 'stopping' conjunct is dead and untestable
  • packages/cli/src/serve/server/serve-features.ts:161 — [probe] both tags are advertised when trust is not authoritative while the primary runtime is trusted: false
  • packages/cli/src/serve/workspace-runtime-coordinator.test.ts:181 — [probe] nothing pins the success-gate derived Skills/MCP reschedule
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:1026 — [review] the check-for-updates poll has no abort signal and no unmount teardown

Convergence: round 3 posted 14 inline comment(s), 6 of them reported for the first time; the previous round posted 13 (11 new). Findings keep coming back to the same files: packages/cli/src/serve/workspace-runtime-coordinator.ts (findings in rounds 1, 2; 2 more now); packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx (findings in round 2; 2 more now); packages/cli/src/serve/routes/workspace-extensions.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.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 21 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 predates 2.72.0), so the set is UNKNOWN rather than empty; the motivating-incident replay ran against the PR's own narrative instead.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally, so the assertions this PR adds in integration-tests/cli/qwen-serve-routes.test.ts were never executed anywhere.

未审查(原文为英文):build-and-test — the test-efficacy probe could not validate its harness (harnessValidated null: the positive control never ran, no probe file was green in the unmutated baseline) and ran no mutants or hunks (24+3 mutants and 70+6 hunks skipped for cap/baseline), so no coverage claim rests on it.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"did not compare the scope of the legacy DELETE /workspace/extensions/:name (used by actions.uninstallExtension ) against the new user-store DELETE /extensio…"agent reverse-audit (round 2)"did not confirm that Extension.id always matches /^[a-f0-9]{64}$/ ( parseExtensionId , workspace-extensions.ts:1785-1795 ), so I could not rule out a 400 …"agent 1c"did not execute the new/changed unit suites ( packages/cli/src/serve/workspace-runtime-coordinator.test.ts , packages/cli/src/serve/routes/workspace-qualified-…"agent reverse-audit (round 1)"design-doc claim "Source installs use the V2 global route. Archive uploads remain on the legacy workspace route until a V2 archive endpoint exists, so they reta…"agent reverse-audit (round 1)"design-doc fan-out claim "Global mutations invalidate every managed runtime. Workspace activation and resource-state mutations invalidate only the selected runt…,另有 8 条。

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

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

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-extensions-controller.ts Outdated

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

Unresolved, please confirm:

  • [Critical] R9-9 (packages/cli/src/serve/workspace-runtime-coordinator.ts:1188) — could not determine whether the failure cooldown still admits two re-drives per window: this round's agents split, one tracing steady state to a single failing attempt pe…
  • [Critical] R10-2 (packages/cli/src/serve/routes/workspace-extensions.ts:2296) — could not determine: the ledger's claim text was not recoverable in full within this round's time budget, so the mechanism could not be re-traced.
  • [Critical] 3 entries — could not determine: claim text not recoverable in full within the time budget.:
    • R10-3 (packages/cli/src/serve/routes/workspace-extensions.ts:2239)
    • R10-6 (packages/cli/src/serve/workspace-runtime-coordinator.ts:731)
    • R10-7 (packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:1672)
  • [Critical] R10-8 (whole-PR) — could not determine: the claim that narrowing the 30-second reconciler to live coordinator runtimes leaves cold workspaces unconverged; this round confirmed the narrowing is implemented and documented as intended, but c…
  • [Critical] R11-2 (packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx) — could not determine whether the overlay-less first-paint merge still states "No description" as fact: applyFirstPaint now keeps the previous rows when non…

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 is older than the 2.72.0 the query needs), so no linked-issue evidence was available; the root-cause-ownership and motivating-incident duties still ran against the PR's own narrative.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": the design doc's "Their preparation deadline cancels pending input" — I confirmed a preparation deadline exists ( workspace-extensions-controller.ts:569 , opti…; "agent reverse-audit (round 2)": "Prepared resources remain owned by the route until its finally disposal, including when the deadline prevents commit" — not traced to the route's finally b…; "agent reverse-audit (round 2)": the new design doc's Web Shell steps 3-4 and its "badges show unknown, not the global default" and notice-attribution/in-flight-lock sentences — not walked agai…; "agent reverse-audit (round 1)": did not trace @ext: resolution from the ACP prompt handler into the owning runtime's session config ( packages/cli/src/ui/hooks/atCommandProcessor.ts:268,295 …; "agent reverse-audit (round 1)": did not confirm from the workspace registry state machine that a registered secondary can be listed in /capabilities workspaces[] while state !== 'active' a…, and 26 more.

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

Not reviewed: "agent verify" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

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

  • docs/design/workspace-runtime-architecture.md:551 — [review] This diff declares the Extension generation/epoch contract…
  • docs/developers/qwen-serve-protocol.md:526 — [review] The changed transition sentence describes a single forward…
  • docs/developers/qwen-serve-protocol.md:602 — [review] The documented trusted field on the workspace activation…
  • packages/acp-bridge/src/bridge.ts:6424 — [review] The only place that stamps runtimeEpoch onto a live…
  • packages/cli/src/serve/routes/workspace-extensions.ts:2474 — [review] sendRuntimeCatalog awaits a cross-process runtime read…
  • packages/cli/src/serve/routes/workspace-extensions.ts:2493 — [review] The new legacy-primary route has no daemon-side test:…
  • packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:1635 — [review] All six new tests hand-inline the same coordinator-runtime…
  • packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:3229 — [review] The route-conditional expectation is inert for the…
  • packages/cli/src/serve/workspace-runtime-coordinator.test.ts:697 — [review] This PR adds this.extensionsQueuedWork > 0 to…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:493 — [review] The reconciliation counters are only recoverable from an…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:538 — [review] The coalescing of two *overlapping* deferred Extension…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:823 — [review] The recoveringFromError term makes…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:1193 — [review] Everything invalidateDerivedCapabilities writes except…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:1259 — [review] The "one retry before latching" marker is revision-scoped…
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:900 — [review] The runtime-catalog freshness guard (ensure → capability…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:218 — [review] The new tests rebind the shared hoisted fixture…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:391 — [review] renders an unowned runtime Extension error in the detail…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:967 — [review] The second half of clears a stale refresh failure when a…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:2056 — [review] The last test never sets state.workspace.workspaceCwd ,…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:706 — [review] The split-runtime load only re-arms its 2 s retry for…
  • …and 10 more (see the run report)

Convergence: round 12 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 10 (4 new). Findings keep coming back to the same files: packages/cli/src/serve/routes/workspace-extensions.ts (findings in round 10; 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.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

[Critical] R9-3 (packages/cli/src/serve/routes/workspace-extensions-controller.ts:752) — still stands: the deferral warning is chosen from a liveness sample taken before the reconcile, so a runtime that is live but draining is told to "Retry the runtime refresh" even though the coordinator has already queued that reconciliation and will replay it after the drain; the message asks the user for an action the daemon has already taken. Re-confirmed at the moved line this round (the finding is recorded at low confidence because the drain window could not be executed, but the sampling order is unchanged code).

[Critical] R10-4 (packages/cli/src/serve/routes/workspace-extensions.ts:2520) — still stands: the projection read re-sources appliedGeneration from the coordinator capability but observes the store generation with no storeReadRevision, so a fresh client read can never adopt an automatic backup recovery. After a rollback from generation 7 to 6 the same response body reports desiredGeneration: 6 beside appliedGeneration: 7 — a pairing the documented readiness rule treats as impossible — and holds it until the daemon-internal 30 s poller (which does sample the revision) repairs it.

[Critical] R11-1 (packages/cli/src/serve/workspace-runtime-coordinator.ts:49) — still stands: the sanitization invariant this diff declares in its own comment ("Sanitize once at the producer so both sinks stay safe") is enforced for the Extensions producer only. recordSkillsError and recordMcpError store the child runtime's raw error text verbatim into the same capabilities envelope and the same drain-cause log field, so the sibling capabilities can still carry unredacted credentials and ANSI control characters.

[Critical] R11-3 (packages/cli/src/serve/workspace-runtime-coordinator.ts:407) — still stands: observeExtensionGeneration is monotonic while the durable store can reuse a generation number for different content. Every mutation-driven reconcile observes without a hash and clears observedExtensionStoreHash, so the next hash-carrying poll records the new identity instead of diffing it — the reused-generation detection is disarmed for the whole window after any install/update/uninstall, which is the common case rather than the exception.

中文说明

仅完成部分审查,审查缺口已披露。

未决,请确认:共 7 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 is older than the 2.72.0 the query needs), so no linked-issue evidence was available; the root-cause-ownership and motivating-incident duties still ran against the PR's own narrative.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"the design doc's "Their preparation deadline cancels pending input" — I confirmed a preparation deadline exists ( workspace-extensions-controller.ts:569 , opti…"agent reverse-audit (round 2)""Prepared resources remain owned by the route until its finally disposal, including when the deadline prevents commit" — not traced to the route's finally b…"agent reverse-audit (round 2)"the new design doc's Web Shell steps 3-4 and its "badges show unknown, not the global default" and notice-attribution/in-flight-lock sentences — not walked agai…"agent reverse-audit (round 1)"did not trace @ext: resolution from the ACP prompt handler into the owning runtime's session config ( packages/cli/src/ui/hooks/atCommandProcessor.ts:268,295 …"agent reverse-audit (round 1)"did not confirm from the workspace registry state machine that a registered secondary can be listed in /capabilities workspaces[] while state !== 'active' a…,另有 26 条。

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

未审查:"agent verify"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

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

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

[Critical] R9-3 (packages/cli/src/serve/routes/workspace-extensions-controller.ts:752) — still stands: the deferral warning is chosen from a liveness sample taken before the reconcile, so a runtime that is live but draining is told to "Retry the runtime refresh" even though the coordinator has already queued that reconciliation and will replay it after the drain; the message asks the user for an action the daemon has already taken. Re-confirmed at the moved line this round (the finding is recorded at low confidence because the drain window could not be executed, but the sampling order is unchanged code).

[Critical] R10-4 (packages/cli/src/serve/routes/workspace-extensions.ts:2520) — still stands: the projection read re-sources appliedGeneration from the coordinator capability but observes the store generation with no storeReadRevision, so a fresh client read can never adopt an automatic backup recovery. After a rollback from generation 7 to 6 the same response body reports desiredGeneration: 6 beside appliedGeneration: 7 — a pairing the documented readiness rule treats as impossible — and holds it until the daemon-internal 30 s poller (which does sample the revision) repairs it.

[Critical] R11-1 (packages/cli/src/serve/workspace-runtime-coordinator.ts:49) — still stands: the sanitization invariant this diff declares in its own comment ("Sanitize once at the producer so both sinks stay safe") is enforced for the Extensions producer only. recordSkillsError and recordMcpError store the child runtime's raw error text verbatim into the same capabilities envelope and the same drain-cause log field, so the sibling capabilities can still carry unredacted credentials and ANSI control characters.

[Critical] R11-3 (packages/cli/src/serve/workspace-runtime-coordinator.ts:407) — still stands: observeExtensionGeneration is monotonic while the durable store can reuse a generation number for different content. Every mutation-driven reconcile observes without a hash and clears observedExtensionStoreHash, so the next hash-carrying poll records the new identity instead of diffing it — the reused-generation detection is disarmed for the whole window after any install/update/uninstall, which is the common case rather than the exception.

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

Comment thread packages/cli/src/serve/routes/workspace-extensions.ts Outdated
Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Lint & Static (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Lint & Static (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

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

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • documented trusted field on the projection unasserted — docs/developers/qwen-serve-protocol.md:602 — already deferred in round 12
  • only site stamping runtimeEpoch onto a live extensions status — packages/acp-bridge/src/bridge.ts:6423 — already deferred in round 12
  • six tests hand-inline the same coordinator-arming fixture — packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:1639 — already deferred in round 12
  • runtime-catalog freshness guard duplicated — packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:900 — already deferred in round 12

Unresolved, please confirm:

  • [Critical] R9-9 (packages/cli/src/serve/workspace-runtime-coordinator.ts:1188) — round 12 could not determine whether the failure cooldown still admits two re-drives per window; not re-ruled this round
  • [Critical] R10-2 (packages/cli/src/serve/routes/workspace-extensions.ts:2296) — round 12 could not recover the claim text in full, so the mechanism could not be re-traced; not re-ruled this round
  • [Critical] 3 entries — round 12 could not recover the claim text in full; not re-ruled this round:
    • R10-3 (packages/cli/src/serve/routes/workspace-extensions.ts:2239)
    • R10-6 (packages/cli/src/serve/workspace-runtime-coordinator.ts:731)
    • R10-7 (packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:1672)
  • [Critical] R10-8 (whole-PR) — round 12 could not determine whether narrowing the 30-second reconciler to live coordinator runtimes leaves cold workspaces unconverged; this round confirmed the narrowing and filed the untouched sibling doc sentence (D13…
  • [Critical] R11-2 (packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx) — round 12 could not determine whether the overlay-less first-paint merge still states the absent-description text as fact; not re-ruled this round
  • [Critical] 90 inline blocker threads from the 12 prior rounds were not individually re-ruled this round — the recovered incremental anchor was refused (behind-merge-base), so the round re-read the full 10348-line diff and the remaining budget went to …

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 is older than the 2.72.0 the query needs), so no linked-issue evidence was available; the root-cause-ownership and motivating-incident duties ran against the own narrative of the PR.

Not reviewed: reverse audit — stopped after round 1 by the review time budget; the loop did not converge (round 1 reported roughly 40 new findings, so two consecutive dry rounds were never reached).

Not reviewed: reverse-audit round 1 Suggestions (about 30 findings) — the verifier never ruled on them; reported terminal-only as unverified.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI; only integration-tests/cli/qwen-serve-routes.test.ts ran locally (38/38 passed), the rest of the integration suite did not run.

Not reviewed: test-efficacy probe — harnessValidated was null and all 16 probes came back inconclusive (the vitest globalSetup prerequisite guard stopped the runner before collection), so no mutation-coverage measurement exists for this diff.

Not explored to full depth (tool budget reached): chunk 26: did not run npx vitest run src/components/extensions/extensions-manager-logic.test.ts in packages/web-shell (needs built workspace dist/ per AGENTS.md, be…; chunk 3: whether a generic conformance test outside workspace-qualified-extensions.test.ts (e.g. integration-tests/cli/qwen-serve-routes.test.ts or multi-workspace-…; chunk 3: whether bridge.refreshExtensionsForAllSessions() can preheat a *legacy* (non-coordinator) runtime's channel, i.e. the legacy arm of the poller's pending filte…; "agent reverse-audit (round 1)": did not verify whether any daemon-spawned workspace runtime is actually constructed with enabledExtensionOverrides (the trigger link for the second finding — …; "agent reverse-audit (round 1)": did not run packages/web-shell vitest or typecheck against the new extensions-manager-logic.ts ; both findings are from direct code reads, not from an observ…, and 6 more.

Deferred under the convergence posture (round 13, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/serve/routes/workspace-extensions.ts:995 — [review] Critical [fails-closed] [new-surface] Interaction answer endpoint validates the client id…
  • docs/design/workspace-runtime-architecture.md:9 — [review] Updated design exists only in Chinese under the English path
  • docs/design/workspace-runtime-architecture.md:983 — [review] Status markers flipped to landed contradict the untouched…
  • docs/developers/daemon/13-sdk-daemon-client.md:190 — [review] Untouched sibling doc still promises reconciler…
  • docs/developers/qwen-serve-protocol.md:380 — [review] Documented 400/503/403 gate ordering is unpinned by any test
  • docs/developers/qwen-serve-protocol.md:526 — [review] Documented status chain is one-way but the wire moves…
  • packages/cli/src/acp-integration/acpAgent.ts:13977 — [review] skillsOnly parameter-validation branch has no test
  • packages/cli/src/serve/routes/workspace-extensions.ts:791 — [review] Content-hash identity covers activation only, not artifacts
  • packages/cli/src/serve/routes/workspace-extensions.ts:1941 — [review] GET /extensions pays a second discarded loadSettings() per…
  • packages/cli/src/serve/routes/workspace-extensions.ts:2260 — [review] Adds the 4th and 5th copies of the interactive-operation…
  • packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:2083 — [review] No-op broadcast-suppression test has no positive control
  • packages/cli/src/serve/workspace-runtime-coordinator.test.ts:715 — [review] Deferral coalescing of a full and a skills-only reconcile…
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:703 — [review] Overlapping reconciles both invoke a full runtime refresh
  • packages/web-shell/client/components/MessageList.dom.test.tsx:5160 — [review] Third copy of an unmount-timer test for a file this PR…
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:876 — [review] Verbatim second copy of the composer loader drops the…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:873 — [review] Both it.each owner cases take the identical global-notice…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:460 — [review] Standalone extensions panel stays bound to the primary…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:491 — [review] checkUpdates does not clear loadNoticeRef like its three…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:700 — [review] Sticky loadNoticeRef lets a later load blank a mutation's…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:766 — [review] Fixed 2s retry with no backoff, no cap, ignores Retry-After
  • …and 6 more (see the run report)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

[Critical] R13-1 [certifies-falsely] [new-surface] (packages/cli/src/serve/workspace-runtime-coordinator.ts:301) — when the initial Extension reconcile outlives the ensure() observation budget the wait is abandoned but the queued work is not, and ensure() then starts Skills/MCP on their independent tails; on the initial apply refreshesDerivedCapabilities is false, so afterExtensionApply never runs and both publish ready from a runtime read that predates the applied Extension catalog. Relocated from inline: the resolved anchor at this line collides with a different, already-posted claim (comment 3941458978, about a prepareExtensionsRevision readiness re-check), so this distinct confirmed Critical is carried in the body rather than dropped. Fix constraint: packages/cli/src/serve/workspace-runtime-coordinator.ts:1168-1171 — the comment states the invariant that a ready Skills/MCP status never certifies revisions predating the applied generation; and EXTENSIONS_RECONCILE_TIMEOUT_MS = 5 * 60_000 (:30) deliberately exceeds the ensure observation deadline, so a fix must not shorten the reconcile budget to make the race unreachable. Fix witness: a case in packages/cli/src/serve/workspace-runtime-coordinator.test.ts that holds the Extension reconcile in flight past the ensure budget on a generation-0 store, lets it settle, and asserts invalidateWorkspaceSkillsStatus ran and the Skills/MCP revision bumped; it must go red with the revision > 0 gate restored.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 8 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh 2.45.0 is older than the 2.72.0 the query needs), so no linked-issue evidence was available; the root-cause-ownership and motivating-incident duties ran against the own narrative of the PR.

未审查(原文为英文):reverse audit — stopped after round 1 by the review time budget; the loop did not converge (round 1 reported roughly 40 new findings, so two consecutive dry rounds were never reached).

未审查(原文为英文):reverse-audit round 1 Suggestions (about 30 findings) — the verifier never ruled on them; reported terminal-only as unverified.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI; only integration-tests/cli/qwen-serve-routes.test.ts ran locally (38/38 passed), the rest of the integration suite did not run.

未审查(原文为英文):test-efficacy probe — harnessValidated was null and all 16 probes came back inconclusive (the vitest globalSetup prerequisite guard stopped the runner before collection), so no mutation-coverage measurement exists for this diff.

未探索到全部深度(达到工具调用预算):chunk 26:did not run npx vitest run src/components/extensions/extensions-manager-logic.test.ts in packages/web-shell (needs built workspace dist/ per AGENTS.md, be…;chunk 3:whether a generic conformance test outside workspace-qualified-extensions.test.ts (e.g. integration-tests/cli/qwen-serve-routes.test.ts or multi-workspace-…;chunk 3:whether bridge.refreshExtensionsForAllSessions() can preheat a *legacy* (non-coordinator) runtime's channel, i.e. the legacy arm of the poller's pending filte…"agent reverse-audit (round 1)"did not verify whether any daemon-spawned workspace runtime is actually constructed with enabledExtensionOverrides (the trigger link for the second finding — …"agent reverse-audit (round 1)"did not run packages/web-shell vitest or typecheck against the new extensions-manager-logic.ts ; both findings are from direct code reads, not from an observ…,另有 6 条。

收敛姿态下延后(第 13 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 26 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

[Critical] R13-1 [certifies-falsely] [new-surface] (packages/cli/src/serve/workspace-runtime-coordinator.ts:301) — when the initial Extension reconcile outlives the ensure() observation budget the wait is abandoned but the queued work is not, and ensure() then starts Skills/MCP on their independent tails; on the initial apply refreshesDerivedCapabilities is false, so afterExtensionApply never runs and both publish ready from a runtime read that predates the applied Extension catalog. Relocated from inline: the resolved anchor at this line collides with a different, already-posted claim (comment 3941458978, about a prepareExtensionsRevision readiness re-check), so this distinct confirmed Critical is carried in the body rather than dropped. Fix constraint: packages/cli/src/serve/workspace-runtime-coordinator.ts:1168-1171 — the comment states the invariant that a ready Skills/MCP status never certifies revisions predating the applied generation; and EXTENSIONS_RECONCILE_TIMEOUT_MS = 5 * 60_000 (:30) deliberately exceeds the ensure observation deadline, so a fix must not shorten the reconcile budget to make the race unreachable. Fix witness: a case in packages/cli/src/serve/workspace-runtime-coordinator.test.ts that holds the Extension reconcile in flight past the ensure budget on a generation-0 store, lets it settle, and asserts invalidateWorkspaceSkillsStatus ran and the Skills/MCP revision bumped; it must go red with the revision > 0 gate restored.

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

Comment thread packages/cli/src/serve/workspace-runtime-coordinator.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-extensions-controller.ts Outdated
@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code attempted to resolve merge conflicts but the run did not complete successfully.

Check the workflow run for full logs.

qwen-code-ci-bot and others added 2 commits September 10, 2026 22:12
…e-runtime-extensions-main

# Conflicts:
#	packages/cli/src/serve/workspace-runtime-coordinator.ts
… deferrals

- the mutation receipt now pairs the committed generation with the
  committed store's content hash, so a recovery plus recommit that
  reuses the generation number is detected instead of silently
  re-recording a baseline (R10-1/R11-3)
- an update supersedes only the same extension's parked operations
  with its own cancel wording; installs keep the name-blind
  install-replaces-install deadlock breaker (R12-1)
- reconcile results flag drain-queued deferrals, and the controller
  samples runtime liveness after the await, so a drain deferral no
  longer tells the user to retry (R9-3)
- an initial Extension apply that settles after ensure() abandoned
  its observation budget invalidates and re-drives the Skills/MCP
  capabilities that certified from a pre-catalog read (R13-1)
- the projection GET passes the pre-read revision so a fresh read
  adopts an automatic backup recovery (R10-4)
- Skills/MCP preparation errors are sanitized at the producer like
  Extension errors (R11-1)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary

This round merged origin/main (one conflict in packages/cli/src/serve/workspace-runtime-coordinator.ts: main exported ENSURE_KEEP_ALIVE_MS while the branch added sibling constants; resolution keeps both — main's export plus the branch's EXTENSIONS_RECONCILE_TIMEOUT_MS / EXTENSIONS_ERROR_RETRY_COOLDOWN_MS) and addressed the open Critical feedback. Deferred sections of the feedback are audit records and were not touched.

Feedback dispositions

rc:3978577182 (R12-1, cross-extension supersession) — fixed. supersedeActiveInstallOperations now takes the target extension name from the update call sites (POST /workspace/extensions/:name/update, POST /extensions/:extensionId/update) and skips operations whose recorded name belongs to a different extension (case-insensitive, matching findLoadedExtension semantics); the update path rejects with its own wording ('Extension operation cancelled by a new update request') instead of asserting an install nobody requested, and the supersede fast-path now carries the recorded reason so a not-yet-parked operation gets the same message. Install call sites keep the name-blind install-replaces-install sweep — the install's target name is unknowable before preparation resolves, and that sweep is what breaks the two-interactive-installs deadlock. Witness: workspace-qualified-extensions.test.ts 'keeps an unrelated interactive install waiting when another extension updates' — an install of A parks at waiting_for_input via requestApiKey, an update of B succeeds, A is still parked and completes after its interaction is answered. The pre-existing same-extension it.each stays green.

rc:3978577192 (R10-1) + rc:3982030371 (R11-3, receipt path drops content identity) — fixed (one root cause). The controller now reads the post-commit store snapshot and passes its legacyProjectionHash (paired with the committed generation; skipped when the store has already moved past it, since that state belongs to a newer mutation's own receipt) into reconcileExtensionGeneration(generation, { storeContentHash }), which forwards it to observeExtensionGeneration. A recovery-plus-recommit that reuses a generation number is now diffed against the hash the receipt recorded, so the certification resets and the runtime is re-driven. The snapshot read is guarded so a store read failure cannot fail an operation whose commit already landed. Witnesses: coordinator-level 'diffs a recommitted generation against the receipt-recorded hash' and route-level 're-drives the runtime when a mutation receipt observes a reused generation' (skill-state PUT receipt; asserts the reconcile RPC is invoked and the capability drops to stale with appliedGeneration: 0). The second half of R10-1's suggested fix (refusing skillsOnly certification on a hash-unconfirmed base) was declined with evidence — it contradicts the finding's own "must not break" constraint on the pinned narrow-refresh test and is unreachable in production once every generation-moving observation carries a hash; see the thread reply.

rc:3982030380 (R9-3, drain-deferred shares the retry message) — fixed. reconcileExtensionGeneration now reports drainDeferred: true whenever the deferral is queued for replay on cancelDrain() (entry, catch, and post-apply tail sites). The controller samples runtime liveness after the reconcile await (a runtime that died mid-reconcile no longer gets the live wording) and a drain-deferred result gets its own message: 'Extension runtime is draining; the committed generation is queued and will be applied when the runtime resumes.' Both pre-existing strings are untouched for the genuinely-not-applied and not-live cases. Witnesses: controller tests 'does not ask the user to retry a drain-queued reconciliation' and 'samples runtime liveness after the reconciliation settles', plus coordinator test 'marks a drain-queued deferral and replays it on cancelDrain'.

R13-1 (body; late-settling initial apply leaves pre-catalog Skills/MCP certified) — fixed. ensure() now records the runtime epoch when it abandons the Extensions wait (extensionsEnsureAbandonedAtEpoch), and prepareExtensionsRevision treats a certification at that marked epoch as derived-capability-refreshing: the late-settling apply invalidates and re-drives Skills/MCP instead of leaving their pre-catalog reads published as ready. The marker is consumed when it fires and is inert across later epochs. The revision > 0 gate and the invariant comment at the former :1168-1171 are untouched, and the initial-ensure fast path is unchanged (no abandon → no marker → no extra invalidation — the pinned 'shares an in-flight reconciliation with ensure' behavior holds). Witness: 're-certifies Skills/MCP when the initial Extension apply settles past the ensure budget' — a generation-0 store whose reconcile resolves at 61s; Skills/MCP revisions bump from 0 to 1 and re-prepare after the settle.

R10-4 (round-12 body, still open: projection read can never adopt a recovery rollback) — fixed. GET /workspaces/:workspace/extensions now samples the coordinator's extension revision before the store read and passes it to observeExtensionGeneration, the same discipline the 30s poller uses, so a fresh read adopts a lower recovered generation instead of reporting the impossible desired-6/applied-7 pairing until the poller repairs it. A receipt landing between the sample and the observe bumps the revision and conservatively refuses the adoption. Witness: 'adopts a recovered lower generation on the projection read' — after settling at generation 7, a rollback to 6 is adopted by the GET itself (desiredGeneration: 6, appliedGeneration: 0).

R11-1 (round-12 body, still open: Skills/MCP errors persisted raw) — fixed. recordSkillsError and recordMcpError now route their messages through the same producer-side sanitizer (renamed sanitizeRuntimeErrorMessage) as the Extensions producer, so the persisted capability error and anything reading it stay free of git credentials, ANSI/control sequences, and unbounded output. Witnesses: 'sanitizes Skills preparation errors before persisting them' and 'sanitizes MCP preparation errors before persisting them'.

"Could not determine" items — verified, no defect found

  • R9-9 (failure cooldown admitting two re-drives): traced recordExtensionsError/isExtensionsFailureLatched. First failure arms a one-shot retry marker; the retry's failure latches for the 2-minute window and clears the marker; after expiry a single re-drive re-latches on failure. At most one re-drive per window — confirmed, no change.
  • R10-2 / R10-3 / R10-6 / R10-7 (claim text unrecoverable): re-inspected the cited regions (check-updates route, global install commit path, prepareExtensions tail, detail-panel dropdown). Nothing checkable remained; no defect found on inspection. If the original claims resurface with their text, they can be evaluated then.
  • R10-8 (poller narrowed to live runtimes): confirmed the narrowing is the documented intent — cold workspaces converge via ensure() on next use, and the untouched sibling doc sentence is already filed in this round's deferred-findings list (not this round's work).
  • R11-2 (overlay-less first-paint stating 'No description'): disproved — applyFirstPaint keeps previously overlaid rows whenever any exist, and ExtensionsManagerPage.test.tsx pins No description absent after overlay in both the projection-unavailable and refresh-in-flight paths. The bare placeholder only ever paints on an empty first load, which is the documented intent of that block.

Verification

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx vitest run src/serve/workspace-runtime-coordinator.test.ts src/serve/routes/workspace-extensions-controller.test.ts src/serve/routes/workspace-qualified-extensions.test.ts (packages/cli) — 174 passed / 0 failed
  • Mutation probe: with this round's three source files reverted (git stash of sources only, tests kept), all 10 new tests fail; restored, they pass. Failing pre-round: diffs a recommitted generation against the receipt-recorded hash, marks a drain-queued deferral and replays it on cancelDrain, re-certifies Skills/MCP when the initial Extension apply settles past the ensure budget, sanitizes Skills preparation errors before persisting them, sanitizes MCP preparation errors before persisting them, does not ask the user to retry a drain-queued reconciliation, samples runtime liveness after the reconciliation settles, keeps an unrelated interactive install waiting when another extension updates, re-drives the runtime when a mutation receipt observes a reused generation, adopts a recovered lower generation on the projection read.
  • npx prettier --check on the six touched files — clean after formatting.
中文说明

Autofix 本轮摘要

本轮合并了 origin/mainpackages/cli/src/serve/workspace-runtime-coordinator.ts 有一处冲突:main 将 ENSURE_KEEP_ALIVE_MS 改为导出,分支在其后新增了常量;解决方案两者并保留——main 的导出处加上分支的 EXTENSIONS_RECONCILE_TIMEOUT_MS / EXTENSIONS_ERROR_RETRY_COOLDOWN_MS),并处理了未解决的 Critical 反馈。反馈中的 Deferred 小节是审计记录,未改动。

反馈处置

rc:3978577182(R12-1,跨扩展取代)——已修复。 supersedeActiveInstallOperations 现在从更新调用点(POST /workspace/extensions/:name/updatePOST /extensions/:extensionId/update)接收目标扩展名,跳过 name 属于其他扩展的操作(大小写不敏感,与 findLoadedExtension 语义一致);更新路径使用自己的文案('Extension operation cancelled by a new update request'),不再断言一个没人请求的「安装」,且取代快速路径携带已记录的文案,尚未挂起的操作也会得到同一消息。安装调用点保留不看名字的「安装取代安装」清扫——安装的目标名在 preparation 完成前不可知,而该清扫正是打破两个交互式安装死锁的机制。Witness:workspace-qualified-extensions.test.ts 的 'keeps an unrelated interactive install waiting when another extension updates'——A 的安装经 requestApiKey 停在 waiting_for_input,B 的更新成功,A 仍挂起并在回答交互后完成。既有的同扩展 it.each 用例保持绿色。

rc:3978577192(R10-1)+ rc:3982030371(R11-3,回执路径丢失内容标识)——已修复(同一根因)。 controller 现在读取提交后的 store 快照,将其 legacyProjectionHash(与已提交 generation 配对;store 已前进到更新 generation 时跳过,因为那是更新的变更自己的回执)传入 reconcileExtensionGeneration(generation, { storeContentHash }),后者转发给 observeExtensionGeneration。复用 generation 号的「恢复+重提交」现在会与回执记录的 hash 做 diff,认证被重置并重新驱动 runtime。该快照读取带保护,store 读取失败不会让已提交的操作失败。Witness:协调器级 'diffs a recommitted generation against the receipt-recorded hash',路由级 're-drives the runtime when a mutation receipt observes a reused generation'(skill-state PUT 回执;断言 reconcile RPC 被调用且 capability 落为 staleappliedGeneration: 0)。R10-1 修复建议的后半部分(对未确认 hash 的基底拒绝 skillsOnly 认证)已带证据婉拒——它与该发现自己「不得破坏」的 narrow-refresh 钉住测试相矛盾,且在所有令 generation 前进的观测都携带 hash 之后在生产中不可达;详见线程回复。

rc:3982030380(R9-3,drain 延期与「未应用」共用文案)——已修复。 reconcileExtensionGeneration 现在在延期被排队等待 cancelDrain() 重放时报告 drainDeferred: true(入口、catch、apply 后尾部三处)。controller 在 reconcile await 之后采样 runtime 存活状态(在 reconcile 期间死掉的 runtime 不再得到存活文案),drain 延期获得自己的消息:'Extension runtime is draining; the committed generation is queued and will be applied when the runtime resumes.' 两条既有文案在真正未应用与不存活的场景下保持不变。Witness:controller 测试 'does not ask the user to retry a drain-queued reconciliation' 与 'samples runtime liveness after the reconciliation settles',以及协调器测试 'marks a drain-queued deferral and replays it on cancelDrain'。

R13-1(review 正文;初始 apply 迟于预算落账,Skills/MCP 以旧读数发布 ready)——已修复。 ensure() 放弃 Extensions 等待时记录当前 runtime epoch(extensionsEnsureAbandonedAtEpoch),prepareExtensionsRevision 在被标记 epoch 上的认证视为需要刷新派生能力:迟到的 apply 会使 Skills/MCP 失效并重新驱动,而不是让它们以 catalog 应用前的读数保持 ready。标记在触发时被消费,跨后续 epoch 惰性无害。revision > 0 门与原 :1168-1171 的不变量注释未动,初始 ensure 快路径不变(未放弃 → 无标记 → 无额外失效——'shares an in-flight reconciliation with ensure' 的钉住行为保持)。Witness:'re-certifies Skills/MCP when the initial Extension apply settles past the ensure budget'——generation-0 store 的 reconcile 在 61s 才返回;落账后 Skills/MCP revision 从 0 升到 1 并重新准备。

R10-4(第 12 轮正文仍未解决:projection 读永远无法采纳恢复回滚)——已修复。 GET /workspaces/:workspace/extensions 现在在 store 读取前采样协调器的扩展 revision 并传给 observeExtensionGeneration,与 30s 轮询同一纪律,因此新鲜读取会采纳更低的恢复 generation,而不是报告不可能的 desired-6/applied-7 配对直到轮询修复。采样与观测之间落入的回执会推高 revision 并保守拒绝采纳。Witness:'adopts a recovered lower generation on the projection read'——在 generation 7 就绪后,回滚到 6 由 GET 自身采纳(desiredGeneration: 6, appliedGeneration: 0)。

R11-1(第 12 轮正文仍未解决:Skills/MCP 错误原文持久化)——已修复。 recordSkillsErrorrecordMcpError 现在与 Extensions 生产者一样经过同一个生产者侧消毒函数(更名 sanitizeRuntimeErrorMessage),持久化的 capability 错误及其读者不再携带 git 凭据、ANSI/控制序列或无界输出。Witness:'sanitizes Skills preparation errors before persisting them' 与 'sanitizes MCP preparation errors before persisting them'。

「无法确定」条目——已逐一核实,未发现缺陷

  • R9-9(失败冷却是否每窗口放进两次重驱动): 追踪 recordExtensionsError/isExtensionsFailureLatched。首次失败挂起一次性重试标记;重试失败后在 2 分钟窗口内闩锁并清除标记;窗口过期后允许一次重驱动,失败则重新闩锁。每窗口至多一次重驱动——确认无缺陷,不改动。
  • R10-2 / R10-3 / R10-6 / R10-7(认领文本无法恢复): 已复查对应区域(check-updates 路由、全局安装提交路径、prepareExtensions 尾部、详情面板下拉)。没有可核对的剩余内容;复查未见缺陷。若原始认领文本日后恢复,可再评估。
  • R10-8(轮询收窄到存活 runtime): 确认收窄是文档有意行为——冷 workspace 在下次 ensure() 时收敛;未触及的姊妹文档句子已列入本轮 deferred 清单(非本轮工作)。
  • R11-2(无覆盖首屏把 'No description' 当事实): 证伪——applyFirstPaint 只要已有行就保留覆盖行,ExtensionsManagerPage.test.tsx 在 projection 不可用与刷新在途两条路径都钉住覆盖后不出现 'No description'。裸占位只在空列表首刷出现,这是该代码块的有意行为。

验证

  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0)
  • npx vitest run src/serve/workspace-runtime-coordinator.test.ts src/serve/routes/workspace-extensions-controller.test.ts src/serve/routes/workspace-qualified-extensions.test.ts(packages/cli)—— 174 通过 / 0 失败
  • 变异探针:仅还原本轮三个源文件(git stash 源文件、保留测试)时,10 个新测试全部失败;恢复后全部通过。修复前失败的用例:diffs a recommitted generation against the receipt-recorded hashmarks a drain-queued deferral and replays it on cancelDrainre-certifies Skills/MCP when the initial Extension apply settles past the ensure budgetsanitizes Skills preparation errors before persisting themsanitizes MCP preparation errors before persisting themdoes not ask the user to retry a drain-queued reconciliationsamples runtime liveness after the reconciliation settleskeeps an unrelated interactive install waiting when another extension updatesre-drives the runtime when a mutation receipt observes a reused generationadopts a recovered lower generation on the projection read
  • 对六个改动文件运行 npx prettier --check —— 格式化后干净。

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

🧵 Resolved all 4 selected review thread(s). · 已关闭全部选中的 4 条评审线程。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

…e-runtime-extensions-main

# Conflicts:
#	packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx
#	packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx

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

Unresolved, please confirm:

  • [Critical] 89 inline blocker threads from the 12 prior rounds (of the 92 the comment-status index reports) were not individually re-ruled this round — the recovered incremental anchor was refused (behind-merge-base), so the round re-read the full 1174…

Not reviewed: reverse-audit rounds 1-2 Suggestions (about 54 findings) — the verifier never ruled on them; reported terminal-only as unverified.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally (integration-tests/ is not an npm workspace, so the scoped test command never collects it).

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

Not reviewed: test-efficacy probe — harnessValidated was null and all 17 revert probes came back inconclusive (the vitest globalSetup prerequisite guard aborts the disposable worktree before collection), so no mutation-coverage measurement exists for this diff.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": I did not grep packages/cli/src/serve/server.test.ts or run-qwen-serve.test.ts for an end-to-end assertion on the projection body's appliedGeneration — fi…; "agent reverse-audit (round 1)": did not confirm which structure fixes the advertised order that expect(features).toEqual([...EXPECTED_REGISTERED_FEATURES]) (server.test.ts:3166) compares aga…; "agent reverse-audit (round 1)": did not confirm entry.state === 'active' for the createSingleWorkspaceRegistry primary entry (server.ts:1356-1378), which the suggested trusted: true cont…; "agent reverse-audit (round 2)": did not execute the extensionSnapshotsCurrent → return false mutation against npx vitest run src/components/extensions/extensions-manager-logic.test.ts src…; "agent reverse-audit (round 2)": did not locate the production definition of workspace.actions.loadExtensionsStatus to confirm the legacy loader's literal route is /workspace/extensions as …, and 15 more.

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

Deferred under the convergence posture (round 14, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/serve/routes/workspace-extensions.ts:2277 — [review] Critical [fails-closed] [new-surface] Interaction answer route validates the client id against…
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx:1331 — [review] loadNoticeRef not reset by action paths blanks their notice

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):reverse-audit rounds 1-2 Suggestions (about 54 findings) — the verifier never ruled on them; reported terminal-only as unverified.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally (integration-tests/ is not an npm workspace, so the scoped test command never collects it).

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and only the Linux suites ran locally, so no cross-platform verification of this change exists.

未审查(原文为英文):test-efficacy probe — harnessValidated was null and all 17 revert probes came back inconclusive (the vitest globalSetup prerequisite guard aborts the disposable worktree before collection), so no mutation-coverage measurement exists for this diff.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"I did not grep packages/cli/src/serve/server.test.ts or run-qwen-serve.test.ts for an end-to-end assertion on the projection body's appliedGeneration — fi…"agent reverse-audit (round 1)"did not confirm which structure fixes the advertised order that expect(features).toEqual([...EXPECTED_REGISTERED_FEATURES]) (server.test.ts:3166) compares aga…"agent reverse-audit (round 1)"did not confirm entry.state === 'active' for the createSingleWorkspaceRegistry primary entry (server.ts:1356-1378), which the suggested trusted: true cont…"agent reverse-audit (round 2)"did not execute the extensionSnapshotsCurrent → return false mutation against npx vitest run src/components/extensions/extensions-manager-logic.test.ts src…"agent reverse-audit (round 2)"did not locate the production definition of workspace.actions.loadExtensionsStatus to confirm the legacy loader's literal route is /workspace/extensions as …,另有 15 条。

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

收敛姿态下延后(第 14 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 2 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/cli/src/serve/routes/workspace-extensions.ts Outdated
@@ -515,7 +523,7 @@ An operation snapshot has this shape:
}
```

`status` transitions from `queued` to `running`, then to `succeeded`, `succeeded_with_warnings`, or `failed`. While running, `phase` is `preparing`, `committing`, or `reconciling`. Terminal success may include `result` with `status` equal to `installed`, `enabled`, `disabled`, `updated`, `uninstalled`, `checked`, or `refreshed`; reconciliation results can additionally contain `refreshed`, `failed`, and `error`, while batch activation results contain ordered `results`. Update checks return `result.states`, keyed by extension name, with values such as `checking for updates`, `update available`, `up to date`, `not updatable`, or `error`. Credentials and authorization headers are never operation fields.
`status` transitions from `queued` to `running`, optionally `waiting_for_input`, then to `succeeded`, `succeeded_with_warnings`, or `failed`. While running, `phase` is `preparing`, `committing`, or `reconciling`. Terminal success may include `result` with `status` equal to `installed`, `enabled`, `disabled`, `updated`, `uninstalled`, `checked`, or `refreshed`; reconciliation results can additionally contain `refreshed`, `failed`, and `error`, while batch activation results contain ordered `results`. Update checks return `result.states`, keyed by extension name, with values such as `checking for updates`, `update available`, `up to date`, `not updatable`, or `error`. Credentials and authorization headers are never operation fields.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-2: [certifies-falsely] [new-surface] The operation-status transition chain this diff rewrites promises one optional waiting_for_input before a terminal status, but the code re-parks with a fresh interaction.id per promptable setting.

The rewritten sentence documents queued → running → [waiting_for_input] → terminal. In fact every accepted answer resets the operation to status: 'running', phase: 'preparing', interaction: undefined, and the preparation loop prompts sequentially, so the real chain is running → waiting_for_input → running → waiting_for_input → … → terminal. The adjacent new paragraph at :508-509 states neither the repeat nor the id freshness.

Failure scenario: an extension declares two settings that need prompting. POST /extensions/install returns 202; packages/core/src/extension/extensionSettings.ts:250-253 iterates promptForSensitive.concat(promptForEnv) awaiting each answer, and each park mints id: crypto.randomUUID(). A client written against this documented chain treats the first waiting_for_input as the only one, so it stops polling after answering once and hangs; a client that caches the first interaction.id and replays it after the second park gets 404 Extension interaction not found.

Witness — probe on the PR commit (unmodified), one install whose prepare prompts twice:

PROBE-C5 first park answer status: 200
PROBE-C5 replaying the FIRST interaction id now returns: 404
PROBE-C5 answering the second park: 200
PROBE-C5 observed status sequence: waiting_for_input#fa1b4ca2-581c-4999-ad53-f82c3aab30ed
                                -> waiting_for_input#3ba52d2b-b1c5-4da3-859b-7dfc7776213c
        | terminal: {"operation":"install","status":"succeeded","result":{"status":"installed",...}}

baseline: new-surfacegit show 2f426a64f4:docs/developers/qwen-serve-protocol.md line 518 reads "status transitions from queued to running, then to succeeded, …" with no waiting_for_input clause, so the inaccurate clause is a + line of this diff.

Suggested fix: state the cycle and the per-park id, e.g. replace the first clause with: "status transitions from queued to running, then to succeeded, succeeded_with_warnings, or failed. An operation that needs user input parks in waiting_for_input and returns to running when its answer is accepted; it may park repeatedly (once per required setting), and each park carries a fresh interaction.id — an id from an earlier park answers 404." The same sentence is a good place to record the real running → queued backward edge (workspace-extensions-controller.ts:579-591), which is pre-existing.

Please add a case to packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts that drives an install prompting for two settings and asserts the observed sequence parks twice with two distinct interaction ids, and that replaying the first id returns 404; it must go red if the documented single-park chain is what the code implements.

中文说明

本次 diff 改写的操作状态流转链承诺「终端状态之前至多一次可选的 waiting_for_input」,但代码会为每一个需要提示的设置重新挂起,并且每次都生成全新的 interaction.id

改写后的句子描述的是 queued → running → [waiting_for_input] → terminal。实际上每次应答被接受后,操作都会重置为 status: 'running', phase: 'preparing', interaction: undefined,而准备循环是逐个顺序提示的,因此真实链路是 running → waiting_for_input → running → waiting_for_input → … → terminal。相邻的新段落(:508-509)既未说明会重复挂起,也未说明 id 每次都会更新。

触发场景: 某扩展声明了两个需要提示的设置。POST /extensions/install 返回 202packages/core/src/extension/extensionSettings.ts:250-253 会遍历 promptForSensitive.concat(promptForEnv) 并逐个等待应答,每次挂起都通过 id: crypto.randomUUID() 生成新 id。按本文档链路实现的客户端会把第一次 waiting_for_input 当作唯一一次,于是应答一次后便停止轮询而挂死;而缓存了首个 interaction.id 并在第二次挂起后重放它的客户端会得到 404 Extension interaction not found

证据(在未修改的 PR 提交上探测,一次 prepare 提示两次的 install):第一次挂起应答返回 200;重放第一个 interaction id 返回 404;应答第二次挂起返回 200;观测到的状态序列为两个不同 id 的 waiting_for_input,终态为 {"operation":"install","status":"succeeded"}baseline: new-surface——git show 2f426a64f4:docs/developers/qwen-serve-protocol.md 第 518 行原文为「statusqueued 转为 running,随后转为 succeeded……」,不含 waiting_for_input 从句,因此不准确的这句正是本 diff 的 + 行。

建议修复: 明确写出该循环与「每次挂起都有新 id」,例如将首个从句替换为:「statusqueued 转为 running,随后转为 succeededsucceeded_with_warningsfailed。需要用户输入的操作会停在 waiting_for_input,其应答被接受后回到 running;它可以反复挂起(每个必需设置一次),且每次挂起都携带全新的 interaction.id——使用早前挂起的 id 应答会返回 404。」同一句也适合记录真实存在的 running → queued 反向边(workspace-extensions-controller.ts:579-591,属既有行为)。

请在 packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts 中补充用例:驱动一个需要提示两个设置的 install,断言观测到的序列挂起两次且两次的 interaction id 不同,并重放第一个 id 返回 404;若代码实现的仍是文档所述的单次挂起链路,该用例必须为红。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against 4e35093. The documentation omission is valid: an operation can repeatedly enter waiting_for_input, each prompt has a fresh interaction.id, and replaying an earlier ID returns 404. This is not a runtime Critical: ExtensionsManagerPage restarts polling after each answer and showInteraction handles changed IDs, so the existing client supports sequential prompts. Per the author’s decision, defer the wording clarification (including the running → queued preparation edge) as a documentation follow-up rather than expanding this already heavily reviewed PR. No runtime change is proposed for R14-2.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-2: The newly added lifecycle sentence describes a single optional excursion into waiting_for_input before a terminal status, but an answered interaction returns the operation to running and the same operation can park again — once per prompt.

Answering flips the operation back with status: 'running', phase: 'preparing', interaction: undefined (workspace-extensions.ts:1066-1071), and waitForExtensionInteraction explicitly re-admits an operation already in waiting_for_input (:630-637), so the cycle repeats. It repeats in practice whenever an extension declares more than one setting to prompt for: maybePromptForSettings awaits requestSetting in a loop over settingsChanges.promptForSensitive.concat(settingsChanges.promptForEnv) (packages/core/src/extension/extensionSettings.ts:248-253), and a marketplace install prompts for the plugin choice first and then for settings (extensionManager.ts:2135). An integrator who implements the documented sequence literally — park on waiting_for_input, answer it, then wait for one of the three terminal statuses — never answers the second prompt; the operation sits until the 10-minute interaction deadline and fails with Extension interaction timed out, so the install is lost and the first answer was wasted. Cost is a client whose state machine rejects or ignores a legal, recurring state on the wire.

Witness

witness: not run — a probe would park one install operation twice and quote the two `waiting_for_input` snapshots either side of an answer; it needs a scratch tree, which `qwen review scratch-tree` refused (`available: false`). The recurrence is quoted from code above, and no existing test observes it: every `waiting_for_input` assertion in `packages/cli/src/serve/server.test.ts` (7519, 7551, 7689, 7835, 7913, 7980, 7992, 8136, 8157, 8592, 8605) belongs to a single-interaction scenario, and `answerSettingInteraction` in `workspace-qualified-extensions.test.ts:333-355` answers once.

Suggested fix

State the cycle in the same sentence, e.g. "status transitions from queued to running; an operation that needs input moves to waiting_for_input and returns to running when the interaction is answered, and may do so once per prompt; it ends at succeeded, succeeded_with_warnings, or failed."

Fix witness — N/A (docs-line change; the recurring-prompt behaviour is already pinned by the answer→runningwaiting_for_input cycles in packages/cli/src/serve/server.test.ts, e.g. the interaction-timeout and supersede cases around :7861-8140).

中文说明

[Critical] R14-2 本 diff 重写的操作状态迁移链只承诺一次可选的 waiting_for_input,但代码允许同一个操作反复进入该状态——每个需要提示的设置一次。

回答交互后操作会以 status: 'running', phase: 'preparing', interaction: undefined 回到运行态(workspace-extensions.ts:1066-1071),而 waitForExtensionInteraction 明确允许已处于 waiting_for_input 的操作再次进入(:630-637),因此该循环会重复。当一个扩展声明了多个需要提示的设置时,这在实践中必然发生:maybePromptForSettings 会在 promptForSensitive.concat(promptForEnv) 上循环 await requestSettingpackages/core/src/extension/extensionSettings.ts:248-253),而市场安装会先提示选择插件、再提示设置(extensionManager.ts:2135)。严格按文档实现状态机的集成方在回答第一个提示后只会等待三个终态之一,于是永远不会回答第二个提示;操作会一直挂到 10 分钟的交互超时并以 Extension interaction timed out 失败,安装丢失且用户第一次输入被丢弃。

建议的修复

在同一句话里说明这个循环,例如:「statusqueued 迁移到 running;需要输入的操作会进入 waiting_for_input,交互被回答后回到 running,且每个提示可能发生一次;最终以 succeededsucceeded_with_warningsfailed 结束。」

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

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.

Verified the claim against the code: an accepted answer returns the operation to running and the preparation loop parks again once per promptable setting, each park minting a fresh interaction.id, so replaying an earlier id answers 404 — the rewritten sentence does read as a single optional park. Per the maintainer's ruling on this thread (rc:3987761094), the wording clarification — including the pre-existing running → queued edge — is deferred to a documentation follow-up rather than expanding this PR, so no change is landing here this round. Recorded in the deferred-findings queue so it survives the merge; leaving the thread open for that follow-up.

中文说明

已对代码核实该论断:应答被接受后操作回到 running,准备循环会按每个需提示的设置再次挂起,且每次挂起都生成全新的 interaction.id,重放旧 id 会得到 404 —— 改写后的句子确实读起来像只有一次可选挂起。按照维护者在本线程的裁定(rc:3987761094),该措辞澄清(包括既有的 running → queued 反向边)延后到文档后续跟进,不再扩大本 PR,因此本轮不在此落地改动。已记录进延后队列,合并后仍会保留;线程保持开放,等待后续处理。

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.

Same disposition as the earlier raise of this finding (rc:3987175814): the re-park-per-prompt cycle with a fresh interaction.id per park is confirmed real, but the maintainer has already ruled that the wording clarification is a documentation follow-up outside this PR, with no runtime change. Recorded in the deferred-findings queue; leaving the thread open for that follow-up.

中文说明

与本发现此前提出时(rc:3987175814)的处置相同:每个提示重新挂起、每次挂起生成全新 interaction.id 的循环已确认为真,但维护者已裁定该措辞澄清作为文档后续跟进、不在本 PR 内处理,且不做运行时改动。已记录进延后队列;线程保持开放,等待后续处理。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R14-2: [certifies-falsely] [new-surface] R14-2 (still stands): the lifecycle sentence this diff adds describes a single optional excursion into waiting_for_input before a terminal status, but an answered interaction returns the operation to running and the same operation can park again — once per promptable setting, each with a fresh interaction.id.

Failure scenario: A client written against this page models one optional prompt per operation, so it allocates a single interaction slot and treats a second waiting_for_input snapshot (with a new interaction.id) as an unexpected state; the operation can park once per promptable setting, and answering flips it back to status: running, phase: preparing, interaction: undefined. The sentence is unchanged since the previous round posted, and so is the code it describes.

Witness:

not run — a prose-vs-code mismatch has no run capability here. Age proof instead: `git --literal-pathspecs diff 44983f74c6199feaa044717992cfc65d2cf11b1a..HEAD -- docs/developers/qwen-serve-protocol.md` is empty (the file is not among the four the fix round touched), and the re-park path it describes (`workspace-extensions.ts:638-657,1067-1075`, `extensionSettings.ts:249-253`) is likewise outside every hunk of that diff, so both sides of the mismatch stand as filed.

Suggested fix: State the repetition, e.g. "status transitions from queued to running, then through zero or more waiting_for_input excursions — one per promptable setting, each carrying a fresh interaction.id — before reaching succeeded, succeeded_with_warnings, or failed."

中文说明

[Critical] 本 diff 新增的生命周期语句把 waiting_for_input 描述为终态前“可选的一次”停留,但一次回答会把 operation 退回 running,同一 operation 可再次进入等待——每个可提示的 setting 一次,且每次都是全新的 interaction.id。按此页面实现的客户端只会预留一个交互槽位。该句与它描述的代码自上一轮以来均未改动。

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

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

Agent-assisted review at 5fc7d91a7a926264f375c89cb1d7e5997c0203a5 — Partial review — coverage gaps; historical Criticals are not comprehensively cleared.

Base: 2f426a64f407375013ea8de65b1d84579e800ba1. Comment only — no approval implied. No independently confirmed new Critical in this pass; this is not a clean/C=0 disposition.

Reassessment of recent Critical claims

  • R14-1: canonical update identity fixed; broader claim not verified as stated. Preflight detected the move from 4e3509398660c37892fc634495e867fe47877c38; I inspected the complete one-commit delta (one production line and one test row) and reanchored. packages/cli/src/serve/routes/workspace-extensions.ts:1698 now records the resolved extension.name for legacy source-URL updates, matching V2 at 2354–2356. The added source-URL test row is at workspace-qualified-extensions.test.ts:3180; it was inspected, not executed. The sweep still compares operation.name at 605–611 while install creation at 1369–1374 supplies only source. Preparation concurrency is two (workspace-extensions-controller.ts:60,293–295) and the sweep runs inside preparation. However, “queued indefinitely” is not established by a 1.5-second sample: interactions expire after 10 minutes (workspace-extensions.ts:70–73,648–657) and preparation has its own deadline. More importantly, the claimed install-over-update downgrade needs reconciliation with the real store: packages/core/src/extension/extension-store.ts:681–685 rejects an install when its destination already exists; updates also check artifact generation at 747–755, wired from extensionManager.ts:2772–2782. I did not reproduce a bypass of these guards. Naming/supersession behavior remains unresolved, but I am not treating a mocked ordering witness as proof of silent artifact downgrade. Anchor: #11086 (comment)
  • R14-2 is documentation clarification, not independently demonstrated code Critical. The loop and fresh IDs are real (extensionSettings.ts:249–253; workspace-extensions.ts:638–657,1067–1075). The shorthand at docs/developers/qwen-serve-protocol.md:526 should not imply one prompt only. Current WebShell polling handles repeated interaction snapshots (ExtensionsManagerPage.tsx:875–902, with ID changes at 569–579). No actual client failure was established here; keep this nonblocking/deferred. Anchor: #11086 (comment)
  • The old pre-await liveness warning mechanism is addressed by the post-await sample and drain-specific message (workspace-extensions-controller.ts:774–789). The generation-zero/error recovery path now includes recoveringFromError in derived-capability invalidation (workspace-runtime-coordinator.ts:801–807,905–923). The receipt now passes content hash or explicit null and recovery ID; observeExtensionGeneration invalidates certification for null/reused generation/recovery (425–485). These are specific source checks, not clearance of all older recovery/trust/failure-isolation Criticals.

Ownership and coverage. /extensions and operation history/queues are process-global; /workspace/extensions is the explicit legacy-primary adapter. Qualified runtime catalogs use the selected runtime's workspace service; qualified activation/skill writes are persisted-workspace policy mutations with selected-runtime reconciliation, not primary-runtime execution. The qualified resolver requires an active registry generation and returns mismatch/unavailable rather than primary fallback; trust gates protect runtime execution/mutations, while untrusted projection reads pass runtime.trusted into their manager. The coordinator dispatches through runtime.bridge with its cwd, checks epoch/revision after awaits, defers cold/draining work, and per-target reconciliation catches isolate failures. These checked paths are not a complete trust/TOCTOU audit of every route, bootstrap/removal transition or filesystem/env consumer.

Reviewed selected route/controller/coordinator methods, ACP reconciliation, real store commit guards, protocol and interaction UI. Full 42-file production/test coverage, SDK/UI field propagation, legacy runtime compatibility and the entire historical backlog remain gaps. About 2,462 non-test/non-schema production changed lines is an upper bound, not normalized executable logic; this feature merits advisory maintainer review, not a size-only hard gate. Write access does not establish a maintainer exemption. No PR code/tests/build executed; pinned archive integrity was independently verified. No new Suggestions. Posted partial coverage is not comprehensive review completion.

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

Not reviewed: reverse audit of 49 round-1/2 auditor findings (A24–A72) — the budget stop left them unverified; they are reported terminal-only, never as blockers.

Not reviewed: issue-fidelity (closing-issue set) — gh 2.45.0 cannot query closingIssuesReferences (needs >= 2.72.0), so the linked-issue set is UNKNOWN, not empty; the motivating-incident replay and root-cause ownership were still performed.

Not reviewed: build-and-test — the CI check "Integration Tests (CLI, No Sandbox)" was skipped at this commit; only the diff-changed integration file was run locally (38/38 pass), the rest of that suite did not run.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": whether the deps-injected queryWorkspaceStatus ( packages/cli/src/serve/workspace-service/index.ts:256 , definition not reached) can start an ACP child for an…; "agent reverse-audit (round 1)": whether ServeStatusCell messages inside the ...status spread of sendRuntimeCatalog can carry an unredacted install source — I confirmed extensions[].sour…; "agent reverse-audit (round 1)": did not confirm from the daemon side whether a single queue already serializes the coordinator's workspaceExtensionsReconcile against the legacy refreshExten…; "agent reverse-audit (round 1)": did not read createBootstrapCapabilities to the end, so the integration test's poll predicate ( caps.workspaces !== undefined as the runtime-envelope signal)…; "agent reverse-audit (round 2)": did not grep the rest of docs/developers/qwen-serve-protocol.md for a definition of the projection's appliedGeneration field that would settle whether epoch…, and 25 more.

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

Not reviewed: "agent verify (round 2)" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

⚠️ 73 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Deferred under the convergence posture (round 15, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/serve/workspace-runtime-coordinator.ts:547 — [review] Critical [fails-closed] [new-surface] D15-1: reconcileExtensionGeneration evaluates its "already applie
  • docs/design/workspace-runtime-architecture.md:9 — [review] This PR updates the single-language (Chinese-only) design d
  • docs/developers/qwen-serve-protocol.md:602 — [review] The trust-reporting half of this newly documented contract i
  • packages/acp-bridge/src/bridge.ts:6460 — [review] The only producer of runtimeEpoch on the live Extension ca
  • packages/cli/src/acp-integration/acpAgent.test.ts:31523 — [review] The ~126-line workspaceExtensionsReconcile scenario is app
  • packages/cli/src/acp-integration/acpAgent.ts:14066 — [review] configErrors records the message but not which config prod
  • packages/cli/src/acp-integration/acpAgent.ts:14075 — [review] A failure in *any* leg of a config's reconcile body now supp
  • packages/cli/src/serve/routes/workspace-extensions-controller.test.ts:539 — [review] The three new tests each rebuild the same ~40-line scaffold
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:650 — [review] Of the three deadlineController.signal.throwIfAborted() gu
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:729 — [review] Nothing in the CLI package asserts the store-identity receip
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:791 — [review] The new coordinator branch broadcasts unconditionally, dropp
  • packages/cli/src/serve/routes/workspace-extensions.ts:799 — [review] The store-identity hash is loop-invariant but recomputed ins
  • packages/cli/src/serve/routes/workspace-extensions.ts:1959 — [review] GET /extensions now enters buildLocalExtensionsStatus in
  • packages/cli/src/serve/routes/workspace-extensions.ts:2515 — [review] The new legacy-primary runtime-catalog route has no test any
  • packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts:1633 — [review] The ~45-line coordinator-arming block ( Object.assign(h.seco
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:228 — [review] The new extensionsQueuedWork term in hasActiveWork() is
  • packages/cli/src/serve/workspace-runtime-coordinator.ts:1350 — [review] The Extensions retry budget ( extensionsRefreshRetryRevision
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:996 — [review] The dialog re-implements the composer's new runtime-catalog
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:1545 — [review] Candidates are now workspace-scoped, but the open picker is
  • packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx:335 — [review] The test named for the catalog leg rejecting 403 never rejec
  • …and 7 more (see the run report)

Convergence: round 15 posted 6 inline comment(s), 4 of them reported for the first time; the previous round posted 2 (2 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (6 Critical(s)), the rate of first-time findings is not falling (this round 4, previous 2), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical)

Advisory only — it does not block this review.

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):reverse audit of 49 round-1/2 auditor findings (A24–A72) — the budget stop left them unverified; they are reported terminal-only, never as blockers.

未审查(原文为英文):issue-fidelity (closing-issue set) — gh 2.45.0 cannot query closingIssuesReferences (needs >= 2.72.0), so the linked-issue set is UNKNOWN, not empty; the motivating-incident replay and root-cause ownership were still performed.

未审查(原文为英文):build-and-test — the CI check "Integration Tests (CLI, No Sandbox)" was skipped at this commit; only the diff-changed integration file was run locally (38/38 pass), the rest of that suite did not run.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"whether the deps-injected queryWorkspaceStatus ( packages/cli/src/serve/workspace-service/index.ts:256 , definition not reached) can start an ACP child for an…"agent reverse-audit (round 1)"whether ServeStatusCell messages inside the ...status spread of sendRuntimeCatalog can carry an unredacted install source — I confirmed extensions[].sour…"agent reverse-audit (round 1)"did not confirm from the daemon side whether a single queue already serializes the coordinator's workspaceExtensionsReconcile against the legacy refreshExten…"agent reverse-audit (round 1)"did not read createBootstrapCapabilities to the end, so the integration test's poll predicate ( caps.workspaces !== undefined as the runtime-envelope signal)…"agent reverse-audit (round 2)"did not grep the rest of docs/developers/qwen-serve-protocol.md for a definition of the projection's appliedGeneration field that would settle whether epoch…,另有 25 条。

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

未审查:"agent verify (round 2)"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

⚠️ 循环结束时仍有 73 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

收敛姿态下延后(第 15 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 27 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 15 轮发布了 6 条行内评论,其中 4 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 6 条 Critical),首次发现的速率没有下降(本轮 4,上一轮 2),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

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

Comment thread docs/developers/daemon/13-sdk-daemon-client.md
Comment thread packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx Outdated
Comment thread packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx Outdated
…11086)

- web-shell: reset the load-notice latch when a mutation takes over the
  notice so a later successful load cannot erase the mutation's result
- web-shell: keep recovered operation notices global so they render on
  the list view the user lands on
- web-shell: tolerate runtime-catalog read failures in the split loader
  so a trust-gated 403 no longer discards the fetched catalog/projection
- serve: drop the unreachable install disjunct from the update-side
  supersede sweep and correct its comment (installs carry no name)
- docs: align capabilities-versioning with the live-only generation
  reconciler rule

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #11086 review feedback

Commit: 8c027e5d3a on codex/daemon-workspace-runtime-extensions-main.

Growth audit (required this window)

Window growth: source 148 / test 774 net lines vs budgets 400/400. Verdict: sound (recorded in growth-audit.json). KISS: no structurally simpler approach found that preserves the PR's convergence contract; each accumulated piece traces to a reproduced finding (receipt hashing, generation-reuse detection, recovered store identity, drain deferrals, the supersede sweep, notice ownership). Minimal change: every sampled window hunk traces to an accepted finding or the PR's core purpose. The notice-machinery class of gap recurred on code earlier rounds added, so this round deliberately chose subtractive options: one dead branch and one over-claim removed, three one-line resets aligned with the two existing reset points, no new guard layers.

Feedback dispositions

rc:3990785946 — R15-1 [Critical]: successful load erases a mutation's just-set notice — RESOLVED

Reproduced before fixing: with the load-notice latch set by a capability error and a mutation failing while polled, the next signal-driven load(true) cleared the failure notice (loadNoticeRef.current && messageOwnerRef.current === null over a notice the load path never wrote). Fix: reset loadNoticeRef.current = false where a non-load path takes over the notice — runMutation, setScopeActivation, and settleFailedMutation — matching the two pre-existing reset points (installExtension, refreshList). Witness: new case keeps a failed update notice after the next signal-driven load in ExtensionsManagerPage.test.tsx. Mutation probe: with the three resets removed the witness goes red (expected ... to contain 'probe update failed: disk full'); restored, green. The constraint case clears the load-failure notice once the retried load succeeds stays green. Bounded residual (unchanged by this round's named scope): a recovered mutation's success toast can still be cleared by its own settle reload if a stale latch exists — cosmetic only, rows are fresh; not expanded into given the engaged growth brake.

rc:3990785962 — R15-3 [Critical]: recovery-effect owner attribution hides the notice on the list view — RESOLVED

Verified against the code: setMessageOwner(activeMutation.name ?? 'extension') in the recovery effect, combined with noticeIsGlobal, closes the list-view gate exactly for the names the daemon sends (update/uninstall carry a resolved name). The recovered mutation never navigates, so the user sits on the list view. Fix (subtractive): removed the owner claim entirely, restoring the merge-base behavior where recovered notices are global and render on both views; the two existing tests pinning the runtime-error gate (keeps an owned notice visible…, surfaces a runtime Extension error that arrives after…) stay green. Witness: added the row-matching owner 'demo' to the it.each at shows recovered operation notices with owner %s, restructured so recovery settles after the initial catalog load (otherwise the load's own setMessageOwner(null) masks the claim — discovered while probing). Mutation probe: with the claim re-added, the demo row goes red (toContain('Extension action queued') fails) while the other two rows stay green; removed, all three green.

rc:3990785968 — R15-4 [Critical]: un-tolerated runtime-catalog leg discards catalog + projection, caller reports success — RESOLVED

Verified: the projection leg already tolerates failure (.catch(() => null)) but workspaceRuntimeExtensions() was awaited bare, so a trust-gated 403 (pinned daemon-side for untrusted secondaries) or transient failure skipped apply(...) while the caller still announced success. Fix: .catch(() => undefined), the same degraded value the non-split path and first paint already pass to mergeExtensionCatalog; rows render from catalog + projection without the live overlay. Witness: extended does not refresh the runtime after a user-scope toggle on an untrusted secondary so the projection flips to defaultActivation: 'disabled' when the toggle commits, asserting the badge follows. Mutation probe: without the catch the badge stays enabled (red); with it, green.

rc:3990786210 + rc:3987175805 — R14-1 [Critical]: update-side supersede sweep can never match a parked install — RESOLVED (option (a))

Verified: installs never carry operation.name while active (the only seed point is failureContext.name at the controller, and all three install call sites pass { source }/{}), so the operation.operation !== 'install' disjunct was unreachable dead code and the comment promised a guarantee the code could not honor. Implemented option (a): removed the dead disjunct and corrected the comment to the reachable rule (an update supersedes only the same extension's parked update that is still waiting for input). Behavior-identical: the full route suite (67/67, including keeps an unrelated interactive install waiting when another extension updates and the supersede cases), the controller suite (24/24), and server.test.ts (1278/1278) stay green. The canonical update identity half of rc:3987175805 was already fixed in 5fc7d91a7a (re-verified: workspace-extensions.ts:1698 records the resolved extension.name; source-URL regression row at workspace-qualified-extensions.test.ts:3180). The controller-wide cancellation expansion in rc:3987175805's suggested fix was explicitly declined by the maintainer on that thread (it would also cancel an unrelated extension's interactive operation) and is not implemented.

rc:3987175814 + rc:3990786479 — R14-2 [Critical]: lifecycle sentence implies a single waiting_for_input — DEFERRED (maintainer decision)

Verified real: waitForExtensionInteraction re-parks per promptable setting with a fresh interaction.id each time, and the rewritten sentence at docs/developers/qwen-serve-protocol.md:526 does read as a single optional park. Deferred, not fixed: maintainer @ytahdn ruled on rc:3987761094 that the wording clarification (including the running → queued edge) is a documentation follow-up rather than an expansion of this PR, with no runtime change. Recorded in deferred-findings.json for the follow-up queue; both threads replied to and left open.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/web-shell components/extensions/ExtensionsManagerPage.test.tsx — 40 passed (incl. new R15-1 witness, R15-3 demo row, extended R15-4 case)
  • vitest packages/cli src/serve/routes/workspace-qualified-extensions.test.ts — 67 passed
  • vitest packages/cli src/serve/routes/workspace-extensions-controller.test.ts — 24 passed
  • vitest packages/cli src/serve/server.test.ts — 1278 passed
  • Mutation probes — R15-1: resets removed → witness red → restored green; R15-3: owner claim re-added → demo row red → removed green; R15-4: tolerance removed → badge assertion red → restored green. R14-1: dead-branch removal verified behavior-neutral by the three green CLI suites above (the disjunct had no reachable evaluation).
中文说明

本轮摘要 —— PR #11086 评审反馈处理

提交:8c027e5d3a,分支 codex/daemon-workspace-runtime-extensions-main

增长审计(本窗口必需)

窗口增长:源码 148 / 测试 774 净行,预算 400/400。结论:sound(已记录在 growth-audit.json)。KISS 轴:在保留本 PR 收敛契约的前提下没有找到结构上更简的方案;每一处累积都对应一个已复现的发现(回执哈希、生成号复用检测、恢复后的存储身份、排空延迟标记、取代扫描、通知归属)。最小改动轴:抽查的窗口内 hunk 均可追溯到已接受的评审发现或本 PR 的核心目标。通知机制这一类缺口在早期轮次新增的代码上反复出现,因此本轮刻意选择做减法的方案:删除一个死分支和一个过度声明,按既有的两个重置点对齐加了三个一行重置,没有新增任何防护层。

反馈处置

rc:3990785946 —— R15-1【Critical】成功加载会擦除变更操作刚写下的通知 —— 已解决

修复前已复现:当能力错误置位了加载通知锁存、且一个变更操作在轮询中失败时,下一次信号驱动的 load(true) 会清掉该失败通知(loadNoticeRef.current && messageOwnerRef.current === null 作用于一条并非加载路径写入的通知)。修复:在非加载路径接管通知的位置重置 loadNoticeRef.current = false —— runMutationsetScopeActivationsettleFailedMutation,与两处既有重置点(installExtensionrefreshList)对齐。见证测试:ExtensionsManagerPage.test.tsx 新增用例 keeps a failed update notice after the next signal-driven load。变异探针:移除三处重置后该用例变红(expected ... to contain 'probe update failed: disk full'),恢复后转绿。约束用例 clears the load-failure notice once the retried load succeeds 保持绿色。有界残留(本轮未超出指定范围处理):若存在陈旧锁存,恢复型变更的成功提示仍可能被其自身的收尾重载清除——仅为界面提示层面,行数据是新的;鉴于增长制动已触发,本轮未扩大处理。

rc:3990785962 —— R15-3【Critical】恢复效果的 owner 归属使通知在列表视图不可见 —— 已解决

已对代码核实:恢复效果中的 setMessageOwner(activeMutation.name ?? 'extension')noticeIsGlobal 组合后,恰好对守护进程真实会发送的名称(更新/卸载携带解析后的名称)关闭了列表视图的门;而恢复型变更从不触发导航,用户停留在列表视图。修复(做减法):整体移除该 owner 声明,恢复合并基线的行为——恢复通知为全局,在两个视图均渲染;既有的两个钉住运行时错误门的用例(keeps an owned notice visible…surfaces a runtime Extension error that arrives after…)保持绿色。见证:在 shows recovered operation notices with owner %sit.each 中新增与行同名的 'demo' 取值,并重构为让恢复在初始目录加载完成后才落定(否则加载自身的 setMessageOwner(null) 会掩盖该声明——这是在探针过程中发现的)。变异探针:重新加回声明后 demo 行变红(toContain('Extension action queued') 失败),另两行保持绿色;移除后三行全绿。

rc:3990785968 —— R15-4【Critical】未容错的运行时目录读取丢弃已取得的目录与投影,调用方却报告成功 —— 已解决

已核实:投影读取本就有容错(.catch(() => null)),而 workspaceRuntimeExtensions() 此前是直接 await 的,因此信任门控的 403(守护进程侧已有针对不受信任次工作区的钉住测试)或瞬时失败会跳过 apply(...),调用方仍宣布成功。修复:.catch(() => undefined)——与非 split 路径和首屏绘制传给 mergeExtensionCatalog 的降级取值相同;行由目录+投影渲染,仅缺少实时覆盖层。见证:扩展 does not refresh the runtime after a user-scope toggle on an untrusted secondary,让投影在切换提交时翻转为 defaultActivation: 'disabled',并断言徽标随之更新。变异探针:移除该 catch 后徽标停留在 enabled(红);恢复后转绿。

rc:3990786210 + rc:3987175805 —— R14-1【Critical】更新侧取代扫描永远匹配不到挂起的安装 —— 已解决(方案 a)

已核实:安装操作在活动期间从不携带 operation.name(唯一种子点是 controller 的 failureContext.name,而三个安装调用点传入的都是 { source }/{}),因此 operation.operation !== 'install' 析取项是不可达的死代码,注释承诺了代码无法兑现的保证。按方案 (a) 实施:删除死析取项,并把注释更正为可达规则(更新只取代同一扩展仍在等待输入的挂起更新)。行为等价:完整路由套件(67/67,含 keeps an unrelated interactive install waiting when another extension updates 与各取代用例)、controller 套件(24/24)与 server.test.ts(1278/1278)全部保持绿色。rc:3987175805 的规范更新身份一半已在 5fc7d91a7a 修复(已复核:workspace-extensions.ts:1698 记录解析后的 extension.name;源 URL 回归行在 workspace-qualified-extensions.test.ts:3180)。rc:3987175805 建议修复中的「controller 级取消其他挂起交互」部分已被维护者在该线程明确否决(它会同时取消无关扩展的交互操作),本轮未实现。

rc:3987175814 + rc:3990786479 —— R14-2【Critical】生命周期句暗示单次 waiting_for_input —— 已延后(维护者决定)

已核实为真:waitForExtensionInteraction 会按每个需提示的设置重新挂起,且每次生成全新的 interaction.iddocs/developers/qwen-serve-protocol.md:526 改写后的句子确实读起来像只有一次可选挂起。予以延后而非修复:维护者 @ytahdn 在 rc:3987761094 裁定该措辞澄清(包括 running → queued 反向边)作为文档后续跟进,不再扩大本已多轮评审的 PR,且不做运行时改动。已记录进 deferred-findings.json 进入后续队列;两个线程均已回复并保持未解决。

验证

  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • vitest packages/web-shell components/extensions/ExtensionsManagerPage.test.tsx —— 40 通过(含 R15-1 新见证、R15-3 的 demo 行、扩展后的 R15-4 用例)
  • vitest packages/cli src/serve/routes/workspace-qualified-extensions.test.ts —— 67 通过
  • vitest packages/cli src/serve/routes/workspace-extensions-controller.test.ts —— 24 通过
  • vitest packages/cli src/serve/server.test.ts —— 1278 通过
  • 变异探针 —— R15-1:移除三处重置 → 见证变红 → 恢复转绿;R15-3:加回 owner 声明 → demo 行变红 → 移除转绿;R15-4:移除容错 → 徽标断言变红 → 恢复转绿。R14-1:死分支删除经上述三个全绿的 CLI 套件验证为行为中性(该析取项本无可达求值)。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 148 / test 774 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 148 / 测试 774 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

🧵 Resolved all 5 selected review thread(s). · 已关闭全部选中的 5 条评审线程。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

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

Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (installed gh 2.45.0 predates closingIssuesReferences); the set is UNKNOWN, not empty. Agent 0 fetched the PR-named #10593 and replayed the narrated incident instead..

Not reviewed: build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and its suite did not run locally (Agent 7 measured integration-tests/cli/qwen-serve-routes.test.ts as unreachable from every npm workspace).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": whether the runtime-catalog response's pass-through status.errors[].error can carry an unredacted credentialed source URL — sendRuntimeCatalog spreads ...s…; "agent reverse-audit (round 2)": whether workspaceRegistry can be undefined in a daemon that still advertises workspace_extensions_config_runtime unconditionally ( capabilities.ts :91), whi…; "agent reverse-audit (round 2)": whether the legacy-primary GET /workspace/runtime/extensions needs the same selector/activeness semantics the :380 row documents for the qualified route — it …; "agent reverse-audit (round 1)": I did not execute packages/web-shell/client/components/plugins/PluginManagerPage.test.tsx or useAtMentionMenu.test.tsx to confirm the new/changed cases pass…; "agent reverse-audit (round 1)": whether workspace-qualified-extensions.test.ts (the only file posting /extensions/install , :2632-3294) already drives a V2-created operation through the leg…, and 22 more.

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

1 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).

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

  • integration-tests/cli/qwen-serve-routes.test.ts:391 — [review] R16-6: This changed integration test is outside every npm workspace, so neither this review's build-test scoped run nor the efficacy probe collected it (efficacy kind: "unrea…
  • docs/design/daemon-workspace-runtime-extensions.md:70 — [review] The added sentence states the derived-capability…
  • docs/design/daemon-workspace-runtime-extensions.md:116 — [review] The new design doc states the composer's Extension loader…
  • docs/design/workspace-runtime-architecture.md:9 — [review] This PR updates the Foundation/Target status of a…
  • docs/design/workspace-runtime-architecture.md:32 — [review] This diff flips Extensions to "已落地/已完成" in five places…
  • docs/developers/daemon/11-capabilities-versioning.md:128 — [review] The added convergence guarantee is wider than the code: a…
  • docs/developers/qwen-serve-protocol.md:363 — [review] The rewritten extension_activation_explicit_refresh …
  • docs/developers/qwen-serve-protocol.md:380 — [review] Two of the four documented outcomes for the new GET…
  • docs/developers/qwen-serve-protocol.md:602 — [review] The new workspace_extensions_config_runtime row names…
  • packages/acp-bridge/src/bridge.ts:6460 — [review] The only production code that stamps runtimeEpoch onto…
  • packages/cli/src/acp-integration/acpAgent.ts:13998 — [review] The new skillsOnly type guard is untested —…
  • packages/cli/src/serve/routes/workspace-extensions-controller.test.ts:463 — [review] The it.each title template omits $state , so two rows…
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:650 — [review] The diff adds two deadlineController.signal.throwIfAborted…
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:791 — [review] The coordinator branch broadcasts extensions_changed …
  • packages/cli/src/serve/routes/workspace-extensions-controller.ts:1206 — [review] The new currentManager path bypasses both the cache read…
  • packages/cli/src/serve/routes/workspace-extensions.ts:607 — [review] The new same-extension update supersede fires only when…
  • packages/cli/src/serve/routes/workspace-extensions.ts:642 — [review] The new deadline-abort cancel deletes the pending…
  • packages/cli/src/serve/routes/workspace-extensions.ts:821 — [review] This diff narrows the 30-second generation reconciler to…
  • packages/cli/src/serve/routes/workspace-extensions.ts:822 — [review] The state === 'stopping' half of the new skip is…
  • packages/cli/src/serve/routes/workspace-extensions.ts:844 — [review] The new coordinator branch is nested inside the poller's…
  • …and 45 more (see the run report)

Convergence: round 16 posted 5 inline comment(s), 4 of them reported for the first time; the previous round posted 6 (4 new). Findings keep coming back to the same files: packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx (findings in round 15; 1 more now). The rate of new findings is not falling. 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. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (6 Critical(s)), the rate of first-time findings is not falling (this round 4, previous 4), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical)

Advisory only — it does not block this review.

[Critical] R16-2 This diff makes the process-global POST /extensions/install and POST /extensions/:extensionId/update interactive, but the route that starts the operation validates X-Qwen-Client-Id against every registered workspace runtime's bridge while the only route that can answer the parked interaction validates it against the primary bridge alone — so a client whose id is known only to a secondary runtime can start an install it is then refused the ability to answer. (absorbs the convergence-pair duplicate r064) Failure scenario: sendOperation gates the mutation with primaryController.validateExtensionMutationClient(req, res, { requireClientId: false, bridges: mutationClientBridges(options.refreshRuntimes) }) (:1919-1924), and both changed routes pass refreshRuntimes: () => workspaceRegistry.listAll() (:2296-2299, :2421-2424), so mutationClientBridges returns every runtime's bridge (AcpSessionBridge.knownClientIds() is the union of that bridge's own live sessions — packages/acp-bridge/src/bridge.ts:12498-12508). Take a client id c-S registered only on secondary workspace S's bridge (an SDK/Web Shell client whose sessions all live in S — ExtensionsManagerPage.tsx:1240 passes that same clientId to installUserExtension/installExtension). POST /extensions/install with X-Qwen-Client-Id: c-S is accepted → 202 → the newly attached extensionInteractionHandlers park the operation in waiting_for_input (e.g. a marketplace plugin choice). The only answer endpoint is POST /workspace/extensions/operations/:operationId/interactions/:interactionIdregisterFor is called exactly once, with /workspace/extensions (:1823), and packages/sdk-typescript/src/daemon/DaemonClient.ts:1753 Witness: probe appended to packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts in the isolated tree, reusing the file's own harness (makeHarness, mockExtensionManager, requestApiKey) and the same knownClientIds arming as the diff's new test validates mutation clients against the targeted runtime set (primary → {primary-client}, secondary → {secondary-client}), on unmodified Suggested fix: Validate the interaction answer against the same bridge set the operation was accepted under — e.g. in the registerFor interactions handler pass bridges: mutationClientBridges() (which already falls back to [bridge] when there is no registry, workspace-extensions.ts:771-780) instead of relying on the controller's default single bridge. The fix must not violate an existing fact: The answer endpoint is registered once — registerFor('/workspace/extensions', ...) (packages/cli/src/serve/routes/workspace-extensions.ts:1823) — and docs/developers/qwen-serve-protocol.md:508 states "there is no equivalent global-base alias", so the fix must widen validation on the existing route, not add an /extensions-base alias. Acceptance criterion: packages/cli/src/serve/server.test.ts (the file that already drives /workspace/extensions/operations/:operationId/interactions/:interactionId, e.g. :7841, :8161, :8609): a test that registers a client id on a secondary runtime's bridge only, starts POST /extensions/install with that header, waits for waiting_for_input, then POSTs the answer with the same header and asserts 200 { accepted: true }. Without the widened bridges argument it must fail with 400 invalid_client_id. (Could not be anchored to a diff line — packages/cli/src/serve/routes/workspace-extensions.ts:2279-2288: the quoted snippet matched more than one place once indentation was normalised.) Axes: [fails-closed] [new-surface]

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (installed gh 2.45.0 predates closingIssuesReferences); the set is UNKNOWN, not empty. Agent 0 fetched the PR-named #10593 and replayed the narrated incident instead..

未审查(原文为英文):build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and its suite did not run locally (Agent 7 measured integration-tests/cli/qwen-serve-routes.test.ts as unreachable from every npm workspace).

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"whether the runtime-catalog response's pass-through status.errors[].error can carry an unredacted credentialed source URL — sendRuntimeCatalog spreads ...s…"agent reverse-audit (round 2)"whether workspaceRegistry can be undefined in a daemon that still advertises workspace_extensions_config_runtime unconditionally ( capabilities.ts :91), whi…"agent reverse-audit (round 2)"whether the legacy-primary GET /workspace/runtime/extensions needs the same selector/activeness semantics the :380 row documents for the qualified route — it …"agent reverse-audit (round 1)"I did not execute packages/web-shell/client/components/plugins/PluginManagerPage.test.tsx or useAtMentionMenu.test.tsx to confirm the new/changed cases pass…"agent reverse-audit (round 1)"whether workspace-qualified-extensions.test.ts (the only file posting /extensions/install , :2632-3294) already drives a V2-created operation through the leg…,另有 22 条。

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

1 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。

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

收敛情况:第 16 轮发布了 5 条行内评论,其中 4 条是首次提出;上一轮发布了 6 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/components/extensions/ExtensionsManagerPage.tsx(第 15 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 6 条 Critical),首次发现的速率没有下降(本轮 4,上一轮 4),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

[Critical] R16-2 This diff makes the process-global POST /extensions/install and POST /extensions/:extensionId/update interactive, but the route that starts the operation validates X-Qwen-Client-Id against every registered workspace runtime's bridge while the only route that can answer the parked interaction validates it against the primary bridge alone — so a client whose id is known only to a secondary runtime can start an install it is then refused the ability to answer. (absorbs the convergence-pair duplicate r064) Failure scenario: sendOperation gates the mutation with primaryController.validateExtensionMutationClient(req, res, { requireClientId: false, bridges: mutationClientBridges(options.refreshRuntimes) }) (:1919-1924), and both changed routes pass refreshRuntimes: () => workspaceRegistry.listAll() (:2296-2299, :2421-2424), so mutationClientBridges returns every runtime's bridge (AcpSessionBridge.knownClientIds() is the union of that bridge's own live sessions — packages/acp-bridge/src/bridge.ts:12498-12508). Take a client id c-S registered only on secondary workspace S's bridge (an SDK/Web Shell client whose sessions all live in S — ExtensionsManagerPage.tsx:1240 passes that same clientId to installUserExtension/installExtension). POST /extensions/install with X-Qwen-Client-Id: c-S is accepted → 202 → the newly attached extensionInteractionHandlers park the operation in waiting_for_input (e.g. a marketplace plugin choice). The only answer endpoint is POST /workspace/extensions/operations/:operationId/interactions/:interactionIdregisterFor is called exactly once, with /workspace/extensions (:1823), and packages/sdk-typescript/src/daemon/DaemonClient.ts:1753 Witness: probe appended to packages/cli/src/serve/routes/workspace-qualified-extensions.test.ts in the isolated tree, reusing the file's own harness (makeHarness, mockExtensionManager, requestApiKey) and the same knownClientIds arming as the diff's new test validates mutation clients against the targeted runtime set (primary → {primary-client}, secondary → {secondary-client}), on unmodified Suggested fix: Validate the interaction answer against the same bridge set the operation was accepted under — e.g. in the registerFor interactions handler pass bridges: mutationClientBridges() (which already falls back to [bridge] when there is no registry, workspace-extensions.ts:771-780) instead of relying on the controller's default single bridge. The fix must not violate an existing fact: The answer endpoint is registered once — registerFor('/workspace/extensions', ...) (packages/cli/src/serve/routes/workspace-extensions.ts:1823) — and docs/developers/qwen-serve-protocol.md:508 states "there is no equivalent global-base alias", so the fix must widen validation on the existing route, not add an /extensions-base alias. Acceptance criterion: packages/cli/src/serve/server.test.ts (the file that already drives /workspace/extensions/operations/:operationId/interactions/:interactionId, e.g. :7841, :8161, :8609): a test that registers a client id on a secondary runtime's bridge only, starts POST /extensions/install with that header, waits for waiting_for_input, then POSTs the answer with the same header and asserts 200 { accepted: true }. Without the widened bridges argument it must fail with 400 invalid_client_id. (Could not be anchored to a diff line — packages/cli/src/serve/routes/workspace-extensions.ts:2279-2288: the quoted snippet matched more than one place once indentation was normalised.) Axes: [fails-closed] [new-surface]

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

Comment on lines +765 to +766
storeContentHash:
committedStoreContentHash ?? null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R16-1: [certifies-falsely] [new-surface] When the store has already moved past the committed generation, the receipt coerces "identity not taken" into null, which observeExtensionGeneration defines as "identity could not be read" and answers by dropping the runtime's applied-generation certification — the opposite of what this hunk's own comment says it intends.

Failure scenario: Two installs overlap on one daemon (MAX_UNFINISHED_EXTENSION_OPERATIONS = 10, EXTENSION_PREPARATION_CONCURRENCY = 2, commit queue serialized at 1 but released early by onCommitted at :663–666). Operation A commits generation 5; operation B then acquires the commit queue and commits 6 before A's post-commit tail (disposePreparedExtension, updateExtensionOperation) reaches the receipt read at :727. A's snapshot shows generation: 6 !== 5, so the if at :730 is false and both identity variables stay undefinedstoreContentHash: null is passed to reconcileExtensionGeneration(5, …) for every runtime in refreshTargets (workspaceRegistry.listAll()). In observeExtensionGeneration, null fails the dedup (null === undefined/null === observedHash are both false, coordinator :449–452) and hits storeContentHash === null in the reset at :456–463: appliedExtensionGeneration = 0, appliedExtensionRuntimeEpoch = undefined, observedExtensionStoreHash = undefined, revision bumped, capability status rewritten to {state:'stale', appliedGeneration: 0} — a status that is factually wrong, since the runtime has generation 5 applied. The short-circuit at :510–517 c

Witness:

two probe arms on the **real** `WorkspaceRuntimeCoordinator` (dist), both starting from an identical certified state `{state:'ready', revision:1, runtimeEpoch:1, desiredGeneration:5, appliedGeneration:5}` and then calling `reconcileExtensionGeneration(5, {storeContentHash: X})`:

Suggested fix: Distinguish "read failed" from "not this receipt's generation". Track the failure explicitly and only send null for it, e.g. add let storeIdentityUnreadable = false;, set it in the catch at :734–736, and pass storeContentHash: storeIdentityUnreadable ? null : committedStoreContentHash so the moved-past branch sends undefined. With undefined, observeExtensionGeneration dedups when desired === generation (coordinator :449–452) and otherwise advances desired without the :456–463 reset, leaving the single legitimate prepare to B's receipt / the 30 s poller.

The fix must not violate an existing fact: null must keep meaning "identity unknown → drop the certification": workspace-runtime-coordinator.ts:455–457"Recovery can reuse a generation for different artifacts. null denotes a committed mutation whose identity could not be read; neither it nor an unknown prior baseline may reuse the previous certification." — and the reset it guards at :458–463. Narrowing the call site must not narrow that contract for the genuine read-failure path.

Acceptance criterion: No test reaches this branch today — workspace-extensions-controller.test.ts mocks getExtensionStoreSnapshot only as vi.fn(async () => ({ generation: 2 })) (:498, :566, :636) and never asserts the storeContentHash argument. Add a case in that file: a handler that commits generation 5 (context.commit(async (onCommitted) => { onCommitted(5); return { generation: 5 }; })) with getExtensionStoreSnapshot resolving { generation: 6 }, a spied coordinator.reconcileExtensionGeneration, and expect(spy).toHaveBeenCalledWith(5, expect.objectContaining({ storeContentHash: undefined })). It goes red against the current ?? null, and a companion row with getExtensionStoreSnapshot reject Please prove it by mutation — remove the fix, run that test, confirm it goes red.

中文说明

[Critical] 当 store 已越过本次提交的 generation 时,新的 store 身份回执把“未取到身份”强转为 null,而 observeExtensionGenerationnull 定义为“身份读不出来”,其响应是丢弃该 runtime 的 applied-generation 认证——与本 hunk 注释声明的意图相反。

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

Comment on lines +652 to +654
await expect(
coordinator.reconcileExtensionGeneration(10, { skillsOnly: true }),
).resolves.toMatchObject({ state: 'deferred' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R16-3: [certifies-falsely] [new-surface] A skills-only reconcile at the same generation whose full reconcile just failed certifies that generation — appliedExtensionGeneration === generation - 1 cannot distinguish "N-1 applied, N is a fresh skill delta" from "N's own full apply failed, leaving applied at N-1" — and the test named does not let a skills-only reconcile certify an unapplied generation only exercises the case one generation above the failure, so the reachable hole ships unpinned.

Failure scenario: Runtime at epoch 3, generation 8 fully applied (appliedExtensionGeneration: 8). A mutation commits generation 9 (install, or a skill-state toggle). Two independent production issuers target generation 9: the 30 s poller / ensure() full reconcile (workspace-extensions.ts:846-848 selects the runtime whenever appliedGeneration !== generation || state !== 'ready'; prepareExtensions() reconciles desiredExtensionGeneration with no skillsOnly), and the mutation's own skills-only receipt (workspace-extensions-controller.ts:762-767), also re-issued by cancelDrain() at this.desiredExtensionGeneration with the stored {skillsOnly:true} (workspace-runtime-coordinator.ts:203-213). They serialize FIFO on extensionsTail, so take the order full-then-skills-only. The full reconcile at 9 fails (configsFailed: 1 — e.g. the git-auth failure this file's own sanitizer test uses at :852): recordExtensionsError (:1314) leaves appliedGeneration 8, sets error, and arms one retry (:1355). The skills-only reconcile at 9 then runs: observeExtensionGeneration(9, undefined, undefined, undefined) early-returns (:447-453, same generation, no hash), so the revision stays put and t

Witness:

two probes, each run on the unmodified PR and again with the finding's own fix, plus the 86-test suite under the fix.

Suggested fix: Refuse skills-only certification while this revision's own apply is in error — recoveringFromError is already computed for exactly that state three lines earlier (:805-807), before extensionsStatus is overwritten with 'starting':

The fix must not violate an existing fact: The narrow window must still certify a fresh skill-state generation — preserves the narrow refresh for Extension Skill-state changes (workspace-runtime-coordinator.test.ts:591-623) asserts reconcileExtensionGeneration(7, { skillsOnly: true }) resolves { state: 'reconciled' } after generation 6 applied, and it.each([false, true])('replays interrupted Extension reconciliation with skillsOnly=%s') (:917-969) asserts the drain replay reaches { state: 'ready', desiredGeneration: 7, appliedGeneration: 7 }; in both, the pre-apply status is ready/stale, not error, so !recoveringFromError keeps them green. The fix must also not add a second cooldown: `EXTENSIONS_ERROR_RETRY_COOL

Acceptance criterion: A new case beside workspace-runtime-coordinator.test.ts:625 — full reconcile at generation N fails (configsFailed: 1), then reconcileExtensionGeneration(N, {skillsOnly: true}) must leave capabilities.extensions.state !== 'ready' and appliedGeneration === N - 1, and the following ensure() must re-issue the full reconcile (assert the qwen/control/workspace/extensions/reconcile call count rises, i.e. the retry marker at :891-893 survived). I ran that probe: it is red today, and it goes red again if !recoveringFromError is removed from the fix. Please prove it by mutation — remove the fix, run that test, confirm it goes red.

中文说明

[Critical] 在同一 generation 上,若完整 reconcile 刚失败,随后的 skills-only reconcile 仍会认证该 generation:appliedExtensionGeneration === generation - 1 无法区分“N-1 已应用、N 是新 skill 增量”与“N 自身完整应用失败、applied 仍停在 N-1”。

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

Comment on lines +359 to +363
// The Extensions work outlived the observation budget but stays
// queued; the Skills/MCP preparation below then certifies from a
// runtime read that predates the applied catalog, so the
// late-settling apply must invalidate and re-drive them.
this.extensionsEnsureAbandonedAtEpoch = status.runtimeEpoch;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R16-4: [certifies-falsely] [regression] When the Extensions prepare is abandoned at the ensure deadline, ensure() returns a status whose capabilities.skills.state / capabilities.mcp.state are still their pre-ensure values (not_started on a cold runtime), not starting — so loadReadyWorkspaceSkills, which polls only on 'starting', returns undefined immediately and the composer never replaces its config-derived skill list with the matching-epoch runtime catalog.

Failure scenario: Trace of the two moments. Produced: ensure() (line 337-402) awaits withTimeout(this.prepareExtensions(), remainingMs); the new Extensions reconcile has a 5-minute physical budget (EXTENSIONS_RECONCILE_TIMEOUT_MS = 5 * 60_000, line 30) against a 60-second observation budget (DEFAULT_ENSURE_TIMEOUT_MS = 60_000, line 27), so on a workspace whose refreshTools() re-initialises MCP servers, LSP, subagents, hooks and memory it routinely outlives it. On timeout, Date.now() >= deadline, so the second const remainingMs = deadline - Date.now(); if (remainingMs > 0) at line 394-395 is false and the await withTimeout(Promise.all([skillsPrep, mcpPrep]), …) is skipped. prepareSkills() was called at line 385, but queueSkillsWork (line 1050-1066) only chains .catch().then() onto this.skillsTail, so its body — and the this.skillsStatus = { state: 'starting', … } assignment inside prepareSkillsRevision (line 1115) — runs on a later microtask. const finalStatus = this.status(); at line 404 executes in the same synchronous run, so the returned envelope carries skills: { state: 'not_started', revision: 0 } (and mcp likewise). Needed: App.tsx:7830-7838 feed

Witness:

` [probe] isolated copy at the reviewed commit, replaying the PR's own 61 s scenario (extensions reconcile settles at 61 s against the 60 s ensure budget): ``` PR: AB1 envelope = {"extensions":{"state":"starting",…},"mcp":{"state":"not_started","revision":0},"skills":{"state":"not_started","revision":0}} extReconcileCalls = 1 BASE: AB1 envelope = {"mcp":{"state":"ready",…,"runtimeEpoch":3},"skills":{"state":"ready",…,"runtimeEpoch":3}} extReconcileCalls = 0 (arm proved: grep -c prepareExtensions → PR 5 / BASE 0; 1424 / 742 lines; same probe file, only the coordinator swapped) B003 status() one

Suggested fix: Make the derived capabilities report 'starting' at the moment their preparation is queued rather than when the queued body first runs — assign this.skillsStatus = { state: 'starting', revision, runtimeEpoch: snapshot.runtimeEpoch } in prepareSkills() before queueSkillsWork(...) (and the mirror in prepareMcp()), the way scheduleSkillsReconciliation() already does at lines 659-663. Then finalStatus is accurate whether or not the remainingMs > 0 await ran. (Alternatively, widen the loadReadyWorkspaceSkills loop guard to 'not_started', but that leaves the wire status lying to every other client.)

The fix must not violate an existing fact: packages/cli/src/serve/workspace-runtime-coordinator.ts:27,30const DEFAULT_ENSURE_TIMEOUT_MS = 60_000; and const EXTENSIONS_RECONCILE_TIMEOUT_MS = 5 * 60_000;. The physical refresh budget deliberately exceeds the observation budget (design doc: "The shorter ensure observation deadline does not cancel that refresh; a later successful result can still certify readiness"), so the fix must not shrink the reconcile timeout to fit inside ensure().

Acceptance criterion: packages/cli/src/serve/workspace-runtime-coordinator.test.ts — the existing test 're-certifies Skills/MCP when the initial Extension apply settles past the ensure budget' asserts only await expect(ensured).resolves.toMatchObject({ runtimeLive: true }) and then reads coordinator.status() after the fact; extend it to assert capabilities.skills.state === 'starting' and capabilities.mcp.state === 'starting' on the resolved ensured value. That assertion is red today. A web-shell-side witness belongs in packages/web-shell/client/daemon/workspace/load-ready-skills tests: given an initial runtime with skills.state === 'not_started' that later reports 'starting' then `'ready Please prove it by mutation — remove the fix, run that test, confirm it goes red.

中文说明

[Critical] 当 Extensions 准备在 ensure 观察超时后被放弃时,ensure() 返回状态里的 capabilities.skills.state / capabilities.mcp.state 仍是 ensure 之前的值(冷启动为 not_started)而非 starting;只按 starting 轮询的 loadReadyWorkspaceSkills 立刻返回 undefined,composer 因此不会用匹配 epoch 的 runtime 目录替换按配置推导的 skill 列表。

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

Comment on lines +636 to +638
if (requestId !== loadRequestRef.current) return observedTrusted;
// Resolve the trust as soon as the projection answers: the runtime
// leg below is trust-gated, so its 403 must not discard a trust

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R16-5: [certifies-falsely] [new-surface] The new supersede guard returns observedTrusted before it is assigned from the projection the load already holds, so a superseded activation reload reports "trust not observed" and the refresh decision at :1414 (const observedTrust = (await load(true)) ?? workspaceTrusted;) silently falls back to the render-time snapshot — the exact thing the comment this diff adds at :625-627 ("resolved per branch so awaiting callers decide on the fresh value, not the render-time state snapshot") forbids. This is a regression: at the merge base the loader had one exit, `return activation ? activatio

Failure scenario: observedTrusted is declared = null at :628 and first assigned at :640 — one line after the guard at :636 — so on that path the return value is provably null even though activation is in hand. Concurrent loads are not gated by busyName: the signals?.extensionsVersion effect calls void load(true) unconditionally (:871-879), as do the Refresh button (:1144), the install/update/checkUpdates reloads (:1025, :1099, :1341) and the 2 s retry timers (:716, :773). Trigger: the page is mounted on a workspace whose projection reported trusted: true; trust is revoked out of band; the user toggles an activation, and the daemon's own generation broadcast for that mutation lands signals.extensionsVersion while the activation's load(true) is inside its Promise.all([extensionCatalog(), workspaceExtensions()]) round-trip. The signal-driven load bumps loadRequestRef.current, the activation's load early-returns null, and observedTrust resolves to the closure's stale workspaceTrusted === true. activationRequiresExplicitRefresh && observedTrust is then true, so refreshExtensionRuntime() is fired at an untrusted workspace, the daemon 403s it, and `se

Witness:

probe arm 1 (trust revoked out of band; the activation's reload gated inside its catalog round trip via `extensionCatalog.mockImplementationOnce`; a daemon-pushed `signals.extensionsVersion` load supersedes it; gate released) — PR code: `AssertionError: expected "spy" to not be called at all, but actually been called 1 times` → the refresh fires at a workspace the same load had just read as `trust

Suggested fix: Resolve the trust from the projection before the supersede check, keeping only the state write behind the guard:

The fix must not violate an existing fact: The setWorkspaceTrusted(observedTrusted) write must stay inside the requestId guard — ExtensionsManagerPage.tsx:641-643 (if (observedTrusted !== null) { setWorkspaceTrusted(observedTrusted); }), matching apply's own guard at :593 (if (requestId !== loadRequestRef.current) return;); only the pure local assignment may move above it.

Acceptance criterion: A case in packages/web-shell/client/components/extensions/ExtensionsManagerPage.test.tsx beside decides the refresh on the trust the activation reload observes, even when the runtime leg fails (:2105): gate the reload's projection leg with a deferred workspaceExtensions, flip trusted = false, bump state.signals = { extensionsVersion: 1 } inside act (fires the :876 reload that supersedes the activation's load), release the gate, then assert expect(refreshExtensionRuntime).not.toHaveBeenCalled() and not.toContain('session refresh failed'). Revert the reordering (guard before assignment) and both assertions go red, because load(true) returns null and ?? workspaceTrusted Please prove it by mutation — remove the fix, run that test, confirm it goes red.

中文说明

[Critical] 新增的 supersede 保护在 observedTrusted 尚未从本次 load 已持有的 projection 赋值之前就返回它,被取代的 activation reload 因此报告“未观察到信任”,影响 :1414 处的刷新判定。

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

@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real two-workspace daemon + real Web Shell (Linux)

Verified on a real qwen serve daemon with two registered workspace runtimes and the real Web Shell driven by a browser. The PR body marks Linux as ⚠️ (not locally run), so this covers that gap.

Verdict: works as described — recommend merge. One non-blocking observation and one pre-existing gap (reproduced identically on base, so not caused by this PR) are recorded at the end.

Head 8c027e5d3a (also re-ran the decisive checks against the earlier 4e35093986) · Base = merge-base 2f426a64f4 · Node 22.22.2 · npm ci && npm run build green on both arms.

Setup

Two workspaces ws-a (primary) and ws-b (secondary), both trusted, isolated HOME/QWEN_HOME, no model credentials needed for these routes:

qwen serve --workspace <…>/ws-a --workspace <…>/ws-b --port … --token …

Two probe extensions installed globally at user scope, each shipping one skill and one slash command:

extension ws-a ws-b
wsprobe enabled (inherits global) disabled (workspace override)
wsprobe2 enabled enabled

The whole A/B rests on that single asymmetry: with the composer/manager pointed at ws-b, anything that answers for ws-a will wrongly show wsprobe.


1. The defect this PR fixes, reproduced on base

With the composer workspace set to ws-b, base resolves Extension references through the primary-bound GET /workspace/extensions, so the @ and + Extension menus list wsprobe even though it is disabled in the selected workspace. After the PR the same menus read GET /workspaces/<ws-b>/runtime/extensions and the list is correct.

composer @ menu before/after

Observed network reads (captured from the browser, same store state on both arms):

arm composer workspace request the client issued @/+ Extensions list
base 2f426a64 ws-b GET /workspace/extensions wsprobe, wsprobe2
PR 8c027e5d ws-b GET /workspaces/<ws-b>/runtime/extensions wsprobe2
PR 8c027e5d ws-a GET /workspaces/<ws-a>/runtime/extensions wsprobe, wsprobe2

Selecting ws-a still shows both, so this is not a blanket suppression — screenshot.

2. Extension management follows the selected workspace

Base has no workspace selector on Plugins ▸ Extensions and reads the primary only, so wsprobe reads enabled regardless of which workspace you care about. The PR adds the selector and resolves through /extensions + /workspaces/<ws-b>/extensions + /workspaces/<ws-b>/runtime/extensions.

extensions manager before/after

The detail pane separates the two scopes correctly, and the live capability counts (Commands 1, Skills 1) come from the selected workspace's runtime:

extension detail for ws-b

3. Enablement is reconciled into the live secondary runtime

This is the core claim, so it was driven through the real UI and read back from the daemon. Toggling Workspace setting → Enabled for ws-b in the manager page, with the runtime already live:

BEFORE  ws-b  runtimeEpoch=1  capabilities.extensions={state:ready, desiredGeneration:8, appliedGeneration:8}
              /workspaces/<ws-b>/runtime/extensions  wsprobe.isActive = false
              /workspaces/<ws-b>/runtime/skills      wsprobe-skill    = disabled (inactive_extension)

AFTER   ws-b  runtimeEpoch=1  capabilities.extensions={state:ready, desiredGeneration:9, appliedGeneration:9}
              /workspaces/<ws-b>/runtime/extensions  wsprobe.isActive = true
              /workspaces/<ws-b>/runtime/skills      wsprobe-skill    = ok

runtimeEpoch is unchanged at 1 across the toggle — the runtime was not replaced, the catalog was reconciled into it. ws-a kept wsprobe enabled throughout.

Installing a new extension while both runtimes are live behaves the same way (measured on 4e35093986): the store generation advanced and appliedGeneration followed in both runtimes at runtimeEpoch 1, with ws-b retaining its own per-workspace override:

ws-a  epoch 1  {state:ready, desiredGeneration:3, appliedGeneration:3}   wsprobe isActive=true   wsprobe2 isActive=true
ws-b  epoch 1  {state:ready, desiredGeneration:3, appliedGeneration:3}   wsprobe isActive=false  wsprobe2 isActive=true

A live session in ws-b sees the extension's slash command with its [wsprobe] badge — screenshot.

4. Documented contracts spot-checked

contract (from qwen-serve-protocol.md) result
workspace_extensions_config_runtime + workspace_extension_mentions advertised when the workspace runtime is available both present on PR, both absent on base
GET /workspace{,s/:ws}/runtime/extensions gated behind the feature 404 on base, 200 on PR
the runtime-catalog read "does not start or prepare a cold runtime" on a never-ensured ws-b: initialized:false, answered in 15 ms, runtime/status still cold afterwards
runtime-catalog reads require a trusted target untrusted ws-b403 untrusted_workspace
the projection stays readable without trust and reports it in the body untrusted ws-b200 with trusted:false
trusted target unaffected ws-a200

5. Tests

All PR-touched suites pass locally at 8c027e5d3a:

package files tests
cli (workspace-runtime-coordinator, workspace-qualified-extensions, workspace-extensions-controller, acpAgent, server, run-qwen-serve) 6 2586 ✅
web-shell (extensions, useComposerCore.dom, useAtMentionMenu, AtMentionPanel, PluginManagerPage) 6 227 ✅
core (extension-store) 1 97 ✅
sdk-typescript (DaemonClient) 1 426 ✅

Non-vacuity — four mutations, all killed. Each reverts one specific guard this PR adds:

mutation killed
GET /workspaces/:workspace/runtime/extensions resolves registry.primary instead of the request's runtime 3 route tests fail — and the live E2E regressed to exactly the base defect: with the mutated build the ws-b composer listed wsprobe again, from a workspace-qualified URL
useComposerCore always uses the primary-bound legacy loader 7 useComposerCore.dom tests fail
mergeExtensionCatalog drops the runtime/coordinator epoch-agreement gate 1 extensions-manager-logic test fails
appliedGeneration no longer re-certified against the live runtime epoch in status() 1 coordinator test fails

The first mutation is the important one: it proves the E2E oracle above is sensitive to the fix itself and not to incidental setup. Three of the four mutated files are byte-identical between 4e35093986 and 8c027e5d3a; the route mutation was re-run at the tip.


Findings (neither blocks merge)

1 · Pre-existing, not caused by this PR — re-enabling an extension does not restore its slash command in an already-live session.

Same session, same runtime, ws-b:

initial (enabled)   /wsprobe → /wsprobe-skill, /wsprobe-cmd   ✅
after disable       /wsprobe → (no matches)                   ✅  live refresh works
after re-enable     /wsprobe → (no matches)                   ❌  command does not come back

At that last point the daemon is correct — /workspaces/<ws-b>/runtime/extensions reports isActive:true and runtime/skills reports ok — and a newly created session in ws-b immediately shows /wsprobe-cmd. Only the pre-existing session is stale. The identical sequence produces the identical result on base 2f426a64, so this is a pre-existing asymmetry in session-scoped re-activation refresh, not a regression from this PR. Worth a follow-up issue rather than a change here.

2 · Observation — the new composer loader is not wrapped in the client action timeout.

workspace.actions.loadExtensionsStatus (the loader being replaced) goes through withActionTimeout(..., 30_000). The inline loader added in useComposerCore.ts does not, and it retries ensureRuntime() up to COMPOSER_EXTENSIONS_MAX_ATTEMPTS = 3 on 503 runtime_still_starting. Because the coordinator's own ensure deadline is 60 s and answers that exact 503 on expiry, a persistently slow secondary could hold the @/+ Extensions menu on Loading for roughly 3 × 62 s + 2 × 2 s ≈ 190 s instead of failing at 30 s.

This is bounded and narrow, and I did not reproduce it — on a genuinely cold ws-b the menu settled in 1.77 s. Flagging it only because the timeout wrapper was dropped in the move, and a withActionTimeout around the whole loop (or a total budget across attempts) would restore the old bound cheaply.

3 · Note, no action needed — the untrusted-secondary client branches are defence-in-depth.

useComposerCore's "omit the loader for an untrusted non-primary target" branch and the manager page's untrusted handling cannot be reached through the UI: with folder trust on, an untrusted workspace renders aria-disabled in both the composer workspace chip and the Plugins workspace selector, so it cannot be selected in the first place. The enforced boundary is the server's 403 untrusted_workspace, which is verified above. The branches are unit-tested; this is just a note that they are belt-and-braces rather than the live path.

Screenshots, plus the full report in both languages, are on wenshao/qwen-code@assets-pr11086report.en.md · report.zh-CN.md

中文版报告(完整版含截图:report.zh-CN.md

维护者验证 — 真实双工作区 daemon + 真实 Web Shell(Linux)

在一台注册了两个工作区运行时的真实 qwen serve daemon 上验证,并用浏览器驱动真实的 Web Shell。PR 描述中 Linux 标记为 ⚠️(未本地运行),本次验证补上了这一块。

结论:行为与描述一致,建议合并。 末尾记录了一个不阻塞的观察项,以及一个在 base 上同样复现的既有问题(因此不是本 PR 引入的)。

Head 8c027e5d3a(关键结论在更早的 4e35093986 上也复跑过一遍)· Base 取 merge-base 2f426a64f4 · Node 22.22.2 · 两个分支 npm ci && npm run build 均通过。

环境

两个工作区 ws-a(primary)与 ws-b(secondary),均受信任,HOME/QWEN_HOME 完全隔离,这些路由不需要模型凭据:

qwen serve --workspace <…>/ws-a --workspace <…>/ws-b --port … --token …

两个探针扩展以 user scope 全局安装,各带一个 skill 和一个斜杠命令:

扩展 ws-a ws-b
wsprobe 启用(继承全局) 禁用(工作区覆盖)
wsprobe2 启用 启用

整个 A/B 就建立在这一处不对称上:当输入区/管理页指向 ws-b 时,任何"回答成 ws-a"的实现都会错误地列出 wsprobe


1. 本 PR 修复的缺陷,已在 base 上复现

输入区工作区选为 ws-b 时,base 通过绑定 primary 的 GET /workspace/extensions 解析扩展引用,因此 @+ 扩展菜单会列出在所选工作区已被禁用的 wsprobe。合入本 PR 后,同样的菜单改读 GET /workspaces/<ws-b>/runtime/extensions,列表即正确。

浏览器实际抓到的请求(两侧扩展存储状态完全一致):

分支 输入区工作区 客户端实际发出的请求 @/+ 扩展列表
base 2f426a64 ws-b GET /workspace/extensions wsprobewsprobe2
PR 8c027e5d ws-b GET /workspaces/<ws-b>/runtime/extensions wsprobe2
PR 8c027e5d ws-a GET /workspaces/<ws-a>/runtime/extensions wsprobewsprobe2

切回 ws-a 仍然两个都在,说明这不是一刀切地把列表抹掉。

2. 扩展管理跟随所选工作区

base 的 Plugins ▸ Extensions 没有工作区选择器、只读 primary,所以无论你关心哪个工作区,wsprobe 都显示 enabled。本 PR 加入选择器,并通过 /extensions + /workspaces/<ws-b>/extensions + /workspaces/<ws-b>/runtime/extensions 解析。详情页正确区分了两个作用域(Global setting = Enabled,Workspace setting = Disabled),并且 Commands 1Skills 1 这些能力计数来自所选工作区的运行时。

3. 启用状态被协调进存活的次级运行时

这是核心论断,所以通过真实界面操作、再从 daemon 读回验证。在运行时已经存活的情况下,于管理页把 ws-bWorkspace setting 切到 Enabled

切换前  ws-b  runtimeEpoch=1  capabilities.extensions={state:ready, desiredGeneration:8, appliedGeneration:8}
               /workspaces/<ws-b>/runtime/extensions  wsprobe.isActive = false
               /workspaces/<ws-b>/runtime/skills      wsprobe-skill    = disabled (inactive_extension)

切换后  ws-b  runtimeEpoch=1  capabilities.extensions={state:ready, desiredGeneration:9, appliedGeneration:9}
               /workspaces/<ws-b>/runtime/extensions  wsprobe.isActive = true
               /workspaces/<ws-b>/runtime/skills      wsprobe-skill    = ok

runtimeEpoch 全程保持为 1 —— 运行时没有被替换,而是把目录协调进了同一个运行时。ws-a 全程保持启用。

在两个次级运行时都存活时新装一个扩展,表现一致:store generation 推进,两个运行时的 appliedGeneration 都在 runtimeEpoch 1 下跟上,同时 ws-b 保留自己的工作区覆盖:

ws-a  epoch 1  {state:ready, desiredGeneration:3, appliedGeneration:3}   wsprobe isActive=true   wsprobe2 isActive=true
ws-b  epoch 1  {state:ready, desiredGeneration:3, appliedGeneration:3}   wsprobe isActive=false  wsprobe2 isActive=true

ws-b 中的存活会话可以看到该扩展的斜杠命令及其 [wsprobe] 标记。

4. 文档契约抽查

契约(出自 qwen-serve-protocol.md 结果
工作区运行时可用时声明 workspace_extensions_config_runtimeworkspace_extension_mentions PR 两者均有,base 两者均无
GET /workspace{,s/:ws}/runtime/extensions 受该 feature 门控 base 404,PR 200
运行时目录读取「不会启动或准备冷运行时」 对从未 ensure 过的 ws-binitialized:false15 ms 返回,之后 runtime/status 仍为 cold
运行时目录读取要求目标受信任 未受信任的 ws-b403 untrusted_workspace
projection 在不受信任时仍可读,并在 body 中报告 未受信任的 ws-b200trusted:false
受信任目标不受影响 ws-a200

5. 测试

8c027e5d3a 上,本 PR 涉及的测试套件全部本地通过:

文件 用例
cliworkspace-runtime-coordinatorworkspace-qualified-extensionsworkspace-extensions-controlleracpAgentserverrun-qwen-serve 6 2586 ✅
web-shell(extensions、useComposerCore.domuseAtMentionMenuAtMentionPanelPluginManagerPage 6 227 ✅
coreextension-store 1 97 ✅
sdk-typescriptDaemonClient 1 426 ✅

非空洞性 —— 四个变异,全部被杀掉。 每个变异都只回退本 PR 新增的某一处保护:

变异 被杀情况
GET /workspaces/:workspace/runtime/extensions 改为解析 registry.primary 而非请求对应的运行时 3 个路由测试失败 —— 并且真实 E2E 退化成与 base 完全一样的缺陷:变异构建下 ws-b 输入区又列出了 wsprobe,尽管 URL 仍是工作区限定的
useComposerCore 始终使用绑定 primary 的旧加载器 7 个 useComposerCore.dom 测试失败
mergeExtensionCatalog 去掉 runtime/coordinator 的 epoch 一致性门槛 1 个 extensions-manager-logic 测试失败
status()appliedGeneration 不再按存活运行时 epoch 重新认证 1 个 coordinator 测试失败

第一个变异最关键:它证明上面那条 E2E 判据确实对这个修复本身敏感,而不是被环境凑出来的。四个被变异文件中有三个在 4e350939868c027e5d3a 之间逐字节相同;路由变异在 tip 上重跑过。


发现(均不阻塞合并)

1 · 既有问题,非本 PR 引入 —— 重新启用扩展后,已存活会话不会恢复它的斜杠命令。

同一会话、同一运行时、ws-b

初始(启用)    /wsprobe → /wsprobe-skill, /wsprobe-cmd   ✅
禁用之后        /wsprobe → 无匹配                         ✅  实时刷新生效
重新启用之后    /wsprobe → 无匹配                         ❌  命令没有回来

此时 daemon 侧是正确的 —— /workspaces/<ws-b>/runtime/extensionsisActive:trueruntime/skillsok;而且在 ws-b 新建的会话立刻就能看到 /wsprobe-cmd,只有那个已存在的会话是陈旧的。同样的操作序列在 base 2f426a64 上得到完全相同的结果,所以这是会话级重新激活刷新的既有不对称,不是本 PR 的回归。建议另开 issue 跟进,而非在本 PR 中改。

2 · 观察项 —— 新的输入区加载器没有套客户端 action 超时。

被替换掉的 workspace.actions.loadExtensionsStatus 走的是 withActionTimeout(..., 30_000)useComposerCore.ts 中新增的内联加载器没有,并且会在 503 runtime_still_starting 上按 COMPOSER_EXTENSIONS_MAX_ATTEMPTS = 3 重试 ensureRuntime()。由于 coordinator 自身的 ensure 截止时间是 60 s,超时后返回的正是这个 503,一个持续启动缓慢的次级工作区理论上可以让 @/+ 扩展菜单停在 Loading3 × 62 s + 2 × 2 s ≈ 190 s,而不是 30 s 就失败。

范围有限,而且我没有复现出来 —— 在真正冷启动的 ws-b 上菜单 1.77 s 就出来了。之所以提出来,只是因为这次改动把超时包装丢掉了;在整个重试循环外面套一层 withActionTimeout(或给多次尝试设一个总预算)就能廉价地恢复原有上界。

3 · 说明,无需处理 —— 未受信任次级工作区的客户端分支属于纵深防御。

useComposerCore 中「未受信任的非 primary 目标就不提供加载器」这一分支,以及管理页对未受信任目标的处理,在界面上都走不到:开启 folder trust 后,未受信任的工作区在输入区工作区芯片Plugins 工作区选择器中都会渲染为 aria-disabled,根本无法被选中。真正强制的边界是服务端的 403 untrusted_workspace,上面已验证。这些分支有单测覆盖,这里只是说明它们是双保险而非实际路径。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:

Round summary

This round is a same-run verification repair: the round-16 commit (62ac6405ef) was rejected by deterministic verification because npm run build failed on it, while the baseline leg was green. The round-16 findings themselves were already fixed in that commit; this round preserves it and adds one follow-up commit (54c37f0365) that repairs the exact rejection. No review feedback is re-litigated here.

The rejection and its repair

Rejection (deterministic gate): npm run build failed in packages/cli with
src/serve/workspace-runtime-coordinator.test.ts(710,11): error TS2493: Tuple type '[]' of length '0' has no element at index '0'.
Baseline re-run at 8c027e5d3a was green, so the failure belonged to the round.

Root cause: the round-16 witness test does not let a skills-only reconcile certify a generation whose full apply failed counted reconcile RPCs with
harness.invokeWorkspaceCommand.mock.calls.filter(([method]) => ...).
The harness declares invokeWorkspaceCommand as vi.fn(async (): Promise<ServeWorkspaceExtensionsRefreshResult> => ...) — zero declared parameters — so mock.calls is typed as an array of empty tuples and destructuring [method] is a type error. Vitest (transpile-only, no typecheck) passed, and the round's npm run typecheck did not cover it the way tsc --build does, so the break surfaced only at the build step.

Fix: index the call tuple through the file's own established pattern — `(call) =>

Why it was not pushed:

Note: the base has since been auto-updated; the verdict below predates that update, and the next round's re-measurement may charge the round.

tests failed in packages/core

g later statements �[33m 982�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22msupports cancellable one-shot timers �[33m 988�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22mdoes not keep exec alive for an unawaited timer �[33m 1101�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22mdoes not charge timer wait time against the guest CPU budget �[33m 1075�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22msurfaces errors thrown by timer callbacks �[33m 1044�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22mbounds the number of live timers �[33m 999�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22mdoes not expose Node, network, console, or WebAssembly �[33m 2107�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22muses a fresh global context for every call �[33m 2243�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m isolated code mode host�[2m > �[22mcancels unawaited nested calls when the program settles �[33m 1138�[2mms�[22m�[39m

�[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m

�[41m�[1m FAIL �[22m�[49m src/tools/exec-context-tools.test.ts�[2m > �[22mexec context tool results�[2m > �[22mclears loaded skill tracking if cancellation prevents delivery
�[31m�[1mAssertionError�[22m: expected "spy" to be called once, but got 0 times�[39m
�[36m �[2m❯�[22m src/tools/exec-context-tools.test.ts:�[2m198:45�[22m�[39m
    �[90m196| �[39m      controller�[33m.�[39msignal�[33m,�[39m
    �[90m197| �[39m    )�[33m;�[39m
    �[90m198| �[39m    �[35mawait�[39m vi�[33m.�[39m�[34mwaitFor�[39m(() �[33m=>�[39m �[34mexpect�[39m(dispatch)�[33m.�[39m�[34mtoHaveBeenCalledOnce�[39m())�[33m;�[39m
    �[90m   | �[39m                                            �[31m^�[39m
    �[90m199| �[39m    controller�[33m.�[39m�[34mabort�[39m()�[33m;�[39m
    �[90m200| �[39m    �[35mawait�[39m �[34mexpect�[39m(pending)�[33m.�[39mrejects�[33m.�[39m�[34mtoThrow�[39m()�[33m;�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m208 passed�[39m�[22m�[2m | �[22m�[33m1 skipped�[39m�[90m (210)�[39m
�[2m      Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m11190 passed�[39m�[22m�[2m | �[22m�[33m8 skipped�[39m�[90m (11199)�[39m
�[2m   Start at �[22m 08:11:21
�[2m   Duration �[22m 75.49s�[2m (transform 167ms, setup 1.27s, collect 214.71s, tests 271.04s, environment 34ms, prepare 14.82s)�[22m

JUNIT report written to /home/github-runner/actions-runner-hk2-19/_work/qwen-code/qwen-code/packages/core/junit.xml
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /home/github-runner/actions-runner-hk2-19/_work/qwen-code/qwen-code/packages/core
npm error workspace @qwen-code/qwen-code-core@0.23.3
npm error location /home/github-runner/actions-runner-hk2-19/_work/qwen-code/qwen-code/packages/core
npm error command failed
npm error command sh -c vitest run --changed origin/main --passWithNoTests --maxWorkers=25% --testTimeout=60000 --hookTimeout=60000 --coverage.enabled=false
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

验证门的拒绝原因与日志证据见上方英文部分(gate-rejection 不翻译)。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34654953653


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:

Autofix round — PR #11086 (extension workspace runtimes)

Same-run verification repair

The previous commit (b6c97591b2) was rejected by deterministic verification:
npm run build failed in packages/cli with two TS2493 errors in
src/serve/workspace-runtime-coordinator.test.ts (lines 728 and 732) — the new
round-16 test destructured ([method]) => … from the call tuples of
invokeWorkspaceCommand, a vi.fn() declared with zero parameters, so each
call is typed as the empty tuple []. Vitest (esbuild) does not typecheck,
which is why the failure only surfaced in tsc --build.

Repair: the two filters now use the file's own existing convention —
(harness.invokeWorkspaceCommand.mock.calls as unknown[][]).filter((call) => call[0] === …) (same pattern as lines 366, 395, 441, 1108, 1334). Type-only
change; runtime behavior and the assertion are unchanged (the coordinator suite
still passes 86/86, including the repaired test). The rejected commit is
preserved untouched; this round adds one follow-up commit.

Feedback dispositions

Fixed in code this round

  1. R16-2 (round-16 review body, rv:5183943862) — the interaction-answer
    endpoint validated X-Qwen-Client-Id against the primary bridge alone,
    while the interactive POST /extensions/install /
    POST /extensions/:extensionId/update routes accept it against every
    registered runtime's bridge.
    Reproduced first: a new test in
    workspace-qualified-extensions.test.ts arms `p

Why it was not pushed:

Note: the base has since been auto-updated; the verdict below predates that update, and the next round's re-measurement may charge the round.

tests failed in packages/core

 �[33m=�[39m () �[33m=>�[39m {
    �[90m171| �[39m    protocolError �[33m=�[39m �[35mnew�[39m �[33mError�[39m(
    �[90m   | �[39m                    �[31m^�[39m
    �[90m172| �[39m      �[32m`JavaScript execution timed out after �[39m�[36m${�[39mtimeoutMs�[36m}�[39m�[32mms.`�[39m�[33m,�[39m
    �[90m173| �[39m    )�[33m;�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/code-mode/scheduler.test.ts�[2m > �[22mCodeModeOnly scheduler dispatch�[2m > �[22mruns Promise.all reads in one scheduler batch
�[31m�[1mAssertionError�[22m: expected +0 to be 2 // Object.is equality�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[32m- 2�[39m
�[31m+ 0�[39m

�[36m �[2m❯�[22m vi.waitFor.timeout src/code-mode/scheduler.test.ts:�[2m468:46�[22m�[39m
    �[90m466| �[39m    )�[33m;�[39m
    �[90m467| �[39m    �[35mtry�[39m {
    �[90m468| �[39m      await vi.waitFor(() => expect(started).toBe(2), { timeout: 4000 …
    �[90m   | �[39m                                             �[31m^�[39m
    �[90m469| �[39m    } �[35mfinally�[39m {
    �[90m470| �[39m      �[34mrelease�[39m()�[33m;�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/code-mode/scheduler.test.ts�[2m > �[22mCodeModeOnly scheduler dispatch�[2m > �[22mroutes nested permission approval through the visible scheduler
�[31m�[1mAssertionError�[22m: expected false to be true // Object.is equality�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[32m- true�[39m
�[31m+ false�[39m

�[36m �[2m❯�[22m vi.waitFor.timeout src/code-mode/scheduler.test.ts:�[2m584:11�[22m�[39m
    �[90m582| �[39m                call�[33m.�[39mstatus �[33m===�[39m �[32m'awaiting_approval'�[39m�[33m,�[39m
    �[90m583| �[39m            )�[33m,�[39m
    �[90m584| �[39m        )�[33m.�[39m�[34mtoBe�[39m(�[35mtrue�[39m)�[33m;�[39m
    �[90m   | �[39m          �[31m^�[39m
    �[90m585| �[39m      }�[33m,�[39m
    �[90m586| �[39m      { timeout�[33m:�[39m �[34m4000�[39m }�[33m,�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m2 failed�[39m�[22m�[2m | �[22m�[1m�[32m207 passed�[39m�[22m�[2m | �[22m�[33m1 skipped�[39m�[90m (210)�[39m
�[2m      Tests �[22m �[1m�[31m3 failed�[39m�[22m�[2m | �[22m�[1m�[32m11330 passed�[39m�[22m�[2m | �[22m�[33m8 skipped�[39m�[90m (11341)�[39m
�[2m   Start at �[22m 20:39:06
�[2m   Duration �[22m 191.28s�[2m (transform 157ms, setup 1.17s, collect 200.10s, tests 610.75s, environment 32ms, prepare 13.76s)�[22m

JUNIT report written to /home/github-runner/actions-runner-hk1-16/_work/qwen-code/qwen-code/packages/core/junit.xml
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /home/github-runner/actions-runner-hk1-16/_work/qwen-code/qwen-code/packages/core
npm error workspace @qwen-code/qwen-code-core@0.23.3
npm error location /home/github-runner/actions-runner-hk1-16/_work/qwen-code/qwen-code/packages/core
npm error command failed
npm error command sh -c vitest run --changed origin/main --passWithNoTests --maxWorkers=25% --testTimeout=60000 --hookTimeout=60000 --coverage.enabled=false
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

验证门的拒绝原因与日志证据见上方英文部分(gate-rejection 不翻译)。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/34687215427


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.23.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants