Skip to content

fix(daemon): parallel subAgent text chunks interleave in transcript (串台) #4687

@doudouOUC

Description

@doudouOUC

问题描述

Daemon 模式下,/review 等 skill 派发多个并行 subAgent 时,不同 subAgent 的文本 chunk 会交错合并到同一个 transcript block 中,导致 WebShell 上显示乱码/碎片化文本。

根因分析

问题贯穿三层:

1. 发射层:SubAgentTracker.createStreamTextHandler 不带 subagentMeta

SubAgentTracker.ts:275 调用 messageEmitter.emitMessage(text, 'assistant', thought)没有传 subagentMeta(对比 createToolCallHandler/createUsageMetadataHandler 都传了 this.getSubagentMeta())。

结果:所有 subAgent 的 agent_message_chunk SSE 事件的 _meta 都不包含 parentToolCallId/subagentType,无法区分来源。

2. 归一化层:normalizer 不提取 parentToolCallId

normalizer.ts:398-401agent_message_chunkagent_thought_chunk 只提取 text,完全忽略 _meta。对比 normalizeToolUpdate(line 511-520)会从 _meta 提取 parentToolCallId

结果:DaemonUiTextEvent 没有任何 subAgent 标识。

3. 归约层:单一 activeAssistantBlockId

transcript.ts:367-373appendTextDelta 用唯一一个 activeAssistantBlockId 追加文本。所有并行 subAgent 的 chunk 往同一个 block 里拼。

复现

已在 packages/sdk-typescript/test/unit/daemonUi.test.ts 中添加复现测试(describe('parallel subAgent text interleaving (reproduction)')),模拟两个并行 subAgent 交替发射 assistant.text.delta,验证所有 chunk 被错误合并到同一个 block:

"Agent A says: hello Agent B says: world from agent Afrom agent B"

修复方向

沿用 tool block 已有的 parentToolCallId 隔离模式,扩展到 text/thought 事件:

  1. 发射端MessageEmitter.emitAgentMessage/emitAgentThought/emitMessagesubagentMeta? 参数;SubAgentTracker.createStreamTextHandlerthis.getSubagentMeta()
  2. 类型DaemonUiTextEvent + DaemonTextTranscriptBlockparentToolCallId?DaemonTranscriptStateactiveAssistantBlockByParent/activeThoughtBlockByParent map
  3. normalizeragent_message_chunk/agent_thought_chunk case 从 _meta 提取 parentToolCallId
  4. reducerappendTextDeltaparentToolCallId 路由到 keyed map,不带 parentToolCallId 走现有 scalar 路径(零行为变更);clearActiveText 支持 scoped clear(只清对应 parent 的 text block);finishAssistant 遍历 map 设 streaming = false

详细设计见 worktree 分支 worktree-wiggly-meandering-lightning 中的 .claude/plans/wiggly-meandering-lightning.md

涉及文件

文件 变更
packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts subagentMeta? 参数
packages/cli/src/acp-integration/session/SubAgentTracker.ts this.getSubagentMeta()
packages/sdk-typescript/src/daemon/ui/types.ts 类型扩展
packages/sdk-typescript/src/daemon/ui/normalizer.ts 提取 parentToolCallId
packages/sdk-typescript/src/daemon/ui/transcript.ts per-parent 隔离、scoped clear
packages/sdk-typescript/src/daemon/ui/store.ts createState 浅拷贝新 map

已知局限(后续 PR)

  • WebUI transcriptToMessages.ts 渲染层需要用 parentToolCallId 替代位置启发式
  • History replay 不透传 parentToolCallId(顺序回放不实际串台)
  • shell/permission/status 事件在并行 subAgent 流期间触发 global clear,不丢文本但会多一次 block 切换

Metadata

Metadata

Assignees

Labels

category/uiUser interface and displaydaemonscope/session-managementSession state and persistencestatus/ready-for-agentFully specified; an AFK coding agent can pick this up with no human contexttype/bugSomething isn't working as expected

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions