Skip to content

feat(web-shell): add workspace channel management - #7498

Closed
qqqys wants to merge 51 commits into
QwenLM:mainfrom
qqqys:codex/issue-7209-channel-management
Closed

feat(web-shell): add workspace channel management#7498
qqqys wants to merge 51 commits into
QwenLM:mainfrom
qqqys:codex/issue-7209-channel-management

Conversation

@qqqys

@qqqys qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a complete workspace-scoped Channel management page to qwen serve, including channel discovery, manual configuration, lifecycle controls, startup selection, secret-preserving edits, and pending pairing approvals. The add-channel entry currently exposes DingTalk, WeCom, and Feishu only, while previously configured channel instances remain manageable for compatibility.

It also adds typed daemon and SDK contracts for workspace-owned channel operations, bearer-token authorization for mutations, and runtime isolation so every read and write stays with the selected workspace.

Why it's needed

The WebShell previously had no entry point for configuring channels, so users had to leave the UI and edit settings or use the CLI. With the daemon's multi-workspace architecture, channel settings, runtime state, credentials, and pairing approvals also need an explicit workspace owner instead of falling back to process-global state.

Reviewer Test Plan

How to verify

  1. Start qwen serve with a bearer token in a trusted workspace and open the Channel page from the sidebar or Settings.
  2. Confirm the add-channel picker exposes only DingTalk, WeCom, and Feishu, and that the editor shows the active workspace, platform-specific credentials, and Pairing/Open access policies.
  3. Save a channel, start and stop it, toggle start-with-service, edit it without replacing its saved secret, and delete it. Confirm status and errors update inline.
  4. In Pairing mode, send a message from an unapproved user, confirm the pending request appears directly below the policy, approve it, and resend the message.
  5. Switch to another workspace and confirm its channel list and pairing requests are isolated from the first workspace.

Locally verified with 255 WebShell unit tests, 11/11 Channel Playwright scenarios, 401 CLI/daemon channel tests, 11 WebUI pairing-hook tests, 306 SDK tests, the SDK browser bundle budget check, WebShell typecheck, and the repository build.

Evidence (Before & After)

Before: qwen serve WebShell had no Channel configuration entry.

After: the Channel management screenshot is attached in a separate PR comment so GitHub hosts the image without adding a binary asset to the repository.

Tested on

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

Environment (optional)

Node.js 22, local qwen serve, Chromium Playwright, bearer-token authenticated WebShell.

Risk & Scope

  • Main risk or tradeoff: The change crosses daemon, SDK, WebUI, and WebShell boundaries; workspace resolution, secret isolation, and worker lifecycle behavior are the primary review areas.
  • Not validated / out of scope: Windows and Linux manual UI verification, and quick-binding QR creation for DingTalk, WeCom, or Feishu.
  • Breaking changes / migration notes: No migration is required. Existing channel configuration remains readable, secrets are never returned to the browser, and existing configured channel types remain manageable even though only three platforms are offered for new entries.

Linked Issues

Closes #7209

中文说明

本 PR 做了什么

qwen serve 增加完整的、按工作区隔离的频道管理页,覆盖频道发现、手动配置、启动与停止、随服务启动、保留密钥的编辑,以及待处理配对请求审批。新增频道入口目前只开放钉钉、企业微信和飞书;为了兼容已有配置,已存在的其他频道实例仍可继续管理。

同时增加类型化的 daemon 与 SDK 协议,通过 bearer token 保护写操作,并确保所有频道读写、运行状态、凭据和配对审批都归属当前选中的工作区,不回退到进程全局状态。

为什么需要

此前 WebShell 没有频道配置入口,用户必须离开页面修改设置或使用 CLI。daemon 进入多工作区架构后,频道设置、运行状态、凭据和配对审批也必须明确归属工作区,不能继续使用进程级全局状态。

Reviewer 测试计划

如何验证

  1. 在受信任的工作区中携带 bearer token 启动 qwen serve,从侧边栏或设置页进入频道管理。
  2. 确认新增频道只展示钉钉、企业微信和飞书;编辑器展示当前工作区、平台专属凭据,以及配对/开放两种准入策略。
  3. 保存频道后执行启动、停止、随服务启动切换、保留已有密钥的编辑和删除,确认状态与错误在页面内正确更新。
  4. 在配对模式下由未审批用户发送消息,确认待处理请求直接出现在策略下方;批准后让用户重新发送消息。
  5. 切换到另一个工作区,确认频道列表和配对请求与前一个工作区完全隔离。

本地已验证 255 个 WebShell 单测、11/11 个频道 Playwright 场景、401 个 CLI/daemon 频道测试、11 个 WebUI 配对 hook 测试、306 个 SDK 测试、SDK 浏览器 bundle 体积检查、WebShell typecheck 和仓库构建。

前后对比证据

Before:qwen serve WebShell 没有频道配置入口。

After:频道管理页截图会作为独立 PR 评论上传,由 GitHub 托管图片,避免把二进制截图加入仓库。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境

Node.js 22、本地 qwen serve、Chromium Playwright、使用 bearer token 认证的 WebShell。

风险与范围

  • 主要风险或权衡:改动跨越 daemon、SDK、WebUI 和 WebShell,工作区解析、密钥隔离和 worker 生命周期是重点审查区域。
  • 未验证/范围外:Windows 和 Linux 的手工 UI 验证,以及钉钉、企业微信或飞书的扫码快捷创建。
  • 破坏性变更/迁移说明:无需迁移。现有频道配置仍可读取,密钥不会返回浏览器;虽然新增入口只开放三个平台,但已配置的其他频道类型仍可管理。

关联 Issue

Closes #7209

qqqys added 30 commits July 19, 2026 14:19
@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

UI Evidence

The new Channel management page exposes DingTalk, WeCom, and Feishu for new configuration and keeps pairing approvals directly below Pairing mode.
issue-7209-channel-management

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

本地验证报告 — PR #7498

论点:架构设计严谨,但本地测试存在 16 个失败需要修复后再合并。

验证环境

  • macOS darwin, Node v22.22.1
  • 分支:codex/issue-7209-channel-management(通过 pull/7498/head 拉取)

1. 单元测试(部分失败)

channel-management-service.test.ts(37/39 通过,2 失败):

× lists sanitized config, secret presence, startup selection, and runtime
× deeply redacts webhook literals and QQ app secrets from snapshots
✓ 37 other tests passed

channel-settings-store.test.ts(14 失败):

ChannelSettingsError: Channel type "telegram" does not provide safe management metadata.

失败原因:测试环境中 telegram 插件未提供 management 元数据,导致 upsert 抛出 channel_settings_unmanageable

channel-selection.test.ts:全部通过。

workspace-channel-management.test.ts:加载失败(vite transform error)。

2. tmux CLI 启动验证

$ npx tsx packages/cli/src/cli.ts --version
0.20.1

CLI 在此分支上正常编译运行。

3. 架构审查

设计文档质量极高(376 行 15-channel-adapters.md),覆盖了:

Phase 1 配置管理合约

  • 8 个 REST 路由(channel-types / channels CRUD / startup / start / stop / restart)
  • Revision-based 乐观并发控制expectedRevision(SHA-256 digest)防止写-写冲突,mismatch → 409
  • Secret 操作语义preserve / replace / clear 三态,读响应只暴露 presence/source,不暴露值
  • 实例名校验:portable filesystem component,排除路径分隔符、控制字符、Windows 设备名、保留名 all

Worker 生命周期

  • ChannelWorkerManagerChannelWorkerGroup(per-workspace)→ ChannelWorkerSupervisor(per-child)
  • 替换失败回滚:新 worker 停止,旧 worker 恢复
  • 部分连接:startupFailures 暴露在 snapshot 中,worker 继续运行
  • 全部失败:502 channel_worker_start_failed,带 workspace 注释的失败列表

Startup 选择解析

  • 优先级:argv.channel > 主 workspace serve.channels > disabled
  • all 哨兵展开:禁用一个实例时将 all 展开为其他已配置实例名
  • 配置 ≠ 启动:仅配置实例不意味着启动

论据链

  1. 配置管理是 settings + manager 的投影,不是第二个生命周期所有者——避免双所有权
  2. Secret 永远不进入读响应——resolved environment values never enter the response
  3. 严格 bearer gate + client ID 校验——每个 mutation 都经过
  4. 未知/模糊/不受信任/排空/已删除的 workspace target → fail closed
  5. 但 16 个测试失败(主要是 telegram 插件缺少 management 元数据)需要修复

结论:架构设计严谨,但测试失败需要修复。建议修复后合并。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

capabilities

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

Qwen Code · serve A/B

@qqqys
qqqys marked this pull request as ready for review July 22, 2026 11:09
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR! This is a substantial piece of work.

Template looks good ✓

Problem: Real and well-documented. Issue #7209 describes a clear gap — WebShell has no channel configuration entry, forcing users to hand-edit JSON or use the CLI. The issue is detailed with acceptance criteria and a phased delivery plan.

Direction: Aligned. Channel management in the WebShell is within qwen-code's core mission as the browser control plane for qwen serve. Claude Code's CHANGELOG references channel features (connections, --channels flag, notifications), confirming this is a relevant area. The issue carries roadmap/background-automation, daemon, and scope/web-shell labels.

Size: This PR touches core paths (packages/cli/src/config/settingsSchema.ts, 11 lines) and spans 6 packages (channels, cli, sdk-typescript, web-shell, webui, vscode-ide-companion). Breakdown:

  • Production logic: 70 files, ~10,856 lines (+10,709 / -147)
  • Tests: 37 files, ~9,009 lines (+8,930 / -79)
  • package-lock.json: ~241 lines

⚠️ Maintainer awareness required — 10,856 production lines is well above the 500-line threshold for core-touching feat PRs, and above the 1,000-line large-PR advisory. Flagging for maintainer attention.

Approach: Two concerns worth discussing before deeper review:

  1. Phasing. Issue feat(web-shell): add full Channel management with browser QR authentication #7209 explicitly asks for "three independently mergeable phases" (Daemon+SDK → Authentication → Web Shell). This PR delivers all three in one shot. Was splitting considered? A phased approach would make each piece reviewable and revertable independently, and would let the daemon/SDK contract stabilize before the UI builds on it.

  2. Scope vs. title. The title says feat(web-shell) but the diff spans daemon routes, SDK client, channel adapters, CLI serve infrastructure, WebUI hooks, and a VSCode schema update. If this stays as one PR, the title undersells the blast radius.

Neither is a blocker — the code may well be correct — but at ~11k production lines across 6 packages, this is difficult to review and revert safely. If splitting is feasible, it would significantly reduce risk.

Moving on to code review. 🔍

中文说明

