Skip to content

feat(ui): show Agent Team status in CLI and WebShell - #11072

Draft
yiliang114 wants to merge 28 commits into
mainfrom
codex/agent-team-roster-web-shell
Draft

feat(ui): show Agent Team status in CLI and WebShell#11072
yiliang114 wants to merge 28 commits into
mainfrom
codex/agent-team-roster-web-shell

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a leader-facing Agent Team roster to the existing CLI live-agent panel and projects the same team state into WebShell's environment and workflow views. Teammates retain their own lifecycle semantics: idle remains distinct from completed, current shared-task ownership is visible, and pressing Enter on a CLI teammate row opens the teammate's existing Agent tab.

It also completes the qwen serve coordination path. An idle WebShell leader now resumes when a teammate reports, teammate tool approvals use the existing WebShell permission dialog, and replacing or deleting a team detaches its callbacks, cancels pending approvals, and discards queued notifications. Named Agent launches retain the teammate name through the daemon transcript so WebShell merges the launch entry with the live team entry instead of showing duplicates. A teammate launched from an Agent definition now inherits and discovers that definition's MCP servers in its isolated runtime.

Why it's needed

Agent Team already supports independent teammate loops, shared tasks, mailbox messaging, approvals, and dedicated Agent tabs, but the leader had no glanceable view of who was active, idle, or working on which task. In WebShell, teammate reports and approvals were also not connected to the ACP session, so displaying a roster alone would not have produced a usable team workflow.

Reviewer Test Plan

How to verify

  1. Start qwen serve with Agent Team enabled and open its WebShell in a fresh browser tab.
  2. Ask the leader to create a team and launch a named teammate. Confirm the Environment panel shows one row for that teammate, with its name, current task, and running or idle status.
  3. Let the teammate report. Confirm the leader automatically resumes, reconciles the report, and can shut down and delete the team without stale rows or a late continuation.
  4. Ask a teammate to use a tool that requires approval. Confirm the WebShell permission dialog identifies the teammate and that rejecting the request prevents the tool action.
  5. Define an Agent with a private MCP server, launch a named teammate with that definition, and confirm the teammate can call the private MCP tool. Delete the team and confirm its MCP process exits.
  6. In the CLI, confirm the same teammate appears in the live-agent panel and Enter opens the teammate's existing Agent tab. Confirm an ordinary subagent row still uses the existing background-task detail behavior.

Evidence (Before & After)

Before: teammates were visible only through their dedicated CLI Agent tabs. WebShell omitted live team state, did not resume the leader from teammate reports, and did not route teammate approvals through its permission dialog.

After: the CLI roster and WebShell environment/workflow views show current team state. Real tmux-hosted qwen serve runs created named teammates, showed one row per teammate in Chrome, automatically resumed the leader from teammate reports, and cleaned up the team. A teammate write_file request appeared as a teammate-labelled WebShell approval; rejecting it left the target file absent. A teammate launched from an Agent definition called its private stdio MCP tool, returned MCP_TEAM_PROOF:REAL_TEAM_MEMBER, and left no MCP child process after team deletion.

Tested on

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

Environment (optional)

Node.js 22, local package builds, tmux-hosted qwen serve, and Chrome WebShell with a configured live model.

Risk & Scope

  • Main risk or tradeoff: The UI remains a read-only projection, while leader continuation and approvals reuse the existing serialized ACP notification and permission paths. Focused tests cover manager replacement, stale callback rejection, queued-message cleanup, approval cancellation, Agent-definition MCP inheritance, roster derivation, rendering, navigation, idle polling, and deduplication.
  • Not validated / out of scope: Remote Agent registration, durable Workspace/Run scheduling, cross-machine recovery, WebShell teammate transcript browsing, and Windows/Linux manual testing.
  • Breaking changes / migration notes: None. The session-agent and daemon preview shapes are extended additively, and ordinary subagent/background-task behavior is preserved.

Linked Issues

Closes #11069

中文说明

本 PR 做了什么

本 PR 在现有 CLI 实时 Agent 面板中加入面向 Leader 的 Agent Team 成员列表,并把相同的团队状态投影到 WebShell 的环境与工作流视图中。队友保留独立的生命周期语义:idle 与 completed 明确区分,可以看到当前负责的共享任务,在 CLI 队友行上按 Enter 会打开该队友已有的 Agent 标签页。

它同时补齐了 qwen serve 的协作链路。WebShell Leader 处于空闲状态时,现在会在队友报告后自动恢复;队友工具审批复用现有 WebShell permission dialog;Team 被替换或删除时会解绑回调、取消待处理审批并丢弃旧 Team 的排队通知。具名 Agent 启动会通过 daemon transcript 保留队友名,因此 WebShell 会把启动条目与实时团队条目合并,不再显示重复行。通过 Agent 定义启动的队友会在隔离运行时中继承并发现该定义声明的 MCP Server。

为什么需要它

Agent Team 已经支持独立的队友循环、共享任务、邮箱通信、审批和专属 Agent 标签页,但 Leader 缺少一个可以快速查看谁在运行、谁处于 idle、谁在负责哪个任务的入口。在 WebShell 中,队友报告和审批此前也没有连接到 ACP Session,因此只展示 roster 并不能形成可用的团队工作流。

Reviewer 验收计划

如何验证

  1. 启用 Agent Team 后启动 qwen serve,并在全新浏览器标签中打开 WebShell。
  2. 让 Leader 创建 Team 并启动一个具名队友。确认 Environment 面板只显示一行该队友,并展示名称、当前任务以及 running 或 idle 状态。
  3. 让队友返回报告。确认 Leader 自动恢复、汇总报告,并可以停止队友和删除 Team,不残留旧行,也不会出现延迟的旧 Team 续跑。
  4. 让队友使用需要审批的工具。确认 WebShell permission dialog 标出队友身份,拒绝后工具操作没有发生。
  5. 定义一个带私有 MCP Server 的 Agent,以该定义启动具名队友,确认队友可以调用私有 MCP 工具;删除 Team 后确认 MCP 进程退出。
  6. 在 CLI 中确认同一队友出现在实时 Agent 面板,按 Enter 打开其已有 Agent 标签页;普通 subagent 行仍保持现有后台任务详情行为。

证据(修改前与修改后)

修改前:队友只能通过各自的 CLI Agent 标签页查看。WebShell 不展示实时 Team 状态,不会因队友报告自动恢复 Leader,也不会把队友审批路由到 WebShell permission dialog。

修改后:CLI roster 与 WebShell 环境/工作流视图会展示当前 Team 状态。真实 tmux qwen serve 运行创建了具名队友,在 Chrome 中每个队友只显示一行;队友报告后 Leader 自动恢复并清理 Team。另一次真实队友 write_file 请求以带队友名的 WebShell 审批出现,拒绝后目标文件不存在。通过 Agent 定义启动的队友调用了其私有 stdio MCP 工具,返回 MCP_TEAM_PROOF:REAL_TEAM_MEMBER,删除 Team 后没有残留 MCP 子进程。

已测试平台

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

环境(可选)

Node.js 22、本地 package 构建、tmux 中运行的 qwen serve,以及连接已配置真实模型的 Chrome WebShell。

风险与范围

  • 主要风险或权衡:UI 仍是只读投影,Leader 续跑和审批复用现有串行 ACP 通知与权限链路。聚焦测试覆盖 manager 替换、旧回调拒绝、排队消息清理、审批取消、Agent 定义 MCP 继承、roster 推导、渲染、导航、idle 轮询和去重。
  • 未验证或不在范围内:远程 Agent 注册、持久化 Workspace/Run 调度、跨机器恢复、WebShell 队友 transcript 浏览,以及 Windows/Linux 手工测试。
  • 破坏性变更或迁移说明:无。session-agent 与 daemon preview 协议仅做增量扩展,并保留普通 subagent/后台任务行为。

