Skip to content

feat(serve): Add workspace-qualified Voice - #6839

Merged
wenshao merged 9 commits into
QwenLM:mainfrom
doudouOUC:agent/feat-workspace-qualified-voice
Jul 14, 2026
Merged

feat(serve): Add workspace-qualified Voice#6839
wenshao merged 9 commits into
QwenLM:mainfrom
doudouOUC:agent/feat-workspace-qualified-voice

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR completes Phase 4b workspace-qualified Voice for multi-workspace qwen serve daemons. Trusted registered runtimes can read and update Voice settings, submit batch transcription, and open streaming transcription through workspace-qualified REST and WebSocket routes selected by workspace id or encoded absolute cwd. Each request uses the selected runtime's cwd, effective environment, bridge, and workspace-scoped settings persistence; legacy Voice routes remain primary-only.

The daemon now shares one process-level admission coordinator across legacy and workspace-qualified REST and WebSocket Voice operations, preserving the existing total limit of eight. Workspace drain rejects new Voice work, non-force removal reports active Voice sessions as busy, and force removal or daemon shutdown aborts only the selected runtime's Voice leases while waiting up to five seconds for real operations to settle. The TypeScript workspace client exposes the qualified settings and batch transcription methods, and the protocol advertises the additive workspace_qualified_voice capability.

Why it's needed

Multi-workspace daemons already isolate sessions, core REST, ACP, channel workers, and runtime lifecycle, but Voice remained bound to primary workspace settings and environment. A secondary workspace therefore could not safely select its own Voice model or credentials, and its active transcription work was invisible to workspace removal. This change closes that Phase 4b gap without changing ACP methods, Voice payload schemas, storage formats, or existing primary-workspace URLs.

Reviewer Test Plan

How to verify

Start a daemon with two trusted workspaces whose Voice models use distinct runtime environment values. Confirm that the qualified settings and transcription routes resolve both workspace id and encoded cwd, persist settings only in the selected workspace scope, send transcription through the selected runtime environment, and never fall back to primary for an unknown or untrusted selector. Confirm that legacy routes still use primary.

Open a mix of legacy and qualified batch and streaming operations. The ninth concurrent operation should return 503 voice_capacity_exceeded with Retry-After: 5 for REST or an error frame followed by WebSocket close code 1013; completing, failing, disconnecting, or timing out an operation should release exactly one slot.

While a secondary Voice operation is active, non-force workspace removal should return 409 workspace_busy with activity.voiceSessions. Force removal should close only that workspace's stream with code 1012, preserve other workspace streams, and allow a later same-path runtime generation to acquire fresh Voice capacity.

Locally verified on the final rebased commit with the focused Voice, workspace lifecycle, ACP upgrade, telemetry, realtime retry/session, SDK client, full serve server, and run-qwen-serve test files. The focused CLI matrix passed 267 tests, the SDK matrix passed 352 tests, server.test.ts passed 693 tests on full rerun, and root build, typecheck, and lint passed.

Evidence (Before & After)

N/A — daemon protocol, lifecycle, SDK, documentation, and tests only; no TUI or Web Shell UI changes.

Tested on

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

Environment (optional)

macOS, Node.js v22.22.3, npm 10.9.8, native local test runtime with mock HTTP/WebSocket Voice providers.

Risk & Scope

  • Main risk or tradeoff: This is a cross-package CLI and TypeScript SDK change around WebSocket upgrade routing, abort propagation, and runtime removal. The process-global Voice limit remains fixed at eight, so this PR intentionally does not introduce per-workspace fairness or configuration.
  • Not validated / out of scope: Real cloud ASR credentials/providers, Windows and Linux local runs, and secondary-workspace Web Shell Voice controls were not exercised. ACP method names, WebShell UI, per-workspace quotas, and Voice storage/schema changes are out of scope.
  • Breaking changes / migration notes: None. All changes are additive; legacy /workspace/voice, /workspace/voice/transcribe, and /voice/stream remain primary-only with their existing contracts. Clients should gate qualified settings, batch, and streaming behavior on workspace_qualified_voice intersected with the corresponding existing Voice capability.

Linked Issues

Part of #6378.

Built on the runtime drain/removal lifecycle merged in #6745.

中文说明

本 PR 做了什么

本 PR 完成多 workspace qwen serve daemon 的 Phase 4b workspace-qualified Voice。已注册且受信任的 runtime 可以通过按 workspace id 或编码后的绝对 cwd 选择的 workspace-qualified REST 与 WebSocket 路由读取和更新 Voice 设置、提交批量转写以及建立流式转写。每个请求都使用所选 runtime 的 cwd、有效环境、bridge 和 workspace scope 设置持久化;legacy Voice 路由继续只绑定 primary workspace。