感谢贡献!这是一个相当大的工程。

模板完整 ✓

问题: 真实且有充分记录。Issue #7209 描述了一个明确的缺口——WebShell 没有频道配置入口,用户只能手动编辑 JSON 或使用 CLI。该 issue 有详细的验收标准和分阶段交付计划。

方向: 对齐。WebShell 中的频道管理属于 qwen-code 作为 qwen serve 浏览器控制平面的核心使命。Claude Code 的 CHANGELOG 中有频道相关功能(连接、--channels 标志、通知),确认这是相关领域。Issue 带有 roadmap/background-automationdaemonscope/web-shell 标签。

规模: 本 PR 触及核心路径(packages/cli/src/config/settingsSchema.ts,11 行),跨越 6 个包(channels、cli、sdk-typescript、web-shell、webui、vscode-ide-companion)。明细:

  • 生产逻辑:70 个文件,约 10,856 行(+10,709 / -147)
  • 测试:37 个文件,约 9,009 行(+8,930 / -79)
  • package-lock.json:约 241 行

⚠️ 需要维护者关注 —— 10,856 行生产代码远超触及核心的 feat PR 的 500 行阈值,也超过 1,000 行大 PR 建议线。标记供维护者关注。

方案: 深入审查前有两个值得讨论的顾虑:

  1. 分阶段。 Issue feat(web-shell): add full Channel management with browser QR authentication #7209 明确要求"三个可独立合并的阶段"(Daemon+SDK → 认证 → Web Shell)。本 PR 一次性交付了全部三个阶段。是否考虑过拆分?分阶段交付可以使每个部分独立可审查、可回滚,并让 daemon/SDK 协议在 UI 构建之前稳定下来。

  2. 范围与标题。 标题写的是 feat(web-shell),但 diff 跨越了 daemon 路由、SDK 客户端、频道适配器、CLI serve 基础设施、WebUI hooks 和 VSCode schema 更新。如果保持为一个 PR,标题低估了影响范围。

两者都不是阻塞项——代码可能是正确的——但约 11k 行生产代码跨 6 个包,审查和安全回滚都很困难。如果拆分可行,将显著降低风险。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: For workspace channel management, I'd add daemon CRUD routes with bearer-token auth, SDK client methods, WebUI hooks, and WebShell components — with secrets never returned to the browser and workspace isolation via hashed state directories.

Comparison with the diff: The PR matches this approach closely. The architecture is sound:

  • Daemon routes (workspace-channel-management.ts, 723 lines): Well-structured Express routes with proper input validation, workspace runtime resolution via requireTrustedWorkspaceRuntime, and bearer-token gating on all mutations. The resolveTarget helper cleanly separates runtime resolution from service resolution.

  • Settings store (channel-settings-store.ts, 519 lines): Revision-based optimistic concurrency (SHA-256 of serialized state), secret preserve/replace/clear semantics, prototype pollution guards (UNSAFE_OBJECT_KEYS), and field-level validation against plugin descriptors.

  • Management service (channel-management-service.ts, 585 lines): Mutation lane serialization prevents concurrent writes, runtime state derived from the existing worker manager (no second lifecycle owner), and diagnostics sanitized through redactLogCredentials + sanitizeLogText.

  • State directory (channel-state-dir.ts, 37 lines): Credential isolation via hashDaemonWorkspace(workspaceCwd) + hashed name + hashed type — no raw user input in paths.

  • Channel name validation (channel-selection.ts): Rejects path traversal (/, \, ..), control characters, Windows device names, and overlong names. Well-tested with 69 dedicated test cases.

  • SDK types (daemon/types.ts, +137 lines): Comprehensive typed contracts for all operations including auth sessions.

  • WebShell (ChannelsManagerPage.tsx, 1009 lines): Full management UI with capability detection, read-only mode when no bearer token, and proper error handling for 409 conflicts.

No critical blockers found. The security model is correct: secrets never leave the daemon, mutations require auth, workspace isolation is enforced at the runtime level, and input validation prevents path traversal.

One observation (non-blocking): The readPairingRequests route uses deps.mutate({ strict: true }) — a GET-equivalent read gated behind mutation auth. This is intentional per the issue's security requirements (pairing requests contain sender identities), but worth noting for reviewers.

Testing

Unit tests (all passing):

  • CLI channel management: 147 tests ✓
  • Route tests: 33 tests ✓
  • WebShell channel components: 87 tests ✓
  • SDK DaemonClient: 292 tests ✓
  • TypeScript typecheck: clean ✓

Real-scenario API testing (tmux unavailable in this CI environment — tested via direct API calls against qwen serve):

$ QWEN_SERVER_TOKEN=test-token-123 node dist/cli.js serve --port 18923 --workspace /tmp/triage-workspace
qwen serve listening on http://127.0.0.1:18923 (mode=http-bridge, workspace=/tmp/triage-workspace)

$ curl -s /workspace/channel-types -H "Authorization: Bearer test-token-123"
6 channel types: telegram, weixin, dingtalk, wecom, feishu, qq (all manageable)

$ curl -s /workspace/channels -H "Authorization: Bearer test-token-123"
{"revision":"8c575c...","instances":{}}

$ curl -s -X PUT /workspace/channels/test-dingtalk -H "Authorization: Bearer ..." -d '{...}'
→ 200: {"instance":{"name":"test-dingtalk","config":{"type":"dingtalk","clientId":"test-id"},
   "secrets":{"clientSecret":{"present":true,"source":"literal"}},"runtime":{"state":"stopped"}}}
   ✓ Secret value NOT returned — only presence metadata

$ curl -s -X PUT /workspace/channels/test-dingtalk (preserve secret, update config)
→ 200: Secret preserved: {'present': True, 'source': 'literal'}, ClientId updated: updated-id

$ curl -s -X PUT /workspace/channels/test-dingtalk/startup -d '{"enabled":true}'
→ 200: startsWithServe: True

$ curl -s -X DELETE /workspace/channels/test-dingtalk
→ 200: Instances after delete: []

$ curl -s -X PUT /workspace/channels/x -d '{"expectedRevision":"stale"}'
→ 409: {"error":"Channel settings changed; reload before trying again.","code":"channel_settings_conflict"}

$ curl -s -X PUT /workspace/channels/no-auth (no bearer token)
→ 401: {"error":"Unauthorized"}

$ curl -s -X PUT /workspace/channels/..%2F..%2Fetc (path traversal)
→ 400: {"error":"Channel instance names must be non-empty portable path components...","code":"invalid_channel_instance_name"}

All security invariants hold: secrets never exposed, mutations require auth, path traversal blocked, revision conflicts detected.

中文说明

代码审查

独立方案: 对于工作区频道管理,我会添加带 bearer-token 认证的 daemon CRUD 路由、SDK 客户端方法、WebUI hooks 和 WebShell 组件——密钥永远不返回浏览器,工作区隔离通过哈希状态目录实现。

与 diff 对比: PR 与此方案高度吻合。架构合理:

  • Daemon 路由(723 行):结构良好的 Express 路由,正确的输入验证,通过 requireTrustedWorkspaceRuntime 进行工作区运行时解析,所有写操作都有 bearer-token 保护。
  • 设置存储(519 行):基于 revision 的乐观并发控制(SHA-256),密钥 preserve/replace/clear 语义,原型链污染防护,字段级验证。
  • 管理服务(585 行):mutation lane 序列化防止并发写入,运行状态从现有 worker manager 派生(无第二生命周期所有者),诊断信息经过脱敏处理。
  • 状态目录(37 行):通过 hashDaemonWorkspace(workspaceCwd) + 哈希名称 + 哈希类型实现凭据隔离——路径中无原始用户输入。
  • 频道名验证:拒绝路径遍历、控制字符、Windows 设备名和过长名称。69 个专用测试用例。
  • SDK 类型(+137 行):所有操作的完整类型化协议。
  • WebShell(1009 行):完整管理 UI,带能力检测、无 token 只读模式和 409 冲突处理。

未发现关键阻塞项。 安全模型正确:密钥不离开 daemon、写操作需要认证、工作区隔离在运行时层面强制执行、输入验证防止路径遍历。

测试

单元测试(全部通过): CLI 147 + 路由 33 + WebShell 87 + SDK 292 = 559 个测试 ✓

真实场景 API 测试(此 CI 环境无 tmux——通过直接 API 调用测试):

  • 频道类型列表:6 种类型,全部可管理 ✓
  • 创建频道:密钥仅返回存在性元数据,不返回实际值 ✓
  • 保留密钥编辑:配置更新,密钥保留 ✓
  • 启动切换:startsWithServe 正确切换 ✓
  • 删除频道:实例清空 ✓
  • 409 冲突:过期 revision 正确拒绝 ✓
  • 未认证写操作:401 拒绝 ✓
  • 路径遍历:400 拒绝 ✓

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5

Reflection

This is a well-engineered PR. The security model is correct (secrets never leave the daemon, bearer-token auth on mutations, workspace isolation via hashed paths, path traversal prevention). The architecture reuses the existing worker manager as the sole runtime owner rather than creating a parallel lifecycle. Test coverage is comprehensive (559 tests across 4 packages, all passing). The API behaves correctly under adversarial inputs (stale revisions, missing auth, path traversal attempts).

The code follows project conventions: ESM, strict TypeScript, collocated tests, proper error codes, and bilingual documentation.

Verdict

Escalating to maintainer — not because of quality concerns, but because of scope:

  1. Size: 10,856 production lines across 70 files and 6 packages exceeds what any automated review can certify with 100% confidence. I cannot name every downstream consumer of the daemon route changes, the SDK surface additions, or the WebShell component tree.

  2. Phasing: Issue feat(web-shell): add full Channel management with browser QR authentication #7209 explicitly requests "three independently mergeable phases." This PR delivers all three at once. Whether that's acceptable is a maintainer judgment call — the code may be correct, but the reviewability and revertability tradeoffs are real.

  3. Cross-repository fork PR at this scale warrants human eyes on the full diff before merge.

Recommendation: If the maintainer is comfortable with the scope, the implementation quality supports approval. If splitting is preferred, the natural seams are: (1) daemon routes + settings store + management service, (2) SDK types + client methods, (3) WebShell UI + WebUI hooks.

中文说明

反思

这是一个工程质量很高的 PR。安全模型正确(密钥不离开 daemon、写操作需 bearer-token、工作区隔离通过哈希路径、防止路径遍历)。架构复用现有 worker manager 作为唯一运行时所有者,而非创建平行生命周期。测试覆盖全面(4 个包共 559 个测试,全部通过)。API 在对抗性输入下行为正确(过期 revision、缺少认证、路径遍历尝试)。