关联 Issue

Closes #11069

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 5, 2026
@yiliang114

yiliang114 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

E2E test report

Real Agent Team run

  • Started the current source with Agent Team enabled using tmux and qwen serve, then drove the real WebShell in Chrome.
  • Created a team and launched one named teammate from an Agent definition whose private stdio MCP server was not configured on the leader.
  • The teammate successfully called mcp__agent_private__echo, returned MCP_TEAM_PROOF:REAL_TEAM_MEMBER through send_message, and automatically resumed the idle leader.
  • The leader deleted the team; /session/:id/agents returned an empty task list and no MCP child process remained.
  • A separate live run verified running/idle/cleaned roster transitions and rejected a teammate write_file request through WebShell without creating the file.

Agent Team MCP inheritance verified in WebShell

The first model turn produced three invalid agent calls without subagent_type; all were rejected before spawn. One exact follow-up produced the valid call and the single real teammate run described above. The full transcript is retained locally.

Automated checks

  • Core Agent Team tests: 66/66.
  • ACP Session tests: 820/820.
  • Focused CLI roster/background tests: 96/96.
  • WebShell App, environment panel, and transcript tests: 930/930.
  • WebShell browser gates: transcript gate 4/4 and Playwright smoke 51/51.
  • Full repository build and typecheck: passed; focused lint and formatting checks: passed.

Prior CI failures

  • The prior Linux MessageList failure passes repeatedly locally and is outside this PR's touched paths; it is consistent with the existing timing-sensitive test.
  • The prior WebShell E2E job exhausted its 20-minute limit during npm ci/the prepare build. Playwright installation and browser tests never started; both browser gates pass locally.

Windows and Linux manual UI runs remain out of scope. The PR stays Draft for review and the fresh CI run.

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 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 dce16b4. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

terminal-turn-error-copy-narrow-light before/after

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

Qwen Code · web-shell visuals

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval not posted — the PR head moved (or the PR closed) after the review of d65e41f; approving now would attest to unreviewed code. Re-run @qwen-code /triage on the new head. finalize run

⚠️ 延迟审批未提交 —— 审查 d65e41f 之后 PR head 已变更(或 PR 已关闭),此时审批会为未审查的代码背书。请在新 head 上重新运行 @qwen-code /triage查看 finalize 运行

Resolves the LiveAgentPanel import conflict introduced by #10957, which
moved cli production files off the core root barrel. Keeps main's deep
`subagents/builtin-agents.js` specifier and takes `AgentStatus` from
`agents/runtime/agent-types.js` rather than re-adding a root-barrel
import.

Also converts this branch's new `use-team-agent-roster` module and its
test to per-module core specifiers, for the same reason: a cli file that
imports the package root costs about 11.5s of test collection, and no
test mocks the core package here.
Two defects in the team roster this branch adds.

`buildSessionAgentsStatus` awaited `listTasks` unguarded. That call throws
on every readdir failure except ENOENT — by design, so a leader never
mistakes an unreadable board for an empty one — which meant an EACCES or
EIO on `~/.qwen/tasks/<team>` failed the entire `/session/:id/agents`
response, hiding ordinary subagents and background tasks along with the
team rows. The shared-task label is decoration on rows the route can still
render, so it now degrades to no label.

`useTeamAgentRoster` built its result in the render body, so it returned a
new array on every render — including the `[]` for the no-team case, which
every user hits. `LiveAgentPanel` keys its one-second elapsed-time interval
on that array, so the interval was torn down and recreated on every render
and, under render churn, never lived long enough to fire. Memoizing on the
manager, the shared tasks, the registered agents, and the lifecycle-event
revision restores a stable identity.
The multi-agent coordination page told users teammates are visible
"through the existing Agent View UI" and stopped there. This branch adds
a leader-facing roster in both surfaces, so the page now says where it
appears, that idle is a distinct state from completed, that Enter opens
the teammate's tab, and that Web Shell shows status plus approvals but
keeps conversations in the CLI.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Template is complete, including the Chinese mirror and a real reviewer test plan. ✓

Problem: A genuine product gap, not theoretical hardening — #11069 is open and asks for exactly this ("Show Agent Team teammates in the existing live agent roster"). The Why it's needed section makes the stronger case well: a roster alone would not be usable in Web Shell while teammate reports and approvals were still disconnected from the ACP session.

Direction: Aligned. The design doc names the right precedent (Claude Code Agent Teams: compact lead-side roster, explicit idle, shared-task ownership, direct navigation) and — more importantly — reuses the surfaces that already exist instead of adding a second execution path: LiveAgentPanel for the CLI rows, GET /session/:id/agents for the Web Shell projection, the existing serialized background-notification queue for leader continuation, and the existing ACP permission channel for teammate approvals. No new HTTP route, SSE event, or client store. That is the right shape for this repo. CHANGELOG has no direct reference, but Agent Team is an actively developed area and the roster is the obvious missing piece.