daemon 现在通过一个进程级 admission coordinator 统一管理 legacy 与 workspace-qualified 的 REST 和 WebSocket Voice 操作,并保持现有总上限 8。workspace drain 会拒绝新的 Voice 工作,非 force removal 会把活动 Voice session 报告为 busy,force removal 或 daemon shutdown 只中止所选 runtime 的 Voice lease,并最多等待 5 秒让真实操作收敛。TypeScript workspace client 暴露 qualified 设置与批量转写方法,协议新增可叠加的 workspace_qualified_voice capability。

为什么需要它

多 workspace daemon 已经隔离 sessions、core REST、ACP、channel workers 与 runtime 生命周期,但 Voice 仍绑定 primary workspace 的设置和环境。因此 secondary workspace 无法安全选择自己的 Voice 模型或凭证,活动转写也不会被 workspace removal 感知。本变更补齐 Phase 4b 缺口,同时不改变 ACP method、Voice payload schema、存储格式或既有 primary-workspace URL。

Reviewer 测试计划

如何验证

使用两个受信任 workspace 启动 daemon,并为它们配置使用不同 runtime 环境值的 Voice 模型。确认 qualified 设置与转写路由都能通过 workspace id 和编码 cwd 解析目标,只向所选 workspace scope 持久化设置,使用所选 runtime 环境发起转写,且 unknown 或 untrusted selector 绝不 fallback 到 primary。确认 legacy 路由仍使用 primary。

同时打开 legacy 与 qualified 的批量和流式操作。第 9 个并发操作在 REST 上应返回带 Retry-After: 5503 voice_capacity_exceeded,在 WebSocket 上应先发送 error frame 再以 1013 关闭;操作完成、失败、断连或超时后都应准确释放一个槽位。

当 secondary Voice 操作活动时,非 force workspace removal 应返回带 activity.voiceSessions409 workspace_busy。force removal 应只以 1012 关闭该 workspace 的 stream,保留其他 workspace 的 stream,并允许随后相同路径的新 runtime generation 获取全新的 Voice 容量。

已在最终 rebase 后的提交上本地验证 Voice、workspace 生命周期、ACP upgrade、telemetry、realtime retry/session、SDK client、完整 serve server 和 run-qwen-serve 相关测试文件。CLI 专项矩阵 267 项通过,SDK 矩阵 352 项通过,server.test.ts 完整复跑 693 项通过,根目录 build、typecheck 和 lint 均通过。

证据(变更前与变更后)

N/A — 仅涉及 daemon 协议、生命周期、SDK、文档和测试;没有 TUI 或 Web Shell UI 变更。

测试平台

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

环境(可选)

macOS、Node.js v22.22.3、npm 10.9.8,使用 mock HTTP/WebSocket Voice provider 的原生本地测试 runtime。

风险与范围

  • 主要风险或权衡:这是涉及 CLI 与 TypeScript SDK 的跨 package 变更,覆盖 WebSocket upgrade 路由、中止传播和 runtime removal。进程级 Voice 上限固定为 8,因此本 PR 有意不引入 per-workspace 公平性或配置项。
  • 未验证或不在范围内:未使用真实云端 ASR 凭证/provider,也未在 Windows 和 Linux 本地运行,未验证 secondary-workspace Web Shell Voice 控件。ACP method 名称、WebShell UI、per-workspace quota 和 Voice 存储/schema 变更不在范围内。
  • 破坏性变更或迁移说明:无。所有变更均为增量;legacy /workspace/voice/workspace/voice/transcribe/voice/stream 继续只绑定 primary,并保持既有契约。客户端应使用 workspace_qualified_voice 与对应现有 Voice capability 的交集来启用 qualified 设置、批量和流式能力。

关联 Issue

属于 #6378 的一部分。

基于 #6745 已合入的 runtime drain/removal 生命周期。

doudouOUC and others added 2 commits July 14, 2026 00:22
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review July 13, 2026 16:42
@doudouOUC
doudouOUC requested a review from wenshao July 13, 2026 16:42
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present, bilingual, linked to #6378 and #6745.

Problem: This is an observed architectural gap, not a theoretical concern. Multi-workspace daemons already isolate sessions, REST, ACP, and lifecycle, but Voice remained bound to the primary workspace. Phase 4b of the workspace qualification roadmap is the natural place to close this. The PR description is clear about what's in scope and what isn't.

Direction: Aligned. This extends the existing workspace-qualified routing pattern (already used for settings, files, trust, lifecycle) to the Voice surface. Adding workspace_qualified_voice as an additive capability tag is the right protocol-level approach — clients can gate on it without breaking existing behavior.