代码遵循项目规范:ESM、严格 TypeScript、同目录测试、 proper 错误码、双语文档。

结论

转交维护者 ——不是因为质量问题,而是因为范围:

  1. 规模: 10,856 行生产代码跨 70 个文件和 6 个包,超出任何自动审查能 100% 确认的范围。我无法列出 daemon 路由变更、SDK 表面新增或 WebShell 组件树的所有下游消费者。

  2. 分阶段: Issue feat(web-shell): add full Channel management with browser QR authentication #7209 明确要求"三个可独立合并的阶段"。本 PR 一次性交付全部三个阶段。是否可接受是维护者的判断——代码可能是正确的,但可审查性和可回滚性的权衡是真实的。

  3. 跨仓库 fork PR 在此规模下需要人工审查完整 diff 后才能合并。

建议: 如果维护者对范围没有异议,实现质量支持批准。如果倾向于拆分,自然的分界线是:(1) daemon 路由 + 设置存储 + 管理服务,(2) SDK 类型 + 客户端方法,(3) WebShell UI + WebUI hooks。

Qwen Code · qwen3.8-max-preview

Reviewed at fd675899aeb6afd616ac509ee674c9c6fc4fafbd · 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.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: chunk 1, chunk 2, chunk 5, chunk 6, chunk 7, chunk 8, chunk 12, chunk 13, chunk 16, chunk 17, chunk 21, chunk 26, chunk 27, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 42, chunk 43, chunk 44, chunk 45, chunk 46, chunk 47, chunk 50, chunk 51, chunk 55, chunk 56, chunk 57, chunk 58, chunk 59, chunk 60, chunk 61, chunk 62, chunk 63, chunk 64, chunk 65, chunk 67 — no agent reported covering these; nobody read them. Not reviewed: chunk 14, chunk 9, chunk 29, chunk 4, chunk 30, chunk 19, chunk 24, chunk 10, chunk 3, chunk 28, chunk 22, chunk 15, chunk 25, chunk 18, chunk 11, chunk 20 — launched with a prompt that is not the one the CLI built. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, chunk 1, chunk 2, chunk 5, chunk 6, chunk 7, chunk 8, chunk 12, chunk 13, chunk 16, chunk 17, chunk 21, chunk 23, chunk 26, chunk 27, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43, chunk 44, chunk 45, chunk 46, chunk 47, chunk 48, chunk 49, chunk 50, chunk 51, chunk 52, chunk 53, chunk 54, chunk 55, chunk 56, chunk 57, chunk 58, chunk 59, chunk 60, chunk 61, chunk 62, chunk 63, chunk 64, chunk 65, chunk 66, chunk 67, chunk 68, chunk 69, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries. Not reviewed: verification — its prompt was built, but no agent was launched with it, so the posted findings cannot be counted as verified. [Critical] docs/developers/qwen-serve-protocol.md: The PR added channel_auth to CONDITIONAL_SERVE_FEATURES but did not add a corresponding row to the protocol doc table. The contract test capabilities-docs-contract.test.ts fails. Fix: add | channel_auth | workspace-scoped Channel QR authentication sessions are wired. | to the conditional features table.

— qwen3.7-max via Qwen Code /review

});

it('is advertised as the runtime driver behind serializable QR metadata', () => {
expect(plugin.management?.auth).toEqual(['qr']);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Test asserts plugin.management?.auth is ['qr'], but the QQ plugin in packages/channels/qqbot/src/index.ts:23 declares auth: ['credentials', 'qr']. The companion test in channel-registry.test.ts correctly asserts ['credentials', 'qr'] for the same plugin. — Failure scenario: npx vitest run src/login.test.ts fails with expected ['credentials', 'qr'] to deeply equal ['qr']. CI test suite fails on every run.

Suggested change
expect(plugin.management?.auth).toEqual(['qr']);
expect(plugin.management?.auth).toEqual(['credentials', 'qr']);

— qwen3.7-max via Qwen Code /review

Comment on lines +546 to +549
record.commitStarted = true;
credentials.delete(record.id);
try {
await record.driverSession.commit(resolvedCredentials);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The expiry timer is not cleared before the await in commit(). If the TTL elapses during driverSession.commit(), the expiry timer fires → finish(record, 'expired')cancelDriver(record) calls driverSession.cancel() on the same session mid-commit. — Failure scenario: A session near its TTL limit enters commit. The await yields, expiry fires and cancels the driver mid-commit. The driver may have actually succeeded, but the caller receives channel_auth_commit_failed. The QQ driver's cancel() rejects the ready promise; the Weixin driver's cancel() aborts the controller.

Suggested change
record.commitStarted = true;
credentials.delete(record.id);
try {
await record.driverSession.commit(resolvedCredentials);
record.commitStarted = true;
credentials.delete(record.id);
clearExpiry(record);
try {
await record.driverSession.commit(resolvedCredentials);

— qwen3.7-max via Qwen Code /review

Comment on lines +1020 to 1021
channelManagementAvailable: true,
persistentWorkspaceRegistrationAvailable: true,

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] currentServeFeaturesForRunQwenServe sets channelManagementAvailable: true but omits channelAuthAvailable, so the bootstrap /capabilities response and WS feature updates never include channel_auth — even though the auth session manager is wired and the REST /capabilities endpoint correctly advertises it. — Concrete cost: Clients reading capabilities from the bootstrap envelope will not see channel_auth and may assume QR authentication is unavailable.

Suggested change
channelManagementAvailable: true,
persistentWorkspaceRegistrationAvailable: true,
channelManagementAvailable: true,
channelAuthAvailable: true,
persistentWorkspaceRegistrationAvailable: true,

— qwen3.7-max via Qwen Code /review

Comment on lines +291 to +293
const literal = source['secret'];
const secretEnv = source['secretEnv'];
delete source['secret'];

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] secretEnv is read from the webhook source but never deleted, so the environment variable name (e.g. $MY_TOKEN) leaks into the returned config snapshot — inconsistent with how secret is stripped and how regular secret keys are entirely excluded. — Concrete cost: A client calling list() receives config.webhooks.sources.<name>.secretEnv, revealing infrastructure naming conventions to an attacker with read access.

Suggested change
const literal = source['secret'];
const secretEnv = source['secretEnv'];
delete source['secret'];
const literal = source['secret'];
const secretEnv = source['secretEnv'];
delete source['secret'];
delete source['secretEnv'];

— qwen3.7-max via Qwen Code /review

Comment on lines +39 to +41
export function isSafePathComponent(value: string): boolean {
return (
isWellFormedUnicode(value) &&

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] isSafePathComponent, isSafeChannelName, isWellFormedUnicode, hasControlCharacter, and isWindowsDeviceName are the security boundary for HTTP route parameters and filesystem path construction, but none have dedicated unit tests. — Concrete cost: A future refactor could silently weaken validation without any test catching the regression.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Size review — suggest splitting

This PR is 20,773 additions across 112 files and 50 commits. The diff exceeds GitHub's 20k-line limit, making it impossible to review via the GitHub UI or API diff endpoint. Given it crosses four package boundaries (channels → cli/serve → sdk → webui → web-shell), I'd like to suggest splitting it into 3–4 smaller PRs along the natural dependency layers:

# Scope ~Lines Notes
1 channels-adapters + cli channel commands (base types, qqbot/weixin accounts/login/persistence, channel registry, config utils) ~2,100 Pure backend, independently testable
2 cli-serve daemon routes + worker management (channel-management-service, worker-group/supervisor, settings-store, state-dir, workspace routes) ~6,800 Depends on #1
3 sdk-typescript + webui hooks/actions (DaemonClient channel methods, React hooks) ~2,500 Depends on #2 route contracts
4 web-shell UI + docs + e2e (management page, editor dialog, QR auth, i18n, Playwright tests, docs) ~9,000 Depends on #3; mostly components + tests, lower review complexity

Each layer already has its own test coverage (401 daemon tests, 306 SDK tests, 255 web-shell tests, 11 Playwright scenarios), so the split should be feasible without losing verification. The intermediate states (daemon routes without UI consumers, SDK methods without frontend callers) are fine since each PR is self-tested.

The main cost is reorganizing the 50 commits, but the reviewability gain is significant — right now the diff literally cannot be loaded.

中文

本 PR 共 20,773 行新增、112 个文件、50 个 commits,diff 已超过 GitHub 的 20k 行限制,无法通过 GitHub UI 或 API 查看完整 diff。由于变更跨越 channels → cli/serve → sdk → webui → web-shell 四个包,建议按依赖层级拆分为 3–4 个 PR:

  1. channels-adapters + cli channel 命令(~2,100 行)— 底层类型、accounts/login/persistence、registry、config utils
  2. cli-serve daemon 路由 + worker 管理(~6,800 行)— 依赖 pre-release: fix ci #1
  3. sdk + webui hooks(~2,500 行)— 依赖 Where is the config saved? #2 的路由契约
  4. web-shell UI + docs + e2e(~9,000 行)— 依赖 如何自定义密钥文件 .env可能与其他文件冲突 #3,主要是组件和测试

每层已有独立测试覆盖,拆分后不会丢失验证。主要成本是重新整理 50 个 commits,但当前 diff 已无法加载,拆分后 review 可行性会大幅改善。

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Heads-up on why this PR shows no visual preview: the render job failed, so nothing was uploaded and the bot never commented — 1 failed / 29 passed.

All 29 screenshot scenarios passed (including your new channels manager and channels manager narrow zh-CN). The one failure is the flow you added, flow: open channel management and its editor in packages/web-shell/client/e2e/visuals/flows.spec.ts:

Test timeout of 60000ms exceeded.
Error: locator.click: Test ended.
  client/e2e/visuals/.playwright/flows-flow-open-channel-management-and-its-editor-chromium(-retry1)/error-context.md

It fails on both the initial run and the retry, so it's a deterministic hang, not a flake — one of the .click()s in the settings → Channels → Manage channels → Add channel → Credential Adapter chain never becomes actionable against the mock (the button/heading the flow waits for doesn't render). Since the screenshots pass, the ChannelsManagerPage itself renders fine — it's specifically the flow's navigation path that gets stuck.

Two things:

  1. Your side: fix the flow (or drop it and keep the screenshots) so the render goes green and the preview posts. The screenshots already cover the new UI well.
  2. Infra side: a single failing scenario shouldn't have erased the entire preview — that's a fragility in the visuals workflow itself (the after-capture step wasn't continue-on-error, and compose/upload weren't if: always()). I've opened fix(ci): don't let one failing scenario sink the whole visual preview #7511 to fix that: once it lands, a broken flow will show a "render incomplete" note plus the passing screenshots, instead of zero comment.
中文

这个 PR 没有视觉预览是因为渲染 job 失败了(1 failed / 29 passed),产物没上传、bot 也就没评论。

29 个截图场景全过(包括你新加的 channels manager 两个),唯一挂的是你加的 flow flow: open channel management and its editor(flows.spec.ts):60s 超时,locator.click: Test ended,初次和 retry 都挂,是确定性卡死而非 flake——settings → Channels → Manage channels → Add channel → Credential Adapter 这条链里某个 .click() 的目标在 mock 下始终没出现。截图能过说明 ChannelsManagerPage 本身渲染正常,卡的是 flow 的导航路径。

两件事:① 你这边修好这个 flow(或删掉它、保留截图),渲染转绿预览就会发出来,截图已经把新 UI 覆盖得不错了;② 基础设施那边——一个场景失败不该抹掉整个预览,这是视觉工作流本身的脆弱点,我已开 #7511 修复:合入后,坏 flow 会显示"渲染不完整"提示 + 通过的截图,而不是一条评论都没有。

@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

This PR is being replaced by a staged series so each ownership boundary can be reviewed independently:

  1. feat(serve): persist workspace channel configuration #7514 — channel metadata and workspace-scoped configuration persistence
  2. workspace runtime lifecycle integration
  3. authenticated daemon routes and SDK/WebUI client contracts
  4. WebShell channel overview and navigation
  5. manual configuration for DingTalk, WeCom, and Feishu only
  6. pairing request list and approval

QR-code authentication remains out of scope. Closing this large PR in favor of the smaller series; its screenshots and discussion remain available here for reference.

@qqqys qqqys closed this Jul 22, 2026
github-merge-queue Bot pushed a commit that referenced this pull request Jul 23, 2026
…#7511)

The web-shell visuals render runs every screenshot and flow in a single
`test:e2e:visuals`, and that step had no `continue-on-error`, while the compose
and upload steps had no `if: always()`. So one failing or timing-out scenario
failed the job, the artifact was never uploaded, and the publish workflow had
nothing to post — the entire preview vanished even when every other scenario
passed and its PNG was already on disk. A flow (a long multi-click sequence) is
the most fragile scenario kind, so the fragile one silently takes down the
deterministic screenshots. PR #7498 hit exactly this: 29 scenarios passed, one
new channel-management flow timed out, and the PR got no preview and no comment
at all.

Make the after-capture step `continue-on-error` so the passing captures survive
and the later steps still compose and upload them. The publish job only runs on
a `success` conclusion, so the job must stay green — but a masked failure must
not read as a clean preview. Ship the step's real `.outcome` (which
continue-on-error does NOT mask, unlike `.conclusion`) to the publisher as
`render-status.txt`, and have the comment builder use it: an empty preview whose
render failed says "one or more scenarios failed to render" and is explicitly
NOT the reassuring green check or the coverage-gap prompt (both imply the render
ran); a partial preview is labelled partial above the shots that did render. A
missing status file (older run) defaults to complete, so this only ever adds a
warning, never suppresses a real preview.

The failing scenario still needs fixing — it's now surfaced in the comment
rather than by silently deleting everyone else's preview.

Co-authored-by: wenshao <wenshao@example.com>
chiga0 pushed a commit that referenced this pull request Jul 23, 2026
…#7511)

The web-shell visuals render runs every screenshot and flow in a single
`test:e2e:visuals`, and that step had no `continue-on-error`, while the compose
and upload steps had no `if: always()`. So one failing or timing-out scenario
failed the job, the artifact was never uploaded, and the publish workflow had
nothing to post — the entire preview vanished even when every other scenario
passed and its PNG was already on disk. A flow (a long multi-click sequence) is
the most fragile scenario kind, so the fragile one silently takes down the
deterministic screenshots. PR #7498 hit exactly this: 29 scenarios passed, one
new channel-management flow timed out, and the PR got no preview and no comment
at all.

Make the after-capture step `continue-on-error` so the passing captures survive
and the later steps still compose and upload them. The publish job only runs on
a `success` conclusion, so the job must stay green — but a masked failure must
not read as a clean preview. Ship the step's real `.outcome` (which
continue-on-error does NOT mask, unlike `.conclusion`) to the publisher as
`render-status.txt`, and have the comment builder use it: an empty preview whose
render failed says "one or more scenarios failed to render" and is explicitly
NOT the reassuring green check or the coverage-gap prompt (both imply the render
ran); a partial preview is labelled partial above the shots that did render. A
missing status file (older run) defaults to complete, so this only ever adds a
warning, never suppresses a real preview.

The failing scenario still needs fixing — it's now surfaced in the comment
rather than by silently deleting everyone else's preview.

Co-authored-by: wenshao <wenshao@example.com>
yiliang114 added a commit to he-yufeng/qwen-code that referenced this pull request Jul 23, 2026
)

* fix(cli): correct queued message display style and ordering

Mid-turn steer messages (user input queued while the model is
responding) had two display bugs:

1. They rendered with notification styling (● icon) instead of
   user-input styling (> prefix) because accept() added them to
   UI history as MessageType.NOTIFICATION.

2. They appeared below the model's reply because accept() was
   only called in the finally block after the entire response
   stream completed, appending the user message after all model
   response items.

Fix: use MessageType.USER with sentToModel: true for steer
messages, and settle the steer input on the first stream event
(after the user-content push lands but before model-response
events are committed to UI history). Pass steer inputs through
to recursive sendMessageStream calls so all takeSteerInput paths
benefit from early settlement. Add a WeakSet guard to
settleSteerInput for idempotency across recursive invocations.

* test(core): add ordering test for early steer settlement

Verify that accept() is called after the first stream event is
pulled but before subsequent events reach the consumer, pinning
the settle-before-content timing that ensures queued user
messages render above the model's reply.

* fix(cli): use sentToModel: false for steer messages, address review

- Use sentToModel: false instead of true: steer messages are injected
  into an existing tool-result turn, not standalone user turns.
  sentToModel: true would make isRealUserTurn() count them as real
  turns, inflating the rewind turn index.
- Remove unnecessary as HistoryItemWithoutId cast.
- Add post-cleanup assertion in ordering test to verify the WeakSet
  guard prevents double-settlement.

* fix(cli): align resumed mid-turn steer display with live session (#7381)

Resume path now renders mid_turn_user_message as MessageType.USER with
sentToModel: false, matching the live-session styling. Add a comment
documenting the intentional sentToModel: false choice.

* fix(cli): exclude steer messages from user-turn filters (#7381)

Steer messages (sentToModel: false) were counted as real user turns by
five downstream consumers that filter on type === 'user' without checking
sentToModel, breaking cancel auto-restore, telemetry turn count, prompt
recall, away-recap thresholds, and resume collapse boundaries.

Add sentToModel !== false guards at each site.

* test(cli): add coverage for sentToModel !== false guards (#7381)

* test(cli): add coverage for sentToModel !== false guard in input-history filter (#7381)

* test(cli): add coverage for sentToModel !== false guard in YOLO turn-count telemetry (#7381)

* fix(cli): restore corrupted docs and classify steer items as synthetic (#7381)

* fix(docs): restore corrupted autogenerated input names in GitHub Action docs (#7381)

* fix(cli): deduplicate findLastUserItemIndex and add steerInput forwarding test (#7381)

* fix(cli): keep code-block copy numbering continuous across steer items (#7381)

* test(core): add Hook continuation steerInput forwarding test

Verify that steerInput is forwarded through the Stop-hook
continuation path and settled early on the first content event
of the continuation turn, matching the existing Steer
continuation coverage.

* fix(cli): sync selection test fixtures with ink FrameCell/ReadonlyFrame types (#7381)

* fix(core): align cron day wildcard semantics (#7464)

Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>

* feat(core): keep completed background agents resident (#7426)

* feat(core): keep background agents resident

* fix(core): harden background continuation boundaries

* docs(core): move per-spawn cleanup comment to subagentDispose

The comment describing the per-spawn cleanup (which stays undefined on
the fork-resume path) had drifted above the launchModel declaration,
where it no longer applied and could mislead readers. Relocate it to the
subagentDispose assignment in the non-fork branch it actually documents.

* fix(core): close finishing window and release resident on error in background GOAL path

- Non-worktree GOAL completion drained the message queue but never called
  registry.beginFinishing(), unlike the worktree path. A send_message racing
  the terminal transition could be accepted (status still running,
  finishingAgents empty) and then orphaned by complete(). Call beginFinishing()
  after the empty drain to reject the racing message instead.
- The completion catch block never reset keepResident, so a throw from
  patchAgentMeta/registry.complete left the runtime resident but finalized as
  failed — a zombie that cleanupRuntime never reclaimed. Reset keepResident in
  the catch so the finally block disposes it.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* ci(autofix): continue environment-specific fixes (#7444)

* ci(autofix): continue environment-specific fixes

* docs(autofix): align verification wording

* docs(autofix): require bundle before integration tests

* docs(autofix): scope surrogate verification rules

* docs(autofix): require focused tests before integration checks

* docs(autofix): clarify review verification guidance

* fix(acp-bridge): close prompt-terminal follow-ups from the PR #7400 self-review (#7453)

* fix(acp-bridge): close prompt-terminal follow-ups from PR #7400 self-review

Keep a removed RUNNING prompt visible to the teardown flush via a removed flag so its terminal still publishes when the session closes before the agent cooperates; gate broadcastTurnError's session turn-state mutation to running prompts; propagate the typed PromptDeadlineExceededError from the pre-dispatch abort check; document the deadline FIFO-release overlap trade-off, the trailing prompt_cancelled after flush, and the result.then/finally ordering invariant; route the dedup log to the debug channel; drop the prompt-deadline re-export that pulled the bridge into a leaf module.

Fixes #7451

* test(acp-bridge): cover promote-then-remove-then-settle duplicate completed guard (#7453)

---------

Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>

* fix(core): strip Qwen-internal daemon secrets from agent-spawned child env (#7256)

* fix(core): strip Qwen-internal daemon secrets from agent-spawned child env

Shell subprocesses (and the monitor tool and stdio MCP servers) inherited
the full daemon process.env, including QWEN_SERVER_TOKEN (the serve-daemon
bearer credential), so an agent-run command like printenv QWEN_SERVER_TOKEN
could read an internal secret. Add a shared sanitizeChildEnv() that removes
Qwen-internal daemon/server tokens (QWEN_SERVER_TOKEN, QWEN_DAEMON_TOKEN)
before spawning, and apply it at the shell child_process + PTY paths,
monitor.ts, and the mcp-client stdio transport.

The denylist is deliberately narrow: it does NOT strip third-party
credentials (GH_TOKEN, AWS_*, NPM_TOKEN, ...) that real shell workflows
legitimately inherit -- only Qwen-internal secrets. Exported from the
package root so the desktop denylists can consolidate onto it later.

Fixes #6601.

* test(core): cover daemon-secret stripping on monitor and mcp-client spawn sites

* test(core): replace process.env instead of mutating in shell sanitization tests

The file restores process.env by reference in afterEach, so in-place key
mutations leaked into later tests. Use the replacement pattern already used
by setupConflictingPathEnv.

* docs(core): align JSDoc @param names with actual function signatures (#7492)

Fix 6 instances where JSDoc @param tags had drifted from their
corresponding function signatures — parameters were renamed, removed,
or undocumented over time but the doc blocks were not updated.

Closes #7446

* feat(serve): support forced MCP reconnects (#7488)

* feat(serve): support forced MCP reconnects

* test(serve): cover forced MCP reconnect options

---------

Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>

* fix(cli): insert newline on Shift+Enter and stop streaming thinking-block flicker (#7397)

* fix(cli): re-push Kitty keyboard flags onto the alternate screen in VP mode

In VP mode the app renders on the alternate screen (`alternateScreen: true`),
but the Kitty keyboard progressive-enhancement flags were pushed only once at
startup on the main screen. The Kitty spec tracks these flags per screen
buffer, so the alternate screen's stack stays empty and the terminal never
reports modifiers: Shift+Enter arrives as a bare Enter (submit) or, when the
terminal emits an ESC-prefixed variant, as an orphaned Escape that trips the
empty-buffer double-Esc rewind prompt — so Shift+Enter can never insert a
newline in VP mode even on Kitty-capable terminals (e.g. cmux).

Re-push the flags onto the alternate screen right after Ink enters it (Ink
writes the enter-alt-screen sequence synchronously inside render(), so the
push is correctly ordered). Ink discards the alternate screen and its flag
stack on unmount, leaving the startup main-screen push balanced by the
existing disableKittyProtocol() on cleanup.

Generated with AI

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

* fix(cli): stabilize streaming thinking block height to stop flicker

The pending "Thinking…" block renders the tail of the reasoning stream in a
content-sized box. As the model emits paragraph separators, a blank line
enters and leaves the tail window (and `trimEnd` drops trailing blanks), so the
visible line count oscillates and the block flickers 2→3→5 rows during
streaming.

Track the tallest height the block has reached for the current thought and
never render fewer rows than that (capped at the streaming window size),
padding at the top so the newest line stays pinned to the bottom. The tracker
resets when streaming ends or when the buffer shrinks (a new thought replaced
it), so height is monotonic within a thought without leaking across thoughts.

Generated with AI

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

* fix(cli): decode xterm modifyOtherKeys Shift/Ctrl/Alt+Enter so it inserts a newline

Terminals such as Ghostty report Shift+Enter as the xterm modifyOtherKeys
sequence `ESC [ 27 ; <mods> ; <key> ~` (e.g. `ESC [ 27 ; 2 ; 13 ~`) when the
Kitty keyboard protocol is not negotiated — which is the default, since Kitty
detection does not always succeed. Two bugs kept this from inserting a newline:

1. The CSI-u parser read the leading `27` marker as the key code (matching the
   Escape key code 27) instead of the real key code in the third parameter, so
   with Kitty enabled Shift+Enter was mistaken for Escape and tripped the
   double-Esc rewind prompt.
2. The reassembly path that stitches readline's shredded CSI fragments back
   together was gated behind `kittyProtocolEnabled`, so with Kitty disabled the
   `ESC [ 27 ; 2 ;` head plus the stray `13~` tail leaked into the composer as
   literal text and no newline was inserted.

Decode the third parameter as the real key code for the `27;…~` form, and route
those sequences through the reassembly buffer even when Kitty is off (only the
`ESC [ 27` marker opts in, so keys readline already parses cleanly are
untouched). Shift/Ctrl/Alt+Enter now insert a newline in both VP and non-VP
mode regardless of Kitty negotiation.

Generated with AI

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

* fix(cli): anchor VP viewport to the top until a conversation turn exists

On a fresh VP-mode session the virtualized list holds the banner plus startup
notices (tips / MOTD / info), so it is longer than one item. Keying the initial
scroll anchor off list length alone selected scroll-to-end, which pinned the
banner to the bottom of the full-height viewport and left the top half of the
screen blank.

Anchor to the top until there is an actual conversation turn (a user/user_shell
history item or a pending response), then resume scroll-to-end so the latest
output stays in view. Startup notices no longer count as content that forces
bottom alignment.

Generated with AI

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

* fix(cli): stabilize streaming thinking window against availableTerminalHeight drift

The grow-only streaming thinking window still flickered because its line cap was
derived from availableTerminalHeight. While a thought streams the terminal keeps
constrainHeight on, so availableTerminalHeight (and the derived maxLines) drifts
up and down as sibling pending content grows, and the grow-only clamp
`min(maxLines, …)` shrank the block whenever it dipped.

Use a constant window height (MAX_STREAMING_THINKING_VISUAL_LINES) for the
pending window instead. The window is only a few lines, so a fixed cap cannot
meaningfully overflow (VP scrolls anyway), and the height stays stable while
still growing monotonically within a thought.

Generated with AI

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

* Revert "fix(cli): anchor VP viewport to the top until a conversation turn exists"

This reverts commit fbe86a9e159b75ea1f5b689cc327599c9dc91090.

* fix(cli): guard modifyOtherKeys detection against keypresses without a sequence

The modifyOtherKeys prefix check ran on every keypress, but some synthetic
keypresses (and the useKeypress test harness) emit a key with no `sequence`,
so `key.sequence.startsWith(...)` threw an unhandled rejection. Use optional
chaining so a missing sequence is simply not a modifyOtherKeys start.

Generated with AI

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

* test(cli): mock pushKittyProtocolFlags in gemini.test.tsx kitty mock

The kittyProtocolDetector mock omitted the newly added pushKittyProtocolFlags
export. Add it so the mock stays in sync with the real module and a VP-mode
startup path exercised through this suite cannot hit an undefined call.

Generated with AI

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

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): open singleton subagent details (#7495)

Co-authored-by: ytahdn <ytahdn@gmail.com>

* fix(web-shell): avoid redundant git status requests (#7496)

Co-authored-by: ytahdn <ytahdn@gmail.com>

* fix(agent): ignore empty working_dir placeholders (#7343)

* fix(agent): ignore empty working_dir placeholders

* test(agent): align empty working_dir expectations

* feat(prompts): allow overriding core identity via QWEN_SYSTEM_IDENTITY_MD (#7478)

* feat(prompts): update prompts.ts for QWEN_SYSTEM_IDENTITY_MD

* feat(prompts): update prompts.test.ts for QWEN_SYSTEM_IDENTITY_MD

* fix(prompts): address CR on QWEN_SYSTEM_IDENTITY_MD

Keep getDefaultCoreIdentitySentence private, fail loud on path
resolution errors, use trimEnd, and resolve identity only on the
default-prompt branch.

* test(prompts): align identity override tests with CR feedback

Sample default identity from live prompt, cover trimEnd trailing
whitespace, and assert homedir resolution failures throw.

---------

Co-authored-by: 易良 <1204183885@qq.com>

* fix(cli): yield to single-slot background agents (#7258)

Co-authored-by: hogeheer <267467744+hogeheer499-commits@users.noreply.github.com>

* docs(autofix): require evidenced pre-commit verification, not a bare "verified" (#7486)

* docs(autofix): require evidenced pre-commit verification, not a bare "verified"

The skill already said to run build/typecheck/lint/Vitest before
committing, but softly — and #7408 committed a fix with a TS error the
gate then rejected while its summary claimed "verified all 3 commits".
A self-assessment the gate contradicts wastes a whole round.

Strengthens the address-review contract from "run the checks" to:
- actually run them, do not assert them from reading the diff;
- if typecheck or a touched-package test fails, do NOT commit — treat
  the feedback as unresolved (failure.md);
- end address-summary.md with a `## Verification` section listing each
  command run and its result; a bare "verified" is not acceptable.

The framing is structural, not etiquette: the deterministic gate re-runs
the same commands and discards the round on any failure, so skipping them
only moves the rejection later. Pinned by a test so it cannot soften back.

This is the checkable half of "audit before committing" — the
undirected/reverse-audit-until-clean practice does not transfer to an
unsupervised agent (no verifiable stopping condition, and it worsens the
timeouts seen on large PRs), but "run the gate's own checks first and
show the evidence" does.

* fix(autofix): clarify Verification section precedes collapsed Chinese translation (#7486)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>

* feat(autofix): stop a PR that fails to push for N rounds in a row (#7482)

* feat(autofix): stop a PR that fails to push for N rounds in a row

Under takeover the round cap is 100, which is right for a PR that needs
many PRODUCTIVE rounds. It is wrong for one that fails every round: #6723
ran 7 consecutive failed rounds (3 agent timeouts at 50 min, 4 gate
rejections whose fix broke tests) over 8 hours, heading for round 100,
because it is a 5700-line, 47-file, 5-day-old PR racing a fast-moving
main — every round re-resolves a conflict it cannot finish or that fails
the gate. Retrying at the same per-round budget will not converge; a
human has to rebase or split it.

Adds CONSECUTIVE_FAILURE_CAP (5), distinct from the total round cap. The
handoff step already runs only when a round did NOT push, so it counts
the unbroken run of prior failure markers — stopping at the first push
("Addressed the latest review feedback") or legitimate no-op ("no
changes needed"), either of which proves progress and resets the streak.
At the cap it forces the terminal round even under takeover, with a
handoff that names the real fix (rebase/split, then /retry). Cause-
agnostic: a timeout and a gate rejection both count.

* fix(autofix): address review feedback on consecutive-failure circuit breaker (#7482)

- Fix misleading comment: the walk is oldest-first (API order) with
  reset-on-success, not newest-first with early stop
- Prefer the already-fetched ic.json over a redundant gh api call,
  falling back to the API only when the file is missing
- Filter eval markers by re-arm window (win=) so pre-re-arm failures
  do not immediately re-terminate a re-armed PR
- Add test coverage for the MARK_ROUND == MAX_ROUNDS guard and for
  window-scoped streak counting

* fix(autofix): exempt transient model errors from consecutive-failure breaker (#7482)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>

* feat(core): restore background agent roster (#7459)

* feat(core): restore background agent roster

* fix(web-shell): add list_agents to TOOL_DISPLAY_NAMES

The new list_agents core wire tool was added to core's ToolNames but not
to the web-shell TOOL_DISPLAY_NAMES map, causing toolFormatting.drift.test.ts
to fail (expected ['list_agents'] to deeply equal []). Add the missing
'ListAgents' display-name entry so the browser panel shows a friendly name
instead of the raw wire name and the drift guard passes.

* fix(cli): reload old-session background agents on failed resume rollback

When /resume fails after core has swapped but before the UI swap, the catch
block rolls core back to the old session via startNewSession(oldSessionId).
However the forward path already called resetBackgroundStateForSessionSwitch,
which cleared the old session's in-memory background agents. The rollback did
not reload them, so list_agents returned empty for the old session (whose
sidecars are still on disk) until the next process start or successful resume.

Reload the old session's paused background agents after rolling core back, so
the restored roster matches on-disk state. Placed after startNewSession so the
loadPausedBackgroundAgents current-session guard is satisfied; best-effort via
.catch so it never blocks the rollback path.

* fix(web-shell): add zh translation for list_agents tool name

The toolFormatting test 'has a zh translation for every tool in the
display-name map' failed with expected ['list_agents'] to deeply equal []
because list_agents was added to TOOL_DISPLAY_NAMES without a matching
toolName.list_agents zh-CN entry. Add the translation to restore parity.

* fix(cli): resolve CI failures for background-agent roster restore

- Add toolDisplayName.ListAgents translations (en, zh, zh-TW, ca) so the
  new list_agents tool has a zh entry; fixes i18n/index.test.ts.
- Add loadPausedBackgroundAgents and consumePendingRecoveredAgentsNotice
  to the acpAgent worktree test config mock, which loadSession now calls
  via #restoreBackgroundAgentsOnResume; fixes acpAgent.worktree.test.ts.

* refactor(core): extract incompatible-isolation blocked reason to a const

Move the incompatible-isolation blocked-reason string out of an inline
literal into a module-level INCOMPATIBLE_ISOLATION_BLOCKED_REASON const,
matching its four sibling reasons so the text is discoverable by
constant-name grep and edited alongside the others.

* fix(core): preserve retained activity state on failed agent revive

Address review feedback on the background-agent roster restore:

- On a failed completed-agent revive, restore UI state with a non-empty
  guard instead of `??`. Because `restorePausedEntry` resets the paused
  entry's `recentActivities` to `[]`, the previous `failedEntry?.field ??
  completedEntry.field` kept that empty array and dropped the pre-revive
  snapshot (the UI Progress section rendered empty). Applied consistently
  to pendingMessages, recentActivities, and pendingApprovals.

Add regression coverage for previously untested paths:

- failed revive preserves pre-revive recentActivities
- terminal-agent cap admits only the newest MAX_RETAINED_TERMINAL_AGENTS
  completed sidecars on restore
- /resume rollback reloads the old session's background agents
- headless resume prepends the recovered-agents notice to the prompt

* test(cli): cover interrupted-turn continuation not consuming recovered-agents notice

Add ACP and headless regression tests asserting an interrupted-turn
continuation does not consume the one-shot recovered-agents notice
(the !isContinue / !continueInterrupted guards), so it is delivered on
the user's next ordinary prompt. Mirrors the existing slash-command
coverage.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat(cli): support custom skill directories via settings (#7395)

* feat(cli): support custom skill directories via settings (#7394)

Add skills.directories setting that accepts an array of additional
directory paths to scan for skills (SKILL.md files). Paths support
~ expansion. Directories are scanned recursively at user level,
after the default ~/.qwen/skills/ directory.

Example settings.json:
{
  "skills": {
    "directories": ["~/.agent/skills", "~/.claude/skills"]
  }
}

Changes:
- settingsSchema.ts: add skills.directories array setting
- core Config: add customSkillDirs param and getCustomSkillDirs()
- SkillManager: append custom dirs to user-level skill base dirs
- CLI config: read skills.directories and pass to core Config

* fix(cli): regenerate settings schema for skills.directories (#7394)

* fix(core): address review feedback for custom skill directories (#7395)

- Use optional chaining for getCustomSkillDirs() to prevent TypeError
  on partial Config mocks (workspace-skill-management, workspace-skills-status)
- Reuse expandHomeDir utility instead of inline tilde expansion
- Fix inaccurate 'scanned recursively' wording to 'one level deep'
- Correct JSDoc: paths are raw, expansion happens in SkillManager
- Trim whitespace from custom dir entries in CLI layer
- Add tests for custom dir expansion, dedup, and partial config safety

* fix(core): address review feedback for custom skill directories (#7395)

* fix(core): address review feedback for custom skill directories (#7395)

* test(core): add relative path resolution test for custom skill dirs (#7395)

* fix(cli): add Array.isArray guard for skills.directories and safe mode test (#7395)

* fix(skills): address review feedback on custom skill directories (#7395)

- Add bare mode test for skills.directories guard
- Include resolved absolute path in relative directory warning
- Clarify that dedup applies to default user dirs, not bundled skills
- Regenerate settings schema

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>

* fix(core): add image modality support for qwen3.8-max and kimi-k3 models (#7491)

* fix(core): add image modality support for qwen3.8-max models

qwen3.8-max-preview supports image input but was falling through to the
catch-all text-only rule because no pattern matched it. This caused the
vision bridge to unnecessarily transcribe images via a secondary model
instead of sending them directly to the primary model.

* fix(core): also add image modality for kimi-k3

Kimi K3 officially supports image + video input but was falling through
to the catch-all text-only rule, same issue as qwen3.8-max.

* fix(dingtalk): preserve non-bot mention context (#7473)

* fix(dingtalk): preserve non-bot mention context

* test(dingtalk): cover plural mentions, staffId fallback, and edge cases (#7473)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>

* fix(core): harden the usage salvage around session deletion (#7425)

Post-merge review follow-ups on #7391 (three findings):

- Salvage the archived transcript in the active-branch deletion too:
  when both copies co-exist (an interrupted archive) and the fresh
  active transcript carries no telemetry, the archived copy holds the
  session's usage history and was deleted unsalvaged. The dedup guard
  makes the extra call a no-op whenever the active copy already wrote.
- Enforce the "never blocks deletion" contract at the call site: a
  salvageUsageBestEffort wrapper catches and warns, so the guarantee is
  structural rather than an implementation detail of
  persistUsageBeforeTranscriptDeletion. The new failure-tolerance test
  (salvage rejects -> deletion still succeeds) fails without the
  wrapper — the bare await let the rejection escape through
  removeSessionFiles' rethrowing catch.
- Clear the salvage module mock in beforeEach so the wiring test's
  invocationCallOrder assertions can never read stale calls.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(core): make fork subagents discoverable (#7460)

* test(core): cover Shell truncation without an artifact (#7470)

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

* fix(ci): autofix route checks existing labels on non-trigger label events (#7481)

* fix(ci): autofix route checks existing labels on non-trigger label events

When triage adds multiple labels in sequence, per-issue concurrency
cancels earlier runs. If the last label is not a trigger label
(e.g. scope/build-system), the surviving run skips the issue phase
even though the issue already has autofix/approved +
status/ready-for-agent.

Before ignoring a non-trigger label event, check ISSUE_LABELS_JSON
for both required labels. If present and the issue is open, proceed
with the issue phase. Trust was already established when the trigger
labels were applied (both require triage+ permission).

* fix(ci): require trusted sender for label fallback

* feat(cli): preserve semantic text when copying VP selections (#7286)

* docs(cli): define semantic copy fidelity scope

* docs(cli): address semantic frame review gaps

* docs(cli): preserve soft-wrap source separators

* feat(cli): preserve semantic selection copy

* fix(cli): address semantic copy review findings

* fix(cli): preserve clipped semantic boundaries

* fix(cli): limit separator carrier joiner to visible width in wrap metadata

The greedy /\s+/ match in wrapTextWithMetadata could capture more
source whitespace than the separator carrier row actually consumed
(e.g. a tab following a space), causing duplicated whitespace in
semantic copy. Limit the match to visibleLine.length characters and
add a mixed space/tab regression test.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>

* test(core): stub the registry methods agent.ts actually calls (#7538)

The shared stubRegistry in agent.test.ts was missing six methods that
agent.ts reaches: bridgeApprovalEvents, getQueuedCount,
registerResidentAgent, restartCompletedAgent, unregisterResidentAgent and
waitForMessages.

That is not a benign omission. The background body wraps its work in a
try/catch that routes any throw into registry.fail(), so a missing method
never surfaces as 'not a function' — it silently converts a successful
run into a failed one. On the GOAL completion path
unregisterResidentAgent is called immediately before complete(), so the
TypeError replaced the completion entirely:

  registry.fail('fork-...', 'registry2.unregisterResidentAgent is not a
  function', ...)

That is what broke 'runs a non-interactive fork through the background
registry' on main. #7460 added the registry.complete assertion, which
exposed the incomplete stub — before it, nothing checked whether the
background body finished successfully and the TypeError was swallowed.

Stub all six with their real return shapes (unregisterResidentAgent
returns boolean, bridgeApprovalEvents returns the unsubscribe callback
agent.ts later invokes, waitForMessages resolves to a list) and assert
registry.fail was not called before asserting completion, so a future
gap reports the actual error instead of 'complete: 0 calls'.

* perf(startup): lazy-load Google GenAI SDK on first use (#7512)

* perf(startup): lazy-load Google GenAI SDK on first use

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

* codex: address PR review feedback (#7512)

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

* codex: address PR review feedback (#7512)

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

---------

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

* fix(vscode): use file picker image paths for vision input (#7493)

* fix(vscode): use image paths from file picker

* fix(vscode): keep image picker paths raw

* fix(vscode): resolve image picker paths on submit

* fix(vscode): send picked images as vision context

* fix(vscode): encode prompt image file URIs

* fix(vscode): address image path review comments

* test(vscode): cover image file reference edge cases

* fix(cli): open the actual serve fallback port (#7501)

* fix(cli): open actual serve fallback port

* test(cli): match serve URL to fallback listener

* docs(cli): clarify serve listen error handling

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(ci): don't let one failing scenario sink the whole visual preview (#7511)

The web-shell visuals render runs every screenshot and flow in a single
`test:e2e:visuals`, and that step had no `continue-on-error`, while the compose
and upload steps had no `if: always()`. So one failing or timing-out scenario
failed the job, the artifact was never uploaded, and the publish workflow had
nothing to post — the entire preview vanished even when every other scenario
passed and its PNG was already on disk. A flow (a long multi-click sequence) is
the most fragile scenario kind, so the fragile one silently takes down the
deterministic screenshots. PR #7498 hit exactly this: 29 scenarios passed, one
new channel-management flow timed out, and the PR got no preview and no comment
at all.

Make the after-capture step `continue-on-error` so the passing captures survive
and the later steps still compose and upload them. The publish job only runs on
a `success` conclusion, so the job must stay green — but a masked failure must
not read as a clean preview. Ship the step's real `.outcome` (which
continue-on-error does NOT mask, unlike `.conclusion`) to the publisher as
`render-status.txt`, and have the comment builder use it: an empty preview whose
render failed says "one or more scenarios failed to render" and is explicitly
NOT the reassuring green check or the coverage-gap prompt (both imply the render
ran); a partial preview is labelled partial above the shots that did render. A
missing status file (older run) defaults to complete, so this only ever adds a
warning, never suppresses a real preview.

The failing scenario still needs fixing — it's now surfaced in the comment
rather than by silently deleting everyone else's preview.

Co-authored-by: wenshao <wenshao@example.com>

* feat(web-shell): add selective shadow DOM isolation (#7551)

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>

* feat(web-shell): add renderChatHeader slot for custom session header (#7553)

* fix(cli): say review coverage gaps in the author's units, not chunk ids (#7550)

The posted review body rendered coverage disclosures with the run's own
bookkeeping as subjects: bare chunk ids, unsorted, one per subject. On a
run that certified nothing (PR #7268) the body enumerated all 49 chunk ids
across two sentences while opening with "Reviewed. Suggestions are
inline." — the opener certified the exact thing every following sentence
took back, and nothing on the PR page maps a chunk id to code.

Three changes, all render-time — the structural entries, the caps, the
caller-echo dedup and the stderr remediation still key on chunk ids, which
is where the id is the selector a reader can act on:

- Coverage now returns the plan's chunk→files table (DiffChunk.files was
  already in the plan JSON; the coverage type slice dropped it).
- compose-review renders chunk gaps through describeChunkGap: every
  planned chunk collapses to "the entire diff", a narrow gap with known
  files names the files, and anything wider is counted against the plan's
  total. Applied to the receipt sentence, the uncoverable sentence (bare
  CLI entries only — caller-authored entries render verbatim) and the
  grouped per-cause sentences.
- The COMMENT opener may no longer say "Reviewed." over a disclosure set
  that denies it: when no chunk is both covered and undisclosed — or no
  chunk universe could be read at all — it opens with a zero-certified
  warning instead. A rewritten launch demonstrably read its chunk, so
  coverage alone is not the test; certified is covered with no disclosure
  against it.

Co-authored-by: verify <verify@local>

* fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal (#7490)

* fix(autofix): retry a skipped-Prepare instead of stranding the PR terminal

A base/infra failure BEFORE the agent runs was misread as an agent crash
and terminated the PR forever. When an early step fails — installing or
building the trusted base, checkout, node setup — the `Prepare branch and
feedback` step is skipped, so NEWEST is empty, and the report step's
"crashed before reading feedback" branch fired: MARK_ROUND=MAX_ROUNDS,
terminal, scan skips it on every future tick.

Observed: a web-shell TypeScript break on `main` failed `Install
dependencies and build` (which builds the trusted base) across a whole
scan batch, and SIX healthy PRs were stranded terminal at round=100 in
one run — including ones at round 9 and 11 that had nothing to do with
the break. `round=100` there is a terminal sentinel, not 100 attempts.

NEWEST-empty now splits on steps.prepare.outcome:
- 'skipped' (an earlier step failed, the agent never ran) is infra/base
  and transient: retry with a sentinel ts so the feedback stays live,
  incrementing the round so a PERSISTENTLY broken base is still bounded
  and stops at the cap (recoverable with /retry).
- 'success'/'failure' (Prepare ran, no feedback produced) is a genuine
  pre-read agent crash: unchanged terminal behaviour.

This is the reverse of the asymmetry #7482 addresses: that bounds a
crash AFTER reading that retried forever; this stops a transient failure
BEFORE reading from going terminal after one.

* docs(autofix): note a pre-Prepare cancel also retries intentionally (#7490)

* fix(autofix): also retry a cancelled/empty prepare outcome, not just skipped

A previous review comment on this PR noted that a job cancelled before
Prepare should retry too. It was right about the intent but the code did
not do it: `steps.prepare.outcome` is 'cancelled' for a cancel and '' for
a job that stopped before Prepare entered the step context — both DISTINCT
from 'skipped', so `== 'skipped'` sent them to the terminal branch, the
same over-termination this PR exists to fix.

Match on "not a real Prepare run" (`!= 'success' && != 'failure'`)
instead, so skipped, cancelled, and empty all retry; only a Prepare that
actually ran to a verdict (success/failure) with no feedback stays
terminal — the genuine pre-read agent crash. Test extended to drive the
cancelled and empty cases (retry) and both real-run outcomes (terminal);
mutation-verified that reverting to `== 'skipped'` reddens the cancelled
case.

* test(autofix): update the pre-read-crash case for the broadened retry

The prior commit broadened NEWEST-empty retry to skipped/cancelled/empty
but left the older 'replays the handoff decision' test asserting the old
terminal behaviour for an unset PREPARE_OUTCOME (which now retries). That
test's terminal cases now set PREPARE_OUTCOME=success/failure explicitly —
the only outcomes that still terminate — so it exercises the genuine
pre-read agent crash rather than the infra/cancel path.

* test(autofix): anchor the skipped-Prepare extraction past the CONSEC block

CI reddened `retries a skipped-Prepare` after main's consecutive-failure
cap (#7482) merged into this branch: that block was inserted between this
decision block and the report `{`, and it calls `gh api`. The test's
`{`-anchored regex over-captured through it, so the extracted script ran
the unstubbed `gh api` and failed. Anchor the end on the same
`# Consecutive-failure` comment the sibling gate-crash test already uses,
so the extraction stops at this decision block's own closing `fi`.

* fix(autofix): exempt skipped-Prepare from the consecutive-failure breaker

A broken base build skips Prepare, producing no API error file — so the
consecutive-failure breaker ran on the new retry path and, after 5
scans, re-introduced the exact mass-stranding this PR exists to prevent.
Exempt pre-agent infra failures (skipped/cancelled/empty outcome) from
the breaker, mirroring the transient 429/5xx exemption: same failure
class (not the PR's fault, self-heals, hits the whole batch). The round
cap + sentinel-ts /retry recovery already bounds a persistently broken
base.

Also trim "checkout" from the retry headlines (checkout failures do not
land in this branch) and hoist the duplicated MARK_TS assignment.

* fix(autofix): reset the consecutive-failure streak on prior infra-failure markers

The streak walker counted prior infra-failure headlines ("AutoFix could
not start —…") as failures, inflating the consecutive-failure count on
subsequent rounds.  A PR with 3 real agent failures, then 3 rounds of
base-build infra failures, then 1 more real failure would trip the
cap-5 breaker even though only 4 rounds were the PR's fault.

Add the two infra-failure headline patterns as reset strings in the
streak walker, alongside the existing push and no-op resets.  The
genuine agent-crash headline ("AutoFix could not start evaluation —…")
is deliberately excluded — it is a real failure and must still count.

* fix(autofix): clarify infra-failure headlines and else-branch comment (#7490)

Address review nits: the retry headline now mentions cancelled runs,
the cap headline says 'reached the round cap' instead of overstating
'could not start for N rounds', the else-branch comment says 'prepare
itself crashed' instead of 'agent crash', and the streak-reset pattern
is simplified now that both infra headlines share the same prefix.

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>

* fix(cli): keep role codenames and brief paths out of the posted review body (#7560)

The posted body still carried two operator registers #7550 left in place:
roster role subjects rendered their internal codenames ("Agent 1c:
Cross-file tracer", "Test coverage matrix (whole-diff)"), and an unread
brief's disclosure interpolated its filesystem path. And when verify and
the reverse audit failed the same way, the body said it twice, in two
near-identical sentences.

- Every Brief now carries a publicLabel — the dimension said as what it
  checks ("the cross-file consistency pass") — and coverage's structural
  disclosures carry it as publicSubject beside the internal subject, plus
  a path-free publicReason for unread briefs. The internal label and the
  path stay on stderr, where they are the selector an operator acts on;
  every dedup and certification check still keys on the internal subject.
- compose-review renders the public fields and groups by the reason the
  body PRINTS, so two unread briefs share one path-free sentence instead
  of repeating it per role.
- verificationGaps merges verify and reverse-audit failures of the same
  delivery shape into one sentence with both subjects and both
  consequences; mixed shapes keep their precise per-role texts, and the
  per-role rebuild commands stay on stderr either way.

Co-authored-by: verify <verify@local>

* fix(autofix): retry an agent timeout instead of advancing past its feedback (#7563)

A timeout evaluated NOTHING — the agent ran out of budget before finishing,
so nothing was committed and the feedback is unaddressed. It was treated as
an evaluated verdict (real ts, watermark advances), which strands that
feedback: the next scan sees "nothing new" and never retries. Observed on
#7471 (round 13/100), a heavily-reviewed 1871-line PR: rounds 11 and 13
timed out, but round 12 pushed — so a timeout is transient far more often
than not, and advancing past it left the round-13 feedback unhandled.

run-agent.mjs now drops an `agent-timeout` signal on result.timedOut, and
the handoff routes it like a pre-verdict crash: sentinel ts (feedback stays
live) and a retry, with a headline that names the real fix at the cap
(split the PR or raise the budget). A PR that PERSISTENTLY times out is
bounded by the round cap and the consecutive-failure cap, so this cannot
loop forever — it just stops treating a one-off budget blip as a verdict.

The loop guard stays terminal (a tool-call loop is a real defect, not a
budget blip). An API error still routes to its own model-key handoff; the
timeout signal is written only when NOT an API error.

Co-authored-by: wenshao <wenshao@example.com>

* feat(serve): add workspace-level generation (#7552)

* feat(serve): add workspace-level generation

* docs(serve): document workspace generation capability

* fix(serve): align workspace generation contracts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>

* ci: matrix ECS runner update + sudo install + repository_dispatch trigger (#7513)

* ci: matrix ECS runner update with sudo install

- Use matrix strategy (ecs-update-sg, ecs-update-64c) to update both
  physical ECS hosts in parallel (fail-fast: false).
- Always use sudo npm install -g so the package lands in /usr/local
  (system-wide PATH) instead of the runner user's home directory.
- Move concurrency to job level (matrix context not available at
  workflow level per actionlint).
- Add repository_dispatch trigger for release-driven updates.
- Register new runner labels in actionlint.yaml.

* fix(ci): use dispatch version for runner update

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(web-shell): include managed id in artifact open requests (#7570)

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>

* feat(serve): persist workspace channel configuration (#7514)

* feat(serve): persist workspace channel configuration

* fix(serve): harden channel settings snapshots

* fix(serve): validate startup channel names

* fix(serve): reserve all channel name

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(sdk-python): require canonical form in validate_session_id (#7532)

uuid.UUID() accepts several non-canonical spellings — braced
{...}, urn:uuid:..., and dash-less hex — so validate_session_id let them
through after the RFC 4122 variant check. The value is then forwarded to
the CLI verbatim as --session-id/--resume, producing a malformed session
id downstream rather than a clear error at the SDK boundary.

Reject anything whose canonical form differs from the input. Case is
deliberately not part of the comparison: UUID() lowercases, and an
all-uppercase spelling is still valid canonical input.

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(web-shell): sync background agent status (#7561)

* fix(web-shell): sync background agent status

* fix(web-shell): harden background agent reconciliation

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>

* feat(core): propagate trusted daemon invocation context (#7279)

* feat(core): propagate trusted daemon invocation context

* test(cli): update ACP startup expectation

* refactor(core): centralize ACP capability env key

* test(cli): update worktree ACP core mock

* test(integration): run daemon context smoke on PRs

* test(ci): update no-AK smoke expectation

* test(core): cover invocation context isolation

* fix(cli): compare ACP capability safely

* fix(docs): restore GitHub action input names

* fix(core): sanitize private ACP capability from child env

* fix(core): reuse private ACP capability env constant

* test(cli): cover malformed trusted invocation context

* test(acp-bridge): assert exact child environment

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: 易良 <1204183885@qq.com>

* fix(feishu): await stream cancels in media download teardown (#7465)

* fix(feishu): await stream cancels in media download teardown

downloadMedia left two reject paths' stream teardown unawaited:

- the oversize-stream path called reader.cancel() without awaiting, so a
  cancel error during teardown became an unhandled rejection (fatal under
  Node's default --unhandled-rejections=throw);
- the Content-Length reject path returned without cancelling resp.body,
  leaving the connection pinned until GC.

Both were already fixed for the sibling DingTalk downloader in #7361 (which
was itself modelled on this Feishu code), so this brings Feishu to parity.
Adds a regression test that pins the reader.cancel() await via a rejecting
cancel, plus an assertion that the Content-Length path releases the body.

* test(feishu): cover a rejecting body.cancel() on the Content-Length path

Mirrors the existing reader.cancel() teardown test for the other reject
path, per review feedback. Removing the await on resp.body?.cancel()
flips execution onto the 'rejected: size ... exceeds' branch and the
test fails.

* fix(autofix): make the review-address report wrapper lines bilingual (#7569)

The agent's address-summary.md / no-action.md already ends with a
collapsed Chinese translation, but the workflow-appended wrapper lines
around it — the "Addressed/Reviewed the latest feedback" lead-in, the
"Base-conflict check" line, and the "Re-review when you have a moment"
footer — were English-only and sat outside that block. So the posted
comment was only half translated, unlike the takeover-ack comments
(full collapsed Chinese block) and the "model/模型" sign-off in this
same report (already inline-bilingual).

Give each wrapper line an inline Chinese translation, matching the
model/模型 idiom. The English halves are preserved verbatim — the
streak-reset detector globs on "Addressed the latest review feedback"
and "no changes needed", and a test extracts these lines — so behaviour
is unchanged and old English-only comments still match. A new test pins
each English-Chinese pair so a future reword that drops the Chinese
fails. The terminal handoff/failure comment is left English-only for
now (SKILL.md keeps it so by design); that is a separate change.

Co-authored-by: wenshao <wenshao@example.com>

* feat(cli): post the review body bilingually when the PR description is Chinese (#7564)

When the PR author writes Chinese, the posted /review body was
English-only. fetch-pr now records whether the PR description contains
Han characters (prDescriptionHasHan, detected from the same gh pr view
call and stamped into the plan report), and compose-review renders the
body bilingually off that flag: the English body leads, the complete
Chinese version rides collapsed in a <details><summary>中文说明</summary>
block, and the model footer stays outside the fold. The signal is the
CLI's own — the caller cannot toggle the register of a certified body —
and a local plan has no field, so nothing changes for terminal-only
reviews.

Every deterministic body fragment carries an en/zh pair end to end:
compose-review's clause templates and describeChunkGap phrases, the
coverage disclosures (reasons, publicLabel role subjects via a new
publicLabelZh, the path-free unread-brief reason) and the Step 4/5 gap
texts including the combined same-shape sentence. Fragments with no
deterministic translation — model-written findings, caller echoes,
interpolated errors — ride verbatim in both halves. verificationGaps now
returns structural {subject, reason, subjectZh, reasonZh} entries, which
also removes compose-review's last recover-the-boundary-from-prose parse.

SKILL.md instructs the same format for the model-authored inline
comments: English finding first (marker and suggestion block stay in the
English half — tooling filters on them), full Chinese translation
collapsed beneath, footer last.

Co-authored-by: verify <verify@local>

* feat(autofix): auto-rerun a check that died on infrastructure, once (#7562)

* feat(autofix): auto-rerun a check that died on infrastructure, once

A failed check can be red because the machine died, not the code — a
self-hosted runner losing the server, the disk filling. #7490's E2E
failed with "runner lost communication with the server" and went green
on a rerun. The scan now reruns such a check's failed jobs automatically.

Detection is a conservative annotation whitelist (INFRA_FAILURE_SIGNATURES)
— only unambiguous machine failures, never a test-level timeout, which
could be a real regression. The one-shot guard is run_attempt, not a
marker: a run already retried to attempt 2 and still infra-failing is
persistent, so it is left for a human; after a rerun the attempt
increments, so the next scan will not rerun it. Every step is fail-safe
(any API error → no rerun), it runs only when the PR actually has a
failed check, and the gate carries the same review-address carve-out as
the other check selectors so the loop never reruns its own runs.

This is the transient-infra sibling of #7554 (stale-base): that merges
current main when a check is base-inherited; this reruns when a check
died on the runner. Neither touches a check that is a genuine failure.

Note: rerun-failed-jobs needs the PAT to hold `actions: write`.

* fix(autofix): use POSIX ERE groups in infra-failure regex, cover all signatures in tests (#7562)

* fix(autofix): also treat a git fetch/clone transport death as infra

#6506's checkout died mid-transfer — "fetch-pack: invalid index-pack
output" and "RPC failed; curl 92 ... CANCEL" — which then hung the job
into the 20m limit. That is infra, not the PR (it only touches a doc),
and a re-run made it green. But the infra-signature whitelist did not
cover it, so the auto-rerun did not fire and it waited on a human.

Add `invalid index-pack output` and `RPC failed` — the two canonical
git-transport-death phrases — to INFRA_FAILURE_SIGNATURES. A co-present
job-timeout line does not block the match (one matching line classifies
the run), and a BARE timeout with no transport signature is still left
alone, since it can be a real regression. Both new signatures are pinned
in the test's per-signature loop, plus a case on #6506's real composite
annotation and a bare-timeout-is-not-rerun guard.

* fix(autofix): paginate annotations and filter Autofix runs in infra-rerun loop (#7562)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>

* fix(serve): detect stale SSE cursors across daemon restarts via epoch token; preserve turn attribution and surface compaction failures in replay (#7458)

* fix(daemon): epoch-token restart detection, compaction attribution, and degraded-snapshot signaling (DAEMON-001/007/008)

* fix(acp-bridge): field-level turn attribution merge and replayDegraded bridge test (#7458)

* fix(serve): skip bus epoch lookup for virtual subagent SSE streams (#7458)

The REST SSE route looked up the bus epoch for every session id, but
virtual subagent sessions ride their own bus and their compound ids are
not in the bridge's byId map, so the lookup threw and aborted the
subscription — breaking subagent event streams. Skip the lookup for the
virtual path and degrade a torn-down real session to a headerless stream
(mirrors the /acp route). Also bumps the daemon browser SDK bundle budget
(167KB -> 168KB) for the epoch fields and declares eventEpoch on
DaemonSession so the create/attach path drops its inline type cast.

* fix(serve): stamp eventEpoch on accepted continuations and surface replayDegraded in the SDK (#7458)

Address three review suggestions:
- POST /session/:id/continue now returns eventEpoch alongside lastEventId,
  mirroring the prompt 202 envelope so continuation-seeded SSE cursors
  detect daemon restarts (DAEMON-001)
- DaemonSessionClient exposes replayDegraded from the load response so SDK
  consumers can prefer the full transcript over a degraded snapshot
- add /acp dispatch-level regression test for the degraded-snapshot stderr
  breadcrumb (fires only when snapshot.degraded is set)

* test(cli): fix load-reply race in the degraded-breadcrumb transport test

Await each session/load reply frame before opening the session stream so
the GET cannot race conn.ownSession() into a 403; addresses the review
Critical on the deg-0 arm.

* fix(serve): allow and expose X-Qwen-Event-Epoch in CORS headers

Cross-origin SSE clients must send the epoch header through preflight and
read it from the response, or stale-cursor detection (DAEMON-001) is
silently disabled for every CORS client.

---------

Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Autofix <qwen-autofix[bot]@users.noreply.github.com>

* feat(core): Align GenAI telemetry with ARMS (#7536)

* feat(core): align GenAI telemetry with ARMS

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

* fix(core): remove estimated token usage splits

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

* fix(core): address GenAI telemetry review feedback

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(serve): avoid TOCTOU race dropping live sessions from list response (#7556)

* Initial plan

* fix(serve): avoid TOCTOU race dropping live sessions from list response

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 易良 <1204183885@qq.com>

* fix(cli): prevent monitor turns after task_stop (#7573)

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix[bot]@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: destire-mio <qppque@gmail.com>
Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com>
Co-authored-by: Dragon <52599892+DragonnZhang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: jinye <djy1989418@126.com>
Co-authored-by: chinesepowered <nlai@rediffmail.com>
Co-authored-by: ovochouovo <18212194+ovochouovo@users.noreply.github.com>
Co-authored-by: Edenman <67549719+BZ-D@users.noreply.github.com>
Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: ytahdn <1294726970@qq.com>
Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: Truraly <94105924+Truraly@users.noreply.github.com>
Co-authored-by: zjgzx1988 <zjgzx1988@hotmail.com>
Co-authored-by: hogeheer499-commits <hogeheer499@gmail.com>
Co-authored-by: hogeheer <267467744+hogeheer499-commits@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Nothing Chan <chenliu.cl@alibaba-inc.com>
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: yuanyuanAli <135116774+yuanyuanAli@users.noreply.github.com>
Co-authored-by: verify <verify@local>
Co-authored-by: qqqys <qys177@gmail.com>
Co-authored-by: callmeYe <512217680@qq.com>
Co-authored-by: Qwen Autofix <qwen-autofix[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
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.

feat(web-shell): add full Channel management with browser QR authentication

4 participants