Skip to content

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

Merged
ytahdn merged 2 commits into
QwenLM:mainfrom
chiga0:codex/fix-background-agent-status-and-sidebar
Jul 23, 2026
Merged

fix(web-shell): sync background agent status#7561
ytahdn merged 2 commits into
QwenLM:mainfrom
chiga0:codex/fix-background-agent-status-and-sidebar

Conversation

@ytahdn

@ytahdn ytahdn commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR reconciles detached Agent cards with terminal background-task state without adding interval polling. Live terminal notifications update the matching card directly, while restored legacy transcripts use a one-shot subagent resolution after catch-up, reconnect, or an otherwise unlinked terminal Agent notification. Multiple pending agents resolve independently, and previously observed terminal states remain monotonic when a later lookup fails.

Background Agent cards remain clickable in the shared detail panel, use a static “background task” label instead of a running animation and ticking duration, and are excluded from the composer footer while remaining available in the full Tasks view. Other background task types retain their existing footer polling.

The docked detail panel now expands from the right edge while continuously pushing the chat area left. Its tabs have a fixed width, truncate long titles, scroll horizontally without shrinking, and reveal the close button for inactive tabs on hover or keyboard focus.

Why it's needed

A detached Agent can finish successfully while its original tool card remains visually stuck in a running state because the launch result only records that the task moved to the background. The previous footer integration also polled the task endpoint for Agent activity even though the Agent transcript and detail endpoint already provide the required state. The detail panel opening and crowded tab behavior additionally caused visible layout jumps and compressed labels.

Reviewer Test Plan

How to verify

  1. Start an Agent in background mode and confirm its main transcript card immediately shows a static “background task” label with no shimmer or ticking elapsed time.
  2. Open that card and confirm the shared right-side detail panel displays the running transcript. Let the Agent finish and confirm the original card changes to completed or failed without periodic task-list polling.
  3. Reload a session containing a legacy background Agent notification without a tool-call identifier and confirm the card resolves once after transcript catch-up. With multiple pending Agents, make one resolution fail and confirm successful terminal results still apply and never regress on a later failed lookup.
  4. Confirm background Agents do not appear in the composer footer, still appear in the full Tasks view, and background shell tasks retain their existing footer behavior.
  5. Open the docked detail panel and confirm it smoothly pushes the chat area left. Open enough tabs to overflow the header and confirm each tab remains 180px wide, long titles show an ellipsis, the list scrolls horizontally, the active tab always shows its close button, and inactive tabs reveal it on hover or keyboard focus.

Evidence (Before & After)

Before: completed background Agents could remain labeled as running with an animated indicator and ticking duration; opening the docked panel resized the chat before the panel animation; crowded tabs compressed.

After: terminal Agent state is reconciled without interval polling, detached cards use a static background label, the panel pushes the chat continuously, and fixed-width tabs scroll with truncated titles.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Local Web Shell unit tests, type checking, linting, and production build; CLI virtual-subagent resolver unit tests.

Risk & Scope

  • Main risk or tradeoff: legacy transcripts without an exact tool-call link still use the existing best-effort launch matching, and restored cards perform bounded one-shot resolution requests rather than polling.
  • Not validated / out of scope: manual validation on Windows and Linux, changes to the background task lifecycle, and changes to the full Tasks view.
  • Breaking changes / migration notes: none.

Linked Issues

N/A

中文说明

本 PR 做了什么

本 PR 在不增加定时轮询的前提下,让后台 Agent 卡片与后台任务的终态保持同步。实时终态通知会直接更新对应卡片;恢复旧会话时,则在转录追平、重新连接或收到无法直接关联的 Agent 终态通知后,通过现有子 Agent 解析接口执行一次查询。多个待处理 Agent 会分别解析,后续查询失败也不会让已经确认的终态回退。

后台 Agent 卡片仍可在共用详情面板中打开,并使用静态“后台任务”标识,不再展示运行动画和持续跳动的耗时;它们不会出现在输入框底部状态栏中,但仍保留在完整 Tasks 视图里。其他后台任务类型继续使用原有的底部状态栏轮询。

停靠式详情面板现在从右侧展开,并连续推动聊天区域向左移动。页签使用固定宽度,长标题以省略号显示,数量过多时可横向滚动且不会被压缩;未选中页签的关闭按钮仅在悬停或键盘聚焦时显示。

为什么需要

后台 Agent 完成后,原始工具卡片可能仍然显示运行中,因为启动结果只记录了任务已转入后台。此前输入框底部的集成还会针对 Agent 活动轮询任务接口,尽管 Agent 转录和详情接口已经能提供所需状态。此外,详情面板打开和页签拥挤时还会产生明显的布局跳动与标题挤压。

审查者测试计划

如何验证

  1. 以后台模式启动 Agent,确认主转录卡片立即显示静态“后台任务”,没有流光动画或持续跳动的耗时。
  2. 打开该卡片,确认共用右侧详情面板展示正在运行的转录。等待 Agent 结束,确认原始卡片无需定时轮询任务列表即可变为完成或失败。
  3. 重新加载一个包含旧式后台 Agent 通知且没有工具调用标识的会话,确认卡片在转录追平后只解析一次。存在多个待处理 Agent 时,让其中一次解析失败,确认其他成功终态仍会应用,并且之后查询失败不会让终态回退。
  4. 确认后台 Agent 不出现在输入框底部状态栏中,但仍出现在完整 Tasks 视图中;后台 shell 任务保持原有底部状态栏行为。
  5. 打开停靠式详情面板,确认它平滑推动聊天区域向左移动。打开足够多的页签使其溢出,确认每个页签保持 180px 宽,长标题显示省略号,列表可横向滚动,选中页签始终显示关闭按钮,未选中页签在悬停或键盘聚焦时显示关闭按钮。

前后对比证据

修改前:已完成的后台 Agent 可能仍显示运行中,并带有动画和持续跳动的耗时;打开停靠面板时聊天区先改变尺寸再播放面板动画;页签过多时会被压缩。

修改后:后台 Agent 终态无需定时轮询即可同步,后台卡片使用静态标识,面板连续推动聊天区,固定宽度页签可滚动并截断长标题。

测试平台

系统 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

本地 Web Shell 单元测试、类型检查、lint 和生产构建;CLI 虚拟子 Agent 解析器单元测试。

风险与范围

  • 主要风险或取舍:没有精确工具调用关联的旧转录仍使用现有的尽力匹配逻辑;恢复的卡片会执行有界的一次性解析请求,而不是轮询。
  • 未验证或不在范围内:Windows 和 Linux 手动验证、后台任务生命周期修改、完整 Tasks 视图修改。
  • 破坏性变更或迁移说明:无。

关联 Issue

N/A

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed bug — a background Agent finishes but its tool card stays visually "running" with a shimmer and ticking timer, because the launch result only records status: background and nothing later reconciles the card with the terminal task state. The before/after is described in prose; no linked issue, but the mechanism is well-explained in the design doc and the scenario is concrete.

Direction: aligned. Background agent status display is a real gap in the Web Shell. CHANGELOG shows active work on background subagent lifecycle (concurrency caps, budget stops, session restore) — this fills the corresponding UI hole. No auth/sandbox/public-contract concerns.

Size: cross-package (packages/cli + packages/web-shell), but the CLI delta is 5 production lines (a status-preference guard in the virtual-subagent resolver). Web Shell production logic is ~473 lines; test code adds ~614. Under the 500-line awareness threshold. Not a refactor type.

Approach: the core reconciliation design is sound — live SSE notifications update the card directly, legacy transcripts get a one-shot resolveSubagentSession call after catch-up/reconnect/notification, and stale responses are discarded by key comparison. No interval polling. One observation: the PR bundles three concerns — (1) status reconciliation, (2) composer-footer exclusion for background agents, and (3) docked-panel animation + tab layout. The first two are tightly coupled; the third is a separate UX polish that could have been its own PR. Not a blocker, but worth noting for reviewability.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug——后台 Agent 完成后,工具卡片仍显示"运行中"(带流光动画和跳动计时),因为启动结果只记录了 status: background,后续没有任何机制将卡片与终态同步。前后对比以文字描述;没有关联 issue,但设计文档清楚解释了机制,场景具体。

方向:对齐。后台 Agent 状态展示是 Web Shell 的真实缺口。CHANGELOG 显示后台子 Agent 生命周期方面已有持续工作(并发上限、预算停止、会话恢复)——本 PR 补上了对应的 UI 缺口。无认证/沙箱/公共契约方面的顾虑。