Size: Touches core paths (packages/core/src/agents/**) and spans five packages, so for transparency: 722 production lines, 791 test/spec lines, 72 doc lines, 15 lines of e2e mock support — 1600 total across 42 files. That is above the 500-production-line mark where the core gate normally flags a PR for maintainer awareness, but you are a /packages/core/ CODEOWNER with admin on the repo, so AGENTS.md's maintainer exemption applies and I am not escalating on that basis. Recording the number so a second reviewer can see the shape at a glance. Below the 1000-line large-PR advisory.

Approach: The roster half is minimal and I would not cut anything from it. The part I would still question is the third commit, fix(core): inherit Agent MCP servers in teams. It is a real feature and the implementation is correct (Stage 2), but it has nothing to do with showing team status — it is the only change touching InProcessBackend spawn behaviour, and it would review and revert cleanly on its own. Worth considering whether it belongs in a separate PR. Not a blocker, and the body does disclose it.

Risk: Stage 1e matched the acp-integration high-risk path pattern — packages/cli/src/acp-integration/session/Session.ts and tasksSnapshot.ts. That is the strongest triage-time revert-correlation signal in this repo, so I did the full-depth review rather than a skim and I am treating green CI as a precondition for approval rather than a nicety. The changes there are additive (a new callback registration alongside the existing goal-runtime and background-notification ones), which is the low-risk way to touch that file.

One process note: the head moved twice while I was reviewing (0293497e653b8f2cd65e41fe). This gate is written to fail closed on that, so I restarted against the new head rather than attest to code I had not re-read. Everything below and in the following comments reflects d65e41fe.

Moving on to code review. 🔍

中文说明

感谢贡献!模板完整,包含中文对照和真实的 reviewer 验收计划 ✓

问题: 这是真实的产品缺口,不是理论性加固 —— #11069 仍处于 open 状态,诉求正是「在现有 live agent roster 中展示 Agent Team 队友」。Why it's needed 里的论证更有说服力:如果队友报告和审批仍未接入 ACP session,只在 Web Shell 里展示 roster 并不构成可用的团队工作流。

方向: 对齐。设计文档给出了正确的参照(Claude Code Agent Teams:紧凑的 lead 侧 roster、显式 idle、共享任务归属、直接跳转),更关键的是它复用了已有界面,而不是新造第二条执行链路 —— CLI 行复用 LiveAgentPanel,Web Shell 投影复用 GET /session/:id/agents,Leader 续跑复用已有的串行后台通知队列,队友审批复用已有的 ACP permission 通道。没有新增 HTTP 路由、SSE 事件或客户端 store。对这个仓库来说这是正确的形态。CHANGELOG 没有直接对应条目,但 Agent Team 是正在活跃开发的领域,roster 是明显缺失的一环。

规模: 触及核心路径(packages/core/src/agents/**)并跨五个 package,因此透明列出:生产代码 722 行测试/spec 791 行文档 72 行e2e mock 支持 15 行 —— 42 个文件合计 1600 行。这超过了核心闸门通常会提请维护者关注的 500 生产行阈值,但你是本仓库 /packages/core/ 的 CODEOWNER 且拥有 admin 权限,按 AGENTS.md 的维护者豁免条款适用,因此我不据此升级。记录这个数字只是便于第二位 reviewer 一眼看清规模。低于 1000 行的大 PR 建议阈值。

方案: roster 这一半已经是最小实现,我不会砍任何东西。我仍想提出来讨论的是第三个 commit fix(core): inherit Agent MCP servers in teams。它是一个真实的功能,实现也正确(见 Stage 2),但它和「展示团队状态」没有关系 —— 它是唯一改动 InProcessBackend spawn 行为的部分,单独成 PR 会更易 review、也更易回滚。可以考虑是否拆出去。这不是阻塞项,PR 正文也确实披露了它。

风险: Stage 1e 命中了 acp-integration 高风险路径特征 —— packages/cli/src/acp-integration/session/Session.tstasksSnapshot.ts。这是本仓库 triage 阶段最强的回滚相关性信号,所以我做的是全深度 review 而非略读,并把 CI 全绿当作 approve 的前置条件而不是可选项。这两处的改动是增量式的(在已有的 goal-runtime 与后台通知注册之外新增一个回调注册),这是修改该文件时风险最低的方式。

一个流程说明:我在 review 期间 head 移动了两次(0293497e653b8f2cd65e41fe)。这个闸门在这种情况下会 fail closed,所以我选择针对新的 head 重新开始,而不是为我没有重读过的代码背书。下文及后续评论中的所有内容都对应 d65e41fe

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Code review

No Critical blockers. The implementation stays on the reuse path the design doc promises, and the two commits that landed mid-review (6552a00, d65e41f) removed the one substantive issue I had found.

I went in expecting four problems and all four turned out to be fine on inspection — recording them because they are the places a reviewer would otherwise spend time:

  • Provider ordering. BackgroundTaskViewProvider now calls useAgentViewState(), which would silently yield an empty agents map (and therefore an empty roster) if it were mounted outside AgentViewProvider. It is not — startInteractiveUI.tsx nests AgentViewProviderBackgroundTaskViewProvider, so the dedup set is populated in the real app.
  • The 'idle' union widening. Adding 'idle' to ServeSessionAgentTaskStatus['status'] and DaemonSessionAgentTaskStatus['status'] is a public-contract change, so I traced the read sites rather than trusting it. It propagates correctly: EnvironmentAgentTask is defined as DaemonSessionAgentTaskStatus & {…}, and DaemonSessionTaskWithWorkflowStatus is a union that includes the agent member, so taskStatusKey / taskStatusIcon accept 'idle' without a cast. Worth knowing that CI never runs npm run typecheck (only typecheck:integration, in a different lane), so this is hand-traced, not machine-verified.
  • Rejected approvals reported as success. approved = outcome !== ToolConfirmationOutcome.Cancel looked wrong until I read the enum — it has no separate reject member, Cancel is the rejection. The finally block's completed / failed split is correct.
  • The aria-disabled CSS override. .task[aria-disabled='true'] { opacity: 1 } has the same specificity (0,2,0) as the .task:disabled { opacity: .55 } it is meant to undo, but it comes later in source order, so it wins. Fine as written.

On the mid-review fix. 6552a00 is worth calling out because it caught something sharper than my own read. I had flagged the fresh-array-per-render return from useTeamAgentRoster as render churn; the real consequence is that LiveAgentPanel keys its one-second elapsed-time interval on that array identity, so a new identity each render tears the interval down and recreates it before it can ever fire — freezing elapsed times. And the [] no-team branch meant it hit every user of the live-agent panel, not just teams. The fix (module-level NO_TEAM_ENTRIES plus a useMemo keyed on [manager, tasks, registeredAgents, revision]) is right, the revision-as-change-token comment explains the exhaustive-deps disable honestly, and use-team-agent-roster.identity.test.ts pins the contract so it cannot silently regress. tasksSnapshot.ts also gained a try/catch around listTasks so an unreadable shared-task board degrades to rows without a task label instead of failing the whole agents snapshot — a hole I had not spotted, with a test that forces a non-ENOENT readdir failure by putting a file where the directory belongs.

Two non-blocking items:

  1. Scope. As in Stage 1 — fix(core): inherit Agent MCP servers in teams is separable from the roster. The implementation itself is clean parity rather than new behaviour: subagent-manager.ts already merges runtimeContext.getMcpServers() with an agent definition's mcpServers (definition wins on collision), forces a registry rebuild, and discovers only the definition-owned server names for ordinary subagents; createPerAgentConfig now does the same for teammates. Frontmatter shape is already validated upstream by parseAgentMcpServers (null-prototype, malformed blocks dropped with a warning), so the as cast in TeamManager is not swallowing unvalidated input. Transport cleanup rides the existing agentRegistry.stop() paths. It just is not a roster change.
  2. Per-poll disk read. buildSessionAgentsStatus now awaits listTasks(team.name) on every snapshot, and App.tsx keeps polling while any agent is 'idle'. Idle is an indefinitely sustained state, so a live team means a directory read and JSON parse of $QWEN_HOME/tasks/<team>/ on every poll for as long as the team exists. This is consistent with how 'paused' already behaves and the new try/catch makes it degrade safely, so it is a note, not a request.

Minor, take it or leave it: buildTeamAgentRosterEntries still allocates a throwaway new AbortController() per teammate and mutates the terminalEndTimes ref during the render phase. The useMemo makes both rare now, and team rows never reach the cancellation path (they route to switchToAgent, and the dialog still reads entries), so nothing depends on that controller.

sequenceDiagram
    participant P1 as Teammate
    participant P2 as TeamManager
    participant P3 as ACP Session
    participant P4 as Notification queue
    participant P5 as Web Shell leader
    P1->>P2: report result to leader
    P2->>P3: leader message callback
    P3->>P4: enqueue teammate notification
    P4->>P5: resume the idle leader turn
    P5-->>P3: reconcile and continue
    P1->>P2: tool call needs approval
    P2->>P3: TEAMMATE_APPROVAL_REQUEST
    P3->>P5: requestPermission, labelled with teammate
    P5-->>P3: selected option
    P3-->>P2: respond with outcome
    Note over P3,P4: replacing or deleting a team aborts pending approvals and drops queued notifications
Loading
Files changed (29 of 42 shown)
File What changed
packages/cli/src/ui/hooks/use-team-agent-roster.ts New hook. Derives display-only teammate rows from the live TeamManager; the identity-stable memo is the part that matters.
packages/cli/src/acp-integration/session/Session.ts Binds the TeamManager leader callback into the existing background-notification queue, routes teammate approvals through the ACP permission dialog, and detaches both (aborting approvals, dropping queued teammate notifications) on team replacement or dispose.
packages/cli/src/acp-integration/session/tasksSnapshot.ts Adds team members to the agents snapshot with an explicit idle state and their owned shared task; unreadable task board degrades instead of throwing.
packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx Renders teammate rows: status glyph per AgentStatus, teammate color on the name, inline status suffix. Team rows bypass the background-registry liveness re-pull.
packages/cli/src/ui/contexts/BackgroundTaskViewContext.tsx Exposes an optional liveAgentEntries (ordinary subagents plus teammates, sorted) alongside the untouched entries.
packages/cli/src/ui/components/InputPrompt.tsx Enter on a teammate row opens its existing Agent tab; an ordinary subagent row keeps the background-task detail behavior.
packages/cli/src/ui/components/background-view/liveAgentPanelVisibility.ts Widens the visibility and layout-key helpers to accept team rows.
packages/cli/src/ui/components/agent-view/AgentTabBar.tsx Roster-presence check reads the merged list.
packages/cli/src/ui/AppContainer.tsx Panel layout key derives from the merged roster.
packages/core/src/agents/backends/InProcessBackend.ts Per-agent config merges an agent definition's MCP servers over the session map, discovers only those, and releases them with the registry.
packages/core/src/agents/team/TeamManager.ts Passes the definition's mcpServers through to the in-process spawn config.
packages/core/src/agents/backends/types.ts InProcessSpawnConfig.mcpServers added.
packages/core/src/agents/runtime/agent-core-test-mock.ts Mock registry gains discoverToolsForServer.
packages/acp-bridge/src/status.ts Serve-side agent status union widened with 'idle'; optional teamName, color, teamTask added.
packages/sdk-typescript/src/daemon/types.ts Same additive widening on the daemon-side contract.
packages/sdk-typescript/src/daemon/ui/types.ts subagent_delegation preview gains optional teammateName.
packages/sdk-typescript/src/daemon/ui/toolPreview.ts Delegation detection also matches the agent tool name and lifts name into teammateName.
packages/web-shell/client/App.tsx Merges a named launch with its live team row so one teammate renders once; keeps polling while a row is idle.
packages/web-shell/client/adapters/transcriptToMessages.ts Rebuilds name from the preview so transcript-replayed launches still dedup.
packages/web-shell/client/components/panels/EnvironmentPanel.tsx Idle icon, inline shared-task line, team rows non-interactive, hex colors accepted.
packages/web-shell/client/components/panels/EnvironmentPanel.module.css Idle color, task line styling, hover and disabled states separated.
packages/web-shell/client/components/artifacts/AgentWorkflow.tsx Idle icon; team nodes non-clickable since Web Shell has no teammate transcript route.
packages/web-shell/client/components/artifacts/AgentWorkflow.module.css Matching idle and disabled-node styling.
packages/web-shell/client/customization.tsx WebShellAgentTask status union widened with 'idle'.
packages/web-shell/client/i18n.tsx tasks.idle in EN and ZH.
packages/web-shell/client/e2e/utils/mockDaemon.ts Mock daemon serves /session/:id/agents so the smoke test can exercise the panel.
docs/design/agent-team-roster-web-shell.md Design doc: reused pieces, scope, verification, acceptance results.
docs/users/features/multi-agent-coordination.md User-facing section describing the roster, idle semantics, and Web Shell behavior.
…and 13 test files (791 lines) Session leader-continuation, approval routing and cancellation; snapshot serialization and unreadable-board degradation; roster derivation and identity; panel rendering and keyboard routing; Web Shell dedup, idle polling, panel rendering; daemon preview taxonomy; Playwright smoke.

Test evidence

This is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI, read through the API.

On the reviewed head d65e41fe, 18 checks are green and 4 are still in flight. I fetched once and did not poll; the table below reflects that single read and the finalize job will rewrite it when CI settles.

The green set is more informative than it looks for this particular diff. TUI parity snapshots (ink vs opentui) and the OpenTUI no-flicker gate both passed, which is real signal for a change that adds rows to the live-agent panel and rewires its layout key. Integration Tests (no-AK, No Sandbox) and Capture web-shell visuals also passed, and the visuals job posted before/after screenshots for this head.

Not verified — and this is the honest gap:

  • Test (ubuntu-latest, Node 22.x)in progress on this head, and it has never completed green on any commit of this PR. On 0293497e it was cancelled at the 120-minute timeout-minutes ceiling in ci.yml (job started 16:58:59Z, the Run tests and generate reports step was cancelled at 18:59:13Z) with vitest workers still alive — the log ends with Terminate orphan process … node (vitest). The macOS and Windows Test legs and Integration Tests (CLI, No Sandbox) were skipped as a consequence. So the PR's 791 new and changed test lines have no green unit-suite run behind them yet.
  • Lint & Static and Real daemon E2E / Java 11 — in progress on this head. (Lint & Static was green on 0293497e.)
  • npm run typecheck — not a CI step in this repo at all, so nothing gates it. My union-widening trace above is by hand.
  • Windows and Linux behaviour — the author marked both ⚠️ in Tested on; the manual evidence is macOS only. That is the author's own report, not something I re-ran.

The cancelled unit lane is pre-existing infrastructure, not this PR. Two independent pieces of evidence, both from GitHub-set metadata and the repo's own workflow file rather than from log prose: main's own Test (ubuntu-latest, Node 22.x) is red on its two most recent commits (49e3ef62 failure 10:20→11:34Z, 101b003f failure 09:02→10:06Z), and the 49e3ef62 failure diagnostic dumped the runner at 97% disk (/dev/nvme0n1p3 504G 465G 19G 97% /) immediately before exit 1 — ci.yml only prints that dump on a non-zero return code. ci.yml also records, in the comment that split this lane out of lint_and_static, that the vitest lane "was measured failing 41% of post-merge runs in the week of the split", and that runner-pool contention is why the ceiling is 120 minutes at all. I am not attributing the cancellation to the diff; I am also not treating it as a pass.

Sandboxed verification would settle what neither the diff nor CI can. The central claims here are behavioural, the PR carries review/self-reported, and the manual evidence is one platform:

  • @qwen-code /verify — that #detachTeamManager genuinely fails closed. Stale-leader-callback rejection, discarding queued teammate notifications, and cancelling an in-flight teammate approval when the team is replaced mid-approval are all asserted against mocks in Session.test.ts; an A/B against the base build is what would show those guards are load-bearing rather than decoration. Same for the rejected-approval path actually preventing the tool action.
  • @qwen-code /tmux — the CLI roster surface. One row per teammate (no duplicate launch-plus-live rows), idle visually distinct from completed, elapsed time actually advancing after the identity fix, and Enter on a teammate row opening its Agent tab while an ordinary subagent row still opens the background-task detail.
中文说明

代码审查

没有 Critical 阻塞项。 实现严格走在设计文档承诺的「复用既有能力」路线上,review 期间落地的两个 commit(6552a00d65e41f)解决了我发现的唯一一个实质性问题。

我原本预判有四个问题,实际查证后都没问题 —— 记录下来是因为这些正是 reviewer 会花时间的地方:

  • Provider 嵌套顺序。 BackgroundTaskViewProvider 现在调用了 useAgentViewState();如果它挂在 AgentViewProvider 之外,agents 会静默返回空 map,roster 也就永远为空。实际不是这样 —— startInteractiveUI.tsx 的嵌套是 AgentViewProviderBackgroundTaskViewProvider,所以真实应用里去重集合是有值的。
  • 'idle' 联合类型放宽。ServeSessionAgentTaskStatus['status']DaemonSessionAgentTaskStatus['status']'idle' 属于公共契约变更,所以我是逐个追踪读取点而不是直接相信它。传导是正确的:EnvironmentAgentTask 定义为 DaemonSessionAgentTaskStatus & {…},而 DaemonSessionTaskWithWorkflowStatus 是包含 agent 成员的联合类型,因此 taskStatusKey / taskStatusIcon 无需 cast 就能接受 'idle'。需要知道的是:CI 根本不跑 npm run typecheck(只有另一条 lane 里的 typecheck:integration),所以这一条是人工追踪,不是机器验证。
  • 拒绝审批被当成成功。 approved = outcome !== ToolConfirmationOutcome.Cancel 看起来是错的,直到我读了那个 enum —— 它没有独立的 reject 成员,Cancel 就是拒绝。所以 finally 里的 completed / failed 分支是正确的。
  • aria-disabled 的 CSS 覆盖。 .task[aria-disabled='true'] { opacity: 1 } 与它要抵消的 .task:disabled { opacity: .55 } 特异度相同(0,2,0),但它在源码中靠后,所以生效。按现在的写法没问题。

关于 review 期间的修复。 6552a00 值得单独说,因为它抓到的比我自己的判断更准确。我原本把 useTeamAgentRoster 每次 render 返回新数组标记为 render churn;真正的后果是 LiveAgentPanel 把它的「一秒一次」耗时刷新 interval 挂在这个数组的 identity 上,所以每次 render 换新 identity 都会在 interval 触发之前把它拆掉重建 —— 耗时数字就此冻结。而 [](无 team)分支意味着这会影响 live-agent panel 的每一个用户,不只是有 team 的用户。修复方式(模块级 NO_TEAM_ENTRIES 加上以 [manager, tasks, registeredAgents, revision] 为 key 的 useMemo)是对的,关于 revision 作为 change token 的注释诚实地解释了为什么要 disable exhaustive-depsuse-team-agent-roster.identity.test.ts 把这个契约钉住了,不会静默回退。tasksSnapshot.ts 也给 listTasks 加了 try/catch,共享任务看板读不出来时降级为「不显示任务标签」,而不是让整个 agents snapshot 失败 —— 这个洞我没看出来,而且配套测试是通过「在目录位置放一个文件」来强制触发非 ENOENT 的 readdir 失败的。

两个非阻塞项:

  1. 范围。 和 Stage 1 一致 —— fix(core): inherit Agent MCP servers in teams 可以从 roster 里拆出去。实现本身是干净的「对齐既有行为」而不是新行为:subagent-manager.ts 早就为普通 subagent 做了同样的事(把 agent 定义的 mcpServers 合并到 runtimeContext.getMcpServers() 之上、冲突时定义优先、强制重建 registry、只发现定义声明的 server 名),createPerAgentConfig 现在为队友做同样的事。frontmatter 形状在上游已由 parseAgentMcpServers 校验(null-prototype、形状不对就丢弃并告警),所以 TeamManager 里的 as cast 并没有吞掉未校验的输入。transport 清理走的是既有的 agentRegistry.stop() 路径。它只是不属于「roster」这个主题。
  2. 每次轮询的磁盘读取。 buildSessionAgentsStatus 现在每次快照都会 await listTasks(team.name),而 App.tsx 在任意 agent 处于 'idle' 时会持续轮询。idle 是可以无限期维持的状态,所以只要 team 存在,每次轮询都会对 $QWEN_HOME/tasks/<team>/ 做一次目录读取加 JSON 解析。这和 'paused' 现有的行为是一致的,加上新增的 try/catch 让它能安全降级,所以这是一条备注,不是要求。

一个很小的点,改不改都行:buildTeamAgentRosterEntries 仍然会为每个队友分配一个用不到的 new AbortController(),并且在 render 阶段修改 terminalEndTimes ref。加了 useMemo 之后两者都变得很少发生,而且队友行走不到取消路径(它们路由到 switchToAgent,dialog 读的仍然是 entries),所以没有任何逻辑依赖那个 controller。

测试证据

这是一次无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何代码 —— 下面的证据是通过 API 读取的本 PR 自己的 CI 结果。

在被 review 的 head d65e41fe 上,18 项检查已通过,4 项仍在进行。我只取了一次数据、没有轮询;下表反映的就是那一次读取,CI 落定后 finalize job 会重写它。

对这个 diff 来说,通过的那批检查比看上去更有信息量。TUI parity snapshots (ink vs opentui)OpenTUI no-flicker gate 都通过了 —— 对一个往 live-agent panel 增加行、并重接其 layout key 的改动来说这是真实信号。Integration Tests (no-AK, No Sandbox)Capture web-shell visuals 也通过了,visuals job 已为这个 head 发布了前后对比截图。

未验证 —— 这是诚实的缺口:

  • Test (ubuntu-latest, Node 22.x) —— 在这个 head 上仍在进行,而且在本 PR 的任何 commit 上都没有跑绿过。在 0293497e 上它是被取消的,卡在 ci.yml 里 120 分钟的 timeout-minutes 上限(job 16:58:59Z 开始,Run tests and generate reports 步骤在 18:59:13Z 被取消),当时 vitest worker 还活着 —— 日志结尾是 Terminate orphan process … node (vitest)。macOS 与 Windows 的 Test 分支以及 Integration Tests (CLI, No Sandbox) 因此被跳过。所以本 PR 新增和修改的 791 行测试,目前还没有一次绿色的单元套件运行作为支撑。
  • Lint & StaticReal daemon E2E / Java 11 —— 在这个 head 上仍在进行。(Lint & Static0293497e 上是绿的。)
  • npm run typecheck —— 本仓库 CI 完全没有这一步,所以没有任何东西在把关它。上面关于联合类型放宽的追踪是我手工做的。
  • Windows 与 Linux 行为 —— 作者在 Tested on 里两项都标了 ⚠️,手工证据只覆盖 macOS。那是作者自己的报告,不是我又跑了一遍的结果。

被取消的单元 lane 属于既有基础设施问题,不是本 PR 造成的。 两条独立证据,都来自 GitHub 设定的元数据和仓库自己的 workflow 文件,而不是日志正文:main 自己的 Test (ubuntu-latest, Node 22.x) 在最近两个 commit 上就是红的(49e3ef62 失败,10:20→11:34Z;101b003f 失败,09:02→10:06Z),而 49e3ef62 的失败诊断在 exit 1 之前打印了 runner 磁盘 97%/dev/nvme0n1p3 504G 465G 19G 97% /)—— ci.yml 只在返回码非零时才打印那段。ci.yml 里把这条 lane 从 lint_and_static 拆分出来的那段注释也记录了:vitest lane「在拆分那一周被测量为 41% 的 post-merge 运行失败」,而且 runner 池的争用正是上限设成 120 分钟的原因。我不会把这次取消归因于这个 diff;但我也不会把它当成通过。

沙箱验证可以解决 diff 和 CI 都解决不了的部分。这里的核心主张都是行为性的,PR 带着 review/self-reported 标签,手工证据只覆盖一个平台:

  • @qwen-code /verify —— 验证 #detachTeamManager 是否真的 fail closed。旧 leader 回调被拒绝、排队的队友通知被丢弃、team 在审批进行中被替换时待处理审批被取消,这些在 Session.test.ts 里都是对 mock 断言的;只有与 base build 做 A/B 才能说明这些 guard 是真正起作用的,而不是装饰。同样适用于「拒绝审批确实阻止了工具动作」这条路径。
  • @qwen-code /tmux —— CLI roster 界面。每个队友只有一行(不会出现「启动行 + 实时行」重复),idle 在视觉上与 completed 可区分,identity 修复之后耗时数字确实在前进,以及在队友行上按 Enter 打开其 Agent tab、而普通 subagent 行仍然打开后台任务详情。

Final CI results for d65e41f (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Lint & Static (ubuntu-latest, Node 22.x) 🚫 cancelled
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Live Host (macos-latest) ✅ success
macos-latest / Java 21 ✅ success
OpenTUI no-flicker gate ✅ success
Real daemon E2E / Java 11 ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
TUI parity snapshots (ink vs opentui) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the review came back clean and the one real defect I found was already fixed and test-pinned before I posted; the missing point is that the unit lane has never completed on this PR, so I cannot confirm the 791 test lines pass.

Before reading the diff I wrote down what I would do myself: derive a display-only roster from the live TeamManager inside the existing BackgroundTaskViewContext rather than adding a second context, reuse LiveAgentPanel rows and route Enter to the already-existing Agent tab, extend the daemon's session-agent preview additively instead of adding a route or SSE event, and put leader continuation on the existing serialized notification queue with approvals on the existing permission dialog. That is what this PR does. I did not find a simpler path it missed, and the one place I would have shaped it differently is sequencing — ship the roster, then the MCP inheritance as its own change.

What I would thank the author for in six months is the comments. The identity contract on the roster hook, why listTasks throws deliberately and why that must not propagate here, why revision is a change token rather than an input, why bare-task matching was narrowed and now includes agent — those are all why notes at the exact places a future reader would otherwise re-break something. The tests pin the non-obvious invariants (array identity, stale-callback rejection, unreadable task board) rather than restating the implementation.

Stating my reservation plainly, because it is the whole reason this is 4 and not 5: Test (ubuntu-latest, Node 22.x) was cancelled at the 120-minute ceiling on 0293497e and is still running on d65e41fe, so no commit of this PR has a green unit suite behind it. I have satisfied myself that the cancellation is runner-pool contention and disk pressure rather than anything in this diff — main is red on the same lane right now — but "not this PR's fault" is not the same as "verified". Stage 1e also matched acp-integration, which is precisely the path where a green suite that never ran is most expensive. npm run typecheck is not gated by CI in this repo either, so the union-widening trace in Stage 2 is hand-checked only.

So: no blocking findings, and I am not requesting changes. I am also not approving in this run, because the lane that would substantiate the behavioural claims is still in flight. Approval is deferred until CI lands green on d65e41fe14283cddf4e2eb6ec012bbc81b21ba9f; if anything lands red or the head moves again, that deferral is withheld rather than silently honoured. Given how the head moved twice during this review, a maintainer may prefer to just look at it directly — and @qwen-code /verify plus @qwen-code /tmux (Stage 2) would settle the fail-closed and TUI claims that no amount of diff reading can.

The two non-blocking items from Stage 2 stand as questions, not conditions: whether the MCP-inheritance commit belongs in a separate PR, and whether a per-poll listTasks read for as long as a team is idle is the cost you want on that route.

中文说明

Confidence: 4/5 —— review 结果是干净的,我找到的唯一一个真实缺陷在我发帖之前就已经被修复并用测试钉住了;扣掉的那一分在于单元 lane 在本 PR 上从未跑完,所以我无法确认那 791 行测试是通过的。

在读 diff 之前我先写下了自己会怎么做:在既有的 BackgroundTaskViewContext 里从实时 TeamManager 派生一个只读 roster,而不是新增第二个 context;复用 LiveAgentPanel 的行,Enter 路由到已经存在的 Agent tab;对 daemon 的 session-agent preview 做增量扩展,而不是新增路由或 SSE 事件;Leader 续跑放在既有的串行通知队列上,审批走既有的 permission dialog。这个 PR 就是这么做的。我没有找到它漏掉的更简路径;我唯一会做得不同的地方是节奏 —— 先上 roster,再把 MCP 继承作为独立改动。

六个月后我会感谢作者的是那些注释。roster hook 的 identity 契约、listTasks 为什么是故意抛出的以及为什么在这里不能让它传播、revision 为什么是 change token 而不是输入、bare-task 匹配为什么被收窄而现在又加入了 agent —— 这些都是写在「未来读者最可能重新搞坏的地方」上的 why 注释。测试钉住的是不显而易见的不变量(数组 identity、旧回调被拒绝、任务看板读不出来),而不是复述实现。

坦白说出我的保留意见,因为它正是这里是 4 分而不是 5 分的全部原因:Test (ubuntu-latest, Node 22.x)0293497e 上被 120 分钟上限取消,在 d65e41fe 上仍在运行,所以本 PR 没有任何一个 commit 有绿色的单元套件作为支撑。我已经确认这次取消是 runner 池争用和磁盘压力,而不是这个 diff 里的任何东西 —— main 现在在同一条 lane 上就是红的 —— 但「不是本 PR 的错」和「已验证」不是一回事。Stage 1e 也命中了 acp-integration,而那恰恰是「一次从未跑过的绿色套件」代价最高的路径。本仓库的 CI 也不把关 npm run typecheck,所以 Stage 2 里关于联合类型放宽的追踪只是人工核对。

所以:没有阻塞性发现,我不会 request changes。我在这次运行中也不会 approve,因为能够为行为性主张提供支撑的那条 lane 仍在进行。approve 被推迟到 CI 在 d65e41fe14283cddf4e2eb6ec012bbc81b21ba9f 上全绿之后;如果有任何一项变红,或者 head 再次移动,这个推迟会被撤回,而不是被静默执行。考虑到本次 review 期间 head 已经移动了两次,维护者也许更愿意直接自己看一遍 —— 而 @qwen-code /verify@qwen-code /tmux(见 Stage 2)可以解决那些无论怎么读 diff 都无法确定的 fail-closed 与 TUI 主张。

Stage 2 里的两个非阻塞项作为问题保留,不是条件:MCP 继承那个 commit 是否应该拆成独立 PR,以及「只要 team 处于 idle 就每次轮询读一次 listTasks」是否是你想加在那条路由上的成本。

Qwen Code · qwen3.8-max-2026-09-02

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

Durable agent identities that collaborate on a shared thread, as the
substrate for multi-agent work that survives the process it started in.

An Agent Team teammate is a live in-process loop: it receives messages at
a tool-round boundary, and it dies with its leader, taking the
conversation with it. A mesh agent is an identity whose work happens in a
daemon session resumed when the agent is addressed, so the conversation
is durable, visible to every participant, and replayable. The two are
complementary — a team is still the right shape inside one run.

This is the storage and rules layer only; nothing starts a session yet.

Storage follows the durable scheduled-tasks file: per-project runtime dir
rather than the working tree, atomic writes, and a corrupt file that
throws instead of reading as empty. Thread text is written by one agent
and fed to another, so keeping it out of the repo also keeps it from
becoming a committed, pulled prompt-injection surface.

The dispatch rules are the load-bearing part, and four of them are what
Multica arrived at in `handler/comment.go`: coalesce a post into a run
that has not started, defer when one is already executing, never let an
author wake itself, and let an explicit mention take routing away from
the assignee. The fifth is ours — a per-thread auto-turn budget, spent
only by agent-authored posts and reset by a human one. Multica does not
need it because its runs terminate and a person owns the issue; two mesh
agents answering each other have nothing else to stop them.

Posting and booking share one thread lock. Splitting them would let two
concurrent posts each see "no queued run for Alice" and book two, which
is the duplicate-dispatch race the Agent Team lifecycle audit found in
leader assignment (#10207).
Adds the module map (what this branch landed vs what is still unbuilt),
the end-to-end sequence, the full dispatch decision table with the reason
each outcome exists, a demo script, and an explicit scope boundary.

Section 9 lists the seven choices most likely to be wrong — session
binding growth, thread-scoped rather than pair-scoped budget, deferred
work stranded by a run that dies, best-effort concurrency counting,
agent-to-agent prompt injection, silent retention trimming, and the
missing cancellation path — so a reviewer does not have to find them.

Also carries the earlier gap analysis that led here.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Added: agent mesh — storage and dispatch rules, plus the architecture for review

This PR now carries a second, larger piece of work alongside the Agent Team roster. Recording what changed and what to review.

Why it is here

The roster makes an in-session team visible. It does not make several agents collaborate durably — a teammate is a live in-process loop that dies with its leader, taking the conversation with it. The mesh is the substrate for the second thing: durable agent identities that collaborate on a shared thread, with each agent's work happening in a daemon session that is resumed when the agent is addressed.

The two are complementary. Agent Team stays unchanged and remains the right shape for sub-turn collaboration inside a single run.

What landed

Storage and rules only — nothing starts a session yet.

File Responsibility
core/src/agents/mesh/types.ts MeshAgent, Thread, ThreadMessage, ThreadRun, and the three limits
core/src/agents/mesh/mesh-store.ts Paths, validation, locking, CRUD
core/src/agents/mesh/mentions.ts @name → agent ids
core/src/agents/mesh/dispatch-policy.ts decideDispatch — pure, one decision per (post, target)
core/src/agents/mesh/thread-actions.ts postMessage — append and book runs under one lock

Storage follows the durable scheduled-tasks file: per-project runtime dir rather than the working tree, atomic writes, and a corrupt file that throws instead of reading as empty. Thread text is written by one agent and fed to another, so keeping it out of the repo also keeps it from becoming a committed, pulled prompt-injection surface.

Four of the five dispatch rules are what Multica arrived at in server/internal/handler/comment.go: coalesce a post into a run that has not started, defer when one is already executing, never let an author wake itself, and let an explicit mention take routing away from the assignee. The fifth is ours — a per-thread auto-turn budget, spent only by agent-authored posts and reset by a human one, charged at booking rather than completion so a pair of agents that keep failing still runs out.

Still unbuilt

Thread tools (thread_post, thread_assign, thread_status, thread_read), session persona binding (POST /session accepting agentType), the dispatcher service, REST routes, and the Web Shell surface.

What to review

docs/plans/2026-09-06-multi-agent-board-collaboration.md. Section 9 lists the seven choices most likely to be wrong, so a reviewer does not have to find them:

  1. Session bound per (agent, thread) — warm context per work item, but unbounded growth with no reclamation.
  2. Auto-turn budget is per thread, not per agent pair.
  3. defer has no wake-up of its own — a blocking run that dies strands the deferred work.
  4. Concurrency counting is best effort and can overshoot the limit by one.
  5. Thread text is an agent-to-agent prompt-injection surface by construction.
  6. Retention trimming is silent and lossy.
  7. No cancellation path is wired.

Section 7 has the end-to-end sequence, section 8 the full decision table with the reason each outcome exists, and section 10 the demo script.

@yiliang114
yiliang114 marked this pull request as draft September 6, 2026 12:32
yiliang114 and others added 5 commits September 6, 2026 22:20
Turns the earlier sketch into an implementable design. Two findings from
reading the sources changed its shape.

Multica's agents do talk in real time — it resumes the prior agent
session (`daemon/types.go:110`) rather than rebuilding context, and
dispatches over a WebSocket push (`daemon/wakeup.go`), so an @ to an idle
agent starts it in about a second. Its one limitation is that a message
cannot enter a run already executing. Qwen Code does not share that
limitation: `resumeBackgroundAgent` queues into a running agent. The
design takes the durable shared-thread model and keeps mid-run steering.

The execution leg was wrong. An agent cannot be a daemon session carrying
an agent definition's persona: that machinery targets the agent runtime,
and there is no per-session persona hook to reuse. An agent is instead
one long-lived background agent per workspace — where the persona,
transcript, transcript viewer, wake-with-message, cross-restart revival,
auto-compaction and approvals all already exist. The orchestration layer
is the only thing that has to be written, and none of the reuse goes
through Agent Team.

Also records the four changes the settled decisions require in the code
already on this branch, the four questions still genuinely open, and what
is deliberately out of scope.
Re-reading the design as an implementer surfaced two mistakes.

The wall-clock gate measured elapsed time from first dispatch, so a thread
opened on Monday would already be over budget when revisited on Tuesday.
Elapsed time is not cost; a stuck run is the sweeper's job, not the
budget's. Removed, leaving turns and tokens.

`maxConcurrentRuns` contradicted the decisions above it: one long-lived
body per agent, serial across threads, already caps an agent at one
running run. The field was dead and `defer: agent_at_capacity` collapses
into `defer: busy_elsewhere`. Both dropped.

The prompt was one line — "thread context plus new posts" — for the part
that decides whether any of this works. Now specified: an explicit thread
frame on every wake (a long-lived cross-thread body makes confusion a
certainty, not a risk), mention tokens handed over verbatim rather than
guessed, whole thread on first entry with an honest truncation notice
after that, and thread tools that take no thread id from the model —
resolved from ambient context the way Agent Team resolves teammate
identity, so a confused agent cannot post into an unrelated thread.

Also records the comparison against Multica: ahead on mid-run steering,
deliberately without code changes and multi-user features, and genuinely
missing runtime binding. Roughly 35-40% of Multica as a product, most of
the remainder being a server-product category difference rather than a
backlog; roughly 80% of multi-agent collaboration itself.
Read as someone picking it up cold, the document had four gaps.

The worst was silent: it read as an established design, while the code on
this branch has never been built, type-checked, or executed — builds and
test sweeps are not run on this machine, and CI on this PR will be the
first thing to run any of it. A reader would reasonably have assumed a
verified foundation and built on top. Section 0.2 now separates what was
verified by reading source, what was written but never run, and what has
never been prototyped end to end, and names the two claims that would
collapse decisions 6 and 9 if they turn out wrong.

It also had no order of work, no acceptance criteria, and no way for a
reader to re-check the Multica claims independently. Sections 5.2 and 5.3
add a dependency-ordered sequence and the coverage expected at each step;
0.2 names the repo, the commit, and the symbols to argue from — an earlier
version of this design was wrong about Multica precisely because it
reasoned from documentation rather than those files.
Step 1 of the implementation order: the rules on disk now match the
design rather than contradicting it.

Applying the decisions removed two things rather than adding them.
`maxConcurrentRuns` was dead on arrival — one long-lived body per agent,
serial across threads, already caps an agent at one running run, so what
needs bounding is the backlog behind it, and that is now `queueLimit`.
The wall-clock budget measured elapsed time from first dispatch, which
would have refused a thread opened on Monday and revisited on Tuesday;
elapsed time is not cost, and a hung run belongs to the stall sweeper.

Writing it also surfaced a design error worth naming. The `defer` class
of outcomes put a scheduling decision inside a rules function: whether a
booked run can start depends on what the agent's single body is doing,
which is the dispatcher's business, not a rule. The rules now only book,
coalesce, or refuse, and a run that cannot start yet is simply a queued
run. That also collapses "busy on this thread" into a coalesce, which is
correct — mid-run delivery is available here, so a second run would be
waste. This is the case Multica has to defer and we do not.

Budgets are evaluated against the thread tree's root, inherited at
creation rather than resolved by walking parents, so splitting work into
sub-threads cannot mint allowance. Tokens are charged from each run's
usage delta and, unlike turns, are never reset by a human post: turns
measure how long a conversation has run unattended, tokens measure money
already spent.

Still never executed — see the plan's §0.2.
Source-backed review of 2026-09-06-multi-agent-board-collaboration.md at
af7fed7. Records the runtime seams the implementation must not re-derive,
findings by severity, the minimal change set, and the checks that only an
end-to-end run can settle. Nothing here was built or executed.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Design review, round 2 — hand-off committed

Source-backed review of docs/plans/2026-09-06-multi-agent-board-collaboration.md at af7fed70, written up as a hand-off for the implementing agent:

docs/plans/2026-09-07-mesh-review-round2-handoff.md (commit b4e72c2c65)

Nothing in it was built or executed; every claim carries a file:line read at origin/main @ 703678136a (Qwen Code) or multica@7a438bd5b.

Verdict: keep the root model, change the contract. Three Critical items:

  1. Token charge key collides. USAGE_METADATA.round restarts per runReasoningLoop (agent-core.ts:920, :2695), and the runtime runs the loop twice in one run when the final drain finds pending input (background-agent-resume.ts:1255-1261). (runId, attempt, round) dedupes the second segment away.
  2. Resident-continuation prompts never reach the transcript. The writer subscribes only to ROUND_TEXT/STREAM_TEXT/TOOL_CALL/TOOL_RESPONSES_FINALIZED/EXTERNAL_MESSAGE (agent-transcript.ts:864-868); task_prompt from continue() is not among them. "Transcript is the agent's memory" and the run slice are both incomplete after a cold revive.
  3. No system-role seam for the trusted envelope. The chat is created once (agent-headless.ts:296-299) with a fixed system instruction (agent-core.ts:588-595); every later turn is user role.

Also recorded: the seven mesh/ files on this branch are byte-identical to the round-1 revision, so the §5.1 "verified but uncommitted" patch is not here and its four defects are still present in the PR code.

The doc has ten Important findings, a minimal change set in order, and six checks that only an end-to-end run on a build-capable machine can settle (with the C1/C2 reproductions as items 3 and 4). Three items are product decisions for the owner, not the implementer: envelope role (C3), whether a parent agent may post into a child thread it created (I3), and which blockers a human @ reply acknowledges (S5).

@yiliang114

Copy link
Copy Markdown
Collaborator Author

The mesh foundation and design have moved to focused PR #11206. Commit 7e394d9 removes the duplicate mesh files from this branch without rebasing; #11072 now stays scoped to the Agent Team/Web Shell work.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Scope note after the split: this PR is the Agent Team roster/status work only. The shared-thread mesh subsystem, its design doc, the round-2 review hand-off, and the per-step acceptance criteria all live in #11206, which is the single mesh PR. #11140 remains stacked on this branch until #11206 step 9 absorbs the sidebar entry.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Correction to the earlier handoff: the mesh design, foundation fixes, and shared agent-runtime fixes are now integrated directly into this PR. This PR is the canonical relay branch again; the follow-up PRs remain drafts only as review checkpoints. Current head: dce16b4.

@doudouOUC doudouOUC 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 — head dce16b4c, base main (draft)

+4 999 / −141 across 69 files. Author is a repository admin, so the two-tier core gate does not block; the non-blocking size advisory applies. Core-infrastructure surface touched: packages/core/src/agents/**, packages/core/src/tools/**, packages/core/src/services/**, plus packages/cli, packages/web-shell, packages/acp-bridge, packages/sdk-typescript — genuinely cross-package. No local test run.

Web Shell conventions: no violations found. I checked specifically, since this touches web-shell UI. No new component is introduced, no portal surface is added, and nothing new receives or forwards a ref, so the forwardRef and useWebShellPortalRoot() requirements do not come into play. Existing data-web-shell-* attributes and CSS variables are untouched.

Critical — duplicate task rows and duplicate React keys on the team-match arm

In packages/web-shell/client/App.tsx, the liveAgents.find(...) predicate has five arms. The last one is guarded against re-claiming a task:

(!seenTaskIds.has(task.id) &&
  !isPreciselyClaimed(task) &&
  matchesLiveTaskContent(task)),

The team arm immediately above it is not:

(task.teamName != null &&
  subagentName != null &&
  task.label === subagentName) ||

task.label === subagentName is not a unique match — it is the whole point of a team roster that the same named agent can be invoked more than once. Two distinct tool calls with the same subagentName pass the seenToolCallIds filter (their callIds differ), both resolve to the same live task, and both agents.push({ ...liveTask, toolUseId: tool.callId, ... }) — producing two entries carrying the identical liveTask.id.

seenTaskIds.add(liveTask.id) runs after the push, so it does not prevent the second match; and the reconciliation pass at the bottom (seenTaskIds.has(task.id) || ... alreadyListed) only guards live tasks that were never matched, not duplicates already in agents.

Consumers key on id:

  • packages/web-shell/client/components/panels/EnvironmentPanel.tsx:352<li key={task.id}>
  • packages/web-shell/client/components/artifacts/AgentWorkflow.tsx:193 and :209key={task.id}

So this is a duplicate visible row plus a React duplicate-key collision, which will also make state on those rows behave unpredictably across re-renders. Adding the !seenTaskIds.has(task.id) conjunct to the team arm — matching the arm below — looks sufficient.

Suggestions

1 372 lines of agents/mesh/** are unreachable at this SHA. Nothing in that directory is exported from packages/core/src/index.ts, so no consumer outside the package can reach it and it has no in-package production call site. I assume it is being staged ahead of the codex/mesh-step-* series landing, which is reasonable — but shipping it inside an agent-team UI PR makes both halves harder to review, and it inflates the size advisory above with code that cannot be exercised. Splitting it out would help.

Team rows' runtimeMs grows without bound. Rows constructed from a live team task never receive an endTime, so the elapsed computation keeps climbing after the agent is done. The tool-derived branch handles this correctly (tool.endTime ?? tool.startTime ?? startTime); the team branch inherits ...liveTask and has no equivalent.

The 3-second agents poll never quiesces. idle is a steady state rather than a terminal one, so the backoff condition is never satisfied and the poll runs for the life of the session. On an idle session that is pure overhead.

liveAgentEntries? is a dead optional. It is declared optional and read in four places with ?? … fallbacks, but no caller omits it, so all four fallbacks are unreachable. Either make it required or populate it from a path that legitimately can't supply it.

Verdict

C=1, four Suggestions. The Critical is a small fix; the unreachable-mesh point is the one I'd act on before asking anyone else to review this, since it is a third of the non-test diff. Limitations: no local test run, and I did not exercise the UI in a browser, so the duplicate-row consequence is derived from the code and the key sites rather than observed.

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

Agent-assisted review at 088a990b6f61610ca6222967058401885b2c3c4a — 1 confirmed standing Critical; Partial review — coverage gaps.

Pinned base: 4dbfc0c1c1a40f59ceee76bac5477daddc646d9c. This is the current 70-file scope, including the mesh foundation, not the narrower roster-only scope described in some older comments.

Critical — prior duplicate teammate row/key finding still stands

packages/web-shell/client/App.tsx:2541-2551 still lets the team-name arm match a task without checking seenTaskIds. Given two transcript agent calls with distinct call IDs but args.name: 'reviewer', and one live team row labelled reviewer, both calls find that same row. :2567-2578 spreads its same id into both output entries. Recording the ID at :2599 does not help because this arm never reads it; the final reconciliation loop only prevents appending an additional unmatched task.

The result is two visible entries with the same ID, used as React keys in components/panels/EnvironmentPanel.tsx:351-352 and components/artifacts/AgentWorkflow.tsx:191-209. Trace merging is not a universal cure: App.tsx:2631 preserves the array when no trace is available, and trace fetching is conditional on the workflow tab/capability (:6812-6819). This is the same mechanism reported in review 5168484994, not a new independent finding.

Ensure one live task is claimed at most once, including the name-match arm. Extend the existing single-launch test (App.test.tsx:7738-7785) with two different call IDs sharing the teammate name and assert unique result IDs. The defect is established by the producer/consumer trace, not by missing tests alone.

Other checked surfaces and limits

Read the roster projection and memoized CLI roster hook, teammate permission/notification lifecycle, MCP-definition forwarding and per-agent discovery, resident-continuation result consumers, and selected permission/roster tests. Team status remains live-session/Config-owned through the existing agents snapshot; this PR does not add a new daemon route. The new continuation result is compared explicitly by send-message.ts, rather than treating every nonempty string as success.

Historical mesh notes require care: the current usage event uses cumulative rounds (agent-core.ts:1198-1210), and resident continuation already emits EXTERNAL_MESSAGE (agent-headless.ts:279-288); the original claim that those prompts were never transcripted is not supported. Delivery IDs now pass through to transcript records. The proposed trusted-envelope role is still documented as an open product decision, not an implemented dispatcher guarantee.

Coverage gaps: full mesh storage/locking/retention and dispatch-rule audit, all mesh design decisions, every changed test body, complete UI/platform behavior, and exhaustive downstream optional-field consumers. Pinned-source search finds only intra-mesh production imports; no integrated dispatcher was established. This is not a full pass on that foundation or a statement that all design Criticals are resolved. Prior size/polling/elapsed-time/dead-optional Suggestions are not converted into blockers or repeated as new findings.

No PR code, tests, builds, installs, or workflows executed. Author maintainer status was not independently verified; feature size is advisory, not an external-refactor policy block. Comment only; no approval implied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show Agent Team teammates in the existing live agent roster

4 participants