Size: 40 changed files. ~1,443 production logic lines (excluding ~1,702 test lines and ~50 docs lines). Core path touch is minimal — packages/cli/src/services/voice-service.ts and voice-transcriber.ts add ~64 production lines of env passthrough and abortSignal threading to existing functions, which is straightforward and safe. Cross-package (CLI + SDK + web-shell) but focused entirely on voice/serve surfaces. 1000+ production lines advisory: this is large but the scope is well-bounded by the Phase 4b feature set. Splitting would create artificial PR boundaries that don't map to testable units. Maintainer awareness flagged.

Approach: The architecture is clean and follows existing patterns well:

  • WorkspaceVoiceCoordinator as a process-scoped admission controller is the right abstraction — the shared 8-session limit across legacy and qualified paths avoids capacity fragmentation.
  • The Lease with AbortController for disposal propagation is elegant — workspace removal naturally aborts in-flight Voice operations via signal chaining.
  • Qualified routes correctly use runtime.env.effectiveEnv (never process.env) and force SettingScope.Workspace — no leakage between workspace settings.
  • The refactoring of workspace-voice.ts to share handleVoiceStatus/Update/Transcription between legacy and qualified routes avoids duplication.
  • Abort signal plumbing in voice-stream-retry.ts, voice-stream-session.ts, and qwen-asr-realtime-session.ts is minimal and correct — just adds abort-during-connect to existing retry/open paths.

No concerns blocking review. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有章节齐全,双语,关联了 #6378#6745

问题:这是一个已识别的架构缺口,不是理论性问题。多 workspace daemon 已经隔离了 sessions、REST、ACP 和生命周期,但 Voice 仍然绑定 primary workspace。Phase 4b 是补齐这个缺口的自然阶段。PR 描述对范围内和范围外的内容都很清晰。

方向:对齐。这延续了已有的 workspace-qualified 路由模式(已用于 settings、files、trust、lifecycle 等),将 Voice 表面纳入覆盖。新增 workspace_qualified_voice 作为可叠加 capability tag 是正确的协议级做法——客户端可以在不影响现有行为的前提下据此启用功能。

规模:40 个文件变更,约 1,443 行生产逻辑代码(排除约 1,702 行测试和约 50 行文档)。核心路径触及极少——packages/cli/src/services/voice-service.tsvoice-transcriber.ts 新增约 64 行生产代码,将 env 透传和 abortSignal 穿透到已有函数,直接且安全。跨 package(CLI + SDK + web-shell)但完全聚焦 voice/serve。1000+ 生产行建议:PR 较大但范围由 Phase 4b 功能集明确限定,强行拆分会产生无法独立测试的 PR。已标记维护者关注。

方案:架构干净,很好地遵循了已有模式:

  • WorkspaceVoiceCoordinator 作为进程级 admission controller 是正确的抽象——legacy 和 qualified 路径共享 8 session 上限避免了容量碎片化。
  • Lease + AbortController 用于销毁传播设计优雅——workspace removal 通过 signal 链自然中止在途 Voice 操作。
  • Qualified 路由正确使用 runtime.env.effectiveEnv(永不使用 process.env)并强制 SettingScope.Workspace——workspace settings 之间无泄漏。
  • workspace-voice.ts 重构为 legacy 和 qualified 共享 handleVoiceStatus/Update/Transcription,避免重复。
  • voice-stream-retry.tsvoice-stream-session.tsqwen-asr-realtime-session.ts 的 abort signal 改动最小且正确——仅在现有 retry/open 路径添加连接中中止。

