Skip to content

feat: project agent identity and Agent Team delivery onto the ACP/daemon wire (refs #13785) - #13902

Draft
yiliang114 wants to merge 86 commits into
QwenLM:mainfrom
yiliang114:feat/13785-multi-agent-identity
Draft

yiliang114 wants to merge 86 commits into
QwenLM:mainfrom
yiliang114:feat/13785-multi-agent-identity

Conversation

@yiliang114

@yiliang114 yiliang114 commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Projects agent identity and Agent Team coordination onto the ACP/daemon wire, in two deliberately distinct collaboration columns.

Subagent column (parent→child delegation): every projected subagent frame (thought, message, tool call, tool call update) now carries _meta.agent = { agentName, depth, kind } with a path-style name (/root/general-purpose), mirroring OpenAI Responses' per-item agent identity; the Web Shell keeps that path on the subagent panel name as a title tooltip only — no visible path chip ships in this slice (SubAgentPanel.tsx:345; pinned by SubAgentPanel.test.tsx keeps the path on the name title without rendering it as a chip). agentName is a readable role path, not a unique execution id: concurrent same-type subagents share it, and renaming the agent definition changes it. parentToolCallId distinguishes subagent executions; that boundary is documented on both the producer and the normalizer.

Agent Team column (peer complete agents): an ACP-hosted session now attaches a leader sink (team-sink.ts). Before this, TeamManager.pollLeaderInbox() returned before consuming on this host (teammate reports stayed unread in the leader inbox) and TEAMMATE_APPROVAL_REQUEST was void-emitted — the coordination never reached anyone. Now teammate reports are delivered as peer background notifications carrying structured sender identity (kind: 'teammate', minted from the inbox entry's from, never parsed from message text), roster joins/exits are projected as peer notifications so an already-attached client can track membership transitions (there is no current-state snapshot for late attach or reconnect), and teammate tool approvals route to the client's requestPermission (fail-closed: Cancel plus a <team_notice> to the leader when the host cannot answer; YOLO auto-approve only when the request carries no renderable details).

Why it's needed

#13785 records the gap: the public vocabulary names child agents and teams, but nothing a client receives says which agent produced what. Both reference protocols (OpenAI Responses multi-agent, Anthropic Managed Agents) treat agent identity as contractual. This slice delivers the identity dimension on the daemon/ACP projection and removes the "ACP host has no leader sink" failure class — the zero-signal class among the three host classes in #13785 M4 — giving M1b a reference wire shape. Peer delivery reuses core's existing 'peer' background-notification channel (idle wake, loss accounting, kind-agnostic drain/display), without a new peer runtime. Team-binding approval retirement now uses one private, versioned qwen/notify/session/teammate-binding-ended notification on the existing SDK 0.14.1 connection; it adds no daemon REST route or public mediator API.

Reviewer Test Plan

How to verify

At 3499b027c4faa, 2,106 focused cases, targeted ESLint/Prettier, five affected workspace typechecks and integration typecheck passed on the exact committed candidate. Approval retirement, status identity, activity isolation and canonical START behavior were mutation-checked. A serialized ACP component probe verified old-binding cancellation and replacement approval at cap 1; a local macOS browser rendered the real transcript pipeline with injected protocol frames without splitting leader output or taking its usage. Main was synchronized to supply the current CI budget helper; obsolete assistant-activity handling was removed, and the rebuilt transcript's size ceiling was raised from 1,338,000 (main) to 1,340,000 in packages/web-shell/client/build-artifact.test.ts — merged with main the entry measures 1,338,274 UTF-16 units, past both main's ceiling and the 1,338,500 this branch carried. See the current repair report for build results and verification limits. No real-provider or full daemon/browser E2E was run for this repair. The real-model report and screenshots at df760052100d1 and the runtime observations below are historical evidence, not current-head acceptance.

Real machine: boot the daemon from this branch with QWEN_CODE_ENABLE_AGENT_TEAM=1 node scripts/dev.js serve --port <p> --token <t> --workspace <dir>, create a session, send a team prompt (team_create, then spawn teammate correctness asking it to report one sentence ending in a marker). Expected and observed on SSE: peer notifications teammate-roster-1 (status joined) and teammate-message-2/3, each carrying _meta.agent={"agentName":"/root/correctness","depth":1,"kind":"teammate"}, plus leader-reply frames with source=background_notification_response; the same capture contains zero subagent-column frames. Contrast run without a team: 47 parentToolCallId-chained frames carrying kind:'subagent' and zero peer markers — the two columns are mutually attributable at a glance. The Web Shell rendered the peer rows (ⓘ … · peer) and the verbatim teammate report in a real headless Chromium against the same daemon.

Evidence (Before & After)

Before: on this host an ACP session with a team delivered 0 teammate reports (retained unread in the leader inbox) and emitted 0 approval signals; no projected object carried any agent identity. After: the SSE markers and browser rendering described above.

Tested on (earlier runtime acceptance)

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ⚠️ not tested
🐧 Linux ✅ tested

Environment (optional)

Linux host; daemon via node scripts/dev.js serve (no bundle step); headless Chromium (playwright) for Web Shell rendering; unit suites via vitest with env -u QWEN_CODE_SERVE -u QWEN_RUNTIME_DIR.

Risk & Scope

  • Main risk or tradeoff: identity is additive _meta; older readers ignore it. Teammate identity is minted only for single-sender batches — mixed-sender batches stay unattributed rather than misattributed. Status/idle team events are deliberately not projected (each would wake the leader into an automatic turn). Join/exit notifications are incremental, so a client that attaches late still needs a non-waking roster snapshot/query before it can claim authoritative current membership.
  • Not validated / out of scope: managed-agent public-contract projections (Multi-Agent API: an agent-identity dimension on the public contract, so multi-agent execution is attributable, tree-shaped and interruptible #13785 M1a/M2/M3) and client intervention verbs (M4) remain design surfaces; foreground nested subagents do not project their own frames today (verified on real machine: single parentToolCallId layer, depth values only 1), so depth ≥ 2 identity has no wire producer yet — nested trees remain record-projection work (M1a); cross-session teammate projection is M1b/M3. Mailbox consumption and model-visible delivery are not transactional; crash-safe redelivery after process/session destruction remains follow-up work, and reversible-close retention does not provide that guarantee.
  • Breaking changes / migration notes: identity fields are additive. Approval retirement requires this CLI/bridge pair; older bridges that ignore the new private notification retain the previous stale-approval behavior. No new daemon REST routes, public mediator APIs, dependencies or persisted formats.

Linked Issues

Refs #13785 (referenced without a closing keyword: the public-contract milestones remain open design work).

中文说明

本 PR 做了什么

把 agent 身份与 Agent Team 协调投影到 ACP/daemon 的 wire 上,分为刻意保持区分的两条协作列。

subagent 列(父子委派):每个投影出的 subagent 帧(thought、message、tool call、tool call update)现在都带 _meta.agent = { agentName, depth, kind },名字为 path 风格(/root/general-purpose),与 OpenAI Responses 的逐 item agent 身份同形;Web Shell 本片只把该 path 挂在 subagent 面板名字的 title tooltip 上,不渲染可见的 path chip(SubAgentPanel.tsx:345;由 SubAgentPanel.test.tsx 的 keeps the path on the name title without rendering it as a chip 钉住)。agentName 是可读的角色 path,并不是唯一执行 ID:同类型并发 subagent 会共享它,agent 定义改名也会改变它;parentToolCallId 用于区分 subagent 执行,这一边界在生产侧与 normalizer 两侧都有文档。

Agent Team 列(对等完整 agent):ACP 托管会话现在会挂一个 leader sink(team-sink.ts)。在此之前,本宿主上 TeamManager.pollLeaderInbox() 会在消费前早退(队员报告留在 leader inbox 里未读),TEAMMATE_APPROVAL_REQUEST 空 emit——协调过程到不了任何人手里。现在队员报告以 peer 背景通知投递,带结构化发送者身份(kind: 'teammate',从 inbox 条目的 from 铸造,绝不从消息文本解析);roster 的加入/退出投影为 peer 通知,已挂载的客户端可跟踪成员变化(晚挂载或重连时没有当前状态快照);队员的工具审批路由到客户端 requestPermission(fail-closed:宿主无法应答时 Cancel 并向 leader 注入 <team_notice>;仅当请求不带可渲染细节且处于 YOLO 时才自动批准)。

为什么需要

#13785 记录了缺口:公共词表里有 child agent 与 team,但客户端收到的任何东西都不说明「哪条输出是哪个 agent 产的」。两个参照协议(OpenAI Responses multi-agent、Anthropic Managed Agents)都把 agent 身份当契约内容。本片在 daemon/ACP 投影侧交付身份维度,并消掉「ACP 宿主没有 leader sink」这一失效类——即 #13785 M4 三类宿主中的零信号类——为 M1b 给出参考 wire 形状。peer 投递复用 core 既有的 'peer' 背景通知通道(空闲唤醒、丢失记账、kind 无关的 drain/display),不增加 peer runtime。队绑定审批回收新增一条私有、版本化的 qwen/notify/session/teammate-binding-ended 通知,走既有 SDK 0.14.1 连接;不添加 daemon REST 路由或 public mediator API。

评审测试计划

如何验证

3499b027c4faa 的精确提交候选通过 2,106 条聚焦用例、定向 ESLint/Prettier、五个相关 workspace 的类型检查和 integration 类型检查;审批回收、status 身份、活动隔离与 START canonical 行为做过变异反证。真实 ACP 串行化组件链路以 cap 1 验证旧绑定取消和新绑定审批;本地 macOS 浏览器通过真实 transcript 流水线渲染注入的协议帧,leader 答案与 usage 未被活动切断或夺走。已同步 main 补齐当前 CI 预算脚本,并删除旧 assistant 活动路径;重建 transcript 的体积阈值是上调的——由 main 的 1,338,000 提到 packages/web-shell/client/build-artifact.test.ts 里的 1,340,000,因为与 main 合并后该 entry 实测 1,338,274 UTF-16 单位,同时超出 main 的阈值和本分支此前携带的 1,338,500。构建结果与验证限制见本轮修复报告。本轮未跑真实模型或完整 daemon/browser E2E。df760052100d1 的真实模型报告与截图及下方真机记录仅为历史证据,不作为当前 head 验收。

真机:从本分支起 daemon(QWEN_CODE_ENABLE_AGENT_TEAM=1 node scripts/dev.js serve --port <p> --token <t> --workspace <dir>),建会话,发 team prompt(team_create,再派 teammate correctness 要求其回一句以 marker 结尾的话)。SSE 上期望并实测到:peer 通知 teammate-roster-1(status joined)与 teammate-message-2/3,均带 _meta.agent={"agentName":"/root/correctness","depth":1,"kind":"teammate"},以及 source=background_notification_response 的 leader 回话帧;同一抓帧中 subagent 列帧为零。不带 team 的对照跑:47 帧 parentToolCallId 链帧带 kind:'subagent'、peer 标记零命中——两列形状互斥、可一眼归因。Web Shell 在真 headless Chromium 中对同一 daemon 渲染出了 peer 行(ⓘ … · peer)与队员原话。

证据(Before & After)

Before:本宿主上带 team 的 ACP 会话投递 0 条队员报告(留在 leader inbox 未读)、0 个审批信号;任何投影对象都不带 agent 身份。After:上述 SSE 标记与浏览器渲染。

Tested on(较早版本的真机验收)

OS Status
🍏 macOS ⚠️ 未测试
🪟 Windows ⚠️ 未测试
🐧 Linux ✅ 已测试

环境(可选)

Linux 宿主机;daemon 走 node scripts/dev.js serve(无 bundle 步骤);Web Shell 渲染用 headless Chromium(playwright);单测走 vitest 并加 env -u QWEN_CODE_SERVE -u QWEN_RUNTIME_DIR。

风险与范围

  • 主要风险或权衡:身份是 additive _meta,旧读者会忽略它。teammate 身份只为单发送者批次铸造——混合发送者批次宁可无身份也不张冠李戴。status/idle 队事件刻意不投影(每条都会唤醒 leader 起一个自动 turn)。加入/退出通知是增量流,晚挂载客户端仍需要一个不会唤醒 leader 的 roster 快照/查询,才能声称拿到了权威当前成员表。
  • 未验证 / 超出范围:managed-agent 公共契约投影(Multi-Agent API: an agent-identity dimension on the public contract, so multi-agent execution is attributable, tree-shaped and interruptible #13785 M1a/M2/M3)与客户端干预动词(M4)仍是设计面;foreground 嵌套 subagent 今天不投影自身帧(真机验过:parentToolCallId 仅一层、depth 取值仅 1),故 depth ≥ 2 身份暂无 wire 生产者——嵌套树仍是记录投影工作(M1a);跨 session 的 teammate 投影属 M1b/M3。mailbox 消费与模型可见投递不是事务;进程崩溃或会话销毁后的安全重投递留到后续,可逆 close 时保留队列不构成这一保证。
  • 破坏性变更 / 迁移说明:身份字段为 additive。审批回收需要本轮配套 CLI/bridge;忽略新增私有通知的旧 bridge 仍保留此前的陈旧审批行为。不添加 daemon REST 路由、public mediator API、依赖或持久化格式。

关联 Issue

Refs #13785(不用 closing 关键字引用:公共契约里程碑仍是开放的设计工作)。

yiliang114 and others added 6 commits October 11, 2026 13:23
…ames

Every output item in OpenAI's Responses multi-agent contract carries
`agent: { agent_name }` as a path (`/root`, `/root/security`), so a client can
attribute output and rebuild the tree without cross-frame association state.
Our frames carried only `parentToolCallId` plus a `subagentType` name, so two
parallel subagents of the same type were indistinguishable except by walking
parent chains.

Stamp a derived identity through the existing `SubagentMeta` seam rather than
adding a relay hop: both emitters already spread `...subagentMeta` into
`extra`, which `buildUpdateMeta` folds into the wire `_meta`, so message and
tool frames carry it from one field. The client parses it into typed fields and
the reducer carries it onto transcript blocks.

A half-populated identity projects to nothing instead of to a plausible name --
missing depth, missing kind, unknown kind, empty name and negative depth are
all rejected, because a client keys rendering and auto-approval policy on
`agentName` and must never see one whose depth/kind disagree with it.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Team tool registration is gated on `Config.isAgentTeamEnabled()`, which knows
nothing about the host, while the leader callback and the approval listener
were attached by the TUI and the stream-json CLI only. On every other host the
failure was silent: a model created a team, teammates ran, and
`TeamManager.pollLeaderInbox()` returned before consuming because no callback
was attached -- so reports were never delivered and the leader was never woken,
while `TEAMMATE_APPROVAL_REQUEST` was emitted with no listener, leaving a
blocked teammate neither approved nor cancelled.

`attachTeamSink` closes both, reusing the channel core already models for
cross-session messages instead of inventing a wake mechanism: reports are
enqueued as `kind: 'peer'` background notifications, and
`#enqueueBackgroundNotification` already ends by draining, so an idle leader
wakes with no ACP protocol change. Approvals route through the existing
`#requestPermissionQueued`; one the host cannot answer fails closed and tells
the leader why, rather than letting the teammate proceed unreviewed.

`peer` is the right kind, not merely a convenient one: core already carries it
through `GROUP_ORDER`, `droppedNoun`, the `hasPeerLoss` accounting and
`background-turn-context`, and like `cron` it names no task the model could go
and read, so a dropped report is recorded as a real loss.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The wire and the reducer already carried a path-style producer identity, but
nothing read it, so a panel could only name its subagent by type. Two parallel
subagents of the same type were therefore indistinguishable in the UI even
though the frames said which was which.

Carry the transcript block's `agent` through to the tool presentation as
`agentPath`, and show it only where it adds information: at depth 1 the path's
last segment *is* the localized type name, so rendering both would be noise in
the common case. The full path goes on the name's `title` for hover precision,
and only a nested subagent gets a visible path chip.

`appendSubContent` adopts the first identity and never rewrites it, so a later
frame cannot relabel a card the user is already reading.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The producer-side derivation and the client-side type disagreed about what
`agentName` guarantees: the client doc called it stable and invited keying
policy on it, while the path's last segment is the subagent type -- so renaming
an agent definition renames the path, and a policy keyed on the old string
silently stops matching.

State the real contract in both places: deterministic and safe to group or key
in-session policy on, but not a durable id; a consumer that must survive renames
keys on `parentToolCallId` instead. Minting a durable authority-owned agent id
stays open in QwenLM#13785 rather than being faked here.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The peer notification that carries a teammate report named its sender only
inside the formatted display text, so a client had to parse the sender out of a
string -- the exact escape-hatch failure the identity work exists to prevent.
The subagent column already carried `_meta.agent`; the team column did not.

`TeamManager` already knows the sender: the leader inbox entry carries `from`,
and `MESSAGE_SENT` emits it. Thread it through the leader callback as an
optional third argument (the two-argument callers in the TUI and stream-json
are unaffected), and mint `/root/<member>` with `kind: 'teammate'` on the peer
frame. A mixed-sender batch stays unattributed rather than naming one teammate
for several reports.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
A client could see a teammate's report arrive but never learn who is on the
team: roster membership lived only in TeamManager's in-memory state and the
team file, with no wire surface. Join and exit events already exist on the team
event emitter with structured names, so the sink now forwards them and the
session projects each as a `peer` notification carrying the member's identity,
letting a client reconstruct the roster from joins and exits without a roster
route.

Status changes and idle pulses are deliberately not projected: they fire on
every transition, and each would wake the leader into an automatic turn for
noise. Membership changes are the roster-defining subset.

Refs QwenLM#13785

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

qwen-code-review-bot commented Oct 11, 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 6ed7884. 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 4 render-shaping files:

  • packages/web-shell/client/components/MessageItem.tsx
  • packages/web-shell/client/components/messages/SystemMessage.module.css
  • packages/web-shell/client/components/messages/SystemMessage.tsx
  • packages/web-shell/client/components/messages/tools/SubAgentPanel.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

yiliang114 and others added 13 commits October 11, 2026 20:42
The live wire stamps _meta.agent on AgentTool frames from SubAgentTracker,
but persistence keeps only subagentName inside the task_execution display,
so a reloaded session replayed with no identity and the Web Shell lost the
agent path on subagent panels.

Replay now mints the root-parented identity from the task_execution display
on the closing result frame; the SDK reducer adopts it on an existing tool
block with the same first-wins policy as subagentType; the web-shell adapter
maps the block agent onto agentPath so the panel tooltip survives reload.
A teammate report or roster change reached the Web Shell as a generic
system row: the daemon projects `_meta.agent.kind === 'teammate'` on those
frames, but the transcript adapter dropped the identity, so a multi-agent
session read as a stack of anonymous notices instead of a conversation
between named agents. The adapter now carries the minted teammate name onto
the system message and SystemMessage renders it as an `@name` chip, while
task and subagent notifications stay unchipped.

Refs QwenLM#13785

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

Real peer frames carry `_meta.source: 'background_task_completed'`, not
'background_notification', so the teammate chip added in the previous commit
never rendered on a live session: the attribution lived on the wrong push
site. Compute it on the task-completed push site as well and cover that path
with a regression test.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The Lint & Static job failed on three errors: teamSink.ts and
teamSink.test.ts violate the KEBAB_CASE filename convention, and
teamSink.ts imports values from the core package root, which the
no-restricted-imports rule sends to the module defining each symbol
(QwenLM#10908).

Rename both files to team-sink.ts / team-sink.test.ts (updating the
Session.ts import) and split the core import into the defining
modules: config/approval-mode, agents/team/team-events,
agents/team/TeamManager, tools/tools, utils/debugLogger and
config/config. No allowlist entries added; the legacy lists only
shrink.
The Agent Team column stamped teammate identity only on the live wire:
`_meta.agent` rode the SSE frames, but `recordNotification` /
`recordBackgroundTaskCompleted` dropped `item.agent` on the floor, so a
reloaded session replayed peer notices with no attribution — the Web Shell
turn-start row fell back to the generic 'peer' and the notice bubble named
no one.

Persist the identity inside the `backgroundTask` payload (replay passes it
through wholesale), label peer turns with the sender's name so the
turn-start row reads 'Cross-session message · <name>' both live and after
reload, and render the persisted identity as a teammate chip on the
notification bubble.
In a live session the leader consumes each peer notification, so the
`background_task_completed` content row is suppressed by the
awaiting-processing guard and the only peer row the user sees is the
turn-started marker — which carries no identity of its own. Correlate it by
background task id against the display frames (which do carry
`_meta.agent.kind === 'teammate'`) and render the same `@name` chip there, so
a multi-agent session reads as named members talking instead of anonymous
system rows.

Refs QwenLM#13785

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
A replayed peer notice arrives as a flat user_message_chunk (no update
wrapper), which the normalizer routes through the content-part path —
the earlier agent extraction only covered the wrapped plain-text branch.
Extract `_meta.agent` there too, so the block reducer's `block.agent`
and the Web Shell's teammate attribution work after a reload, matching
the live wire.
Teammates ran as a black box: the leader's UI showed roster joins and
final reports, but nothing of the work between. The event bridge
already observed every teammate tool call, tool result, and team
message — and discarded them as activity heartbeats.

Forward that data as display-only frames: a new team activity event
carries tool name, phase, and one-line description; the team sink also
subscribes to the existing message-sent event. The session publishes
them as peer frames stamped with the teammate's identity, without
entering the background notification queue — activity is observational
and must never wake the leader into an automatic turn, which is why
these frames are also not persisted: the report a teammate eventually
sends is what the transcript must keep.

The web shell renders them as attributed notice bubbles: tool calls
and results show the teammate chip and the tool line, inter-agent
messages render the sender-to-recipient thread, so a multi-agent team
now reads as named members collaborating instead of silent workers
punctuated by system rows.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The inter-agent frames rendered direction only in their display text,
so a client that wanted the sender had to parse prose. OpenAI's
Responses multi-agent contract models this natively: an agent_message
item names the recipient through its agent attribute and the sender
through a separate author field.

Carry the same semantics on this wire, following the existing
borrow-shape-not-spelling rule the agent identity type documents: the
peer frame's agent now names the message recipient (the leader's
teammates map to /root when addressed), and the structured payload
gains an author identity with path, depth, and kind. Tool activity
frames gain toolName, phase, and success alongside, so the whole
activity surface is machine-readable rather than display-only.

The web shell renders the sender as a second chip on inter-agent
message rows — recipient chip from the teammate attribution, sender
chip from the structured author — so a message thread reads at a
glance who told whom.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The direction already reads from the row text (writer → researcher: …);
a second from-chip restated it, and the recipient chip alone carries
the structured attribution. The wire keeps the author field — OpenAI's
agent_message semantics stay machine-readable for clients that want
the sender programmatically.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Nakagawa-master commented Oct 11, 2026 •

Copy link
Copy Markdown

There is a same-instance manager notification inverse in the new ACP team sink, independent of the normal manager-replacement path.

At this head, attachTeamSink's handleManagerChange(manager) only calls detachFromManager(boundManager) when boundManager !== manager. It then always re-registers five emitter.on(...) handlers, even if boundManager === manager and those exact handlers are already bound (packages/cli/src/acp-integration/session/team-sink.ts, near handleManagerChange). Meanwhile Config.setTeamManager(manager) (packages/core/src/config/config.ts) unconditionally invokes teamManagerChangeCallbacks even for the same object; it has no identity/dedup guard.

Minimal deterministic control (using this PR's makeFakeTeamManager / FakeTeamEmitter fixture):

const manager = makeFakeTeamManager();
const detach = attachTeamSink(deps);
config.setTeamManager(manager);
config.setTeamManager(manager); // same identity, legal API call
expect(manager.emitter.listenerCount(TeamEventType.TEAMMATE_APPROVAL_REQUEST))
  .toBe(1); // predicted: 2

An emitted TEAMMATE_APPROVAL_REQUEST then runs handleApproval twice, opening two host permission prompts for one teammate tool request and racing two event.respond(...) calls; roster/activity events likewise duplicate. detach() removes just one instance of each function when off has Node EventEmitter semantics, leaving a leaked listener after the session closes. This directly undermines the PR's idempotent-detach/one-approval contract.

Bounded repair: in handleManagerChange, return early on manager === boundManager when it is already attached, or detach existing listeners before every rebind. Pin same-instance re-emission + single prompt + detached listener counts, in addition to the existing A→B manager-swap coverage. Please preserve the fail-closed approval resolution path; this does not suggest relaxing it.

Source-path review only; I have not run the full ACP test suite.

Isolated runtime reproduction: With Node's actual EventEmitter and a minimal mirror of Config.setTeamManager + handleManagerChange (not the Qwen suite), two same-object setter calls produced listenerCount('approval') = 2; emitting one approval called the handler twice; removing the one retained listener once left listenerCount = 1. The upstream Config.setTeamManager callback lacks a same-reference guard and team-sink.ts likewise lacks an already-attached guard. This confirms the event-subscription/cleanup mechanism; the full ACP host path was not executed.

yiliang114 and others added 2 commits October 11, 2026 23:33
Config.setTeamManager fires its change callbacks even when the manager
object is identical, and the sink re-registered all five emitter
listeners on every notification: one teammate approval would open two
host permission prompts and race two respond() calls, roster and
activity events would double, and detach() would leave one copy of
every listener leaked on the manager.

Return early when the notified manager is the one already bound, and
pin the single-listener invariant plus clean detach with a
same-instance re-notification regression test.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Bilingual design doc for the projection this PR lands: the single
AgentIdentity shape across the subagent and teammate columns, the
display-only teammate activity surface (no queue, no persistence, no
leader wake), OpenAI-shaped message direction (agent = recipient,
structured author = sender), and the decisions behind ephemerality.
Records the CJK teammate-name collapse and the follow-up list
(persisted per-teammate summary, colors, collapsed grouping).

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The same-instance re-notification bug is fixed in b1eeae5: handleManagerChange now returns early when the notified manager is the one already bound, so the five listeners are registered exactly once per manager regardless of how many times Config.setTeamManager fires with the same object. One approval = one host prompt, no respond() race, and detach removes the single bound copy.

Pinned by a regression test that re-fires the change callback with the already-bound manager and asserts every listener count stays 1 (and reaches 0 after detach) — alongside the existing A→B swap coverage. The fail-closed approval path is untouched.

Also reproduced your exact fixture shape: config.setTeamManager(manager); config.setTeamManager(manager) through the fake config's change subscribers, matching the unconditional callback invocation you found in Config.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

E2E report: teammate activity & inter-agent message projection

All scenarios ran against a local daemon (qwen serve --port 4399, agent teams enabled, real model) at head 8643572, with SSE captures on the wire and DOM probes in the Web Shell.

S1 — Multi-teammate parallel run, distinct attribution. A three-teammate team (researcher / writer / checker) with sentinel-terminated replies. 15 peer frames on the wire, each stamped with the correct identity; no taskId collisions across teammates. After a full page reload every cross-session row renders its own chip (@researcher ×3, @writer ×4, @checker ×5) — no cross-attribution between concurrent siblings.

S2 — Teammate tool activity without waking the leader. A two-teammate relay produced 20 activity frames in one turn (researcher started read_file: hello.txt … finished), each carrying the acting teammate's identity. The decisive half is pinned in the unit suite: activity frames never call into the model stream — the leader is prompted only by actual reports, not by observational progress.

S3 — Inter-agent messages with structured direction. A coder→critic verification relay produced 8 message frames. Direction follows the agent_message semantics this PR adopted: the frame's agent attribute names the recipient (/root/critic for a teammate-directed message, /root for leader-directed), and the structured payload carries the sender identity (the leader as /root at depth 0). In the UI the recipient chip carries attribution and the prose arrow carries direction (coder → critic: …); a sender chip was tried and removed as redundant with the prose.

S4 — Chinese content end to end. Teammates with pinyin names exchanged Chinese-message content over the wire: 5 of 6 message frames carried Chinese text (3 containing the expected 天空 marker sentence, plus the ZH-OK sentinel). Non-ASCII paths extract correctly. One pre-existing issue surfaced outside this PR: CJK teammate names collapse in the team name sanitizer on main — filed separately as #13920, not introduced here.

S5 — Reload attribution. Live activity frames are ephemeral by design (documented in the design doc); the persisted reports and roster changes reload with frame-level identity, so post-reload the transcript still attributes every peer row (12/12 rows chipped in the DOM probe).

Unit coverage for the same contracts runs in CI: activity direction + no-wake + listener retraction (Session suite), tool/message forwarding + same-instance listener counts (team-sink suite), activity-frame classification and chip rendering (web-shell suites).

Design doc: docs/design/2026-10-11-agent-team-identity-projection.md (+ zh-CN).

yiliang114 and others added 4 commits October 12, 2026 00:37
Teammate activity notices — attributed bubbles for teammate tool calls
and inter-agent messages — grow the transcript entry by 3,487 UTF-16
code units, measuring 1,323,853 locally and identically in CI, past the
1,323,000 ceiling the bundle-size guard pinned. Raise the ceiling to
1,325,000 and record the measurement next to the previous ones, per the
guard's own re-measure-and-raise convention.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
main's QwenLM#13760 (managed cwd slice) and this branch's teammate activity
notices both grew the transcript entry and both edited the same
measurement comment. Keep both narratives — the W2 i18n table growth
and the 3,487-unit teammate notice addition — so the ceiling comment
still explains every contributor to the 1,325,000 bound.
The export document's block author came only from the workspace-agent
message meta, so a peer notice block carried its text but lost the
producer identity the live Web Shell attributes — the exported
transcript read as anonymous system rows for everything an Agent Team
teammate said.

Fall back to the block's wire agent field when no workspace-agent
author exists: the path-style name travels as the author name and the
identity kind alongside it, so an exporter can tell a teammate from a
delegated subagent. The agent-message meta author still wins when both
exist — it names the workspace agent that actually wrote the block,
while the wire field names the projected identity.

Refs QwenLM#13785
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

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

Code Review Summary (QwenCode Reviewer)

Overall Assessment: Changes Requested

This PR projects agent identity and Agent Team coordination onto the ACP/daemon wire. The design is solid, test coverage is extensive (2106+ focused cases), and security posture is strong (fail-closed approvals, binding epoch recycling, XML escaping). However, CI gate is not fully green.


Blocking Issues

  1. CI gate not fully green — Two checks are in fail state:

    • Managed Agent fast tier (run)
    • SDK Java (fast) / Real daemon E2E / Java 11 (run)

    Several additional checks remain pending (TUI parity snapshots, SDK Java full suites, authorize). Before merge, please confirm whether these failures are introduced by this PR or are pre-existing/flaky. Suggest re-running or providing exclusion evidence.

Suggestions (non-blocking)

  1. packages/cli/src/acp-integration/session/team-sink.ts:42 — let nextBindingId = 0 is a module-level mutable global. If multiple ACP hosts exist in the same process (e.g., testing or multi-tenant scenarios), binding IDs increment across sessions without reset. Functionally correct under high-watermark semantics, but consider per-instance scoping if isolation is needed in the future.

  2. packages/cli/src/acp-integration/session/Session.ts (#emitTeammateActivity) — Activity text fields (name, description, message) are concatenated into plain-text display strings without XML/HTML escaping. This is safe under the current architecture (display-only, non-persisted), but if downstream consumers render these in HTML/XML contexts, XSS protection will be needed. Consider adding a comment noting this boundary.

  3. packages/acp-bridge/src/transcript-replay.ts (agentIdentityFromTaskExecution) — Replay can only reconstruct root-parented identities (/root/${subagentName}, depth=1). Nested subagent hierarchy (depth >= 2) is lost. PR description marks this as a known M1a follow-up; suggest adding an inline TODO referencing the tracking issue.

  4. packages/cli/src/acp-integration/session/Session.ts (#emitTeammateActivity vs #enqueueTeammateMessage) — Activity frames are dropped when this.disposed || this.closing, while teammate messages intentionally do not early-return on closing (queue is the sole carrier). The asymmetry is correct but non-obvious; suggest a comment explaining why activity discard is safe here.

  5. Test coverage gap — Integration Tests (CLI, No Sandbox) is skipping in CI. PR description acknowledges no real-model or full daemon/browser E2E was run. Please confirm the CLI integration test skip is unrelated to this change before merge.

Security Review: PASS (with notes)

  • Approval flow is fail-closed: host unavailability results in Cancel, not auto-approve
  • Binding epoch + AbortController correctly recycles in-flight approvals on manager swap/detach
  • Teammate report content is XML-escaped before embedding in <peer-notification> envelope
  • BridgeClient validates all notification fields (type, sessionId, bindingId, teammateName) with strict type checks
  • No hardcoded secrets or tokens found in diff
  • Note: activity display text is unescaped (see suggestion #2 above); safe today but watch downstream consumers

Conclusion

Changes Requested — Core design and implementation quality are high. The only blocker is CI gate status. Once CI failures are confirmed as non-regressions (or fixed), this PR is ready for merge.

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued for an available runner; follow the workflow run for progress. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes.

@qwen-code-review-bot qwen-code-review-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.

Partially reviewed — gaps disclosed.

16 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • SubAgentTracker.ts:88 — parentAgent/agentIdentity dead production surface — already reported (round 5/7/8 review-body deferral lists)
  • team-sink.test.ts:520 — same-instance-guard test unfalsifiable — already reported (round 8 deferral list, team-sink.test.ts:529)
  • team-sink.test.ts:153 — deps.signal abort contract untested — already reported (round 8 deferral list)
  • team-sink.test.ts:216 — answers passthrough untested — already reported (round 5 deferral list, team-sink.ts:259)
  • messageTypes.ts:79 — agentPath doc claims a uniqueness the field lacks — already reported (round 5/8 deferral lists)
  • transcriptToMessages.ts:1792 — comment names the wrong first-wins mechanism — already reported (round 5 deferral list, :1756)
  • TeamManager.ts:1945 — TEAMMATE_ACTIVITY emit sites untested — already reported (round 5/8 deferral lists)
  • Session.ts:12011 — teammate-exited sending-side hop untested — already reported (round 6/7/8 deferral lists)
  • transcript-replay.ts:1833 — background_task_completed identity hoist untested — already reported (rounds 5-8 deferral lists)
  • Session.ts:13705 — unreachable permissionScope guard untested — already reported (rounds 5-8 deferral lists)
  • transcriptToMessages.ts:613 — teammate_activity missing from isHideableStep — already reported (round 8 deferral list, :618)
  • Session.test.ts:1353 — single-sender consumer gate unpinned — already reported (round 5/6/7 deferral lists)
  • Session.test.ts:1361 — label never pinned nor persisted — already reported (round 6 deferral list, :1455)
  • team-sink.ts:260 — host-cancelled maps to bare Cancel with no reason or leader notice — already reported (round 6 deferral list R6-5, team-sink.ts:252)
  • agent-identity.test.ts:258 — text-block identity projection unwitnessed — already reported (round 5 deferral list, transcript.ts:913)
  • transcriptToMessages.ts:576 — pre-scan marker-before-identity ordering untested — already reported (round 5 deferral list, :570)

Not reviewed: test-efficacy probing — the probe harness could not be validated (vitest globalSetup guard in the prebuilt worktree); all 18 mutant probes and 110 hunk probes were inconclusive or budget-skipped.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": I did not trace the bridge replay → Web Shell backgroundTurn construction end-to-end, so finding 3's "after reload" consequence is verified only up to the per…; "agent reverse-audit (round 2)": executing packages/acp-bridge vitest over the eight new tests — I verified the fixtures, helper signatures and expected _meta shapes by reading both sides, …; chunk 8: none — I did not confirm whether the acp-bridge consumer at bridgeClient.ts:1063 handles the new 'teammate_activity' _meta.source value, because cross-fil…; "agent reverse-audit (round 1)": did not trace whether a same-channel session restore replaces the BridgeClientSessionEntry object (which would reset the per-entry retiredThrough fence whil…; chunk 14: none — every check above ran to completion., and 7 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:

  • packages/acp-bridge/src/transcript-replay.ts:324 — [review] SUBAGENT_NAME_PATTERN comment claims a re-check of SubagentValidator's constraint but re-checks only the charset — reserved 'main', 1-char and 51-char names get replay attribution …
  • packages/cli/src/acp-integration/session/Session.ts:12038 — [review] taskId-uniqueness comment cites the public enqueueBackgroundNotification dedup, but the invoked private #enqueueBackgroundNotification has none — the real work-hold Set in…
  • packages/sdk-typescript/src/daemon/ui/transcript.ts:1710 — [review] discrete-frame exemption (message/failed) removes the maxBlocks bound for teammate_activity rows — never-persisted rows accumulate unbounded and trimTranscriptState evicts …
  • packages/sdk-typescript/src/daemon/ui/transcript.ts:1730 — [review] coalesceTeammateActivity's backward walk has no proximity bound — the next unrelated tool call rewrites an arbitrarily distant row in place; the committed test pins the tim…
  • packages/sdk-typescript/src/daemon/ui/types.ts:112 — [review] the five new public agent-field docs contradict the wire: text-side keys recipient-vs-producer on message class not frame family; status-side fields undocumented; tool-side says …
  • packages/sdk-typescript/test/daemon-ui-transcript.test.ts:663 — [review] the clearActiveText:false opt-out coalesce shape — the only shape the wire emits — is witnessed by no test; an unconditional-clearActiveText mutation stays green while…
  • packages/web-shell/client/adapters/transcriptToMessages.ts:622 — [review] activity rows project blockTime from the origin envelope while the coalescer rewrites text in place — latest activity text renders beside the first activity's stale t…
  • packages/web-shell/client/adapters/transcriptToMessages.ts:1795 — [review] mergeToolCall copies 13 fields but not agentPath — in the approved-permission-first flow the placeholder's undefined wins permanently, so the new tooltip never rende…
  • packages/web-shell/client/adapters/transcriptToMessages.ts:661 — [review] peer turn-started marker names the teammate twice — this PR adds both the @name chip and label:sender, and the row renders label as visible text beside the chip, sque…
  • packages/web-shell/client/components/messages/SystemMessage.test.tsx:1110 — [review] two of four assertions in the new peer-report label test cannot fail for any input (label lands only in aria-label/title; notification.peer.* never looked …
  • packages/web-shell/client/components/messages/SystemMessage.tsx:376 — [review] folding teammate_activity into isTaskNotification reuses the background-task label/tone/icon table — ephemeral activity is announced to screen readers as 'Task n…
  • packages/web-shell/client/components/MessageItem.tsx:302 — [review] the teammate prop passthrough from adapter to SystemMessage is asserted by no test (the dom test stubs SystemMessage as () => null) — dropping the prop ships green while ev…

Convergence: round 9 posted 10 inline comment(s), 10 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/session/Session.ts (findings in round 8; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (3 Critical(s)), the rate of first-time findings is not falling (this round 10, previous 3), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical) … … …

Advisory only — it does not block this review.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 16 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):test-efficacy probing — the probe harness could not be validated (vitest globalSetup guard in the prebuilt worktree); all 18 mutant probes and 110 hunk probes were inconclusive or budget-skipped.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":I did not trace the bridge replay → Web Shell backgroundTurn construction end-to-end, so finding 3's "after reload" consequence is verified only up to the per…;"agent reverse-audit (round 2)":executing packages/acp-bridge vitest over the eight new tests — I verified the fixtures, helper signatures and expected _meta shapes by reading both sides, …;chunk 8:none — I did not confirm whether the acp-bridge consumer at bridgeClient.ts:1063 handles the new 'teammate_activity' _meta.source value, because cross-fil…;"agent reverse-audit (round 1)":did not trace whether a same-channel session restore replaces the BridgeClientSessionEntry object (which would reset the per-entry retiredThrough fence whil…;chunk 14:none — every check above ran to completion.,另有 7 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 12 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 9 轮发布了 10 条行内评论,其中 10 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/session/Session.ts(第 8 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 3 条 Critical),首次发现的速率没有下降(本轮 10,上一轮 3),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

— qwen3.8-max via Qwen Code /review (v0.26.1)

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/web-shell/client/adapters/transcriptToMessages.ts
Comment thread packages/core/src/agents/team/TeamManager.ts
Comment thread packages/acp-bridge/src/transcript-replay.ts
Comment thread packages/acp-bridge/src/transcript-replay.ts
Comment thread packages/acp-bridge/src/bridgeClient.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/team-sink.test.ts
Comment thread packages/cli/src/acp-integration/session/team-sink.test.ts
Comment thread packages/cli/src/acp-integration/session/team-sink.ts
@github-actions

Copy link
Copy Markdown
Contributor

Managed Agent gate failed

Run 40190879526 · commit b7a606cb8f95

No test identifier was available; follow the run link for job logs and annotations.

@qwen-code-review-bot qwen-code-review-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.

Partially reviewed — gaps disclosed.

16 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • SubAgentTracker.ts:88 parentAgent/agentIdentity dead production surface — already reported (rounds 5/7/8 deferral lists)
  • Session.ts:13705 unreachable permissionScope guard — already reported (rounds 5-8 deferral lists)
  • TeamManager.ts:1957 TEAMMATE_ACTIVITY emit sites untested on the core side — already reported (rounds 5/8 deferral lists)
  • Session.test.ts:1355 single-sender consumer gate unpinned — already reported (rounds 5/6/7 deferral lists)
  • sdk-typescript transcript.ts:913 text-block agent adoption unwitnessed — already reported (round-5 deferral list)
  • transcript-replay.ts:1832 background_task_completed identity hoist untested — already reported (rounds 5-8 deferral lists)
  • transcriptToMessages.test.ts:785 pre-scan marker-before-identity ordering untested — already reported (round-5 deferral list, :570)
  • transcriptToMessages.ts:648 peer turn-started marker names the teammate twice — already reported (round-9 deferral list, :661)
  • MessageList.tsx:877 teammate_activity missing from isHideableStep — already reported (round-8 deferral list, transcriptToMessages.ts:618)
  • messageTypes.ts:79 agentPath doc claims a uniqueness the field lacks — already reported (rounds 5/8 deferral lists)
  • sdk-typescript types.ts:1134 agent-field docs contradict the wire on recipient-vs-producer — already reported (round-9 deferral list, types.ts:112)
  • sdk-typescript transcript.ts:1731 coalesce backward walk has no proximity bound — already reported (round-9 deferral list, :1730)
  • sdk-typescript transcript.ts discrete-frame carve-out removes the maxBlocks bound — already reported (round-9 deferral list, :1710)
  • transcriptToMessages.ts:1795 mergeToolCall does not carry agentPath — already reported (round-9 deferral list)
  • transcript-replay.ts:325 SUBAGENT_NAME_PATTERN re-checks only the charset — already reported (round-9 deferral list, :324)
  • Session.ts:12011 teammate-exited sending-side hop untested — already reported (rounds 6/7/8 deferral lists)

Unresolved, please confirm:

  • [Critical] R9-1 packages/web-shell/client/adapters/transcriptToMessages.ts:1947 — this round's verification REFUTED the behavioural claim: the design doc shipped in this PR (docs/design/2026-10-11-agent-team-identity-projection.md:41,:46) states the f…

Not reviewed: cross-file tracer (1c) — did not verify whether the daemon's in-session event journal replays teammate_activity frames to a reconnecting client — stopped at the agent tool budget.

Not reviewed: test-efficacy probing — the probe harness could not be validated (harnessValidated: null; all 18 mutant probes and every hunk probe inconclusive on the vitest globalSetup prerequisite guard), so coverage efficacy is unmeasured rather than clean.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": whether any frame is actually emitted from SubAgentTracker in the window between constructor and the START event on the ordinary non-aborted path — this det…; "agent 1c": did not verify whether the daemon's in-session event journal ( entry.events.publish / subscribeEvents({lastEventId}) ) replays teammate_activity frames to a…; "agent reverse-audit (round 1)": I did not determine which Config shape the in-process teammate's CoreToolScheduler receives ( new CoreToolScheduler({ config: this.runtimeContext }) , agent-c…; "agent reverse-audit (round 1)": whether any wire frame carrying subagentMeta.agent is emitted before AgentEventType.START replaces the raw args['subagent_type'] identity — I read both e…; "agent reverse-audit (round 1)": emitResult 's _meta construction (tool-call-emitter.ts:236 onward) — I read emitPreparationDiscarded and the start path's ...params.subagentMeta (tool-ca…, and 19 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/cli/src/acp-integration/session/Session.ts:5183 — [review] Critical [fails-closed] [new-surface] Shutdown flush snapshots only notificationQueue, so an…
  • docs/design/2026-10-11-agent-team-identity-projection.md:65 — [review] design-doc Acceptance per-suite test counts are stale at…
  • packages/acp-bridge/src/bridgeClient.ts:1071 — [review] bridgeClient.ts:1071-1079 every rejection arm of the new…
  • packages/acp-bridge/src/bridgeClient.ts:2937 — [review] Both private routes' malformed-payload reject blocks are…
  • packages/acp-bridge/src/bridgeClient.ts:2965 — [review] The teammate-exited handler ownership guard and payload…
  • packages/cli/src/acp-integration/session/Session.test.ts:1788 — [review] Session.test.ts:1788-1791 toHaveLength(3) is the only…
  • packages/cli/src/acp-integration/session/Session.test.ts:1794 — [review] Session.test.ts:1794-1797 bakes the literal 'unknown', an…
  • packages/cli/src/acp-integration/session/Session.ts:12410 — [probe] Landing an evicted peer report before tallying flips the…
  • packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts:372 — [review] tool-call-emitter.ts:372-373 the new _meta.agent lands…
  • packages/cli/src/acp-integration/session/team-sink.test.ts:148 — [review] team-sink.test.ts:148-149 both retirement mocks only…
  • packages/cli/src/acp-integration/session/team-sink.test.ts:157 — [review] No test sets permissionPersistencePolicy, so the sink…
  • packages/cli/src/acp-integration/session/team-sink.test.ts:444 — [review] The manager-change transition to no manager (team…
  • packages/cli/src/acp-integration/session/types.test.ts:16 — [review] types.test.ts:16-22 the ' ' leg pins a live/replay parity…
  • packages/core/src/agents/team/TeamManager.ts:1249 — [review] the 'unknown' sentinel is not reserved against legal…
  • packages/core/src/agents/team/TeamManager.ts:1955 — [review] TeamManager.ts:1955-1963 two unbounded un-coalesced…
  • packages/core/src/agents/team/TeamManager.ts:2174 — [review] Dead else branch: PendingMessage.from can never be empty…
  • packages/sdk-typescript/test/daemon-ui-transcript.test.ts:472 — [review] daemon-ui-transcript.test.ts:472 the…
  • packages/web-shell/client/adapters/transcriptToMessages.ts:589 — [review] transcriptToMessages.ts:589-591 the pre-scan's text-block…
  • packages/web-shell/client/adapters/transcriptToMessages.ts:1938 — [review] Client-side JSDoc calls the recipient chip an 'author',…
  • packages/web-shell/client/components/messages/SystemMessage.tsx:221 — [review] Of the two chip render sites this diff adds, only the…

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 16 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):cross-file tracer (1c) — did not verify whether the daemon's in-session event journal replays teammate_activity frames to a reconnecting client — stopped at the agent tool budget.

未审查(原文为英文):test-efficacy probing — the probe harness could not be validated (harnessValidated: null; all 18 mutant probes and every hunk probe inconclusive on the vitest globalSetup prerequisite guard), so coverage efficacy is unmeasured rather than clean.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)":whether any frame is actually emitted from SubAgentTracker in the window between constructor and the START event on the ordinary non-aborted path — this det…;"agent 1c":did not verify whether the daemon's in-session event journal ( entry.events.publish / subscribeEvents({lastEventId}) ) replays teammate_activity frames to a…;"agent reverse-audit (round 1)":I did not determine which Config shape the in-process teammate's CoreToolScheduler receives ( new CoreToolScheduler({ config: this.runtimeContext }) , agent-c…;"agent reverse-audit (round 1)":whether any wire frame carrying subagentMeta.agent is emitted before AgentEventType.START replaces the raw args['subagent_type'] identity — I read both e…;"agent reverse-audit (round 1)":emitResult 's _meta construction (tool-call-emitter.ts:236 onward) — I read emitPreparationDiscarded and the start path's ...params.subagentMeta (tool-ca…,另有 19 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 20 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.26.1)

@yiliang114

Copy link
Copy Markdown
Collaborator Author

本地视觉验证(当前 head b7a606c)

按 web-shell-visuals 的同一套流程,在本地分别渲染 merge-base 33e80fc(before) 与 head b7a606c(after),再用仓库自己的合成器做像素级对比:

项 命令 结果
before 渲染 WEB_SHELL_VISUALS_OUTPUT_DIR=/tmp/vis13902/base npx playwright test --config playwright.visuals.config.ts screenshots.spec.ts(merge-base worktree) 56 passed,82 张图
after 渲染 同一命令,head worktree 56 passed,82 张图
对比合成 node .github/scripts/web-shell-visuals-compose.mjs base head out 0 个视图有变化(全部 0% pixel diff)

结论:在当前 head 上,UI 没有可见变化。 这符合改动性质 —— 身份是 ACP/daemon 线上的协议字段,标准视图里看不到差异;此前的视觉预览是旧 head(a309443)渲染的,所以不再引用。

渲染出的界面样本(当前 head,浅色主题):

parallel agents 展开
会话记录

不适用项:无「改动前后差异图」可贴 —— 82 个视图逐个对比均为 0%,这正是本次验证要说明的事实。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Split into three stacked draft PRs without changing this branch:

Layer Draft Base Scope Build / focused tests
A #15657 main Subagent identity across live frames, replay, SDK and tooltip build passed; 600 tests passed
B #15658 A Durable Team reports, roster, session approvals and lifecycle build passed; 3,058 tests passed
C #15659 B Display-only teammate activity, message direction, coalescing and downstream filters build passed; 1,871 tests passed, including 99 core regression tests

The cumulative tree at C (1d6bc91dd3cf29e2010043e397ea8c429b72cf03) is byte-for-byte identical to this PR's pinned head 6ed7884c381dfe1c59bd975d6cf61dbb41ef441c; both have Git tree de3814e262acad9b007d0ca9372394b4d19907d4. This is a scope split, not a new implementation or a claim that the outstanding feedback is fixed.

Each draft links its inherited findings: A owns R9-3/R9-4; B owns R8-3/R9-2/R9-5/R9-7/R9-8/R9-9; C owns R9-1/R9-6. The original threads remain open. Build and focused-test evidence is tied to each split commit; runtime/provider E2E, browser verification and a separate typecheck pass have not been run.

Review and land in A → B → C order. After a parent lands, reconcile the dependent branch with main before retargeting, so its displayed diff does not accidentally include the parent again. This original PR remains open and unchanged as the reference until the replacement stack is ready.

@qwen-code-review-bot qwen-code-review-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.

Partially reviewed — gaps disclosed.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • DaemonAgentIdentity not exported from the sdk-typescript barrels (packages/sdk-typescript/src/daemon/ui/types.ts:345) — already reported: round-1 review-body deferral list (review 5511196624, types.ts:132), re-listed in round 2 (review 5523…
  • Replay identity rebuild over-fires via the 'running' status whitelist and its invariant comment is false (packages/acp-bridge/src/transcript-replay.ts:2716) — already reported: round-1 review-body deferral list (review 5511196624, transcrip…

Not reviewed: incidental findings I4 (agentName JSDoc scope) and I5 (vi.fn absorbs unhandled rejections) — no verifier ruled on them (budget stop); carried as low-confidence, terminal-only.

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

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": whether a teammate name can contain / (which would make ${ROOT_AGENT_IDENTITY.agentName}/${name} a 3-segment path at depth: 1 and teammateOfAgent 's .sp…; chunk 3: confirming that the frame builder wrapping extra: { ...subagentMeta } ( createTranscript*Update , path not located before the ceiling) adds only _meta keys …; "agent reverse-audit (round 1)": the end-to-end agent-respawn path in session-control-plane.ts — I confirmed the BridgeClient is constructed inside constructChannelInfo(channel, …) and th…; "agent reverse-audit (round 1)": did not read sanitizeName itself, so I confirmed the 'unknown' collision for the literal name Unknown / unknown but not for other inputs that might saniti…; "agent reverse-audit (round 1)": did not execute coordination-harness.test.ts or api-user-prompt.test.ts ; the falsifiability claims above are from reading the assertions and the production …, and 7 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round:

  • packages/acp-bridge/src/bridgeClient.ts:857 — [review] BridgeClient's class JSDoc now attaches to the new PendingTeammatePermission interface; the class is undocumented
  • packages/cli/src/acp-integration/session/Session.test.ts:1375 — [review] peer _meta.agent assertion cannot distinguish senders-derived attribution from envelope-text parsing; the hostile-fixture sibling never asserts it
  • packages/cli/src/acp-integration/session/Session.test.ts:1360 — [review] no test drives leaderCallback with a multi-sender batch, so the senders?.length===1 attribution guard is unpinned
  • packages/cli/src/acp-integration/session/Session.ts:14213 — [review] permissionScope==='session' early return in #withBackgroundPermission is unreachable yet cited as the queue-bypass justification
  • packages/cli/src/acp-integration/session/team-sink.test.ts:510 — [review] same-instance-guard test cannot detect the guard's removal (mutation-probed); its comment asserts listener-doubling the ordering makes impossible
  • packages/cli/src/acp-integration/session/team-sink.test.ts:152 — [review] deps.signal AbortController discarded — session-lifetime abort and AbortSignal.any composition entirely unpinned (mutant-verified)
  • packages/core/src/agents/team/TeamManager.ts:1259 — [review] deliverLeaderMessages senders-array contract half-enforced; the length-1 attributability rule lives only in the one consumer that knows it
  • packages/qwen-live/src/adaptor/public-activity.ts:23 — [review] display-only source denylist hand-copied in 4+ places; publicActivity fails open — todo_stop_guard/vision_bridge_notice publish (executed sweep)
  • packages/core/src/services/chatRecordingService.ts:601 — [review] persisted backgroundTask.agent.kind declares 'subagent' but the only replay reader accepts 'teammate'; the writer coerces root to subagent (probe)
  • packages/sdk-typescript/test/agent-identity.test.ts:181 — [review] tool-block create-path agent stamping untested; deleting it keeps 46/46 green (mutant executed)
  • packages/web-shell/client/adapters/transcriptToMessages.ts:576 — [review] the marker-before-identity out-of-order arrival the teammateByTask pre-scan exists for is never tested (mutant executed)
  • packages/web-shell/client/adapters/transcriptToMessages.ts:1792 — [review] comment attributes first-wins agentPath adoption to appendSubContent; the real live-side loss point is the normalizer's progress-frame drop (probe)
  • packages/web-shell/client/adapters/transcriptToMessages.ts:1795 — [review] mergeToolCall does not copy agentPath; approved agent tools registered via the permission placeholder never receive it (probe)
  • packages/acp-bridge/src/transcript-replay.ts:1833 — [review] background_task_completed replay hoist untested; deleting it keeps 157/157 green (probe)
  • packages/sdk-typescript/src/daemon/ui/transcript.ts:913 — [review] text-delta to block.agent reducer copy untested; deleting it keeps 46/46 green (probe)
  • packages/core/src/agents/team/TeamManager.ts:1948 — [review] TEAMMATE_ACTIVITY producer payload has zero tests; producer mutations keep 16/16 green (probe)
  • packages/core/src/agents/team/TeamManager.ts:1789 — [review] teardown emits N TEAMMATE_EXITED while the sink is attached, waking the leader N times about a deleted team; overflow can evict real notifications (probe)
  • docs/design/2026-10-11-agent-team-identity-projection.md:65 — [review] all seven design-doc Acceptance witness counts stale (all six suites executed at HEAD)
  • docs/design/2026-10-11-agent-team-identity-projection.md:35 — [review] design doc states reports enqueue with agent+label unconditionally; multi-sender batches drop both; not in Known gaps
  • packages/acp-bridge/src/bridgeClient.ts:1108 — [review] sessionOwned clears promptId with no replacement bound; one unanswered teammate card pins transcript serving to the bounded journal fallback
  • …and 22 more (see the run report)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (2 Critical(s)), the rate of first-time findings is not falling (this round 1, previous 0), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical) … … …

Advisory only — it does not block this review.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):incidental findings I4 (agentName JSDoc scope) and I5 (vi.fn absorbs unhandled rejections) — no verifier ruled on them (budget stop); carried as low-confidence, terminal-only.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":whether a teammate name can contain / (which would make ${ROOT_AGENT_IDENTITY.agentName}/${name} a 3-segment path at depth: 1 and teammateOfAgent 's .sp…;chunk 3:confirming that the frame builder wrapping extra: { ...subagentMeta } ( createTranscript*Update , path not located before the ceiling) adds only _meta keys …;"agent reverse-audit (round 1)":the end-to-end agent-respawn path in session-control-plane.ts — I confirmed the BridgeClient is constructed inside constructChannelInfo(channel, …) and th…;"agent reverse-audit (round 1)":did not read sanitizeName itself, so I confirmed the 'unknown' collision for the literal name Unknown / unknown but not for other inputs that might saniti…;"agent reverse-audit (round 1)":did not execute coordination-harness.test.ts or api-user-prompt.test.ts ; the falsifiability claims above are from reading the assertions and the production …,另有 7 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改:共 42 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 2 条 Critical),首次发现的速率没有下降(本轮 1,上一轮 0),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

— qwen3.8-max via Qwen Code /review (v0.26.1)

Comment thread packages/cli/src/acp-integration/acpAgent.ts
@wenshao

wenshao commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Verdict: findings — behavior verified green at head 6ed7884c, but the PR is CONFLICTING with current main: one import-only hunk in Session.ts needs a rebase. 988/988 scripted checks passed, 0 failed.

Verified head: 6ed7884c381dfe1c59bd975d6cf61dbb41ef441c · A/B base: 9df10f873b016f2db6b32cfb1a42823bcd8020d1 (merge-base) · trial-merge target measured: 8e1ead35501c10299def7f834cfa31d34d66cd91 · current main re-check: 57f4bd142467e245ef16005cda33066b115178cf (same single conflict).

中文摘要

结论:findings。 行为在真实 daemon + 真实模型下通过:head 上 teammate 帧带 _meta.agent = {"agentName":"/root/correctness","depth":1,"kind":"teammate"},subagent 帧带 /root/general-purpose 与 parentToolCallId;base 两列都是 0 个身份帧,正是本 PR 要修的“零信号”状态。988/988 条脚本化检查通过、0 失败(含 969 条单测/门禁用例、16 条 harness 断言、3 个 mutation kill)。

唯一合并阻塞:GitHub 报 CONFLICTING,冲突只在 Session.ts 的一个 import hunk(main 的 runAcpToolBatch vs 本 PR 的 attachTeamSink)。本地保留双方 import 后,merged tree 构建通过,401 条聚焦用例全绿;因此这是机械冲突,rebase 后即可合并。

未覆盖:Web Shell 浏览器里 peer chip 的 DOM 断言(截图只验证了会话加载与 leader 回复);teammate 审批的真实客户端交互(只有单测 + mutation);Windows 矩阵;depth ≥ 2;崩溃后重投递。详见下方表格与 Not covered。

Central claim and A/B

The central claim is that the ACP/daemon wire now attributes output to the agent that produced it in two mutually exclusive columns: subagent frames carry _meta.agent = { agentName: "/root/<type>", depth: 1, kind: "subagent" } and keep parentToolCallId; Agent Team reports and roster changes arrive as peer background notifications carrying _meta.agent = { agentName: "/root/<name>", depth: 1, kind: "teammate" }. Before this PR the same two scenarios produced no identity at all, and teammate reports were retained unread in the leader inbox.

Each arm below is a real qwen serve daemon built from that worktree, driven over the documented REST + SSE surface with a real model (deepseek-v4.1-flash-1017). The prompt is identical across head/base: create verifyteam, spawn teammate correctness asking for TEAM_MARKER_13902, then report. The subagent arms ask the plain agent tool for SUBAGENT_MARKER_13902.

arm _meta.agent frames teammate subagent peer notifications parentToolCallId frames assertions
head-team 6ed7884c 4 4 0 2 (joined + message) 0 5/5
base-team 9df10f87 0 0 0 0 0 3/3 (expected zero)
head-subagent 6ed7884c 5 0 5 0 5 5/5
base-subagent 9df10f87 0 0 0 0 4 3/3 (expected zero)

The head-team identities are all exactly {"agentName":"/root/correctness","depth":1,"kind":"teammate"}; the head-subagent identities are all /root/general-purpose with kind: "subagent". The team arm contains zero subagent-column frames, and the subagent arm contains zero teammate peer frames — the two columns are mutually attributable on the wire, not just present.

Findings

1. Merge-blocking: the PR is CONFLICTING with current main (one import hunk)

git merge-tree --write-tree origin/main 6ed7884c reports exactly one content conflict, in packages/cli/src/acp-integration/session/Session.ts:

<<<<<<< HEAD
import { runAcpToolBatch, type AcpToolBatchHost } from './acp-tool-batch.js';
=======
import { attachTeamSink } from './team-sink.js';
>>>>>>> pr-13902-head

This is mechanical, not semantic: main added the tool-batch import while the PR added the team-sink import. Keeping both imports produces a clean tree. I built that locally resolved tree at trial-merge target 8e1ead35 and ran the focused gates: CLI 62/62, teammate-filtered Session 9/9, web-shell 330/330 — all green. Main advanced again to 57f4bd14 before posting; git merge-tree at that tip reports the same single Session.ts conflict, so the rebase is still the same mechanical resolution. The PR still needs a rebase before GitHub will merge it; no other code change is implied by this conflict.

2. No behavioral findings in the verified surface

The four-arm A/B, the 568 focused tests at head, the 401 focused tests on the locally resolved merged tree, and the three killed mutations all agree. No regression was observed in the subagent column, the teammate column, the approval wiring, or the Web Shell adapters.

Mutation matrix

mutation witness result
M1 coarse: replace the constructor identity in SubAgentTracker SubAgentTracker "uses START definition" survived — equivalent mutant: the START handler re-derives subagentMeta.agent before the assertion reads it
M1b: remove the frame-level ...(agent ? { agent } : {}) in tool-call-emitter same test killed (2 failed)
M2: change the teammate peer identity kind: 'teammate' → 'subagent' Session "delivers a teammate report as a peer notification" killed (1 failed)
M3: remove emitter.on(TEAMMATE_APPROVAL_REQUEST, approvalListener) team-sink "routes a teammate approval to the client" killed (1 failed)

Every mutated file was restored with cp and its git blob hash re-checked against HEAD; the head tree is clean.

Not covered

  • Web Shell peer chip as a browser DOM assertion. The real session was opened in a real headless Chromium against the live daemon: the session loaded and the leader reply rendered, but the @correctness chip was not visible in the captured collapsed transcript view. The chip and the adapter are covered by 330 passing web-shell unit tests, not by a browser-level presence assertion. I am not calling this a defect; I am saying the browser-level half of the PR's Web Shell claim is unverified here.
  • Teammate approval through a real ACP client. The routing and fail-closed paths are covered by the unit suite and M3, not by a live client answering a permission request.
  • Nested depth ≥ 2 identity. The PR states there is no wire producer today; the run only observed depth 1.
  • Crash-safe redelivery after process/session destruction. The PR lists this as follow-up work.
  • Windows. The daemon arms ran on macOS; the PR's own tested-on table lists Linux only. No Windows arm was run.
  • Full repository suite and full daemon/browser E2E. Only the focused packages and the four real daemon arms above were run.

Methodology

Four real daemon worktrees were built serially (corepack pnpm install --frozen-lockfile, prepare build included): head 6ed7884c, base 9df10f87 (the merge-base, which equals baseRefOid here), and a locally resolved trial merge of origin/main at 8e1ead35. After the merged gates ran, main advanced to 57f4bd14; the conflict set was re-checked at that tip with git merge-tree and is unchanged (one Session.ts import hunk), but the merged-tree build/test numbers above were measured at 8e1ead35. Each arm ran node scripts/dev.js serve --port <p> --hostname 127.0.0.1 --workspace <ws> with an isolated HOME/QWEN_HOME/QWEN_RUNTIME_DIR and a minimal settings copy (user MCP servers stripped); team arms set QWEN_CODE_ENABLE_AGENT_TEAM=1. The driver opened GET /session/:id/events before POST /session/:id/prompt, captured every SSE envelope to events.jsonl, and the assertions read the actual event.data.update._meta fields — no log-text matching for identity. Focused gates were run from each package directory with env -u QWEN_CODE_SERVE -u QWEN_RUNTIME_DIR: CLI 62, core 130, SDK 46, web-shell 330 at head; CLI 62, teammate-filtered Session 9, web-shell 330 on the merged tree. Raw logs and harness scripts are under tmp/pr13902-verify-20261027-201619/.

The two terminal-style images are rendered from the run artifacts with scripts/verify-capture.mjs; the Web Shell image is a real Chromium screenshot of the live head-team daemon at 127.0.0.1:5391.

Evidence captures

Real four-arm A/B, parsed from the SSE captures:

Real four-arm A/B

Exact _meta.agent payloads from the head-team / head-subagent captures, and the base zero-signal arm:

Peer and subagent frames

Web Shell loaded against the same live head-team daemon (real Chromium):

Web Shell transcript


Verification run by @wenshao with Qwen Code; all counts come from the artifacts under tmp/pr13902-verify-20261027-201619/, and every SHA named above is the exact tree that produced them.

…rge-20261027

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.ts
…rge-20261027

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.test.ts
Main's QwenLM#15441 retired Session.getRewindableUserTurnCount and made
rewindToTurn resolve its target through the file-history snapshot's
promptId plus the recorder's turn index. The two peer-notification rewind
pins predicted that accessor and asserted the old numeric contract, so the
merged tree built red.

Mark the two rewindable turns, leave the delivered peer report unmarked
(that is the behaviour under test), seed the snapshots and assert the same
cut point through the identity contract. Both cases still pass with the
peer report outside the rewind surface.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merged origin/main into this branch to clear the conflict (ordinary merge commits; history preserved, no rebase, no force).

What was merged: origin/main @ dee6ace, then again @ 32f6713 after main moved while this ran.

Conflicts and resolutions:

  1. packages/cli/src/acp-integration/session/Session.ts — main's runAcpToolBatch import and this branch's attachTeamSink import landed in the same slot. Union: both kept, both files exist and each import has a consumer.
  2. Session.test.ts — this branch's duplicate getApiHistoryPromptId / markApiHistoryPrompt import against main's copy of the same block. Kept main's, kept the branch-only escapeXml import.
  3. Semantic (build-red without it): main's fix(acp): carry rewind identity through daemon clients #15441 retired Session.getRewindableUserTurnCount and made rewindToTurn resolve its target through the file-history snapshot's prompt id plus the recorder's turn index. The two peer-notification rewind pins still asserted the numeric contract, so the merged tree stopped compiling. Re-pinned them on the identity contract — mark the two rewindable turns, leave the delivered peer report unmarked (that is the behaviour under test), seed the snapshots — and the expected cut point is unchanged (truncateHistory(4)).

Verification (local, on the merged tree): full build pass · npm run typecheck (all workspaces) pass · 16 focused files / 3,320 tests pass (cli 2,332; acp-bridge 463; core 130; sdk-typescript 58; web-shell 330; qwen-live 7) · prettier --check and eslint clean on all 44 changed files.

This PR stays the reference copy for the split stack: its review findings are being addressed in #15657 / #15658 / #15659 — nothing here addresses them, the merge only unbreaks the branch.

Head: 6ed7884 → b732701.

yiliang114 added a commit that referenced this pull request Oct 28, 2026
Two replay findings from the #13902 review, both about trusting the
persisted record too much:

- `executionMode` alone was treated as proof the subagent started, but two
  rejection paths persist it for a session that never registered:
  worktree-provisioning failure (`status: 'failed'`) and the foreground
  model-slot cancel (`status: 'cancelled'`). Require
  `subagentSessionReady === true` on that leg, which is the field current
  writers flip once the subagent session exists; fixtures modelling a
  genuine run-then-fail now carry it.
- the rejection branch was a negative allowlist gated on
  `typeof status === 'string'`, so a truncated or older record with an
  absent/non-string status minted an identity the live wire never stamped.
  Replace it with a positive allowlist over completed/running/background/
  cancelled.

New cases: both rejection-shaped displays stay unattributed, and absent /
non-string / null status stays unattributed. Dropping the session-ready
requirement reddens the first two; restoring the negative gate reddens the
status cases.
…rge-20261027

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merged main here only to clear the conflict — this PR stays the reference for the stack, and its review findings are being handled in #15657, #15658 and #15659.

yiliang114 added a commit that referenced this pull request Oct 28, 2026
…13902 A/3] (#15657)

* feat(acp): project subagent identity through live and replay frames

* style(sdk): format split agent identity regression test

* fix(acp-bridge): mint replay identity only on positive evidence of a run

Two replay findings from the #13902 review, both about trusting the
persisted record too much:

- `executionMode` alone was treated as proof the subagent started, but two
  rejection paths persist it for a session that never registered:
  worktree-provisioning failure (`status: 'failed'`) and the foreground
  model-slot cancel (`status: 'cancelled'`). Require
  `subagentSessionReady === true` on that leg, which is the field current
  writers flip once the subagent session exists; fixtures modelling a
  genuine run-then-fail now carry it.
- the rejection branch was a negative allowlist gated on
  `typeof status === 'string'`, so a truncated or older record with an
  absent/non-string status minted an identity the live wire never stamped.
  Replace it with a positive allowlist over completed/running/background/
  cancelled.

New cases: both rejection-shaped displays stay unattributed, and absent /
non-string / null status stays unattributed. Dropping the session-ready
requirement reddens the first two; restoring the negative gate reddens the
status cases.

* style(acp-bridge): format the replay identity test

The new union-typed `_meta` casts were left in a shape prettier
rewrites, so the PR-stage Prettier gate failed on this file alone.
@github-actions

Copy link
Copy Markdown
Contributor

Managed Agent gate failed

Run 40646661363 · commit 15390380577d

No test identifier was available; follow the run link for job logs and annotations.

@yiliang114
yiliang114 marked this pull request as draft October 28, 2026 12:24
auto-merge was automatically disabled October 28, 2026 12:24

Pull request was converted to draft

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Converting this PR to draft to reduce review-channel noise while its content is re-landed in smaller, reviewable layers. It is not abandoned — it is being superseded by the three-PR stack.

Stack relationship the reviewer should know about:

Layer State Core files it re-lands from this PR
#15657 [13902 A/3] project subagent identity MERGED (commit 4dd7ac7d) none — A/3 deliberately touches zero packages/core/ files; all the core logic in that layer lives on top of main without modification
#15658 [13902 B/3] durable team reports + session approvals MERGEABLE, draft — head c13c04ea after the corruption repair and the writer-shutdown ordering fix TeamManager.ts (+56/−11), team-events.ts (+7), api-user-prompt.ts (+11/−3), chatRecordingService.ts (+10)
#15659 [13902 C/3] teammate activity without affecting leader output draft, waiting on #15778 TeamManager.ts (+19/−2), team-events.ts (+19), chatRecordingService.ts (+24/−22)

This PR itself is the union of those three, plus a few things the stack has not yet re-landed: packages/core/src/agents/team/test-utils/coordination-harness.test.ts, part of the chatRecordingService change-set, and the packages/qwen-live additions. Those are the residual that can be closed out later — either opened as a small follow-up PR, or documented as intentionally dropped. I am not attempting to land them in this PR while the stack is in flight.

The conflicting state + 12 failing checks on this head are downstream of branching before main integrated the stack and the #15778 export-renderer headroom change; they are not evidence of a defect in the remaining content.

@qwen-code-review-bot qwen-code-review-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.

Partially reviewed — gaps disclosed.

Unresolved, please confirm:

  • [Critical] packages/cli/src/acp-integration/session/Session.ts:12800 (R8-3) — carried Critical re-checked at this head: the mechanism stands (blanket escapeXml over model text core already framed, so the inner <teammate_message> envelope and the s…

Not reviewed: reverse audit — stopped after round 3 without two consecutive dry rounds (rounds 1, 2 and 3 each reported; rounds 4-5 were not run, so the audit did not converge).

Not reviewed: packages/cli test suites — the six changed files never executed at this head because the web-templates build failure aborts the package's vitest global setup (see R12-1).

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the macOS/Windows Test matrix cells were skipped too, so no cross-platform run exercised the changed web-shell and cli code.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": did not read chunks other than 18; the two anchors sit on added lines immediately above my range (diff lines 6268-6277), reached from the shared source file and…; "agent reverse-audit (round 1)": whether a BridgeClientSessionEntry outlives its child process — a reused entry would keep a stale retiredThrough while a fresh child restarts nextBindingId…; "agent reverse-audit (round 3)": did not execute team-sink.test.ts to confirm green — packages/web-templates/dist/index.js is absent in this worktree and the export-html build is over its c…; "agent reverse-audit (round 3)": did not execute packages/sdk-typescript/test/daemon-ui-transcript.test.ts ; the coalescing trace above is manual.; "agent reverse-audit (round 3)": did not run packages/cli / packages/acp-bridge suites to confirm the new isAgentTeamEnabled: vi.fn().mockReturnValue(false) , getTeamManager: null and o…, and 4 more.

Deferred under the convergence posture (round 12, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/session/SubAgentTracker.ts:88 — [review] parentAgent option and agentIdentity getter have no production caller or reader; the depth-2 tests pin a shape no producer emits
  • packages/web-shell/client/components/messages/SystemMessage.tsx:222 — [review] turn-start marker row prints the teammate name twice (chip and turn.label come from the same sender)
  • packages/web-shell/client/adapters/messageTypes.ts:227 — [review] teammate JSDoc says the field names the author, but inter-agent rows mint it from the recipient while data.author (the real sender) has no reader
  • packages/cli/src/acp-integration/session/team-sink.test.ts:444 — [review] the null-manager teardown leg ( team_delete -> setTeamManager(null) ) has no test

Convergence: round 12 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (2 Critical(s)), the rate of first-time findings is not falling (this round 1, previous 1), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical) … … …

Advisory only — it does not block this review.

中文说明

仅完成部分审查,审查缺口已披露。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查(原文为英文):reverse audit — stopped after round 3 without two consecutive dry rounds (rounds 1, 2 and 3 each reported; rounds 4-5 were not run, so the audit did not converge).

未审查(原文为英文):packages/cli test suites — the six changed files never executed at this head because the web-templates build failure aborts the package's vitest global setup (see R12-1).

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; the macOS/Windows Test matrix cells were skipped too, so no cross-platform run exercised the changed web-shell and cli code.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)":did not read chunks other than 18; the two anchors sit on added lines immediately above my range (diff lines 6268-6277), reached from the shared source file and…;"agent reverse-audit (round 1)":whether a BridgeClientSessionEntry outlives its child process — a reused entry would keep a stale retiredThrough while a fresh child restarts nextBindingId…;"agent reverse-audit (round 3)":did not execute team-sink.test.ts to confirm green — packages/web-templates/dist/index.js is absent in this worktree and the export-html build is over its c…;"agent reverse-audit (round 3)":did not execute packages/sdk-typescript/test/daemon-ui-transcript.test.ts ; the coalescing trace above is manual.;"agent reverse-audit (round 3)":did not run packages/cli / packages/acp-bridge suites to confirm the new isAgentTeamEnabled: vi.fn().mockReturnValue(false) , getTeamManager: null and o…,另有 4 条。

收敛姿态下延后(第 12 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 12 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 1 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 2 条 Critical),首次发现的速率没有下降(本轮 1,上一轮 1),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

— deepseek-v4.1-flash-1017@e6bf8ffe via Qwen Code /review (v0.26.2)

// arrive before the display frame carrying the identity, so filling this
// inside the loop leaves the marker row — the row the user actually sees —
// anonymous.
const teammateByTask = new Map<string, { name: string }>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R12-1: [build] [regression] This diff's added transcript-renderer code pushes the /export html document renderer 1,004 bytes past its hard ceiling, so npm run build --workspace=packages/web-templates fails at this head — and because packages/cli compiles and tests against packages/web-templates/dist, the failure also blocks every packages/cli unit suite.

Witness (run):

$ npm run build --workspace=packages/web-templates        # head 1539038057
Document export renderer JS is 2050214 bytes; component CSS moved to export-transcript-document.css is 2395565 bytes
Error: Document export renderer JS is 2050214 bytes; expected <= 2050000.
                                         ^ build.mjs:374, npm error code 1

$ same scoped build on merge base 9ef52e153
Document export renderer JS is 2049210 bytes     → exit 0

The ceiling carried 790 bytes of headroom at 9ef52e15; this branch spends 1,004. Inside one run web-templates/dist is never produced, so packages/cli's vitest global setup aborts with workspace package "@qwen-code/web-templates" has not been built (missing packages/web-templates/dist/index.js) and none of the six changed packages/cli suites (Session.test.ts, team-sink.test.ts, acpAgent.test.ts, SubAgentTracker.test.ts, types.test.ts, create-sub-session.test.ts) executed at this head; packages/cli, packages/qwen-live and packages/vscode-ide-companion never built at all.

Fix: either trim the added renderer payload, or raise the ceiling deliberately and record the measurement the way that file's own comments do (MAX_DOCUMENT_RUNTIME_BYTES = 2_060_000, with the measured 2,050,214 at this head against 2,049,210 at 9ef52e15).

Acceptance: npm run build --workspace=packages/web-templates must exit 0 at the new head — the build check is its own witness, and it exits 1 without the fix.

Constraint: build.mjs:77-81 states the rule the new number must respect — "Keep the warning close to the measurement and the hard ceiling close above it: a cap left far above the measurement is a ratchet with enough slack for a whole dependency family to come back unnoticed" — with DOCUMENT_RUNTIME_WARNING_BYTES = 1_970_000 staying proportional, so a round multi-megabyte cap is not the fix.

中文说明

本 diff 新增的 transcript 渲染代码把 /export html 文档渲染包推过硬阈值 1,004 字节,导致 npm run build --workspace=packages/web-templates 在当前 head 直接失败;由于 packages/cli 依赖 packages/web-templates/dist 编译与测试,这同时阻断了全部 packages/cli 单测。

证据(已运行):head 测得 2,050,214 字节 > 2,050,000,build.mjs:374 抛错、退出码 1;merge base 9ef52e15 同一命令测得 2,049,210,退出码 0——即基线只剩 790 字节余量,而本分支用掉 1,004。后果:web-templates/dist 不会生成,packages/cli 的 vitest global setup 直接以「has not been built」中止,本 PR 改动的六个 cli 测试文件在本 head 一个都没能执行;packages/cli、packages/qwen-live、packages/vscode-ide-companion 也完全未构建。

修复:要么削减新增渲染载荷,要么按该文件既有注释的做法显式抬升阈值并记录实测值(如 MAX_DOCUMENT_RUNTIME_BYTES = 2_060_000,注释写明 head 2,050,214 / 9ef52e15 2,049,210)。验收标准就是该构建本身退出 0。约束:build.mjs:77-81 要求阈值贴近实测、警告值保持比例,不能改成粗放的多兆字节上限。

— deepseek-v4.1-flash-1017@e6bf8ffe via Qwen Code /review (v0.26.2)

// Queued teammate reports are already marked read in the on-disk
// mailbox, so land them here: the writer-close loop below runs before
// `dispose()`, and `appendRecord` is a silent no-op once it has closed.
session.flushPendingPeerNotifications?.();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R11-1: [certifies-falsely] [new-surface] Still stands at this head. beginManagedShutdown lands queued teammate reports before peer intake is quiesced, and the landing latches persisted even when the recorder has already stopped accepting writes — so a report arriving in the shutdown window is lost silently while the durable state records it as landed.

session.flushPendingPeerNotifications() runs synchronously right after the unawaited void session.cancelPendingPrompt().catch(…) (this file, 4234), and nothing stops peer intake first. Two entrances put a peer item into notificationQueue after that one-pass flush:

  1. an in-flight notification turn cancelled by the abort re-queues its already-spliced item on a later microtask (notificationQueue.unshift(item), Session.ts:13855 / :13905);
  2. TeamManager's 500 ms leader-inbox poll keeps running until dispose() — no ACP call site calls stopLeaderInboxPolling, and detachTeamSink happens only inside dispose() (Session.ts:5290) — while #enqueueTeammateMessage bails only on disposed (Session.ts:12786), so a report polled during closeManagedRuntime / flushDeferredExternalRecords / recorder.finalize() is pushed and never drained, because both drain paths return on closing (Session.ts:13594-13596).

dispose() then calls #landUndrainedPeerNotifications() again, whose recordNotification reaches appendRecord's silent early return once acceptingWrites is false (chatRecordingService.ts:1792-1795), while item.persisted = true is still latched (Session.ts:5235) and notificationQueue is cleared (Session.ts:5301). The mailbox row was marked read before the callback ran, so the report exists nowhere and nothing is logged. The sibling stored-session path settles turns first (await session.waitForActiveTurnsToSettle(), this file 5049) and only then flushes (5140); this path has no settle.

Witness: not run — the interleaving was not driven (the review's scratch tree returned available:false on the repository's git include), so both the ordering and the drop are quoted from the code at head (acpAgent.ts:4233-4234, :4243-4250, :4262-4295, :4747; Session.ts:5207-5238, :5290-5301; chatRecordingService.ts:1792-1795). A second auditor and this round's verifier each re-traced all five links and found every one present; the residual uncertainty is how often an event lands inside a sub-second window, not whether the mechanism exists.

Fix: quiesce intake before landing — stop the leader-inbox poll and detach the sink at the top of flushPendingPeerNotifications() — await cancelPendingPrompt() (or waitForActiveTurnsToSettle()) ahead of the flush, and set item.persisted only when the recorder still accepts writes.

Acceptance: extend lands queued teammate reports before managed shutdown closes the writers (acpAgent.test.ts) so the fake session's cancelPendingPrompt returns a promise whose continuation pushes a report into the array the flush reads, and assert that report was landed before closeSessionWriter; it reds against today's ordering and greens once the cancel is awaited and intake quiesced. A Session.test.ts case asserting a peer item enqueued after flushPendingPeerNotifications() is still written (or is refused a persisted latch) pins the second half.

Constraint: #enqueueTeammateMessage must keep accepting reports while closing — TeamManager.pollLeaderInbox() consumes and marks the row read before invoking the callback, so returning early there "would turn 'retained, undelivered' into 'consumed, lost'" (Session.ts:12783-12789), and a reversible close that is released must still deliver, pinned by "still projects a roster change that lands inside a reversible close" (Session.test.ts:1736). The fix therefore has to quiesce intake at the irrevocable point, not reject enqueues.

中文说明

R11-1 在当前 head 仍然存在。beginManagedShutdown 在尚未静默 peer 摄入的情况下就落地排队中的队友报告,且落地时在 recorder 已停止接收写入的情况下仍然锁存 persisted——因此关停窗口内到达的报告会被静默丢弃,而持久化状态却记录它已落地。

session.flushPendingPeerNotifications() 紧跟在未被 await 的 void session.cancelPendingPrompt().catch(…)(本文件 4234 行)之后同步执行。有两个入口会在这次一次性 flush 之后把 peer 项压入 notificationQueue:(1) 被 abort 的进行中通知轮次会在之后的 microtask 把已 splice 的项 unshift 回去(Session.ts:13855 / :13905);(2) TeamManager 的 500ms leader-inbox 轮询一直跑到 dispose() 为止(ACP 侧无任何调用 stopLeaderInboxPolling,detachTeamSink 只在 dispose() 内,Session.ts:5290),而 #enqueueTeammateMessage 只在 disposed 时返回(Session.ts:12786),于是在 closeManagedRuntime / flushDeferredExternalRecords / recorder.finalize() 期间轮询到的报告被压入却永不被消费——两条 drain 路径都在 closing 时返回(Session.ts:13594-13596)。

随后 dispose() 再次调用 #landUndrainedPeerNotifications(),其 recordNotification 在 acceptingWrites 为 false 时命中 appendRecord 的静默早退(chatRecordingService.ts:1792-1795),但 item.persisted = true 仍被锁存(Session.ts:5235),随后 notificationQueue 被清空(Session.ts:5301)。邮箱行在回调前已被标记为已读,因此报告在任何地方都不存在,也没有任何日志。同级的 stored-session 路径会先 await session.waitForActiveTurnsToSettle()(本文件 5049)再 flush(5140);本路径没有 settle。

证据:未运行——scratch tree 返回 available:false(仓库 git include 不可读),故顺序与丢弃均引用 head 代码(acpAgent.ts:4233-4234、:4243-4250、:4262-4295、:4747;Session.ts:5207-5238、:5290-5301;chatRecordingService.ts:1792-1795)。另一名审计者与本轮 verify 各自复走了五个环节,全部成立;残余不确定性是事件落入亚秒窗口的频率,而非机制是否存在。

修复:落地前先静默摄入——在 flushPendingPeerNotifications() 开头停止 leader-inbox 轮询并 detach sink;在 flush 之前 await cancelPendingPrompt()(或 waitForActiveTurnsToSettle());仅在 recorder 仍接收写入时才置 item.persisted。验收:扩展 acpAgent.test.ts 的 lands queued teammate reports before managed shutdown closes the writers,让 fake session 的 cancelPendingPrompt 在 continuation 中向 flush 读取的数组推入一条报告,并断言该报告在 closeSessionWriter 之前落地——当前顺序为红,await 取消并静默摄入后转绿。约束:不得因为 closing 就拒绝入队(Session.ts:12783-12789:邮箱先被消费并标记已读,早退会把「保留未投递」变成「已消费即丢失」),可逆 close 释放后仍须投递(Session.test.ts:1736 钉住)。

— deepseek-v4.1-flash-1017@e6bf8ffe via Qwen Code /review (v0.26.2)

This branch has not been deployed

No deployments
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.

8 participants