规模:跨包(packages/cli + packages/web-shell),但 CLI 改动仅 5 行生产代码(虚拟子 Agent 解析器中的状态优先逻辑)。Web Shell 生产逻辑约 473 行;测试代码约 614 行。低于 500 行关注阈值。非 refactor 类型。

方案:核心同步设计合理——实时 SSE 通知直接更新卡片,旧转录在追平/重连/通知后执行一次性 resolveSubagentSession 调用,过期响应通过 key 比较丢弃。无定时轮询。一点观察:PR 打包了三个关注点——(1) 状态同步,(2) 后台 Agent 从输入框底部排除,(3) 停靠面板动画 + 页签布局。前两者紧密耦合;第三项是独立的 UX 优化,本可单独成 PR。不构成阻塞,但值得注意。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

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

  • packages/web-shell/client/App.module.css
  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/StatusBar.tsx
  • packages/web-shell/client/components/artifacts/ArtifactPanel.module.css
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx
  • packages/web-shell/client/components/messages/ToolGroup.tsx
  • packages/web-shell/client/i18n.tsx

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

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

Qwen Code · web-shell visuals

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I would have done essentially what this PR does — consume the existing SSE background-agent notifications in transcriptToMessages for the live path, add a one-shot resolveSubagentSession call in useMessages for legacy transcripts (keyed to avoid re-requests), show a static label on background agent cards, and exclude them from the composer footer. The PR matches this closely.

Findings — no critical blockers.

The reconciliation design is clean: live notifications are folded into the transcript projection pass (collectBackgroundAgentTaskUpdates + applyBackgroundAgentTaskUpdate), so no extra state is needed for the common path. The legacy one-shot resolution in useMessages is well-guarded — it keys on sessionId:pendingKey:notificationKey, discards stale responses by comparing the request ref, and resets on disconnect/session-switch. The monotonicity guarantee (terminal states never regress on a later failed lookup) is enforced by only merging fulfilled results with terminal status.

The CLI change (virtual-subagent-sessions.ts, 5 lines) is a targeted guard: when the parent transcript metrics say running but the sidecar already has a terminal status, prefer the terminal status. This prevents the resolver from overwriting a completed task back to running.

The ToolGroup.tsx changes correctly separate foreground-active from background-active: the shimmer/ticking timer only runs when a foreground tool is active, and the summary shows "background task" when only background agents remain. The composerTasks.ts one-liner (task.kind !== 'agent') cleanly removes agents from the footer while keeping shell tasks.

Minor observations (non-blocking):

  • getRecord is duplicated again in useMessages.ts — this is the 5th copy in the web-shell client. Pre-existing pattern, not introduced by this PR.
  • The panel animation and tab layout changes (fixed width, truncation, scroll, close-button hover) are a separate UX concern bundled here. They're clean and well-tested, but could have been a standalone PR for easier review.
sequenceDiagram
    participant P1 as Daemon SSE
    participant P2 as transcriptToMessages
    participant P3 as useMessages hook
    participant P4 as resolveSubagentSession
    participant P5 as Tool Card

    P1->>P2: background notification (toolUseId, status)
    P2->>P5: apply terminal status directly
    Note over P3: legacy transcript, no toolUseId
    P3->>P4: one-shot resolve (after catch-up or reconnect)
    P4-->>P3: resolution (status, durationMs)
    P3->>P5: reconcileBackgroundAgentResolutions