无阻塞性顾虑,进入代码审查 🔍

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: I would have added workspace-qualified REST routes (/workspaces/:workspace/voice/*) that mirror the existing legacy routes but resolve the runtime from the workspace param, use its env for config resolution, and force workspace-scoped persistence. I would have added a process-scoped admission coordinator to share the 8-session limit across both route families. I would have threaded an AbortSignal through the voice stream open/retry path so workspace removal can cancel in-flight connections. The SDK would get thin delegation methods on WorkspaceDaemonClient.

The PR's approach matches this proposal and exceeds it in a few ways:

  • The Lease abstraction with AbortController is cleaner than a raw counter — it gives disposal a natural signal propagation path.
  • The combinedAbortSignal helper (request abort + lease abort via AbortSignal.any) is the right composition for REST transcription.
  • The createRuntimeVoiceDeps factory avoids per-route boilerplate for the qualified registration.
  • The refactoring of workspace-voice.ts into shared handleVoiceStatus/Update/Transcription handlers eliminates duplication between legacy and qualified paths — a good structural improvement.

No critical issues found. The code is well-structured, follows existing patterns, and the new abstractions are minimal and well-scoped.

Reuse check: The coordinator reuses the existing MAX_CONCURRENT_VOICE_SESSIONS constant (previously local to voice-ws.ts, now exported from the coordinator). The qualified route resolution reuses resolveWorkspaceRuntimeFromParam and requireTrustedWorkspaceRuntime from existing workspace route helpers. The voiceAudioBodyParser and admission middleware are shared between legacy and qualified transcribe routes. No new parallel utilities introduced.

Test Results

Tmux real-scenario testing: N/A — this is a daemon protocol/lifecycle/SDK change with no TUI or Web Shell UI. The PR author notes this explicitly. Verification is through unit tests.

Unit tests (run on the PR branch in an isolated worktree):

✓ src/serve/voice/workspace-voice-coordinator.test.ts     8 tests   passed
✓ src/serve/voice/voice-ws.test.ts                       22 tests   passed
✓ src/serve/routes/workspace-qualified-voice.test.ts      7 tests   passed
✓ src/serve/routes/workspace-voice.test.ts               35 tests   passed
✓ src/serve/routes/workspace-management.test.ts          (included in 248 below)
✓ src/serve/run-qwen-serve.test.ts                       (included in 248 below)
✓ src/serve/server/telemetry.test.ts                     (included in 248 below)
✓ src/serve/acp-http/workspace-qualified-acp.test.ts     38 tests   passed
✓ src/services/voice-service.test.ts                     11 tests   passed
✓ src/ui/voice/voice-stream-retry.test.ts                 6 tests   passed
✓ src/ui/voice/voice-stream-session.test.ts              12 tests   passed
✓ src/ui/voice/qwen-asr-realtime-session.test.ts         14 tests   passed
✓ test/unit/DaemonClientVoice.test.ts (SDK)               8 tests   passed

Total: 409 tests passed, 0 failed

Typecheck: passed (all 5 packages: cli, core, sdk, web-shell, webui).

中文说明

代码审查

在阅读 diff 之前我的独立方案:添加 workspace-qualified REST 路由(/workspaces/:workspace/voice/*)镜像现有 legacy 路由但从 workspace 参数解析 runtime、使用其 env 进行配置解析、强制 workspace scope 持久化。添加进程级 admission coordinator 在两组路由间共享 8 session 上限。在 voice stream open/retry 路径中穿透 AbortSignal 使 workspace removal 能取消在途连接。SDK 在 WorkspaceDaemonClient 上添加薄委托方法。

PR 的方案与我的方案一致并在几处超越:

  • Lease + AbortController 比纯计数器更干净——为销毁提供自然的 signal 传播路径。
  • combinedAbortSignal(请求 abort + lease abort 通过 AbortSignal.any)是 REST 转写的正确组合。
  • createRuntimeVoiceDeps 工厂避免了 qualified 注册的逐路由样板代码。
  • workspace-voice.ts 重构为共享的 handleVoiceStatus/Update/Transcription handler 消除了 legacy 和 qualified 路径间的重复——良好的结构改进。

未发现关键问题。 代码结构良好,遵循已有模式,新抽象最小且范围明确。

复用检查: coordinator 复用了已有的 MAX_CONCURRENT_VOICE_SESSIONS 常量(原先在 voice-ws.ts 中本地定义,现由 coordinator 导出)。qualified 路由解析复用了已有的 resolveWorkspaceRuntimeFromParamrequireTrustedWorkspaceRuntimevoiceAudioBodyParser 和 admission 中间件在 legacy 和 qualified transcribe 路由间共享。未引入新的并行工具。

测试结果

Tmux 真实场景测试:不适用 — 仅涉及 daemon 协议、生命周期和 SDK 变更,无 TUI 或 Web Shell UI 变更。

单元测试(在隔离 worktree 的 PR 分支上运行):409 项测试全部通过,0 失败。

类型检查: 通过(全部 5 个 package:cli、core、sdk、web-shell、webui)。

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — Clean review across every stage, but Stage 0 flagged 1000+ production lines touching core services, which needs a maintainer's sign-off before auto-approval.

Stepping back: this PR does exactly what Phase 4b requires — no more, no less. Workspace-qualified Voice routes follow the same pattern already established for settings, files, trust, and lifecycle. The WorkspaceVoiceCoordinator is a clean, minimal state machine: acquire/release for the shared cap, drain for workspace removal, dispose with a 5-second timeout for shutdown. The Lease + AbortController pattern gives disposal a natural signal propagation path without polling.

My independent proposal was simpler (bare counter, no lease abstraction) — the PR's approach is better because it gives every consumer (REST routes, WebSocket, coordinator) a uniform admission interface and built-in abort. The code I would have written needed more plumbing to achieve the same disposal semantics.

Every change in the diff is needed for the stated goal. No drive-by refactors, no scope creep. The workspace-voice.ts refactor (extracting shared handlers) is the only structural change, and it directly eliminates duplication between legacy and qualified paths — worth doing in the same PR.

409 unit tests pass, typecheck and lint are clean. Tests cover the new coordinator lifecycle, qualified route resolution and trust enforcement, abort-during-connect in all three stream openers, and the SDK delegation methods.

This is a well-executed Phase 4b feature. Deferring to a maintainer for the final call given the size advisory.

中文说明

置信度:3/5 — 每个阶段审查均干净,但 Stage 0 标记了 1000+ 生产行触及核心 services,需要维护者确认后才能自动批准。

退一步看:这个 PR 准确完成了 Phase 4b 的要求——不多不少。Workspace-qualified Voice 路由遵循了 settings、files、trust、lifecycle 已建立的相同模式。WorkspaceVoiceCoordinator 是一个干净、最小的状态机:共享上限的 acquire/release、workspace removal 的 drain、带 5 秒超时的 shutdown dispose。Lease + AbortController 模式为销毁提供了自然的 signal 传播路径,无需轮询。

我的独立方案更简单(裸计数器,无 lease 抽象)——PR 的方案更好,因为它给每个消费者(REST 路由、WebSocket、coordinator)提供了统一的 admission 接口和内建的 abort 机制。我写的代码需要更多额外管道才能达到相同的销毁语义。

diff 中的每个改动都是实现目标所必需的。没有顺手重构,没有范围蔓延。workspace-voice.ts 的重构(提取共享 handler)是唯一的结构变更,它直接消除了 legacy 和 qualified 路径间的重复——值得在同一 PR 中完成。

409 项单元测试通过,类型检查和 lint 均干净。测试覆盖了新的 coordinator 生命周期、qualified 路由解析和信任执行、三个 stream opener 的连接中 abort,以及 SDK 委托方法。

这是一个执行良好的 Phase 4b 功能。因规模建议转交维护者做最终决定。

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts
Comment thread packages/cli/src/serve/acp-http/index.ts
Comment thread packages/cli/src/serve/voice/voice-ws.ts Outdated
Comment thread packages/cli/src/serve/voice/voice-ws.ts Outdated
Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.test.ts
Comment thread packages/cli/src/serve/routes/workspace-qualified-voice.test.ts
Comment thread packages/cli/src/ui/voice/voice-stream-session.ts Outdated
Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

Comment thread packages/cli/src/serve/acp-http/workspace-qualified-acp.test.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-qualified-voice.test.ts
Comment thread packages/cli/src/services/voice-service.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification (maintainer) — build, typecheck, lint & focused tests all green

I built this PR from source and ran the real toolchain in an isolated worktree to corroborate the author's test plan before merge.

Environment

PR head 3a672c267 (test(cli): address workspace Voice review feedback)
Base main 36cf31fb6 — 5 commits ahead, merge-base 536cb713c
Runtime Node v22.23.1, npm 10.9.8, macOS arm64
Diff 35 files, +2086 / −252

Quality gate

Step Result
npm ci ✅ exit 0
npm run build:packages ✅ exit 0 (core · cli · sdk · web-shell · webui · channels)
npm run typecheck ✅ exit 0 (tsc --noEmit, all packages)
npm run lint ✅ exit 0 (eslint . --ext .ts,.tsx + integration-tests)

Focused test suite — the 15 files this PR touches

Group Files Tests Result
A · Voice core — coordinator, qualified-voice routes, voice-ws, workspace-voice 4 64
B · Daemon lifecycle — run-qwen-serve, server, qualified-acp, workspace-mgmt, telemetry, facade 6 1049
C · Voice UI/service — voice-service, qwen-asr-realtime, voice-stream-retry, voice-stream-session 4 42
SDK — DaemonClientVoice 1 7
Total 15 1162 ✅ 0 failed

quality gate & tests

These focused tests directly exercise the behaviors in the Reviewer Test Plan:

  • Trusted-only selection, no fallback/workspaces/:id|:encodedCwd/voice resolves by both selector forms, writes settings in workspace scope, and returns 400 workspace_mismatch for unknown / 403 untrusted_workspace for untrusted before reading settings or audio. Primary Voice updates invalidate the primary feature cache; qualified ones don't.
  • Shared 8-slot admission — batch capacity → 503 voice_capacity_exceeded + Retry-After: 5, drain → 503 workspace_draining, both rejected before the audio body is parsed (route order: admitbodyParser). WS close codes 1012 (removed) / 1013 (capacity) and error frames covered in voice-ws.
  • Removal lifecycle — active Voice work surfaces as activity.voiceSessions in the non-force busy snapshot; disposal aborts only the selected runtime's leases and waits ≤5s.
  • Real daemonrun-qwen-serve spins an actual daemon, hits /capabilities over HTTP, and confirms workspace_voice_transcription flips on after a live settings_changed.
  • SDK — qualified workspaceVoice / setWorkspaceVoice / transcribeWorkspaceVoice + timeoutMs plumbing.

Controlled experiment — is the invariant load-bearing?

To confirm the tests aren't vacuous, I broke only the coordinator's central invariant — the process-global cap of 8 shared across runtimes — by changing this.active to per-runtime state.leases.size. Exactly one test flipped red (shares capacity across runtimes …); reverting restored 7/7. The shared cap is genuinely enforced.

controlled experiment

Verdict

Build, typecheck, lint, and all 15 touched test files pass locally, matching the PR's claims. The change is cleanly additive (legacy primary-only routes untouched), the shared-admission invariant is verified load-bearing, and CI is green. LGTM from a local-verification standpoint.

Scope note (unchanged from the PR): real cloud ASR providers and Windows/Linux local runs were not exercised — Voice upstreams are mocked in these tests, as in CI.

🇨🇳 中文版本

✅ 本地验证(维护者)— build、typecheck、lint 与专项测试全部通过

我在独立 worktree 中从源码构建了本 PR 并运行真实工具链,在合并前对作者的测试计划进行交叉验证。

环境

PR head 3a672c267test(cli): address workspace Voice review feedback
Base main 36cf31fb6 — 领先 5 个提交,merge-base 536cb713c
运行时 Node v22.23.1、npm 10.9.8、macOS arm64
差异 35 个文件,+2086 / −252

质量门禁

步骤 结果
npm ci ✅ exit 0
npm run build:packages ✅ exit 0(core · cli · sdk · web-shell · webui · channels)
npm run typecheck ✅ exit 0(tsc --noEmit,全部 package)
npm run lint ✅ exit 0(eslint . --ext .ts,.tsx + integration-tests)

专项测试 — 本 PR 改动的 15 个测试文件

分组 文件 用例 结果
A · Voice 核心 — coordinator、qualified-voice 路由、voice-ws、workspace-voice 4 64
B · Daemon 生命周期 — run-qwen-serve、server、qualified-acp、workspace-mgmt、telemetry、facade 6 1049
C · Voice UI/服务 — voice-service、qwen-asr-realtime、voice-stream-retry、voice-stream-session 4 42
SDK — DaemonClientVoice 1 7
合计 15 1162 ✅ 0 失败

(截图见上方英文部分)

这些专项测试直接覆盖了 Reviewer 测试计划中的行为:

  • 仅受信任、无 fallback/workspaces/:id|:编码cwd/voice 能通过两种 selector 解析,设置写入 workspace scope,并在读取设置或音频之前对未知 selector 返回 400 workspace_mismatch、对 untrusted 返回 403 untrusted_workspace。primary Voice 更新会失效 primary feature 缓存,qualified 更新不会。
  • 共享 8 槽准入 — 批量容量 → 503 voice_capacity_exceeded + Retry-After: 5,drain → 503 workspace_draining,二者都在解析音频 body 之前被拒绝(路由顺序:admitbodyParser)。WS 关闭码 1012(removed)/1013(capacity)与 error frame 在 voice-ws 中覆盖。
  • 移除生命周期 — 活动 Voice 工作会在非 force 的 busy 快照中以 activity.voiceSessions 呈现;disposal 只中止所选 runtime 的 lease,并最多等待 5 秒。
  • 真实 daemonrun-qwen-serve 启动真实 daemon,通过 HTTP 访问 /capabilities,确认 settings_changedworkspace_voice_transcription 会打开。
  • SDK — qualified 的 workspaceVoice / setWorkspaceVoice / transcribeWorkspaceVoice 以及 timeoutMs 的接线。

受控实验 — 该不变量是否是关键约束?

为确认这些测试并非“空测”,我破坏了 coordinator 的核心不变量——跨 runtime 共享的进程级 8 上限——把 this.active 改为按 runtime 计数的 state.leases.size。恰好一个测试变红(shares capacity across runtimes …),回退后恢复 7/7。共享上限确实被测试真正约束。

结论

build、typecheck、lint 以及全部 15 个受影响测试文件本地均通过,与 PR 声明一致。变更为纯增量(legacy primary-only 路由未动),共享准入不变量被证明是关键约束,且 CI 为绿。从本地验证角度 LGTM。

范围说明(与 PR 一致):未测试真实云端 ASR provider,也未在 Windows/Linux 本地运行——这些测试与 CI 一样使用 mock 的 Voice 上游。

Local verification on an isolated worktree; Voice upstreams mocked (same as CI). Not an official approval.

wenshao
wenshao previously approved these changes Jul 13, 2026
Comment thread packages/cli/src/serve/acp-http/index.ts Outdated
Comment thread packages/cli/src/serve/server/serve-features.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-management.ts
Comment thread packages/cli/src/serve/routes/workspace-voice.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-voice.ts
Comment thread packages/cli/src/serve/server/telemetry.ts
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/cli/src/serve/capabilities.ts Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread packages/cli/src/serve/voice/voice-ws.ts Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

Comment thread packages/cli/src/serve/routes/workspace-voice.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-voice.ts
Comment thread packages/cli/src/serve/routes/workspace-voice.ts
Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-voice.test.ts
Comment thread packages/cli/src/serve/routes/workspace-voice.test.ts
@doudouOUC doudouOUC self-assigned this Jul 13, 2026
wenshao
wenshao previously approved these changes Jul 13, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts
Comment thread packages/cli/src/ui/voice/qwen-asr-realtime-session.ts Outdated
return;
}
wss!.handleUpgrade(req, socket, head, (ws: WebSocket) => {
if (disposed) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The disposed check inside the Voice WS upgrade callback correctly closes the socket with code 1012 on server shutdown, but no test exercises this path. The existing test "returns 503 server_disposed after dispose()" only covers REST endpoints, not the Voice WS upgrade flow. Consider adding a test that calls handle.close() then attempts a Voice WS upgrade, asserting the socket closes with code 1012.

— qwen3.7-max via Qwen Code /review

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

All 16 previously posted Critical findings have been verified against the current commit and are fixed by this diff (disposeRuntime sets completed, parallel shutdown via Promise.all, WebShell voiceSessions counter, coordinator/removal pairing validation, isPortableAbsolutePath guard, env mode handling, abort signal propagation, REST routing for qualified SDK methods, partial-persist broadcast).

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/routes/workspace-voice.ts Outdated
Comment thread packages/cli/src/serve/voice/voice-ws.ts
Comment thread packages/cli/src/serve/capabilities.ts Outdated
Comment thread packages/cli/src/serve/voice/workspace-voice-coordinator.ts
Comment thread packages/cli/src/serve/routes/workspace-voice.ts Outdated
Comment thread packages/cli/src/serve/server.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
wenshao previously approved these changes Jul 14, 2026
@wenshao

wenshao commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge July 14, 2026 02:37
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to maintainer — this is a clean Phase 4b implementation (409 tests pass, typecheck/lint clean, no code concerns), but Stage 0 flagged 1,443 production logic lines touching core services (packages/cli/src/services/voice-service.ts, voice-transcriber.ts), which exceeds the 500-line maintainer-awareness threshold and the 1,000-line large-PR advisory. The review is clean — this defer is pure policy, not doubt about the code. Needs a human call on whether to approve at this size.

中文说明

⏸️ 转交维护者 — 这是一个干净的 Phase 4b 实现(409 项测试通过,类型检查/lint 干净,无代码顾虑),但 Stage 0 标记了 1,443 行生产逻辑代码触及核心 services(packages/cli/src/services/voice-service.tsvoice-transcriber.ts),超过 500 行维护者关注阈值和 1,000 行大 PR 建议。审查本身是干净的——此转交纯粹是策略性的,不是对代码的疑虑。需要人工决定是否在此规模下批准。

@wenshao

wenshao commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no new blockers. 11 of 14 previously reported Criticals are fixed by this diff; 3 still stand (capability scoping in serve-features.ts, active-only resolver for GET/POST settings during drain). Suggestions are inline.

— qwen3.7-max via Qwen Code /review

const result = deps.acquireVoiceLease?.();
if (!result) return true;
if (result.kind === 'rejected') {
if (result.reason === 'draining') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The draining and capacity rejection branches in installAdmissionLease have no test coverage. A mock acquireVoiceLease returning { kind: 'rejected', reason: 'draining' } or { kind: 'rejected', reason: 'capacity' } would verify the 503 response shape, Retry-After header, and that next() is never called.

Concrete cost: a refactor of the rejection response format (e.g. renaming voice_capacity_exceeded) would silently break the client contract without any test catching it.

— qwen3.7-max via Qwen Code /review

@@ -87,11 +96,13 @@ async function defaultOpenStream(
voiceModel: ctx.voiceModel,
env: ctx.env,
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test verifies that operationController.signal actually cancels an in-flight openStream when the lease is aborted. The existing test "aborts an in-flight upstream open" verifies the lease release path, but does not capture the signal passed to openStream and assert signal.aborted after coordinator disposal.

Failure scenario: a future refactor drops the abortSignal parameter from openVoiceStreamWithRetry, and stuck upstream connections survive workspace removal by up to the DNS/HTTP timeout.

— qwen3.7-max via Qwen Code /review

return { kind: 'admitted', lease };
}

beginWorkspaceDrain(runtime: WorkspaceRuntime): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] beginWorkspaceDrain and completeWorkspaceDrain call this.stateFor(runtime) without checking the disposed WeakSet first. If called after disposeRuntime (which adds to disposed and may have already removed the entry via deleteIfIdle), stateFor creates a new phantom entry with completed: false that deleteIfIdle can never reclaim — a small but permanent Map leak per occurrence.

Suggested change
beginWorkspaceDrain(runtime: WorkspaceRuntime): void {
beginWorkspaceDrain(runtime: WorkspaceRuntime): void {
if (this.disposed.has(runtime)) return;
this.stateFor(runtime).draining = true;
}

— qwen3.7-max via Qwen Code /review

publishWorkspaceEvent: (event) => {
if (
event.type === 'settings_changed' ||
event.type === 'settings_reloaded'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The publishWorkspaceEvent handler invalidates the serve features cache for both settings_changed and settings_reloaded event types, but the run-qwen-serve test only triggers settings_changed. The settings_reloaded branch is untested.

Failure scenario: if the settings_reloaded string were misspelled or the event shape changed, cache invalidation for that event type would silently break.

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

export async function assertVoiceBaseUrlNetworkAllowed(
voiceConfig: VoiceTranscriptionConfig,
lookupHost?: VoiceHostLookup,
abortSignal?: AbortSignal,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new abortSignal parameter is forwarded by the serve-side caller (voice-ws.ts) but the TUI caller at InputPrompt.tsx:488 still calls assertVoiceBaseUrlNetworkAllowed(streamConfig) with one argument. If the user cancels voice input during DNS resolution, the lookup will not be interrupted.

Consider forwarding the abort signal from InputPrompt.tsx:

await assertVoiceBaseUrlNetworkAllowed(streamConfig, undefined, abortSignal);

— qwen3.7-max via Qwen Code /review

if (state === 'closed') return;
cleanup();
try {
ws.close(1012, voiceLeaseCloseReason(lease.signal));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] WebSocket close code 1012 (Service Restart) is used for both workspace removal and daemon shutdown. Per RFC 6455, 1012 signals the client should reconnect — correct for workspace removal (the workspace may be re-added), but misleading for daemon shutdown (permanent). A client that auto-reconnects on 1012 will repeatedly fail against a stopped daemon.

Consider using 1001 (Going Away) for daemon shutdown:

const closeCode = signal.reason instanceof VoiceLeaseAbortError &&
  signal.reason.kind === 'daemon_shutdown'
    ? 1001
    : 1012;
ws.close(closeCode, voiceLeaseCloseReason(lease.signal));

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

…ion_management_v2

Both features are independent additions to the serve capability registry:
- workspace_qualified_voice (PR branch): workspace-qualified Voice REST/WS routes
- extension_management_v2 (main): global extension catalog and mutation surface

Resolved by including both capabilities in the registry, docs feature list,
and test expectations. Updated the workspace_qualified_rest_core comment to
reference both adjacent capabilities.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge conflict resolution summary — PR #6839

Base branch: main

Conflicts (3 files)

1. docs/developers/qwen-serve-protocol.md

What conflicted: The capability feature list code block. The PR branch added workspace_qualified_voice to the list; origin/main added extension_management_v2.

Resolution: Included both features in the list. Order: workspace_qualified_rest_core, workspace_qualified_voice, extension_management_v2, workspace_persisted_transcript.

2. packages/cli/src/serve/capabilities.ts

What conflicted: The SERVE_CAPABILITY_REGISTRY object. The PR added a workspace_qualified_voice entry (with comment about Voice REST/WebSocket routes) after workspace_qualified_rest_core. Main added an extension_management_v2 entry (with comment about global extension catalog/mutations) at the same position.

Resolution: Kept both entries. Updated the workspace_qualified_rest_core comment to reference both adjacent capabilities (Voice via workspace_qualified_voice and extensions via extension_management_v2). Registry order: workspace_qualified_rest_coreworkspace_qualified_voiceextension_management_v2workspace_persisted_transcript.

3. packages/cli/src/serve/server.test.ts

What conflicted: The EXPECTED_REGISTERED_FEATURES array. The PR added 'workspace_qualified_voice'; main added 'extension_management_v2'.

Resolution: Included both entries in the same order as the registry: 'workspace_qualified_voice', then 'extension_management_v2'.

Rationale

Both features are independent, additive capabilities:

Neither feature depends on or conflicts with the other semantically — they only conflicted because both were inserted at the same position in ordered data structures (registry o

@wenshao
wenshao added this pull request to the merge queue Jul 14, 2026
Merged via the queue into QwenLM:main with commit c7250df Jul 14, 2026
35 of 36 checks passed
@doudouOUC
doudouOUC requested a review from qqqys July 14, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants