问题描述
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-401 对 agent_message_chunk 和 agent_thought_chunk 只提取 text,完全忽略 _meta。对比 normalizeToolUpdate(line 511-520)会从 _meta 提取 parentToolCallId。
结果:DaemonUiTextEvent 没有任何 subAgent 标识。
3. 归约层:单一 activeAssistantBlockId
transcript.ts:367-373 中 appendTextDelta 用唯一一个 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 事件:
- 发射端:
MessageEmitter.emitAgentMessage/emitAgentThought/emitMessage 加 subagentMeta? 参数;SubAgentTracker.createStreamTextHandler 传 this.getSubagentMeta()
- 类型:
DaemonUiTextEvent + DaemonTextTranscriptBlock 加 parentToolCallId?;DaemonTranscriptState 加 activeAssistantBlockByParent/activeThoughtBlockByParent map
- normalizer:
agent_message_chunk/agent_thought_chunk case 从 _meta 提取 parentToolCallId
- reducer:
appendTextDelta 按 parentToolCallId 路由到 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 切换
问题描述
Daemon 模式下,
/review等 skill 派发多个并行 subAgent 时,不同 subAgent 的文本 chunk 会交错合并到同一个 transcript block 中,导致 WebShell 上显示乱码/碎片化文本。根因分析
问题贯穿三层:
1. 发射层:
SubAgentTracker.createStreamTextHandler不带subagentMetaSubAgentTracker.ts:275调用messageEmitter.emitMessage(text, 'assistant', thought)时没有传subagentMeta(对比createToolCallHandler/createUsageMetadataHandler都传了this.getSubagentMeta())。结果:所有 subAgent 的
agent_message_chunkSSE 事件的_meta都不包含parentToolCallId/subagentType,无法区分来源。2. 归一化层:normalizer 不提取
parentToolCallIdnormalizer.ts:398-401对agent_message_chunk和agent_thought_chunk只提取text,完全忽略_meta。对比normalizeToolUpdate(line 511-520)会从_meta提取parentToolCallId。结果:
DaemonUiTextEvent没有任何 subAgent 标识。3. 归约层:单一
activeAssistantBlockIdtranscript.ts:367-373中appendTextDelta用唯一一个activeAssistantBlockId追加文本。所有并行 subAgent 的 chunk 往同一个 block 里拼。复现
已在
packages/sdk-typescript/test/unit/daemonUi.test.ts中添加复现测试(describe('parallel subAgent text interleaving (reproduction)')),模拟两个并行 subAgent 交替发射assistant.text.delta,验证所有 chunk 被错误合并到同一个 block:修复方向
沿用 tool block 已有的
parentToolCallId隔离模式,扩展到 text/thought 事件:MessageEmitter.emitAgentMessage/emitAgentThought/emitMessage加subagentMeta?参数;SubAgentTracker.createStreamTextHandler传this.getSubagentMeta()DaemonUiTextEvent+DaemonTextTranscriptBlock加parentToolCallId?;DaemonTranscriptState加activeAssistantBlockByParent/activeThoughtBlockByParentmapagent_message_chunk/agent_thought_chunkcase 从_meta提取parentToolCallIdappendTextDelta按parentToolCallId路由到 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.tssubagentMeta?参数packages/cli/src/acp-integration/session/SubAgentTracker.tsthis.getSubagentMeta()packages/sdk-typescript/src/daemon/ui/types.tspackages/sdk-typescript/src/daemon/ui/normalizer.tsparentToolCallIdpackages/sdk-typescript/src/daemon/ui/transcript.tspackages/sdk-typescript/src/daemon/ui/store.tscreateState浅拷贝新 map已知局限(后续 PR)
transcriptToMessages.ts渲染层需要用parentToolCallId替代位置启发式parentToolCallId(顺序回放不实际串台)