Loading
Files changed (16 of 16 shown)
File What changed
docs/design/background-agent-status-and-details.md New design doc covering the reconciliation approach and scope
packages/cli/src/serve/virtual-subagent-sessions.ts Guard: prefer terminal sidecar status over running metrics
packages/cli/src/serve/virtual-subagent-sessions.test.ts Updated test to cover the terminal-status-preference guard
packages/web-shell/client/App.tsx Removed background agents from footer polling; docked panel wrapper
packages/web-shell/client/App.module.css Dock animation (flex-basis keyframes) and reduced-motion support
packages/web-shell/client/adapters/transcriptToMessages.ts Collect background notification updates and apply to tool cards
packages/web-shell/client/adapters/transcriptToMessages.test.ts Tests for notification-to-card projection (completed, failed, cancelled, non-agent)
packages/web-shell/client/components/artifacts/ArtifactPanel.tsx Tab title wrapped in a truncating span
packages/web-shell/client/components/artifacts/ArtifactPanel.module.css Fixed 180px tabs, horizontal scroll, close-button hover/focus reveal
packages/web-shell/client/components/messages/ToolGroup.tsx Static background label, no shimmer or ticking for background agents
packages/web-shell/client/components/messages/ToolGroup.test.tsx Tests for background summary, mixed groups, and click-to-open
packages/web-shell/client/hooks/useMessages.ts One-shot legacy resolution with key dedup, stale-response discard, session-switch reset
packages/web-shell/client/hooks/useMessages.test.ts 7 new integration tests covering reconciliation, reconnect, stale responses, partial failure
packages/web-shell/client/i18n.tsx Added subagent.background label (EN + ZH)
packages/web-shell/client/utils/composerTasks.ts Exclude all agents from composer footer
packages/web-shell/client/utils/composerTasks.test.ts Updated test to match new exclusion rule

Real-Scenario Testing

This is a Web Shell (browser) PR — the changed UI renders in a browser, not in the terminal. tmux is not available in this CI environment, and a headless browser is not set up, so I could not drive the actual Web Shell UI interactively. What I verified instead:

$ npm run build   # all packages compile, Web Shell assets bundled
✅ Build succeeded

$ cd packages/web-shell && npx vitest run client/hooks/useMessages.test.ts \
    client/adapters/transcriptToMessages.test.ts \
    client/components/messages/ToolGroup.test.tsx \
    client/utils/composerTasks.test.ts
 ✓ utils/composerTasks.test.ts (1 test) 2ms
 ✓ adapters/transcriptToMessages.test.ts (107 tests) 28ms
 ✓ hooks/useMessages.test.ts (12 tests) 41ms
 ✓ components/messages/ToolGroup.test.tsx (39 tests) 121ms
 Test Files  4 passed (4)
      Tests  159 passed (159)

$ cd packages/cli && npx vitest run src/serve/virtual-subagent-sessions.test.ts
 ✓ src/serve/virtual-subagent-sessions.test.ts (8 tests) 291ms
 Test Files  1 passed (1)
      Tests  8 passed (8)

$ npx tsc --noEmit -p packages/web-shell/tsconfig.json  # clean
$ npx tsc --noEmit -p packages/cli/tsconfig.json        # clean
$ npx eslint <all 8 changed source files>               # clean

$ node dist/cli.js serve --port 14170
qwen serve: Web Shell UI served from .../dist/web-shell
qwen serve listening on http://127.0.0.1:14170
$ curl -s http://127.0.0.1:14170/ | head -1
<!doctype html>
✅ Daemon starts, Web Shell HTML served

The 7 new useMessages integration tests exercise the full reconciliation lifecycle in jsdom (render → resolve → reconcile → reconnect → stale-response discard → partial failure), which covers the logic paths that would otherwise need browser verification. The visual aspects (shimmer removal, panel animation, tab truncation) still need a human browser check per the Reviewer Test Plan.

中文说明

代码审查

独立方案: 我的做法与本 PR 基本一致——在 transcriptToMessages 中消费现有的 SSE 后台 Agent 通知处理实时路径,在 useMessages 中为旧转录添加一次性 resolveSubagentSession 调用(通过 key 去重),在后台 Agent 卡片上显示静态标签,并将其从输入框底部排除。PR 与此高度吻合。

发现——无关键阻塞。

同步设计清晰:实时通知在转录投影阶段(collectBackgroundAgentTaskUpdates + applyBackgroundAgentTaskUpdate)直接合并,常见路径无需额外状态。旧转录的一次性解析在 useMessages 中保护良好——以 sessionId:pendingKey:notificationKey 为 key,通过比较请求引用丢弃过期响应,断开连接/切换会话时重置。单调性保证(后续查询失败不会让终态回退)通过仅合并状态为终态的 fulfilled 结果来实现。

CLI 改动(virtual-subagent-sessions.ts,5 行)是针对性的守卫:当父转录 metrics 为 running 但 sidecar 已有终态时,优先使用终态。防止解析器将已完成任务覆写回运行中。

ToolGroup.tsx 的改动正确区分了前台活跃与后台活跃:仅在前台工具活跃时运行流光/计时器,当只剩后台 Agent 时摘要显示"后台任务"。composerTasks.ts 的一行改动(task.kind !== 'agent')干净地将 Agent 从底部排除,同时保留 shell 任务。

次要观察(非阻塞):

  • getRecorduseMessages.ts 中再次重复——这是 web-shell 客户端中的第 5 份拷贝。属于已有模式,非本 PR 引入。
  • 面板动画和页签布局改动(固定宽度、截断、滚动、关闭按钮悬停)是打包在此的独立 UX 关注点。改动干净且测试充分,但本可单独成 PR 以便审查。

实际场景测试

本 PR 是 Web Shell(浏览器)改动——变更的 UI 在浏览器中渲染,不在终端中。本 CI 环境无 tmux,也无无头浏览器,因此无法交互式驱动 Web Shell UI。替代验证如上:构建成功、167 个单元测试全部通过、类型检查和 lint 干净、守护进程启动并正常提供 Web Shell HTML。7 个新的 useMessages 集成测试在 jsdom 中覆盖了完整的同步生命周期。视觉方面(流光移除、面板动画、页签截断)仍需人工在浏览器中按审查者测试计划验证。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid, well-tested fix for a real UX gap; only non-blocking nits.

This is a clean fix. The problem is concrete — a background Agent finishes but its card stays visually running — and the solution is well-designed: live SSE notifications reconcile the card directly, legacy transcripts get a bounded one-shot resolution, and terminal states never regress. The implementation matches what I would have proposed independently, and the 167 unit tests (including 7 jsdom integration tests covering the full reconciliation lifecycle) give me confidence in the logic paths.

The code reads well. The key-based dedup in useMessages is clear, the stale-response discard works correctly, and the CLI guard is minimal and targeted. The design doc is a nice touch — it explains the "why" and scopes what's explicitly out.

Non-blocking nits:

  • The panel animation and tab layout changes are a separate UX concern that could have been its own PR. Clean work, but bundling makes this harder to review and revert independently.
  • Visual aspects (shimmer removal, dock animation, tab truncation) couldn't be verified in this headless CI environment — a human browser check per the Reviewer Test Plan is still worthwhile.

Ships the feature cleanly. ✅

中文说明

置信度:4/5 ——扎实、测试充分的修复,解决了真实的 UX 缺口;仅有非阻塞的小问题。

这是一个干净的修复。问题具体——后台 Agent 完成但卡片仍显示运行中——方案设计合理:实时 SSE 通知直接同步卡片,旧转录执行有界的一次性解析,终态不会回退。实现与我独立提出的方案一致,167 个单元测试(包括 7 个覆盖完整同步生命周期的 jsdom 集成测试)让我对逻辑路径有信心。

代码可读性好。useMessages 中基于 key 的去重清晰,过期响应丢弃正确工作,CLI 守卫最小且有针对性。设计文档是加分项——解释了"为什么"并明确了范围。

非阻塞小问题:

  • 面板动画和页签布局改动是独立的 UX 关注点,本可单独成 PR。工作干净,但打包使审查和独立回退更困难。
  • 视觉方面(流光移除、停靠动画、页签截断)无法在本无头 CI 环境中验证——仍建议人工按审查者测试计划在浏览器中检查。

功能实现干净。✅

Qwen Code · qwen3.8-max-preview

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@gwinthis

Copy link
Copy Markdown
Collaborator

Review & Local Verification Report

代码审查

设计评价:完整的状态同步设计。 本 PR 修复了 Web Shell 中后台 agent 工具卡片永远显示 "running" 的问题。

核心设计:

  1. SSE 通知消费:daemon 通过 session SSE 流发送 terminal background-agent 通知(含 statustoolUseId),Web Shell 消费并回写到投影的 Agent 工具卡片
  2. 状态映射:completed/cancelled → 完成卡片,failed → 失败卡片
  3. Legacy 兼容:无 toolUseId 的旧任务通过 launch record 匹配 persisted sidecar
  4. 状态优先级修复virtual-subagent-sessions.ts 中,当 launch result 说 running 但 sidecar 已有 terminal status 时,保留 terminal status

UI 细节:

  • 后台 agent 卡片使用静态 "background task" 标签(非 running shimmer)
  • 从底部状态栏排除(进度可从卡片和详情面板获取)
  • Docked 详情面板从右边缘展开,支持 reduced-motion

设计文档 docs/design/background-agent-status-and-details.md 同步提交。

结论

LGTM。 设计文档完整,状态同步逻辑清晰,legacy 兼容考虑周全。

@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: coverage — could not read the agents' transcripts (no subagent transcripts at /home/github-runner/actions-runner-test-23/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-test-23--work-qwen-code-qwen-code/subagents/3e0bfd85-83a9-4b0f-9ddd-57aad6e89662 (ENOENT: no such file or directory, scandir '/home/github-runner/actions-runner-test-23/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-test-23--work-qwen-code-qwen-code/subagents/3e0bfd85-83a9-4b0f-9ddd-57aad6e89662'). The harness writes one per agent; if there are none, either no agents ran or the harness could not write them.), so this run cannot show that any of the diff was read. Not reviewed: verification — could not check that Step 4 and Step 5 ran (no subagent transcripts at /home/github-runner/actions-runner-test-23/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-test-23--work-qwen-code-qwen-code/subagents/3e0bfd85-83a9-4b0f-9ddd-57aad6e89662 (ENOENT: no such file or directory, scandir '/home/github-runner/actions-runner-test-23/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-test-23--work-qwen-code-qwen-code/subagents/3e0bfd85-83a9-4b0f-9ddd-57aad6e89662'). The harness writes one per agent; if there are none, either no agents ran or the harness could not write them.).

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/utils/composerTasks.ts

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

Review Summary

Design doc is clear and implementation matches. The core approach is correct: toolUseId-bearing notifications map directly to cards, toolUseId-less ones trigger one-shot resolution. Status monotonicity is protected at three layers (transcript projection, resolution merge, daemon side). Tests cover races, session switches, and reconnects well.

A few concerns for long-session / weak-network scenarios:

(1) Notification key grows unbounded (useMessages.ts ~L60): getBackgroundAgentNotificationKey concatenates all terminal notification block IDs into a string. In long sessions this key keeps growing, and each new notification triggers a full pending-agent resolution request. Consider keeping only the latest or hashing.

(2) Resolution snapshot never cleans up (useMessages.ts ~L200): new Map([...current, ...new]) only grows. If a transcript fork makes the same callId pending again, the stale resolution immediately marks it completed. Low probability but no defense.

(3) No idempotency guard on applyBackgroundAgentTaskUpdate (transcriptToMessages.ts ~L80): Multiple notifications for the same toolUseId (e.g. completed then cancelled) — last-write-wins is implicit. Consider a comment or first-terminal-wins.

(4) Rejected resolution has no retry (useMessages.ts ~L185): If a single agent resolution fails in Promise.allSettled, the card stays pending until the next terminal notification or reconnect. If notifications are already consumed, the card is stuck. Design doc says "repeats after reconnect" but users may not reconnect.

(5) Daemon monotonicity fix is narrow (virtual-subagent-sessions.ts:852): Only handles metrics.status === 'running' downgrade. A paused (non-terminal) metrics over a completed sidecar would also downgrade. Consider !isTerminal(metrics) && isTerminal(selected) as the general guard.

Minor: confirm Tasks panel data source doesn't go through isComposerTask filter (design doc says agents remain in full Tasks panel); CSS tab flex: 0 0 180px wastes space on short titles — consider max-width + min-width; test hookState is module-level shared state with implicit cross-test dependencies.

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/web-shell/client/hooks/useMessages.ts Outdated
Comment thread packages/web-shell/client/hooks/useMessages.ts
Comment thread packages/cli/src/serve/virtual-subagent-sessions.ts Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

— qwen3.7-max via Qwen Code /review

const status = task?.['status'];
if (
meta?.['source'] === 'background_notification' &&
task?.['kind'] === 'agent' &&

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] Nice fix — reverse iteration returning only the latest terminal notification eliminates the unbounded key growth. Clean.

— qwen3.8-max-preview via Qwen Code /review

);
}

export function preferTerminalTaskStatus(

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] Good — the general isTerminalTaskStatus guard covers running, paused, and any future non-terminal states. Exporting preferTerminalTaskStatus for direct testing is the right call.

— qwen3.8-max-preview via Qwen Code /review

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

Re-reviewed after e841ea3. The hardening commit addresses the two main concerns from the first pass:

  1. Notification key growth — fixed: reverse iteration returns only the latest terminal notification instead of concatenating all block IDs. O(1) key size regardless of session length.
  2. Daemon monotonicity — fixed: extracted preferTerminalTaskStatus() with a general isTerminalTaskStatus() covering completed/failed/cancelled/canceled, no longer just running.

Also cleaned up StatusBar (agent counts removed from bottom bar pill, consistent with design doc) and exported getBackgroundTaskActivityKey with a proper test.

Remaining items (#2 resolution snapshot cleanup, #4 rejected resolution retry) are lower priority and can be follow-ups.

— qwen3.8-max-preview via Qwen Code /review

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

Code Review Overview (AI Generated)

PR: #7561 fix(web-shell): sync background agent status
Type: Bug fix + UI refinement
Change size: +1151/-81 across 18 files (HEAD e841ea39)

Findings Summary

  • Critical/Major: 0
  • Minor: 1 (dead parameter in an untouched file)
  • Nit: 3

Independent verification of the design

I traced the full reconciliation pipeline and confirm the approach is correct and, importantly, poll-free:

  • Two terminal paths compose cleanly. Live notifications with toolUseId are applied during transcript projection (applyBackgroundAgentTaskUpdate); the one-shot resolver result is applied as an overlay (reconcileBackgroundAgentResolutions). The overlay only touches tools still in an active status, so once the projection itself marks a card terminal the overlay becomes a no-op — no double-apply conflict.
  • Monotonicity holds at three layers. (a) the resolution snapshot merge keeps prior same-session terminals, (b) only terminal statuses are ever stored (isTerminalBackgroundAgentStatus filter in the allSettled reducer), and (c) the active flag + sessionId check discard stale/out-of-session responses. I walked the "older response after the pending set expands" and "session switch" races by hand and both are correctly suppressed.
  • No runaway requests. pendingBackgroundAgentKey is derived from the unreconciled messages, so applying a resolution does not shrink the key and therefore does not re-trigger the effect; resolutionSnapshot is deliberately absent from the effect deps. Streaming changes neither key. The requestKey dedup makes the effect StrictMode- and workspace.client-identity-safe (the test asserting a single resolveSubagentSession call under StrictMode confirms this).
  • CLI resolver fix is general. preferTerminalTaskStatus(metrics, selected) keeps a terminal sidecar over any non-terminal metrics status (running/paused/background) while letting a terminal metrics stay authoritative — I checked all 7 status combinations and the test covers running + paused.
  • Footer decoupling is consistent. isComposerTask now excludes all agents, getBackgroundTaskActivityKey only admits background shells, and the StatusBar pill dropped the agent/paused-agent branches — so a background Agent alone no longer activates footer polling, matching the design doc. The removed fork refreshTrigger bump is consistent because a fork is a background agent (now footer-excluded).

Cross-Validation

Finding Other Reviewer My Assessment
#1 Notification key grows unbounded yiliang114 Confirmed FIXED at HEADgetBackgroundAgentNotificationKey now reverse-scans and returns only the latest terminal notification (${block.id}:${status}), O(1) key size. Regression test present.
#5 Daemon monotonicity only guarded running yiliang114 Confirmed FIXED at HEAD — extracted preferTerminalTaskStatus with a general isTerminalTaskStatus (completed/failed/cancelled/canceled); verified running and paused no longer downgrade a terminal sidecar.
StatusBar dead agent branches qwen-code-ci-bot Confirmed FIXED at HEADcounts is now { shell, monitor }, paused-agent block removed.
Activity-key exclusion untested qwen-code-ci-bot Confirmed FIXED at HEADgetBackgroundTaskActivityKey exported; App.test asserts shell-in / agent-out.
#2 Resolution snapshot never cleans up yiliang114 Valid, low priority (agree with follow-up). Merge is session-scoped (drops prior session on switch) and callIds are unique, so a stale terminal can only bite if a transcript is re-projected as pending within the same session — which append-only transcripts don't do. Theoretical.
#3 applyBackgroundAgentTaskUpdate last-write-wins yiliang114 Valid Nit. updates.set(toolUseId, …) means the latest block wins; fine in practice (one terminal notification per task) but a one-line comment stating the last-write-wins choice would help.
#4 Rejected resolution has no retry yiliang114 Not a defect — design is intentional. Author's rationale is sound: the resolver is a legacy fallback; the live path uses SSE directly, and reconnect / a later terminal notification is the bounded retry. Adding blind retries would contradict the no-polling goal.

Additional Audit Coverage

Areas I independently checked beyond existing findings:

  • Request lifecycle / leak: the retained reconciliationRequestRef promise is bounded (one per key) and cleared on disconnect/session-change; orphaned in-flight requests after a rapid session switch are ignored via active+sessionId (no AbortSignal is passed, consistent with the rest of the REST client — not a leak since they complete).
  • endTime provenance parity: notification path uses the notification timestamp, resolution path uses startTime + durationMs; both yield the same status/rawOutput.status shape for completed/failed/cancelled, so the two paths render identically.
  • Dock animation: base style matches the keyframe to frame (no end-of-animation jump); resize after the 200 ms open works via static flex-basis since custom-property changes don't restart the animation; prefers-reduced-motion disables it. Tab list container has overflow-x: auto; close button is keyboard-reachable via :focus-within.
  • cancelled/canceled spelling handled uniformly across all five touch points.

Minor / Nit polish (non-blocking)

  1. Minor — dead refreshTrigger parameter. With App.tsx no longer passing it, useBackgroundTasks's refreshTrigger = 0 param and its dedicated effect (useBackgroundTasks.ts:19,43-47) are now inert dead code. Worth removing in a follow-up for clarity (file isn't in this PR's diff, so no inline anchor).
  2. Nit — unreachable .catch() in useMessages.ts: Promise.allSettled(…).then(…) never rejects, so the trailing .catch() is dead. Harmless but slightly misleading.
  3. Nit — duplicate getRecord helper: useMessages.ts redefines the same getRecord that already exists in transcriptToMessages.ts; could be shared.
  4. Nit — zero-duration fallback: when a resolution lacks durationMs, endTime = startTime + 0, so a reconciled card can display a 0s duration.

Final Verdict

APPROVE. The hardening commit cleanly resolves the two substantive concerns (unbounded notification key, narrow daemon monotonicity guard) and I verified both fixes at HEAD rather than taking the commit message at face value. The reconciliation design is correct, race-safe, and genuinely poll-free, with strong test coverage of the tricky paths (StrictMode dedup, pending-set expansion, session switch, reconnect, partial-failure). Remaining items are low-priority polish. Ship it.


This review was generated by QoderWork AI

@ytahdn
ytahdn added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit 22cd9c7 Jul 23, 2026
203 of 209 checks passed

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +26 to +33
it.each(['running', 'paused'])(
'keeps a terminal task status over non-terminal %s metrics',
(metricsStatus) => {
expect(preferTerminalTaskStatus(metricsStatus, 'completed')).toBe(
'completed',
);
},
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The unit test only exercises the direction where metricsStatus is non-terminal and selectedStatus is terminal; the reverse direction — a terminal metricsStatus over a non-terminal selectedStatus (the metricsStatus ?? selectedStatus fallback path) — has no direct coverage. — Concrete cost: a regression simplifying preferTerminalTaskStatus to always return selectedStatus would pass both existing parametrized cases (selectedStatus is 'completed' in each). In production, when the parent transcript metrics already record a terminal status (e.g. 'failed') but the sidecar/registry status is still 'running', the function would silently return 'running' instead of 'failed'.

Suggested change
it.each(['running', 'paused'])(
'keeps a terminal task status over non-terminal %s metrics',
(metricsStatus) => {
expect(preferTerminalTaskStatus(metricsStatus, 'completed')).toBe(
'completed',
);
},
);
it.each(['running', 'paused'])(
'keeps a terminal task status over non-terminal %s metrics',
(metricsStatus) => {
expect(preferTerminalTaskStatus(metricsStatus, 'completed')).toBe(
'completed',
);
},
);
it.each(['completed', 'failed', 'cancelled'])(
'preserves a terminal %s metrics status over a non-terminal selected status',
(metricsStatus) => {
expect(preferTerminalTaskStatus(metricsStatus, 'running')).toBe(
metricsStatus,
);
},
);

— qwen3.8-max-preview via Qwen Code /review

chiga0 pushed a commit that referenced this pull request Jul 23, 2026
* fix(web-shell): sync background agent status

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

---------

Co-authored-by: ytahdn <ytahdn@gmail.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.

6 participants