feat(daemon): enumerate installed extension skills locally - #11281
Conversation
E2E test reportVerified on macOS against the local 0.23.0 bundle for Baseline: the globally installed 0.18.5-preview.0 daemon returned HTTP 200 with After: five isolated daemon runs passed. In every run,
Controls: the harness set Commands: PR scope is 382 additions + 30 deletions = 412 lines, including tests and design documentation. |
|
Thanks for the PR — and for writing the design doc first, which is exactly what the direction review on #11274 asked for. Template looks good ✓ Problem: real, and confirmed by reading Direction: aligned, and importantly narrower than the tracking issue proposed. #11274 wanted the daemon-local provider to become the sole list authority and drop child preference; this PR keeps "the live child, when present, stays authoritative" intact and only fills the extension hole in the fallback. That matters, because the direction review on #11274 called the invariant flip a maintainer-level call — this PR sidesteps it. Worth saying plainly though: #11274 is your own issue, still Size: Stage 0 doesn't apply — no core paths ( Approach: scope feels right and the shape deliberately mirrors the ACP child's producer in No drive-by refactors or unrelated churn — every hunk serves the stated goal. The two reordered test expectations are mock artifacts, not a behavior change; I checked why in the review. Risk: no elevated risk signals — none of the three files match the revert-correlated path set. The new surface is a daemon public contract ( Moving on to code review. 🔍 中文说明感谢贡献 —— 也感谢先写了 design doc,这正是 #11274 方向性 review 所要求的。 模板完整 ✓ 问题: 真实存在,而且是通过阅读 方向: 对齐,而且比 tracking issue 的方案更收窄,这点很重要。#11274 想让 daemon 本地 provider 成为列表的唯一来源并移除 child 优先;本 PR 保留了"live child 存在时仍是权威",只填补 fallback 里的 extension 空缺。这一点关键,因为 #11274 的方向性 review 把"翻转该不变量"称为维护者级别的决定 —— 本 PR 绕开了它。不过也要直说:#11274 是您自己提的 issue,目前仍是 规模: Stage 0 不适用 —— 没有触及核心路径( 方案: 范围合理,且形态明确对齐 没有顺手重构或无关改动 —— 每个 hunk 都服务于既定目标。两处测试期望顺序的调整是 mock 造成的假象,不是行为变化;审查里我核对了原因。 风险: 无升级风险信号 —— 三个文件都不匹配与 revert 相关的路径集合。新增面是 daemon 的公开契约( 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
activeWorkStaleMs |
8 |
6 |
— Qwen Code · serve A/B
Code reviewI read the diff against The shape is right. Mirroring Three things I'd want a human to look at. 1. The tests can't catch it: Fix is small, and it's a reuse point rather than new code — 2. The latency half matters more than the writes. That lock is I'm not calling this a defect: fail-closed is consistent with how the same function already treats an unreadable project/user skills dir, the child does the identical store read at session start, and the PR body does flag the lock. But the body frames it as a consistency guarantee, not as a latency and write-on-read change to a route that had neither. Worth a conscious decision — e.g. should extension-store contention degrade to "extension level unavailable, rest of the catalog intact" rather than blanking everything? 3. Nothing invalidates this cache when an extension changes. Extension mutations call Before this PR that gap was invisible, because the route showed no extension skills at all. After it, the route can show a present-but-stale extension list, which is a worse failure mode than an absent one. #11274 does list immediate invalidation after extension commits as in-scope for the program, and this PR explicitly defers invalidation policy to a later stage — so this may be entirely intentional. Flagging it so the deferral is a decision rather than an oversight, and so whichever stage owns the hook knows this consumer needs it too. One drift risk, non-blocking. The inline Checked and fine, so a reviewer doesn't have to re-do it. The added sequenceDiagram
participant P1 as HTTP route
participant P2 as Workspace facade
participant P3 as Local provider
participant P4 as ExtensionManager
participant P5 as ExtensionStore
participant P6 as SkillManager
P1->>P2: GET workspace skills
P2->>P3: child answer and cached answer both unavailable
P3->>P3: loadSettings, derive safeMode and disabledLevels
alt trusted, not safe mode, extension level enabled
P3->>P3: readdir probe on the user extensions dir
P3->>P4: construct unbound manager for this workspace
P4->>P5: refreshCache under the store lock
Note over P5: mkdir plus chmod, recovery passes,<br/>state.json write when changed
P5-->>P4: snapshot plus persisted activation
else untrusted, safe mode, or level disabled
P3->>P3: no manager constructed, no store touched
end
P3->>P6: listSkills via the shim getActiveExtensions
P6-->>P3: name-sorted project, user, extension, bundled
P3->>P3: resolve enabled state, append inactive entries, sort
P3-->>P2: initialized status
TestingWhat this section carries: the PR's own CI check results, read through the API. This run built nothing, ran no test, and started no daemon — under the skill's rules PR-derived code is never executed in CI, and the agent env holds a write PAT that executed code could read. Zero failures at review time. One PR-CI workflow run ( The unit suite ran on Linux only.
Not verified, and why:
The author's E2E report in this thread (five isolated daemon scenarios, 163 focused tests, Sandboxed verification would settle this: 中文说明代码审查我把 diff 与 整体形态是对的。对齐 有三点希望由人来看。 1. 从未传入 测试抓不到它: 修复很小,而且属于复用而非新写代码 —— 上方 40 行处已经加载了 2. 比写入更要紧的是延迟。该锁是 我不把这判为缺陷:fail-closed 与同一函数对不可读 project/user skills 目录的既有处理一致,child 在会话启动时做的是完全相同的 store 读取,PR 描述也确实提到了这把锁。但描述把它写成一致性保证,而没有写成对一条此前既无锁也无写入的路由引入的延迟与"读路径写盘"变化。值得做一次明确决定 —— 例如:extension store 竞争时,是否应降级为"extension 层不可用、目录其余部分完好",而不是清空全部? 3. extension 变化时没有任何机制失效这份缓存。 extension 变更调用的是 在本 PR 之前这个缺口是不可见的,因为该路由根本不显示 extension Skill。本 PR 之后,该路由可能显示一份"存在但陈旧"的 extension 列表 —— 这比"缺失"是更糟的失败模式。#11274 确实把"extension 提交后立即失效受影响目录"列入整个计划的范围,而本 PR 明确把失效策略推迟到后续阶段 —— 所以这完全可能是有意的。提出来是为了让这个推迟成为一个决定而非疏漏,也让负责该钩子的那个阶段知道这个消费者同样需要它。 一个漂移风险,非阻塞。 内联的 已核对且无问题的部分,省得 reviewer 重做一遍。 新增的 (时序图见上,中文不再重复。) 测试本节承载的证据: 通过 API 读取的、该 PR 自身 CI 检查结果。本次运行没有构建、没有跑测试、没有启动 daemon —— 按 skill 规则,CI 中从不执行 PR 派生代码,且 agent 环境持有可被执行代码读取的 write PAT。 审查时点零失败。该 commit 上仍有一个 PR CI workflow run( 单元测试仅在 Linux 上运行。 (CI 表格见上方机器可读区域,中文不再重复。) 未验证项及原因:
作者在本线程中的 E2E 报告(五个隔离 daemon 场景、163 个定向测试、 沙箱化验证可以定案: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 3/5 — the shape is right and the gating is more careful than this diff had to be, but it ships extension display names in the wrong language and quietly changes what a public daemon route does on a read. Both need a human's call rather than mine. Stepping back: I like this PR. Ninety-four production lines, no drive-by churn, a design doc written before the code, and it deliberately mirrors the ACP child's producer instead of inventing a parallel mapping — which is the right instinct, because It also narrowed where the plan didn't. #11274 wanted the daemon-local provider to become the sole list authority and drop child preference; the direction review on that issue called the invariant flip a maintainer-level call. This PR keeps "the live child, when present, stays authoritative" intact and only fills the extension hole underneath it. That's the correct way to stage it, and it's why I'm at 3 rather than 2. What keeps me from approving is that the one concrete defect sits on a surface the tests structurally cannot cover. Extension The other two are judgement calls I shouldn't make alone. A route that used to be a pure filesystem read now takes a cross-process lock, creates directories under On evidence: CI is green on this commit with zero failures, one check ( One sequencing note for whoever picks this up: #10991 ( Not requesting changes — nothing I found is a correctness blocker or a regression, and the code quality is high. Deferring because a public daemon contract changed, one defect needs fixing first, and two design questions belong to a maintainer. 中文说明Confidence: 3/5 —— 形态是对的,门控比这个 diff 所要求的更细致,但它会以错误的语言返回 extension 展示名,并且悄悄改变了一条公开 daemon 路由在"读"时的行为。这两点需要人来定,而不是我。 退一步看:我喜欢这个 PR。九十四行生产代码,没有顺手改动,先写 design doc 再写代码,并且刻意对齐 ACP child 的 producer 而不是另造一套映射 —— 这是正确的直觉,因为 它也做了计划本身没做的收窄。#11274 想让 daemon 本地 provider 成为列表的唯一来源并移除 child 优先;该 issue 的方向性 review 把翻转该不变量称为维护者级别的决定。本 PR 保留了"live child 存在时仍是权威",只填补其下的 extension 空缺。这是正确的分阶段方式,也是我给 3 分而不是 2 分的原因。 让我不去 approve 的是:那个具体缺陷正好落在测试结构上无法覆盖的面上。extension 的 另外两点是我不应独自做出的判断。一条此前是纯文件系统读取的路由,现在会取跨进程锁、在 关于证据:该 commit 上 CI 全绿、零失败,还有一个检查( 给接手的人一条排期提示:#10991( 不提交 request changes —— 我发现的问题没有一条是正确性阻塞或回归,代码质量很高。选择 defer,是因为一条公开 daemon 契约发生了变化、有一个缺陷需要先修,以及两个设计问题属于维护者。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
⏸️ Deferring to a maintainer — not approving, not requesting changes. I tried to resolve an accountable owner deterministically and came up empty: no What I can't settle from the diff, the tests and the PR description:
Also worth a maintainer's eye: the tracking issue #11274 is self-authored, still Full reasoning and the CI evidence are in the stage comments above. 中文说明⏸️ 转交维护者 —— 不 approve,也不 request changes。 我尝试用确定性方式解析出一个可问责的 owner,结果为空:本次运行没有 我无法从 diff、测试和 PR 描述中定案的三点:
另外值得维护者留意:tracking issue #11274 是自拟的,仍是 完整推理与 CI 证据见上方的各阶段评论。如果有人想要证据而非静态阅读, — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
samuelhsin
left a comment
There was a problem hiding this comment.
Reviewed at bb28cd1 — read the diff plus the surrounding code at head (ExtensionManager, ExtensionStore, SkillManager.collectCachedSkills, mapSkillConfigToStatus, resolveSkillSettings, and the sibling workspace-extensions-controller). Approving; the findings below are non-blocking follow-ups.
What I verified myself
- Gating holds: the
ExtensionManageris only constructed whenworkspaceTrusted && !safeMode && !disabledLevels.has('extension'), so safe/untrusted/inert-untrusted/disabled-level providers never touch extension state (and the tests spy thatrefreshCache/refreshToolsare not called). Nothing on this path executes extension code. - The
fs.readdir(Storage.getUserExtensionsDir())precheck reads the same directory the store defaults to (ExtensionStore.extensionsDir ??= Storage.getUserExtensionsDir()), and it correctly distinguishes ENOENT (tolerated;loadExtensionsFromExtensionsDirreturns[]) from ENOTDIR/EACCES (thrown → uninitialized error status), preserving the pre-existing directory-failure semantics. - The
enabledcomputation matches the mapper/settings contracts:enabledNamesis trim+lowercased bynormalizeSkillNames, hard disablements take precedence inmapSkillConfigToStatus, andstate.workspaceEnabled ?? state.defaultEnabledmatchesgetExtensionSkillState's{defaultEnabled, workspaceEnabled: boolean|null}shape.getExtensionSkillState's throw-on-foreign-skill is unreachable here becauseSkillManagersources extension skills from the samegetActiveExtensions()objects. - The final
statuses.sort(localeCompare)is required to merge the appended inactive entries intoSkillManager's own name-sorted order, and V8's stable sort keeps a project entry ahead of a same-name inactive-extension entry — which the precedence test asserts. - Failed
refreshCachecannot poison the cache:managers.setis only reached after every await succeeds, and the regression test covers the retry.
Follow-up 1 (real, small): locale is never passed, so extensionDisplayName/description resolve as English on this route. new ExtensionManager({workspaceDir, isWorkspaceTrusted}) falls back to locale: 'en' (extensionManager.ts:521), which is what resolveExtensionConfigLocale uses at manifest load (extensionManager.ts:1854). Every other producer passes one — the daemon's own extension routes use resolveExtensionLocale(workspaceDir, workspaceTrusted) reading general.language (workspace-extensions-controller.ts:317). So a general.language: "zh" user with a locale-mapped extension sees English names in the pre-child window, then the name flips language once the child answers. Not a regression (pre-PR these skills were absent entirely) and the window is short, but the fix is a few lines — settings is already loaded in buildWorkspaceSkillsStatus, so settings.merged.general?.language (or exporting/reusing resolveExtensionLocale) is in hand. Current tests can't catch it because every fixture uses a plain-string displayName.
Follow-up 2 (judgement, worth a comment in the code): store-busy degrades the whole catalog, slowly. refreshCache → readConsistent takes the proper-lockfile lock with retries: 60, factor: 1.2, 50–500ms (~25s budget) before throwing ExtensionStoreBusyError (extension-store.ts:1479-1494), and the provider's outer catch turns that into initialized: false, skills: [] — blanking project/user/bundled skills too, on a route whose reason to exist is answering instantly. Contention requires a concurrent extension mutation and this only runs on the cold per-workspace call, so it's rare; but consider either a shorter retry budget here or degrading the extension level alone (fall back to getActiveExtensions: () => [], i.e. exactly the pre-PR behavior) in a follow-up.
Noted, fine as staged: the process-global cache has no invalidation hook for extension mutations (deferred to a later stage of #11274 — acceptable since pre-PR this route served no extension list, so "present but stale" is still an improvement), and cold reads may create store directories / write state.json — pre-existing ExtensionStore behavior, new to this endpoint, worth one sentence in the design doc.
The staging itself is right: keeping "the live child stays authoritative" intact and only filling the extension hole underneath it is the correct way to slice #11274.
ytahdn
left a comment
There was a problem hiding this comment.
本 PR 主要做了什么
给 daemon 本地(子进程起来之前)的 /workspace/skills 兜底枚举补上 extension Skill 这块空缺:之前 getActiveExtensions 被 shim 成 () => [],所以没有子进程快照时,已安装 extension 的 Skill 永远不会出现在这份列表里。本 PR 改为从持久化 extension store 构造一个「不绑定运行时 Config」的 ExtensionManager,把 active extension 喂给 SkillManager(保留 project > user > extension > bundled 优先级),把 inactive extension 的 Skill 以 inactive_extension 状态作为管理项追加,并给整份列表加上按名字排序,从而和子进程侧 producer 的形态对齐。映射继续复用共享的 mapSkillConfigToStatus。
What this PR does
Fills the extension-Skill gap in the daemon-local pre-child /workspace/skills fallback. Previously getActiveExtensions was shimmed to () => [], so installed extension Skills never surfaced until a session existed. It now builds an unbound ExtensionManager from the persistent store, feeds active extensions into SkillManager (preserving project > user > extension > bundled precedence), appends inactive-extension Skills as inactive_extension management entries, and sorts the whole list by name to match the child producer. Shape stays shared via mapSkillConfigToStatus.
结论 / Verdict:范围收敛、形态对齐、测试覆盖扎实,无 Critical。有 1 个 Important(display 层面的 locale 漂移)+ 若干 nit,故以 COMMENT 发布。
Important
I-1 · 本地枚举的 ExtensionManager 没有传 locale,非英文工作区会出现显示名漂移 / daemon-local ExtensionManager omits locale, causing a display-name flip on non-English workspaces.
workspace-skills-status.ts:147 里 new ExtensionManager({ workspaceDir, isWorkspaceTrusted }) 未传 locale,于是 extensionManager.ts:521 回落成 this.locale = 'en'。而 i18n.ts 的 resolveExtensionConfigLocale(rawConfig, this.locale) 会按 locale 解析 extension 的 displayName / description。子进程侧 producer 用的是 Config 自带的 manager(config.ts:2939 传了 locale: params.locale),daemon 里规范的工厂 workspace-extensions-controller.ts:315-317 也传了 resolveExtensionLocale(...)。
后果:如果工作区语言非英文、且 extension manifest 写了本地化 displayName/description 对象,那么「首 prompt 之前」这份兜底列表会显示英文扩展名,一旦有子进程接管又翻成本地化名字。影响面只限「manifest 做了本地化」的 extension,属于纯显示问题,但这恰好是本次 design doc 立下的「两份列表不能漂移」这条不变量要防的点。
建议:直接复用 createExtensionManager / resolveExtensionLocale 拿到 per-workspace locale(顺带把 consent/setting 桩统一到一处),而不是在这里裸构造。
The daemon-local manager defaults locale to 'en' (extensionManager.ts:521), while the child uses the workspace locale (config.ts:2939) and the canonical controller resolves it via resolveExtensionLocale(...) (workspace-extensions-controller.ts:315-317). For a non-English workspace whose extensions ship localized displayName/description manifests, the pre-child catalog shows English names, then flips to localized once a child takes over. Display-only and scoped to localized manifests, but it is precisely the "two listings can't drift" invariant the design doc sets out to protect. Prefer reusing createExtensionManager / resolveExtensionLocale.
Nits
- N-1 活跃 extension Skill 的
enabled是手写复刻Config.isSkillEnabled(!state || enabledNames.has(...) || (workspaceEnabled ?? defaultEnabled))。当前与config.ts:6211等价,但将来isSkillEnabled新增禁用来源时不会自动同步到兜底路径。可考虑像mapSkillConfigToStatus那样抽一个共享判定,避免"启用/禁用"决策也漂移。 - N-2
getExtensionSkillState在 skill 名不属于该 extension 时会 throw;因为它在外层try内,单行不匹配会把整份列表退化成initialized:false。概率很低(skills 本就来自同一批 extension),加个 per-row 兜底更稳。 - N-3 缓存的
extensionManager把 store snapshot(workspace override)冻结到 invalidate,而settings.skills.enabled/disabled每次都重读——同一份响应里两个不同的时效时钟。和"best-effort 兜底"注释一致,补一行说明即可。 - N-4
enabledExtensionOverrides(--enable-extension启动覆盖)这里不像Config那样透传;daemon 子进程一般也没有它,如属有意可在 doc 的 out-of-scope 里点一句。
值得肯定 / Positives
- 对 extensions 目录的
fs.readdir预探测把 ENOTDIR 变成 error 状态、同时容忍 ENOENT,符合 design doc「目录失败返回未初始化错误」的要求。 - safe mode / 未信任 /
disabledLevels:['extension']的门控正确,并用it.each做了矩阵覆盖。 - inactive 父 extension 的 Skill 一律
disabled:true,settings 里的 opt-in 无法偷偷激活它——正确。 - 新增的整表按名排序让本地枚举与子进程对齐,也解释了那两处被调序的既有断言(这是想要的 parity 变化,不是 ci-bot 说的"mock 假象")。
发布时 CI 全绿(Test / Lint & Static / Serve A/B / Integration / web-shell E2E Smoke 均 pass),head 为 bb28cd1f。
|
Addressed the locale finding from the reviews by ytahdn and samuelhsin. It affects the new extension metadata returned by this PR, so the fix stays within stage 2's enumeration scope. The daemon-local provider now uses the existing language-setting resolver and existing locale resolver when constructing its unbound extension manager. This preserves configured-language aliases, QWEN_CODE_LANG precedence and auto detection without changing process-global UI language. The locale resolver's implementation is unchanged; it is now exported for this caller instead of duplicating the controller's resolution chain or importing the controller into the provider. The regression was reproduced over real cold HTTP against the previous PR bundle: both active and inactive extension names were English under general.language=zh on /workspace/skills and /workspace/config/skills. The new parameterized regressions also failed before the fix. Scope dispositions: shared enablement refactoring, speculative per-row exception recovery and startup extension-override propagation are not included. Cache expiry/invalidation and store contention policy remain the explicitly deferred stages/design questions of #11274. The current mapping reads Skills from the very same extension objects, so a foreign-skill fallback is not needed for this change. Fixed in The PR remains 4 files, 440 additions + 31 deletions = 471 lines total. No unrelated existing issues were fixed. |
|
CI follow-up for unchanged head The first Smoke failure was a GitHub HTTPS timeout during checkout, before tests ran. Its rerun reached Playwright but 10 cases failed to find UI elements. The retained trace explains the latter failure: Chromium reports widespread The exact first failing split-persist case passes locally on the current head (1 passed). This smoke harness starts Vite and a page-routed mock daemon; it does not execute the changed CLI provider. Web Shell, SDK sources and the lockfile are unchanged relative to this PR's base. Requested another rerun of the failed Smoke job. No product, test or workflow changes are included: runner/network diagnosis is recorded separately from the PR implementation and does not widen its scope. Rerun result (2026-09-07 12:29 UTC): the unchanged head |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": locating DEFAULT_EXCLUDED_ENV_VARS ' definition to confirm whether a workspace env overlay may set QWEN_HOME (the trigger condition for the finding above).; "agent reverse-audit (round 1)": walking every refreshCache() / loadExtension throw site for a message that embeds an extension source URL, which would make the unredacted writeStderrLine a…; "agent reverse-audit (round 1)": enumerating all Storage "pinned runtime context" call sites (storage.ts:169-190) to check whether any daemon route scopes the extension store per runtime by a…; "agent 1a": whether the daemon's ACP child spawn forwards -e / --extensions (the trigger for finding 3) — I read config.ts:2315 but not the spawn-args builder.; "agent 1a": whether resolveSupportedLanguage() normalization in the new resolveLanguage(resolveLanguageSetting(...)) path can yield a different locale than the child's …, and 1 more.
中文说明
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":locating DEFAULT_EXCLUDED_ENV_VARS ' definition to confirm whether a workspace env overlay may set QWEN_HOME (the trigger condition for the finding above).;"agent reverse-audit (round 1)":walking every refreshCache() / loadExtension throw site for a message that embeds an extension source URL, which would make the unredacted writeStderrLine a…;"agent reverse-audit (round 1)":enumerating all Storage "pinned runtime context" call sites (storage.ts:169-190) to check whether any daemon route scopes the extension store per runtime by a…;"agent 1a":whether the daemon's ACP child spawn forwards -e / --extensions (the trigger for finding 3) — I read config.ts:2315 but not the spawn-args builder.;"agent 1a":whether resolveSupportedLanguage() normalization in the new resolveLanguage(resolveLanguageSetting(...)) path can yield a different locale than the child's …,另有 1 条。
— qwen3.8-max via Qwen Code /review (v0.23.0)
|
@qwen-code /takeover |
|
🤝 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 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
…11281) - Give the extension load its own failure domain: a fault inside the store load degrades only extension entries (logged, not cached, so the next read retries), while an unreadable extensions root keeps the documented all-or-nothing error status. - Resolve the extension locale on every call and key the cache on it, so a language change rebuilds instead of serving a frozen locale; guard a non-string general.language from throwing in resolution. - Guard cache installs with a per-workspace invalidation epoch so an invalidate() delivered mid-build cannot be undone by that build, and coalesce concurrent cold builds. - Gate skills.disabledLevels at discovery only; inactive-extension management entries still appear, matching the child producer. - Pin the readdir guard with an error-identity assertion and a searchable-but-unlistable root case; extract assertReadableDir. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Address review round — PR #11281Base: Feedback dispositionsReview rv:5130485638 (@ytahdn, COMMENTED)
Review rv:5132529744 (@qwen-code-ci-bot, CHANGES_REQUESTED) — inline findings
The review's "not explored to full depth" note lists unconfirmed reverse-audit threads; nothing actionable was asserted there, so there is nothing to reproduce. Mutation probes (every guard/branch this round adds has a witness)Each probe mutated the source, ran the focused test(s), confirmed failure, then restored and re-confirmed green:
Verification
中文说明本轮评审处理 — PR #11281基线: 反馈处置评审 rv:5130485638(@ytahdn,COMMENTED)
评审 rv:5132529744(@qwen-code-ci-bot,CHANGES_REQUESTED)— 行内发现
评审中「未探索到全部深度」的说明只列出未确认的反向审计线索,没有可复现的断言,因此无需处理。 变异验证(本轮新增的每个守卫/分支都有见证)每次变异都是:改动源码 → 运行对应测试确认失败 → 还原后确认全绿(见上方英文表格)。 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 7 selected review thread(s). · 已关闭全部选中的 7 条评审线程。 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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-5 sibling-route locale divergence over a workspace .env QWEN_CODE_LANG — already reported (comment 3950188503), author deferred and recorded in this round's design doc
- R1-9 inactive-append and sort assembly duplicated from the child producer — already reported (comment 3950188533), author deferred and recorded in this round's design doc
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
— qwen3.8-max via Qwen Code /review (v0.23.0)
…view (#11281) - Decide directory absence by lstat and readability by readdir, so a dangling symlink at the extensions root fails closed instead of reading as absent and silently dropping every extension Skill. - Degrade an unreadable extensions root for workspaces that disabled extension discovery: the catalog-fatal probe no longer empties a catalog that opted out of extension Skills. - Stop latching daemon-local skills answers in the workspace facade (the latch stays child-only), so a degraded extension enumeration retries on the next poll instead of freezing the pre-child window. - Drop the unreachable disabled-level arm from the active-extensions shim; SkillManager already applies that gate before the only call. - Pin the degrade stderr line, the non-string language guard, and the superseded-build coalescing bookkeeping with discriminating tests; record the child locale-normalization divergence and the correct --extensions flag name in the design doc. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #11281 review feedbackOne commit: Addressed (9 findings)
Deferred to the next round (4 findings, batch bound)This round already implements 9 findings (3 Critical + 6 witness/doc items); per the ~8-finding batch bound the remaining Suggestions are deferred explicitly, not dropped — each has a reply on its own thread: R1-7 (rc:3953203563, coalesce the manager build rather than the whole response), R2-6 (rc:3953203569, Verification
中文说明Autofix 本轮处理 — PR #11281 审查反馈一次提交: 已处理(9 条发现)
推迟到下一轮(4 条发现,受批次上限约束)本轮已实现 9 条发现(3 条 Critical + 6 条见证/文档项);按约 8 条的批次上限,其余 Suggestion 明确推迟而非丢弃 —— 每条都在其原线程下收到回复:R1-7(rc:3953203563,合并 manager 构造而非整份响应)、R2-6(rc:3953203569, 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 9 selected review thread(s). · 已关闭全部选中的 9 条评审线程。 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/模型 |
|
👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply 中文说明👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 |
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #11281 (address-review)Commit: This round deliberately implements the smallest blocking subset (budget warning noted): the two in-footprint documentation Suggestions, plus the documentation alternative the round-4 Critical itself offers. The three contested Criticals are escalated or declined with evidence, not silently dropped. Feedback dispositionsResolved in an earlier commit, re-verified at this head
Resolved by this round's commit
Declined with evidence (thread left open)
Escalated — decision not this bot's to make (threads left open)
Notes
Verification
中文说明Autofix 本轮处理 — PR #11281(address-review)提交: 本轮刻意只实现最小阻塞子集(已注意到预算警告):两条在 PR 范围内的文档类 Suggestion,以及第 4 轮 Critical 自己给出的文档化备选方案。三条存在分歧的 Critical 均以证据升级或婉拒,没有被悄悄丢弃。 反馈处置已由先前提交解决、并在当前 head 重新核实
由本轮提交解决
附证据婉拒(线程保持打开)
升级处理 —— 不属于本 bot 可自行裁定(线程保持打开)
备注
验证
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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-9 inactive-append and sort assembly duplicated from the child producer, dedupe rule already differing — already reported (comment 3950188533), author deferred and recorded in the design doc
- R1-7 full locked extension-store reload on every cache miss, and invalidate discarding the cached manager — already reported (comment 3950188523)
- N-1 hand-derived enabled judgment instead of Config.isSkillEnabled — already reported (@ytahdn review 5130485638) and recorded in the design doc's later-stage items
- R3-3 active-path display-name guard unasserted — already reported (comment 3955145205), its MUTANT A already witnessed there
Not reviewed: test-efficacy — the mutation/hunk probe could not validate its own control this round (harnessValidated: null; the one whole-file revert probe came back inconclusive with reason 'no-output', 6 hunks were skipped for the cap and 1 mutant for no green baseline), so this PR's automated mutation and per-hunk coverage is unmeasured; four mutants were instead run by hand during verification.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and the packages/cli suite ran on Linux only, so the changed package has no Windows or macOS witness (the new dangling-symlink and chmod cases are skipIf win32 and have no Windows witness either).
Not explored to full depth (tool budget reached): "agent 2": I did not execute a runtime check that a workspace's effectiveEnv QWEN_HOME actually reaches the spawned ACP child's Storage (the basis for Finding A's fa….
5 Suggestion(s) were drafted inline past the resolved critical posting floor — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s); the CLI moved them into the deferral list below (floor enforcement).
Deferred under the convergence posture (round 5, not a blocker) — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s) — recorded, not requested in this round:
docs/design/daemon-extension-skill-catalog.md:42 — [review] R5-1: This sentence is the recorded answer to the "this load is not a read" thread, and its steady-state guarantee does not hold: prepareDirectories() re-asserts the store direct…docs/design/daemon-extension-skill-catalog.md:62 — [review] R4-4: (fix-induced) The later-stage-items list restored to answer this finding states that the duplicated assembly keeps the child's level:extensionName:name dedupe key. The code…packages/cli/src/serve/workspace-skills-status.ts:254 — [review] R5-2: No test pins that the appended inactive-extension entries participate in this sort — only the SkillManager -produced prefix is order-asserted, so moving the sort above …packages/cli/src/serve/workspace-skills-status.ts:155 — [review] R5-3: The throw error half of this guard has no test that discriminates it — collapsing the catch to .catch(() => undefined) ships green, and the guard is masked by its ne…packages/cli/src/serve/workspace-skills-status.ts:223 — [review] R5-4: The skill-name-side normalization in this hand-written enabled judgment is unpinned — every fixture skill name is already lowercase, so dropping .trim().toLowerCase()…
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):test-efficacy — the mutation/hunk probe could not validate its own control this round (harnessValidated: null; the one whole-file revert probe came back inconclusive with reason 'no-output', 6 hunks were skipped for the cap and 1 mutant for no green baseline), so this PR's automated mutation and per-hunk coverage is unmeasured; four mutants were instead run by hand during verification.
未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI at this commit and the packages/cli suite ran on Linux only, so the changed package has no Windows or macOS witness (the new dangling-symlink and chmod cases are skipIf win32 and have no Windows witness either).
未探索到全部深度(达到工具调用预算):"agent 2":I did not execute a runtime check that a workspace's effectiveEnv QWEN_HOME actually reaches the spawned ACP child's Storage (the basis for Finding A's fa…。
5 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论——发布下限因首次发现速率连续 2 轮未下降而提前生效;CLI 已将其移入下方延后清单(下限强制执行)。
收敛姿态下延后(第 5 轮,非阻断)——发布下限因首次发现速率连续 2 轮未下降而提前生效——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
|
🔀 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
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- RA3-1 daemon-local locale reads the shared process QWEN_CODE_LANG, so one workspace's .env can override another workspace's display language — same root as already-reported R1-5 in the opposite direction (comment 3950188503)
- S-3 per-workspace rebuild and retention of a workspace-global extension inventory — already reported as R1-7 (comment 3950188523)
- S-7 active-path display-name guard unasserted — already reported as R3-3 (comment 3955145205)
2 candidate finding(s) this round's reviewers re-derived matched entries already carried on this PR and were set aside before verification (R4-1, R3-1) — a matched posted finding is ruled in the previous-round status as always, and a matched deferral stays on the standing deferral record.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI at this commit and the packages/cli suite ran on Linux only, so the changed package has no macOS or Windows witness (the new dangling-symlink and chmod cases are skipIf win32 and have no Windows witness either).
Not reviewed: test-efficacy — the mutation/hunk probe could not validate its own control this round (harnessValidated: null; the one whole-file revert probe came back inconclusive with reason 'no-output' because the probe tree lacks the gitignored packages/cli/src/generated/git-commit.ts, 6 hunks were skipped for the cap and 1 mutant for no green baseline), so this PR's automated mutation and per-hunk coverage is unmeasured; four mutants were instead run by hand during verification.
Not explored to full depth (tool budget reached): "agent 6c": I did not run packages/cli 's vitest for workspace-skills-status.test.ts , so the green-ness of the new tests (in particular whether refreshCache would inde…; "agent 6c": I did not confirm that the ACP child spawn actually applies runtimeEffectiveEnv (including QWEN_HOME ) as its process env — I verified it is threaded as env…; "agent reverse-audit (round 3)": an executed reproduction of finding 1 (plant QWEN_CODE_LANG through one workspace's .env via resolveExtensionLocale , then read a second workspace's config…; "agent reverse-audit (round 6)": did not read ExtensionStore.ensureInitializedUnlocked below extension-store.ts:485 (the no-existing-snapshot, orphan and declarationOnly branches), so the d…; "agent reverse-audit (round 6)": did not execute a live A/B differential between this provider's wire output and the ACP child's buildWorkspaceSkillsStatus on a shared fixture; the parity cla…, and 1 more.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/workspace-skills-status.ts:150 — [probe] D6-1 Both read-only guarantees are hand-rolled at one call site, so GET /extensions answers 200 {extensions: []} while GET /workspace/config/skills answers initialized:false + …packages/cli/src/serve/workspace-skills-status.ts:220 — [probe] D6-2 The hand-written enabled judgment duplicates buildExtensionSkillStates, a Config-free copy of the same rule in this package; a 13-case sweep found 0 divergences today and …
Convergence: round 6 posted 4 inline comment(s), 1 of them reported for the first time; the previous round posted 3 (0 new). Findings keep coming back to the same files: packages/cli/src/serve/workspace-skills-status.ts (findings in rounds 3, 4; 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.)
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (4 Critical(s)), the rate of first-time findings is not falling (this round 1, previous 0), 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.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
本轮评审重新推导出的 2 条候选发现与本 PR 已携带的条目匹配,已在验证前搁置(R4-1, R3-1)——被匹配的已发布条目照常在上一轮状态区裁定,被匹配的延后条目仍保留在延后清单记录中。
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI at this commit and the packages/cli suite ran on Linux only, so the changed package has no macOS or Windows witness (the new dangling-symlink and chmod cases are skipIf win32 and have no Windows witness either).
未审查(原文为英文):test-efficacy — the mutation/hunk probe could not validate its own control this round (harnessValidated: null; the one whole-file revert probe came back inconclusive with reason 'no-output' because the probe tree lacks the gitignored packages/cli/src/generated/git-commit.ts, 6 hunks were skipped for the cap and 1 mutant for no green baseline), so this PR's automated mutation and per-hunk coverage is unmeasured; four mutants were instead run by hand during verification.
未探索到全部深度(达到工具调用预算):"agent 6c":I did not run packages/cli 's vitest for workspace-skills-status.test.ts , so the green-ness of the new tests (in particular whether refreshCache would inde…;"agent 6c":I did not confirm that the ACP child spawn actually applies runtimeEffectiveEnv (including QWEN_HOME ) as its process env — I verified it is threaded as env…;"agent reverse-audit (round 3)":an executed reproduction of finding 1 (plant QWEN_CODE_LANG through one workspace's .env via resolveExtensionLocale , then read a second workspace's config…;"agent reverse-audit (round 6)":did not read ExtensionStore.ensureInitializedUnlocked below extension-store.ts:485 (the no-existing-snapshot, orphan and declarationOnly branches), so the d…;"agent reverse-audit (round 6)":did not execute a live A/B differential between this provider's wire output and the ACP child's buildWorkspaceSkillsStatus on a shared fixture; the parity cla…,另有 1 条。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 4 条行内评论,其中 1 条是首次提出;上一轮发布了 3 条(其中 0 条首次提出)。发现反复回到同一批文件:packages/cli/src/serve/workspace-skills-status.ts(第 3、4 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 4 条 Critical),首次发现的速率没有下降(本轮 1,上一轮 0),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
— qwen3.8-max via Qwen Code /review (v0.23.1)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply 中文说明👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 |
|
Review closure at R6-1 is fixed, not merely deferred. The provider obtains the existing consistent ExtensionStore snapshot and resolves each manifest's Skill default from the already loaded extension object. Workspace overrides use the same snapshot and existing store resolver. It no longer reselects an owner through the ambiguous by-ID manager method. There is no catch-to-undefined/default-enable fallback. The regression uses two manifest names sharing one repository ID, checks repeated reads, preserves the second manifest's disabled default, and checks both workspace-disabled and workspace-enabled overrides. The core identity model and extension state API remain outside this PR. The remaining recurring findings are explicitly disposed within the author's approved staging. R3-1 (config-catalog invalidation), R1-4/R4-2 (in-flight invalidation epochs) remain tracked stage-4 work under #11274. Their observable stale-state limitation is acknowledged; no claim is made that this revision implements them. The author explicitly accepted the stage-2 improvement and prohibited adding these cache/concurrency changes. Reposting the same staged requirement does not create a new in-scope implementation obligation. R1-1/R4-1 asks for extension-only partial-success degradation. That contradicts the established stage-2 error contract. Propagated directory/store failures remain explicit uninitialized/error responses, with their all-or-nothing impact documented and tested. Individual artifact skipping still belongs to the existing shared loader. This requested contract change is rejected, not silently implemented or described as fixed. All repeated threads for these roots are closed with those dispositions. The known limitations remain in the design and tracking issue. Additional Suggestion-level extraction, mutation-test coverage and sibling-route cleanup are follow-up work after six review rounds, not further scope in this PR. Two inaccurate documentation statements about write-free rebuilds and the literal dedupe key were corrected while updating the required bilingual design. Earlier R6 diagnosis correctly rejected the unsafe fallback; the bounded owner/snapshot fix above supersedes the earlier decision to leave the provider failure unresolved. Required repository approval and new-head CI remain required. Clearing superseded automated review metadata is not self-approval or a claim that deferred stage-4 work is implemented. Validation: full build, typecheck and bundle passed; 177 focused provider/mapping/facade tests passed. The collision regression was red before the fix. Six real HTTP identity/default/override scenarios plus the previous twelve daemon scenarios passed (18 total). No child, sessions or MCP/hooks were required. Two clean diff audits, local review, lint/format and the bilingual design checks passed. Total PR diff including both design documents: 866 additions + 31 deletions = 897 lines. |
Superseded after active resolution requested by the author. At 9585979, R6-1 is fixed by owner-object/default plus consistent-store-override resolution (no catch-to-enabled); 177 focused tests and 18 real daemon scenarios passed. Repeated R3-1/R4-2 are author-accepted stage-4 deferrals, and R4-1 partial-success degradation is rejected by the documented stage-2 error contract. Full dispositions: #11281 (comment) . All 11 corresponding threads are resolved; no current human blocking review or unhandled thread remains. This clears stale automated review metadata, not required current-head CI or maintainer approval.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
6 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R7-2 lock-blocked read path — already reported as R1-7 (comment 3950188523)
- R7-3 un-memoized failing load — already reported as R2-7 (comment 3953203576)
- R7-4 store generation bump on a read — already reported (comment 3958206500)
- R7-9 display-name guard unpinned — already reported as R3-3 (comment 3955145205)
- R7-11 locale normalization fork — already reported as R2-5 (comment 3953203567)
- R7-14 untested lstat rethrow arm — already reported as R1-8 (comment 3950188527)
1 candidate finding(s) this round's reviewers re-derived matched entries already carried on this PR and were set aside before verification (R4-1) — a matched posted finding is ruled in the previous-round status as always, and a matched deferral stays on the standing deferral record.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 6a": whether updateLegacyProjectionRemainder ( extension-store.ts:606-609 ) can oscillate between the daemon's user-only identity set and a CLI/child's superset, w…; "agent 6a": whether any workspace runtime's effectiveEnv can actually carry a QWEN_HOME (the premise of finding 4) — I confirmed getRuntimeEffectiveEnv supports a non…; "agent reverse-audit (round 1)": whether any production createServeApp caller enables workspaceTrustHotReloadAvailable without an injected registry or deps.primaryWorkspaceTrusted — the c….
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/workspace-skills-status.ts:161 — [review] fail-closed root probe contradicts the sibling /workspace/extensions route over the same directory
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 6 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
本轮评审重新推导出的 1 条候选发现与本 PR 已携带的条目匹配,已在验证前搁置(R4-1)——被匹配的已发布条目照常在上一轮状态区裁定,被匹配的延后条目仍保留在延后清单记录中。
未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
未探索到全部深度(达到工具调用预算):"agent 6a":whether updateLegacyProjectionRemainder ( extension-store.ts:606-609 ) can oscillate between the daemon's user-only identity set and a CLI/child's superset, w…;"agent 6a":whether any workspace runtime's effectiveEnv can actually carry a QWEN_HOME (the premise of finding 4) — I confirmed getRuntimeEffectiveEnv supports a non…;"agent reverse-audit (round 1)":whether any production createServeApp caller enables workspaceTrustHotReloadAvailable without an injected registry or deps.primaryWorkspaceTrusted — the c…。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.1)
Duplicate automated blockers at unchanged head 9585979. R4-1 retains the explicitly documented all-or-nothing error contract; R4-2 epoch/coalescing remains an author-accepted stage-4 deferral. The round-7 witnesses confirm already acknowledged consequences and do not change those dispositions. Both repeated threads now carry exact-head replies and are resolved; no human blocking review is present. R6 was fixed in this head. See #11281 (comment) . Clearing duplicate metadata does not claim deferred work is implemented or waive required approval/CI.
Independent runtime verification — real
|
ytahdn
left a comment
There was a problem hiding this comment.
PR 主旨 / What this PR does
给 daemon 本地(ACP 子进程起来之前)的 /workspace/skills 兜底枚举补上「已安装 extension 的 Skill」这块:之前 getActiveExtensions 被 shim 成 () => [],没有子进程快照时 extension Skill 永远不出现。本 PR 从持久化 extension store 构造一个不绑定运行时 Config 的 ExtensionManager,把 active extension 喂给 SkillManager(保留 project > user > extension > bundled 优先级),把 inactive extension 的 Skill 以 inactive_extension 禁用管理项追加,按名字排序与子进程对齐;并在每次 provider 返回时从缓存的原始 manifest 用工作区语言重新投影本地化展示名。这是跟踪 issue 11274 的第 2 阶段,显式不引入部分成功语义、provider 并发合并/epoch、facade 缓存改造(这些留给后续阶段)。
Fills the extension-Skill gap in the daemon-local pre-child /workspace/skills fallback (previously getActiveExtensions was shimmed to () => []). It builds an unbound ExtensionManager from the persistent store, feeds active extensions into SkillManager (preserving project > user > extension > bundled precedence), appends inactive-extension Skills as disabled inactive_extension management entries, sorts by name to match the child, and re-projects localized display names from cached raw manifests on every response using the workspace language. This is stage 2 of tracking issue 11274; partial-success semantics, provider coalescing/epoch and facade-cache changes are explicitly out of scope.
审查方式:纯静态,逐 hunk 对照 head tree(95859798),未运行测试/构建。CI 实质 lane(Test / Lint & Static / Serve A/B / Integration no-AK / Desktop Shell)全绿。这是一个已经被充分讨论的 PR:round-7 机器人 ledger 仍挂两条 Critical(R4-1、R4-2),作者在内联线程里逐条rebut/defer 并已请维护者(@wenshao)裁定阶段边界;更早的一条真 Critical(R6-1,by-ID owner 查找)作者称已在本 head 修复。我这次的价值是独立复核这几条,而不是重复它们。
Review method: static only, every hunk cross-checked against the head tree (95859798); no tests/builds run. CI's substantive lanes are green. This PR is already heavily litigated: the round-7 bot ledger still carries two Criticals (R4-1, R4-2), which the author has rebutted/deferred inline and escalated to maintainer judgment (@wenshao); an earlier genuine Critical (R6-1, by-ID owner lookup) is claimed fixed at this head. My contribution here is to independently re-verify these, not repeat them.
🎉 已核实修复 / Verified fixed at this head
R6-1(by-ID owner 查找,早轮 Critical)——确认已修复且实现正确。 extensionSkillStates 现在是 Map<Extension, Map<string, boolean>>,按扩展对象而非 id 建键(:90/:151/:189),键取自 getLoadedExtensions()(:170);查回时 extensionSkillStates.get(extension)(:235)里的 extension 来自 extensions.find(e => e.name === skill.extensionName)(:232),extensions 又是每次调用重取的 getLoadedExtensions()(:227)。我核对了 getLoadedExtensions() 返回 [...this.extensionCache.values()](extensionManager.ts:1554)——是缓存里的同一批对象引用;且 refreshCacheWithSnapshot 只在 if (!cached)(:133-224)内跑,缓存命中时 manager 冻结,所以跨调用的对象身份成立,.get(extension) 不会漏。manifest 默认值按对象读 extension.config.skillStates(:174-178),workspace override 仍按 id 读 store(:181)——因此 id 冲突的复制扩展能保住各自的默认值,只共享 store 的 id 级 override(store 本就按 id 存,属既有设计)。测试 preserves each manifest default and store override when extension ids collide 覆盖此点。
R6-1 (by-ID owner lookup, earlier-round Critical) — confirmed fixed and correct. extensionSkillStates is now keyed by the Extension object, not the id (Map<Extension, Map<string, boolean>>, :90/:151/:189), built from getLoadedExtensions() (:170); the lookup extensionSkillStates.get(extension) (:235) resolves extension via find(e => e.name === skill.extensionName) (:232) over getLoadedExtensions() re-fetched each call (:227). I verified getLoadedExtensions() returns [...this.extensionCache.values()] (extensionManager.ts:1554) — the same cached object references — and refreshCacheWithSnapshot only runs inside if (!cached) (:133-224), so the manager is frozen on cache hits and object identity holds across calls; .get(extension) cannot miss. Per-object defaults come from extension.config.skillStates (:174-178); the store override stays id-keyed (:181), so colliding-ID copies keep their own defaults and share only the id-level store override (the store is id-keyed by design).
我上一轮的 Important(本地枚举没传 locale → 非英文工作区展示名漂移)——确认已解决。 locale 现在从 settings.merged.general?.language 解析(:126-131)并传入 new ExtensionManager({ ..., locale })(:167),展示名在每次返回时用 getExtensionDisplayName(extension, locale) 从缓存的原始 manifest 重新投影(:244 active、:268 inactive),无需重建目录缓存。测试 re-resolves the extension locale when the configured language changes 与 ignores a non-string general.language instead of failing the catalog 覆盖。
My prior-round Important (daemon-local manager omitted locale → display-name drift on non-English workspaces) — confirmed resolved. The locale is now resolved from settings.merged.general?.language (:126-131) and passed to new ExtensionManager({ ..., locale }) (:167), with display names re-projected per response from cached raw manifests via getExtensionDisplayName(extension, locale) (:244 active, :268 inactive) without rebuilding the directory cache.
对机器人两条 lingering Critical 的独立裁定 / Independent adjudication of the bot's two lingering Criticals
R4-1「extension 加载没有自己的失败域,一个扩展目录的故障清空整份目录」——我与作者一致:非阻塞
作者在内联里以「文档化的 all-or-nothing 错误契约」rebut:返回成功的部分目录不是本阶段接受的失败策略,root/store 故障向上传播是有意的,partial-success 明确出范围。我独立复核后同意,理由是「一个坏扩展清空全部」这个前提大部分不成立:
- 最常见的单个 manifest 损坏已被共享 loader 隔离——
loadExtensionsFromExtensionsDir(extensionManager.ts:1614-1623)调用loadExtension时不传throwOnError,其 catch(:1782-1783)if (options.throwOnError) throw e(默认 false,:1629)→ 坏 manifest 返回 null 被跳过,目录存活。测试keeps the shared loader behavior for malformed extension manifests正是断言这点。 - 真正会致命并清空全表的只剩目录/store 的 I/O 故障:
fs.lstat(:154,ENOENT→undefined,其余 rethrow)、被丢弃返回值的 readdir 探针(:161)、loadExtension里位于 try 之外的fs.statSync(extensionManager.ts:1632)、以及readConsistentstore 读。而这些恰是本 PR 文档化的 fail-closed 契约(「extension 目录不可读或 store 读失败 → 明确返回 uninitialized/error」),并有测试returns an error for an unreadable extension directory、does not cache a failed store read as an initialized empty catalog覆盖。 - 新代码里的逐行 helper 都是全函数、不抛:
getExtensionDisplayName(i18n.ts:103-110)+resolveLocalizableString(:38-57),getSkillWorkspaceOverride(extension-store.ts:998-1010,纯 optional-chain 读)。所以不存在「某个 skill 触发抛错清空全表」的路径。
结论:这是一个有意、有文档、有测试的设计选择,不是 Critical。(机器人延后的非阻塞项「:161 readdir fail-closed 探针比 sibling /extensions 路由更严」也属同一有意 fail-closed——sibling routes/workspace-extensions.ts:1841 直接让 refreshCacheWithSnapshot 内部吞掉目录问题。)
The author rebuts R4-1 with the documented all-or-nothing error contract (partial-success is out of scope; root/store failures propagate by intent). I concur, because the premise "one bad extension empties the catalog" is largely false: the common single-manifest fault is already isolated by the shared loader (loadExtension called without throwOnError, its catch at extensionManager.ts:1782-1783 returns null → skipped; test keeps the shared loader behavior for malformed extension manifests). The only catalog-fatal paths left are directory/store I/O faults (lstat :154, the discarded readdir probe :161, loadExtension's fs.statSync outside its try at extensionManager.ts:1632, readConsistent), which are exactly this PR's documented fail-closed contract and are test-covered. The new per-row helpers are total (no throw): getExtensionDisplayName (i18n.ts:103-110) and getSkillWorkspaceOverride (extension-store.ts:998-1010). Not a Critical.
R4-2「cold build 进行中收到的 invalidate() 被吞掉」——竞态属实,但属既有模式 + 作者已明确延后到第 4 阶段;我不作为新阻塞项重提
代码层竞态确实存在:get-miss(:132)→ 一串 await(lstat :154 / readdir :161 / refreshCacheWithSnapshot :169)→ managers.set(:223),而 invalidate = managers.delete(:109)。若 invalidate 落在这个窗口里,它删的是一个尚未写入的键(no-op),随后 in-flight 的 build 把变更前的快照 set 进去 → 陈旧状态越过了这个失效点存活,直到下一次 invalidate 或子进程接管。
但需要如实说明三点,这也是我不把它当新阻塞项的原因:
- get/build/set 这套缓存模式是既有的——本 PR 之前缓存的就是一个裸
SkillManager,同样的竞态已在(diff 只是把值类型从Map<string, SkillManager>拓宽成Map<string, WorkspaceSkillManagers>)。本 PR 的「急切缓存 extension 状态」让陈旧 payload 更丰富/更易被观察到,但没有引入竞态机制本身。 - 作者已把整套 invalidation/coalescing/epoch 显式延后到 11274 的第 4 阶段(内联 :223、:132 的 closed 说明;相关的 R3-1「extension 变更根本不调用
invalidateSkillsConfigStatus」同样延后到第 4 阶段)。dev-bot 已把这个阶段边界上升给维护者裁定(@wenshao),目前尚无维护者拍板。 - 有缓解:这是子进程前的 best-effort 兜底(:98-100 注释),有会话后子进程权威重列;invalidate 在每次 skill 安装/删除和 workspace 移除时都会触发(server.ts:962-965、
onWorkspaceRemoved:2646),所以下一次变更即自愈。
若维护者不接受这个延后,最省的修法是给每个 cwd 加一个 epoch:build 开始时记录 epoch,managers.set 前比对,只有在 build 期间没有 invalidate(epoch 未变)才写入;invalidate 里 delete 的同时 epoch++。这样 in-flight 期间被失效的 build 结果会被丢弃而不是覆盖。
The R4-2 race is real in code (get-miss :132 → awaits → managers.set :223, while invalidate = managers.delete :109; an invalidate inside the window is a no-op delete and the in-flight build then installs the pre-mutation snapshot, so staleness survives past the invalidation point). But I do not re-raise it as a fresh blocker, honestly, for three reasons: (1) the get/build/set cache pattern is pre-existing — pre-PR it cached a bare SkillManager with the identical race; this PR only widens the cached value type and makes the stale payload more observable. (2) The author has explicitly deferred the whole invalidation/coalescing/epoch mechanism to stage 4 of 11274 (inline closures at :223/:132; the related R3-1 "extension mutations never call invalidateSkillsConfigStatus" is likewise stage-4), and dev-bot escalated the stage boundary to maintainer judgment (@wenshao) — not yet ruled. (3) It is a best-effort pre-child fallback (comment :98-100), the live child is authoritative, and invalidate fires on every skill install/delete and workspace removal (server.ts:962-965, :2646), so the next mutation self-heals. If the maintainer rejects the deferral, the cheap fix is a per-cwd epoch guard: record the epoch at build start and only managers.set if it is unchanged, with invalidate bumping the epoch alongside the delete.
我自己这一遍核实干净 / Verified clean on my own pass
mapSkillConfigToStatus的 enabled 语义(workspace-skills-mapping.ts:23-26):disabledReason = opts.disabled ? 'inactive_extension' : (disablement?.reason ?? (opts.enabled === false ? 'default' : undefined))。disablement在opts.enabled === false之前判定,所以给非扩展 skill 传的{ enabled: true }(:248-250)是空操作(无 disablement 时落到 'ok')——没有把 settings 硬禁用的 skill 强行启用的回归。active 扩展的默认禁用 skill →enabled=false→ 'default';settings opt-in(enabledNames)→ true → 'ok'。语义正确。- active/inactive 分区:active 扩展经
listSkills(getActiveExtensionsshim 过滤isActive:203)出现,inactive 经 :253-275 以disabled:true追加,不重复计数;同名 inactive 源与 active 并列追加是有意的(测试preserves project precedence and appends same-name inactive sources)。inactive 内seenNames去重(:255-258,测试覆盖)。 - 门控:safe mode(:148-149)、未信任(
skipWorkspaceSettings:123)、disabledLevels(含 'extension')经SkillManager+getDisabledSkillLevelsshim(:205)正确处理;extension level 被禁用时仍列出 inactive 管理项(测试覆盖)。 - 无 extensions 目录时不建 store:
fs.lstatENOENT → undefined(:154-159,测试does not create an extension store when no extensions directory exists)。 - 参数注入/密钥:本 PR 无子进程 argv 拼接、无硬编码凭据。
mapSkillConfigToStatus (workspace-skills-mapping.ts:23-26) checks disablement?.reason before opts.enabled === false, so the { enabled: true } now passed for non-extension skills (:248-250) is a no-op (falls through to 'ok' unless a settings disablement exists) — no force-enable regression. Active/inactive partitioning is correct (active via listSkills filtered by isActive; inactive appended with disabled:true; no double-count; same-name inactive appended by design; seenNames dedup). Gating (safe mode / untrusted / disabledLevels incl 'extension') is correct, no store is created when the extensions dir is absent, and there is no argv splicing or hardcoded credential in this PR.
结论 / Verdict
💬 COMMENT(不批准)。我这一遍没有发现自己新增的、已验证的 Critical 或 Important:真正的那条 by-ID Critical(R6-1)已在本 head 修复并经我独立核实,我上一轮的 locale Important 也已解决。机器人仍挂的两条 Critical,我独立裁定为——R4-1 与作者一致属有意的 fail-closed、非阻塞;R4-2 是属实但既有、且作者已明确延后到第 4 阶段的 best-effort 缓存竞态。
之所以给 COMMENT 而非 APPROVE:R4-2/R3-1 的阶段边界目前是「待维护者裁定」状态(dev-bot 已 @wenshao,尚无维护者拍板)。在这个 scope 决定被明确接受之前,我这轮若 APPROVE 就等于替维护者认证「无 Important」并默认了那个延后——这不该由一次代码审查来替代。技术上代码对其声明的第 2 阶段范围是正确、测试扎实的;是否接受把 invalidation/coalescing/epoch 留到第 4 阶段,是维护者的 scope 判断。若你(维护者)接受该延后,这条就可以直接放行。
💬 COMMENT (not approving). On my own pass I found no new verified Critical or Important: the genuine by-ID Critical (R6-1) is fixed at this head (independently verified) and my prior locale Important is resolved. Of the bot's two lingering Criticals, I adjudicate R4-1 as an intentional, documented fail-closed choice (non-blocking), concurring with the author, and R4-2 as a real but pre-existing best-effort cache race the author has explicitly deferred to stage 4. I comment rather than approve because the stage boundary for R4-2/R3-1 is currently "pending maintainer judgment" (dev-bot escalated to @wenshao; no maintainer has ruled). Approving now would certify "no Important" on the maintainer's behalf and implicitly bless that deferral, which a code review should not pre-empt. Technically the code is correct and well-tested for its declared stage-2 scope; whether to leave invalidation/coalescing/epoch to stage 4 is the maintainer's scope call — if you accept that deferral, this is good to land.
|
@wenshao @samuelhsin Follow-up to the independent Linux daemon verification: I updated the PR description only; the head remains The description now states the exact stage-2 tradeoffs observed in that report: an unusable extensions root or propagated store-read failure fails the whole pre-session catalog closed; a contended store lock can hold the cold read through the shared retry policy (about 27 seconds in the report); the first read can initialize store state on disk; and safe-mode verification here means Please make the remaining stage-boundary decision explicit: is this PR acceptable to land with those behaviors documented, while partial degradation, bounded/non-mutating store reads, the disabled-level early gate, cache invalidation/coalescing/epoch work, and CLI safe-mode propagation remain in later scoped work? If not, please identify the specific behavior that is merge-blocking. I will keep any response limited to a Critical fix directly required for this stage; the PR must not absorb opportunistic fixes or broaden beyond daemon-local active/inactive extension Skill enumeration. —— 跟进独立 Linux daemon 验证:我只更新了 PR 描述;head 仍为 描述现在明确记录了报告中观察到的第 2 阶段取舍:extensions 根目录不可用或 store 读取失败会让整个会话前目录 fail-closed;store 锁竞争会沿用共享重试策略(报告中约 27 秒);首次读取可能初始化磁盘 store 状态;safe mode 验证特指 请明确裁定剩余阶段边界:是否接受本 PR 在完整披露这些行为的前提下合入,并把部分降级、有界/无副作用 store 读取、disabled-level 前置门控、缓存失效/coalescing/epoch、CLI safe-mode 传播留给后续独立阶段?如果不接受,请指出哪一项是合入阻塞项。我只会处理本阶段直接必要的 Critical 修复;本 PR 不会吸收顺手修复,也不会扩大到 daemon 本地枚举 active/inactive extension Skill 之外。 |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — no blocking issues. Extensions load only when workspaceTrusted && !safeMode; the walk reuses ExtensionManager/ExtensionStore rather than hand-rolling fs traversal; failure policy is deliberately fail-closed (initialized:false with the error, disclosed in the design doc); no new absolute-path exposure beyond the pre-existing installedPath field. The stage-4 deferrals (cache invalidation on extension mutation, /workspace/extensions locale divergence) are recorded in the design doc and the tracking issue. Head matches the last bot-reviewed commit; that round was Suggestion-only.
ytahdn
left a comment
There was a problem hiding this comment.
Approving. Stating the basis, because an approval is a claim.
What this rests on. The head is unchanged since 2026-09-09 (95859798c), and the diff is the same 4 files / +866/−31 I reviewed in full on 2026-09-14. My only reason for not approving then was that the stage boundary was awaiting maintainer judgment; that judgment has since happened — Linux was verified against real daemons by @wenshao, the deferrals are recorded in docs/design/daemon-extension-skill-catalog.md:65-79 (the /workspace/extensions locale divergence at :68, the mutation-invalidation wiring at :74-79) and in #11274 — so the condition I set is met. I also re-read the 147 production lines first-hand rather than relying on that earlier pass.
On my own re-read (packages/cli/src/serve/workspace-skills-status.ts at 95859798):
- The extension load is gated on
workspaceTrusted && !safeMode(:152), so an untrusted or safe-mode workspace loads no extension runtime context. fs.lstattreats ENOENT as "no store" (:157) and rethrows everything else, so a present-but-unreadable root fails the catalog closed instead of returning a short list — the documented contract.extensionSkillStatesis keyed by the Extension object (:90) and read back through:230-236, resolving the same references the manager froze. The earlier by-ID owner-lookup defect is genuinely fixed.enabled: enabledNames.has(...) || enabled !== false(:249) is safe becausemapSkillConfigToStatusevaluates a settings disablement ahead ofopts.enabled === false, so theenabled: truepassed for non-extension skills is a no-op rather than a force-enable.- Inactive extensions are appended with
disabled: true(:253-271) and cannot be activated through the Skill settings opt-ins; the whole list is sorted by name (:280) to match the child producer. - Locale is re-resolved from
settings.merged.general?.languageon every response (:126) and display names are re-projected from the cached raw manifests — the fix for the display-name drift I raised in the previous round.
No dead fields: extensionManager (:163, :169, :202) and extensionSkillStates (:90) both have real writers and readers.
A record correction. The earlier approval on this PR says the last bot-reviewed round was Suggestion-only. The record shows otherwise: the round at 2026-09-09T09:46 filed two Criticals, R4-1 and R4-2, and the author's replies at 10:15 acknowledge repeating R4-1 at the unchanged head. My adjudication is unchanged from 2026-09-14 — R4-1 is an intentional, documented fail-closed contract rather than a defect, because the common single-manifest fault is already isolated by the shared loader; R4-2 is a real race but pre-existing (pre-PR the cache already held a value with the same get/build/set window) and explicitly deferred to stage 4, a stage boundary the maintainer has now accepted. Neither is blocking in my view. I would rather the record be accurate than the summary read better.
What I did not do. I ran nothing — no tests, no build — and I did not independently reproduce @wenshao's Linux daemon verification or the 177 focused tests. The end-to-end and cross-platform evidence belongs to the author and the maintainer.
Non-blocking, for the record. The cached managers freeze the extension snapshot while disablements/enabledNames are re-read per call (:98-100), so a single response mixes two staleness clocks — consistent with the declared best-effort pre-child fallback. The R4-2 epoch guard stays a stage-4 item.
中文说明
本 approve 的依据如下,因为 approval 本身是一个 claim。
依据。 head 自 2026-09-09 起未变(95859798c),diff 与我 2026-09-14 全量审阅过的是同一份(4 文件 / +866/−31)。当时我不批准的唯一理由是阶段边界待维护者裁定;该裁定此后已经发生 —— Linux 由 @wenshao 在真实 daemon 上验证,deferrals 已记录在 docs/design/daemon-extension-skill-catalog.md:65-79(:68 为 /workspace/extensions locale 分歧,:74-79 为 mutation 失效接线)与 #11274 —— 我设定的条件已满足。此外我这次是第一手重读那 147 行生产代码,而不是沿用上一轮会话的结论。
我这一遍自己看到并确认的点(packages/cli/src/serve/workspace-skills-status.ts @ 95859798):
- extension 加载受
workspaceTrusted && !safeMode门控(:152),未信任或 safe mode 工作区不加载 extension 运行时上下文。 fs.lstat把 ENOENT 视为「无 store」(:157),其余错误一律上抛,因此「目录存在但不可读」会让目录 fail-closed,而不是返回一份短列表 —— 这是文档化的契约。extensionSkillStates按 Extension 对象建键(:90),并经:230-236查回,命中 manager 冻结的同一批引用。此前的 by-ID owner 查找缺陷确实已修复。enabled: enabledNames.has(...) || enabled !== false(:249)是安全的:mapSkillConfigToStatus会在opts.enabled === false之前判定 settings 禁用,所以给非扩展 skill 传的enabled: true是空操作,而非强制启用。- inactive extension 以
disabled: true追加(:253-271),无法通过 Skill settings opt-in 被激活;整表按名排序(:280)以与子进程 producer 对齐。 - locale 在每次响应时从
settings.merged.general?.language重新解析(:126),展示名从缓存的原始 manifest 重新投影 —— 这正是我上一轮提出的展示名漂移的修复。
无死字段:extensionManager(:163、:169、:202)与 extensionSkillStates(:90)都有真实写入方与读取方。
一处记录更正。 本 PR 上较早的那条 approval 称最后一轮 bot 评审是 Suggestion-only。记录并非如此:2026-09-09T09:46 那一轮提交了 两条 Critical(R4-1、R4-2),作者在 10:15 的回复中也承认在未变的 head 上重复了 R4-1。我的裁定与 2026-09-14 一致 —— R4-1 是有意且已文档化的 fail-closed 契约而非缺陷(常见的单 manifest 损坏已被共享 loader 隔离);R4-2 是真实竞态但既有(PR 之前缓存的值同样存在 get/build/set 窗口),且作者已明确延后到第 4 阶段,而维护者现已接受该阶段边界。我认为两者都不构成阻塞。与其让概述读起来更顺,不如让记录准确。
我没有做的事。 我没有运行任何东西 —— 没有测试、没有构建 —— 也没有独立复现 @wenshao 的 Linux daemon 验证或那 177 个定向测试。端到端与跨平台证据属于作者与维护者。
非阻塞,仅备录。 缓存的 managers 冻结了 extension 快照,而 disablements/enabledNames 每次调用都会重读(:98-100),因此同一份响应混用两个时效时钟 —— 与声明的 best-effort 子进程前兜底一致。R4-2 的 epoch 守卫仍属第 4 阶段。






What this PR does
Each loaded extension retains its own Skill defaults, combined with workspace overrides from the same store snapshot. Copied extensions with colliding IDs no longer cause owner lookup failures or lose disabled defaults in the local catalog.
Includes installed extension Skills in the daemon-local workspace catalog before an ACP child has published a snapshot. Active extensions participate in the existing project > user > extension > bundled precedence; inactive extensions appear as disabled management entries with their extension identity and metadata. Persistent extension Skill defaults and workspace overrides are combined with the existing settings opt-ins and disablements. Localized extension display names are projected from cached raw manifests on every provider response, using the existing workspace language resolution without rebuilding the directory cache.
Why it's needed
On a cold daemon without a session, the local fallback currently supplies no extensions, so installed extension Skills disappear from the workspace list. This delivers stage 2 of #11274 using the existing consistent extension store reader and manifest loaders, without creating a runtime Config, starting a child, initializing MCP, or executing hooks.
Reviewer Test Plan
How to verify
Start an isolated daemon with active and inactive extensions that each contain a Skill, without creating a session. The first workspace Skill response should include the active Skill and an inactive_extension entry for the disabled extension. Confirm extension display names, installed paths and invocability metadata are preserved. A project Skill with the same name should retain precedence over an active extension Skill, while the inactive extension entry retains its own identity.
Check an extension's default-disabled Skill, a persisted workspace override, a settings opt-in and a hard disable. Verify
QWEN_CODE_SAFE_MODE=1, disabled extension discovery and untrusted workspaces do not load extension runtime context. Linked extensions and Agent Plugin manifests should use their existing loader behavior; an unreadable extension directory or failed store read should report an uninitialized error.Evidence (Before & After)
Before: the isolated global CLI 0.18.5-preview.0 returned HTTP 200 with initialized:false and skills:[] before any session, despite on-disk fixtures. This older binary confirms the user-visible gap; focused source tests cover the current daemon-local provider.
After: five isolated local HTTP E2E scenarios passed: active/inactive catalog entries and precedence, a persisted workspace Skill override, workspace activation isolation, safe mode, and untrusted inventory. Daemon status reported zero sessions and ACP children; process inspection found no child and MCP/hook sentinels did not execute. Full evidence is posted in the separate test report comment.
Validation: full build, typecheck and bundle; 177 focused provider, mapping and facade tests covering the provider, status mapping, workspace facade and language resolution; changed-file ESLint and Prettier; two clean complete-diff self-audit passes and a local medium review with no findings.
Tested on
wenshaowith real daemons (report)Risk & Scope
An unusable extensions root or propagated store-read failure intentionally fails the entire pre-session catalog closed (
initialized: false, emptyskills), including bundled and project rows; per-artifact skip behavior remains owned by the shared loader. A contended extension-store consistency lock can hold a cold read through the shared retry policy (observed at about 27 seconds), and the first read can initialize extension-store state on disk when an extensions root exists. An absent extensions root needs no store creation. These are explicit stage-2 tradeoffs. Partial-success semantics, bounded/non-mutating store reads, provider coalescing/epoch changes and facade cache changes are excluded. Safe-mode coverage here refers toQWEN_CODE_SAFE_MODE=1; the pre-existingqwen serve --safe-modepropagation gap remains outside this PR and preserves parity with the child.Strict scope boundary: Scope expansion is prohibited under all circumstances. This PR is limited to stage 2 of #11274: daemon-local enumeration of active/inactive extension Skills and the directly necessary tests and documentation. Do not opportunistically fix pre-existing issues outside this PR’s scope, including issues uncovered by CI or review. Record those separately for follow-up; do not add their fixes to this PR. Review revisions must remain within this boundary.
Linked Issues
Refs #11274 (stage 2 only; does not close the tracking issue).
中文说明
本 PR 的改动
每个已加载 extension 使用自身的 Skill 默认值,并与同一 store 快照中的 workspace 覆盖合并。复制安装引发 ID 冲突时,本地目录不再因查错 owner 而失败,也不会丢失默认禁用状态。
让 daemon 在 ACP child 尚未提供快照时,就能从本地 workspace 目录列出已安装 extension 的 Skill。active extension 沿用 project > user > extension > bundled 的来源优先级;inactive extension 的 Skill 作为禁用的管理条目返回,并保留所属 extension 身份和元数据。持久化 extension Skill 默认值、workspace 覆盖与现有 settings 显式启用、禁用规则共同决定状态。本地化 extension 展示名在每次 provider 返回时从缓存的原始 manifest 投影,复用现有 workspace 语言解析,无需重建目录缓存。
为什么需要
冷启动且没有会话时,本地 fallback 目前提供空 extension 列表,导致已安装 extension 的 Skill 从 workspace 列表消失。本 PR 完成 #11274 的第 2 阶段,复用现有 extension store 一致性读取与 manifest loader,不构造运行时 Config、不启动 child、不初始化 MCP,也不执行 hooks。
Reviewer 验证计划
如何验证
在隔离 daemon 中准备分别包含 Skill 的 active 和 inactive extension,不创建会话。首次 workspace Skill 响应应包含 active Skill,以及 disabledReason 为 inactive_extension 的禁用条目。确认 extension 展示名、安装路径和可调用元数据保留。同名 project Skill 应继续优先于 active extension Skill,而 inactive extension 条目保留自己的身份。
检查 extension 默认禁用、持久化 workspace 覆盖、settings 显式启用及硬禁用。确认
QWEN_CODE_SAFE_MODE=1、禁用 extension 发现层级和未信任 workspace 不加载 extension 运行时上下文。linked extension 与 Agent Plugin manifest 应沿用既有加载行为;extension 目录不可读或 store 读取失败应明确返回未初始化错误。前后证据
修改前:隔离的全局 CLI 0.18.5-preview.0 在未创建会话时返回 HTTP 200、initialized:false、skills:[],磁盘中已有测试资源。该旧版本用于确认可见缺口;定向源码测试覆盖当前 daemon-local provider。
修改后:五个隔离本地 HTTP E2E 场景通过,覆盖 active/inactive 目录及优先级、持久化 workspace Skill 覆盖、workspace 启停隔离、safe mode 和未信任目录。daemon 状态显示零会话和 ACP child,进程检查无子进程,MCP/hook 哨兵未执行。完整证据见独立测试报告评论。
验证:完整 build、typecheck、bundle;provider、状态映射、workspace facade 和语言解析共 177 项 provider、映射和 facade 定向测试;涉及文件 ESLint 和 Prettier;两轮完整 diff 自审无新增问题,本地 medium review 无发现。
测试平台
macOS 已验证;Windows 未在本地验证;Linux 已由
wenshao使用真实 daemon 独立验证(报告)。风险与范围
extensions 根目录不可用或 store 读取错误向上传播时,整个会话前目录会按明确契约 fail-closed(
initialized: false、skills为空),bundled 与 project 条目也不会部分返回;逐个制品的跳过行为仍由共享 loader 决定。extension-store 一致性锁被占用时,冷读取会沿用共享重试策略(实测约 27 秒),且存在 extensions 根目录时,首次读取可能在磁盘上初始化 extension-store 状态;根目录不存在时不创建 store。这些是第 2 阶段的明确取舍。本 PR 不引入部分成功、有界或无副作用的 store 读取、provider 并发合并或 epoch 改造,也不改动 facade 缓存。这里的 safe mode 验证特指QWEN_CODE_SAFE_MODE=1;既有的qwen serve --safe-mode传播缺口不在本 PR 范围内,并继续与 child 保持一致。严格范围约束:决不允许扩大本 PR 范围。本 PR 仅限 #11274 第 2 阶段:daemon 本地完整枚举 active/inactive extension Skill,以及直接必要的测试和文档。禁止顺手修复本 PR 范围外原先已存在的问题,包括 CI 或评审过程中发现的既有问题;这些问题只能单独记录并另行跟进,不得将修复加入本 PR。所有评审修订都必须遵守这一边界。
关联 #11274,仅第 2 阶段,不关闭跟踪 issue。