fix(serve): keep skill slash commands available when the ACP child is unavailable - #6169
Conversation
…s reaped `GET /workspace/skills` is answered exclusively by the ACP child — the daemon has no local SkillManager. `requestWorkspaceStatus` only checks for an already-live channel (`liveChannelInfo()`, never `ensureChannel()`), so when no child is running it returns the idle placeholder (`initialized: false`, empty `skills`). That is the norm before the first session, and — crucially — again after the child is reaped on session close, which happens immediately by default (`--channel-idle-timeout-ms` defaults to 0 = immediate kill). Unlike `/workspace/providers`, skills have no daemon-local status provider to fall back on. So once a user has created and closed a session, every subsequent pre-first-prompt `/workspace/skills` query returns empty, the Web Shell's slash-command list falls back to the hardcoded built-ins (which omit skills), and `/rev` stops autocompleting `/review`. Retain the last skills status a live child produced and replay it while no channel is live, so skill-backed slash commands keep autocompleting; the next live query refreshes the cache. `initialized` cleanly separates a real child answer (always `true`) from the idle placeholder (always `false`). Completes QwenLM#6153, which wired the Web Shell to fetch `/workspace/skills` in the deferred-connect path but could not surface skills the daemon was unable to answer without a live child.
|
Thanks for the PR! Template looks good ✓ — all required sections present, bilingual body, clear before/after evidence table. On direction: this fixes a real and well-scoped UX bug — skill-backed slash commands ( On approach: the scope is tight — 94 additions, 1 deletion, 2 files. The caching strategy mirrors the existing Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必需章节齐全,双语正文,清晰的 Before/After 证据表。 方向:这修的是一个真实且范围明确的 UX bug —— 用户只要开关过一个 session,ACP 子进程就被立刻回收( 方案:范围很紧 —— 94 行新增、1 行删除、2 个文件。缓存策略复用了代码库中已有的 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): The bug is that Comparison with the diff: the PR's approach matches this exactly. One closure variable ( Correctness: the Reuse check: the pattern mirrors the existing Tests: two new test cases cover the key scenarios — cache replay when channel goes idle, and cache refresh when a newer live answer arrives. The existing idle-fallback test still passes, confirming no regression. All 58 tests in Comments: the inline comments in No blockers. No AGENTS.md violations. E2E TestRan a real Before (main — not reproducible in this environment without a separate build, using PR author's evidence)Per PR evidence table: after After (this PR)Step 1 — Initial skills probe (cache warm via preheat child): Step 2 — Create a session: Step 3 — Delete the session (triggers channel exit): Step 4 — Post-delete skills probe (channel reaped, no live child): ✅ Cache replay works — skills remain available after the ACP child is reaped. The 中文说明代码审查独立方案(读 diff 之前): bug 在于无存活 ACP 子进程时 与 diff 对比: PR 方案与此完全一致。一个闭包变量( 正确性: 复用检查: 模式复用了已有的 测试: 两个新用例覆盖关键场景——通道空闲时回放缓存、更新的 live 答案刷新缓存。既有的 idle 兜底测试仍通过,确认无回归。 注释: 无阻塞问题。无 AGENTS.md 违规。 端到端测试用 PR diff 构建的真实 关键结果:子进程被回收后( — Qwen Code · qwen3.7-max |
|
This is a textbook bug fix — well-scoped, minimal, and the implementation matches exactly what I would have written independently. Going back to my Stage 2a proposal: the PR's approach is identical. A single closure-scoped cache variable, updated on live answers, replayed on idle. No simpler path was missed. The E2E result is the proof: What I like about this PR:
If I had to maintain this in six months, I'd thank the author. The inline comments explain the why (daemon can't answer skills without a child), the condition is obvious ( LGTM — approving. ✅ 中文说明这是一个教科书级的 bug 修复——范围清晰、改动最小,实现与我独立提出的方案完全一致。 回到 Stage 2a 的独立方案:PR 的方案与之完全相同。一个闭包作用域的缓存变量,live 答案时更新,idle 时回放。没有更简的路径被遗漏。 E2E 结果就是证据: 喜欢这个 PR 的地方:
如果六个月后我要维护这段代码,我会感谢作者。行内注释解释了为什么(daemon 没有子进程就答不了技能),条件显而易见( LGTM — 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
…unavailable The cache from the previous commit keeps the last child answer alive across a reap, but it never warms when the child never answers at all — most visibly under `npm run dev`, where the on-demand-transpiled child's `initialize` handshake routinely exceeds the 10s preheat budget, so preheat times out and no channel ever comes up. `/workspace/skills` then stays empty until the first prompt, dropping `/review` and every other skill from the Web Shell's pre-first-prompt autocomplete even though the skills exist on disk (typing `/review` in full still runs it, since submitting spawns a session — hence "not in the list, but usable"). Add a daemon-local skills provider that enumerates skills straight from the filesystem via SkillManager (a lightweight Config shim — no child, no MCP init), mirroring the existing daemon-local providers-status provider. The facade falls back to it only after both a live child answer and the cached last answer are unavailable, so the live child stays authoritative (and keeps extension-provided skills) while a never-preheated child still yields the on-disk skills — `/review` included.
…ows mid-flight Addresses review feedback on QwenLM#6169: the channel can die after `liveChannelInfo()` returns a valid channel but before the RPC completes, so `queryWorkspaceStatus` rejects. Previously that exception propagated even though the cache or the daemon-local provider could still answer. Wrap the query in try/catch (logging via writeStderrLine, matching getWorkspaceEnvStatus / getWorkspacePreflightStatus) and treat a mid-flight failure as "no live child", so the request degrades to the cached last answer or daemon-local enumeration instead of failing.
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
DragonnZhang
left a comment
There was a problem hiding this comment.
This PR adds a daemon-local skill enumeration fallback so skill-backed slash commands (e.g. /review) remain available in the Web Shell when the ACP child is unavailable (cold start, channel reaped, preheat timeout). The fallback logic is well-structured: cache the last live child answer, fall back to daemon-local SkillManager enumeration only when needed, and handle mid-flight channel deaths gracefully. Comprehensive test coverage across all fallback paths. Looks correct.
— qwen3-coder via Qwen Code /review
ytahdn
left a comment
There was a problem hiding this comment.
发现一个需要修正的问题:daemon-local skills fallback 现在写死了 isSafeMode: () => false 和 getBareMode: () => false。这会导致 ACP child 不可用时,/workspace/skills 通过本地枚举把 project/user skills 暴露到 Web Shell 补全里,即使 daemon 是用 --safe-mode 或 --bare 启动的。
这和真实 ACP child 的行为不一致:safe mode 下 SkillManager.refreshCache() 只加载 bundled skills;bare mode 下 listSkillsAtLevel() 会返回空。因此在这个 PR 要修复的“child 不可用”窗口里,补全列表可能展示真实 session 不会展示/不应该展示的 skill。尤其 safe mode 的语义是禁用自定义能力,包括 skills,这里会绕开该限制。
建议把当前有效的 safe/bare 状态传给 createWorkspaceSkillsStatusProvider(),或者用其他方式确保 daemon-local fallback 与 child 侧 SkillManager 行为一致。
其余主逻辑我认为方向是合理的:live child 优先,其次缓存,最后 daemon-local fallback;child 查询中途失败时回退到缓存/本地枚举也合理。
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
- Extract the SkillConfig → ServeWorkspaceSkillStatus mapping into a shared workspace-skills-mapping module used by both the ACP child's buildWorkspaceSkillsStatus and the daemon-local provider, so the two skill listings can't drift; cover it (including the disable-model-invocation branch) with a unit test. - Memoize the SkillManager per workspace so repeat queries reuse its in-memory cache instead of re-scanning every skill level on each call. - Honor the safe-mode env (isSafeModeEnv, as Config does) instead of hardcoding isSafeMode to false; keep bareMode off (the daemon never runs `--bare`). - Log daemon-local enumeration failures via writeStderrLine, matching the rest of the workspace-service error handling.
|
@ytahdn thanks — addressed in 8eb98ab. |
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ — all required sections present, bilingual body, clear before/after evidence table. On direction: this fixes a real and well-scoped bug — skill-backed slash commands (e.g. On approach: the two-layer fallback (child → cache → daemon-local → empty) is the minimal correct fix. The shared 中文说明感谢贡献! 模板完整 ✓ —— 所有必填部分齐全,中英双语正文,清晰的修复前后对比表。 方向:修复了一个真实且范围明确的 bug —— 技能类斜杠命令(如 方案:两层兜底(子进程 → 缓存 → daemon 本地 → 空)是最小且正确的修复。共享的 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): The bug is that Comparison with the diff: The PR's implementation matches this proposal exactly, and does it well:
No correctness bugs, security holes, or regressions found. No AGENTS.md violations. Unit TestsAll pass (this rerun):
Total: 84 + 574 = 658 tests, all green. Real-Scenario TestingStarted the daemon with Before (main, 6509e8d)On this Linux CI box, the ACP child initializes within the preheat budget, so the cold-start race doesn't reproduce here (it's a macOS After (this PR)No regression when the child is live — the child path remains authoritative. The daemon-local fallback is exercised only in the cold-start window that doesn't reproduce on this CI hardware, but the unit tests comprehensively cover that code path (8 dedicated fallback-ordering tests). 中文说明代码审查独立方案(读 diff 前): bug 在于无活 ACP 子进程时 与 diff 对比: PR 实现完全匹配这个方案,且做得很好:兜底链正确、含中途通道断开错误处理; 单测全部通过:84 + 574 = 658 个测试全绿。 真实场景测试在 Linux CI 上子进程在 preheat 预算内完成初始化,冷启动竞态无法复现(这是 macOS — Qwen Code · qwen3.7-max |
|
This is a textbook bug fix — well-scoped, minimal, and the implementation matches exactly what I would have written independently. Going back to my Stage 2a proposal: the PR's fallback chain (child → cache → daemon-local → empty) is the same three-layer approach I would have taken, and it's implemented cleanly. The shared Since the last triage run, two commits landed:
658 tests pass (84 skills-specific + 574 server), no regressions. The real-scenario test confirms no regression when the child is live — the cold-start race is a macOS dev-mode timing issue that doesn't reproduce on this Linux CI box, but 8 dedicated unit tests comprehensively cover the fallback chain. Every change in the diff serves the stated goal. No drive-by refactors, no scope creep. If I had to maintain this in six months, I'd thank the author. LGTM, approving. ✅ 中文说明这是一个教科书级的 bug 修复 —— 范围精准、实现最小化,且与我独立构思的方案完全一致。 自上次 triage 以来新增了两个提交: — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
ytahdn
left a comment
There was a problem hiding this comment.
Incremental commit addresses all prior review feedback: shared mapSkillConfigToStatus with tests, isSafeMode reads actual env, SkillManager memoization, writeStderrLine in catch. LGTM ✅
— qwen3.7-max via Qwen Code /review
…rovider call
- Test the previously-uncovered `buildWorkspaceSkillsStatus` catch branch:
when enumeration fails it returns `{ initialized: false, skills: [],
errors: [{ kind: 'skills', status: 'error', error }] }` and logs to stderr.
Also cover the per-workspace SkillManager memoization.
- Wrap the facade's `workspaceSkillsStatusProvider` call in try/catch so a
throwing injected provider degrades to the idle placeholder instead of
failing the request (matching getWorkspaceEnvStatus / getWorkspacePreflightStatus),
with a facade test for the throw path.
ytahdn
left a comment
There was a problem hiding this comment.
Incremental commit (+82/-3) addresses all remaining review feedback: try/catch on daemon-local provider call in facade, error-path test for buildWorkspaceSkillsStatus, memoization verification test, and missing writeStderrLine assertion in throw-path tests. Clean, well-scoped additions. LGTM ✅
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — cache → daemon-local fallback 的分层策略清晰,safe/bare mode 透传和 SkillManager memoization 的增量修复也很到位。
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
| // Live child unavailable. Prefer the last answer it produced (keeps the | ||
| // full, extension-aware list available across a reap)... | ||
| if (lastWorkspaceSkillsStatus) { | ||
| return lastWorkspaceSkillsStatus; |
There was a problem hiding this comment.
[Suggestion] The cache replay path (return lastWorkspaceSkillsStatus) has no log output. The response is byte-identical to a live child answer (initialized: true, full skills array), so an operator debugging why skills look stale after a child reap has no stderr signal to distinguish "child is alive and skills are current" from "child died and we are replaying the last answer."
Neither getWorkspaceEnvStatus nor getWorkspacePreflightStatus have a comparable cache mechanism, so this is a new observability gap introduced by this PR. A single writeStderrLine before the cache return would close it:
| return lastWorkspaceSkillsStatus; | |
| if (lastWorkspaceSkillsStatus) { | |
| writeStderrLine( | |
| 'qwen serve: getWorkspaceSkillsStatus replaying cached skills status (child unavailable)', | |
| ); | |
| return lastWorkspaceSkillsStatus; | |
| } |
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Well-structured 3-tier fallback (child → cache → daemon-local → idle) with comprehensive error handling and thorough test coverage (84 tests). Build passes, CI 30/30 green. No blocking issues found. LGTM ✅
— qwen3.7-max via Qwen Code /review
What this PR does
Makes the Web Shell's pre-first-prompt slash-command list keep skill-backed commands (e.g.
/review) whenever the ACP child can't answer/workspace/skills, via two complementary daemon-side layers:SkillManager, no child, no MCP init) as a fallback for when the child has never answered — so even a child that never comes up still yields the on-disk skills.Why it's needed
/workspace/skillsis answered only by the ACP child.requestWorkspaceStatuschecks for an already-live channel (liveChannelInfo(), neverensureChannel()) and returns the idle placeholder (initialized: false, emptyskills) when there is none. There is no daemon-local fallback like/workspace/providershas — so the pre-first-prompt list drops every skill and/revstops autocompleting/review. The child is absent in three windows:--channel-idle-timeout-msdefaults to0).npm run dev: the on-demand-transpiled child'sinitializehandshake routinely exceeds the 10s preheat budget (measured ~19s cold, vs a 10s cap), so preheat fails ("ACP preheat failed, will retry on first session: AcpSessionBridge initialize timed out after 10000ms") and no channel ever comes up.The third window is why the reported symptom is "
/reviewis not in the list, but it actually works": the skill exists on disk and runs when you submit/reviewin full (which spawns a session), but it never appears in autocomplete because the child hasn't answered/workspace/skills. The cache alone can't help there — it never warms without a live child. The daemon-local provider closes it: it reads skills from disk instantly, so/reviewis in the list from the first page load. The live child stays authoritative when present (and keeps extension-provided skills, which the daemon-local view omits).This completes #6153, which wired the Web Shell to fetch
/workspace/skillsbut could not surface skills the daemon was unable to answer without a live child.Reviewer Test Plan
How to verify
Unit:
npx vitest run packages/cli/src/serve/workspace-service/ packages/cli/src/serve/workspace-skills-status.test.ts— covers cache replay/refresh, the daemon-local fallback ordering (child → cache → local → empty), and that the local provider enumerates bundled/reviewwith no child (76 tests).npx vitest run packages/cli/src/serve/server.test.tsstays green (574).End-to-end (
npm run dev:daemon, where preheat times out so the child is never live pre-prompt):/rev.Expected (this PR):
/reviewis in the slash menu and Tab completes/rev→/review [pr-number|file-path] [--comment]. Before:/reviewwas absent and Tab did nothing.Evidence (Before & After)
Real daemon,
GET /workspace/skillsbefore any session:hasReview=true(replayed)hasReview=trueinitialized=false skillsCount=0❌initialized=true skillsCount=25 hasReview=true✅Real
npm run dev:daemon+ Playwright (child's preheat still timing out): the browser's/workspace/skillsreturnsinitialized=true count=25 hasReview=trueimmediately, the slash menu shows/review, and/rev+Tab →/review [pr-number|file-path] [--comment].Tested on
Environment (optional)
Local:
node scripts/dev.js serve/npm run dev:daemon(tsx source, loopback), driven withcurland Playwright; plusvitestunit tests.Risk & Scope
Linked Issues
Completes the fix started in #6153.
中文说明
这个 PR 做了什么
让 Web Shell 首个 prompt 之前的斜杠命令列表在 ACP 子进程无法回答
/workspace/skills时,依然保留技能类命令(如/review),通过两层互补的 daemon 侧机制:SkillManager读文件系统,不启子进程、不做 MCP 初始化)作为"子进程从未答复"时的兜底——即便子进程一直起不来,也能给出磁盘上的技能。为什么需要
/workspace/skills只由 ACP 子进程回答。requestWorkspaceStatus只被动查已存活通道(liveChannelInfo(),从不ensureChannel()),无通道就返回空 idle 占位。它没有/workspace/providers那样的 daemon 本地兜底——于是首个 prompt 前列表丢光技能、/rev补不出/review。子进程在三个窗口内缺席:--channel-idle-timeout-ms默认 0)。npm run dev下最明显:按需 tsx 转译的子进程initialize握手经常超过 10s 的 preheat 预算(实测冷启动 ~19s,超时上限 10s),preheat 失败(日志ACP preheat failed ... initialize timed out after 10000ms),通道永不就绪。第三个窗口正是你报的现象——"
/review不在列表里,但真实可用":技能在磁盘上、你完整输入/review提交(会建 session)就能跑,但它从不出现在补全里,因为子进程没答/workspace/skills。仅靠缓存救不了——没有活子进程它永远不被填充。daemon 本地 provider 堵上了这个窗口:直接从磁盘即时读技能,/review从首次打开就在列表里。子进程在线时仍为权威(并保留本地视图省略的扩展技能)。这补全了 #6153:#6153 让 Web Shell 去拉
/workspace/skills,但当 daemon 无活子进程答不出时它无能为力。Reviewer Test Plan(评审验证)
如何验证
单测:
npx vitest run packages/cli/src/serve/workspace-service/ packages/cli/src/serve/workspace-skills-status.test.ts—— 覆盖缓存回放/刷新、兜底顺序(子进程→缓存→本地→空)、以及本地 provider 无子进程枚举出 bundled/review(76 个测试)。server.test.ts保持全绿(574)。端到端(
npm run dev:daemon,其 preheat 超时、首个 prompt 前子进程从不在线):/rev。预期(本 PR):
/review在斜杠菜单里,Tab 把/rev补成/review [pr-number|file-path] [--comment]。修复前:/review缺失、Tab 无反应。证据(Before & After)
真实 daemon,建 session 之前
GET /workspace/skills:hasReview=true(回放)hasReview=trueinitialized=false skillsCount=0❌initialized=true skillsCount=25 hasReview=true✅真实
npm run dev:daemon+ Playwright(子进程 preheat 仍在超时):浏览器/workspace/skills立即返回initialized=true count=25 hasReview=true,斜杠菜单显示/review,/rev+Tab →/review [pr-number|file-path] [--comment]。测试平台
仅本地 macOS 验证(单测 + curl + Playwright 真实 UI);Windows/Linux 未测,交给 CI。
运行环境(可选)
本地:
node scripts/dev.js serve/npm run dev:daemon(tsx 源码、loopback),用 curl 和 Playwright 驱动;外加 vitest 单测。风险与范围
关联 Issue
补全 #6153 开始的修复。