Files

51 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 发现记录
## 2026-07-29
- 演示子代理线程 `019f9160-6dde-7911-928c-925488ab00f9` 的真实 rollout 中有 4 次 `update_plan`:依次推进三个步骤并最终全部完成。
- 当前父会话持久化快照中,`plan_card_demo` 已保存 `planProgress: { completed: 3, total: 3 }`,证明服务端至少在最终快照链路中拿到了计划摘要。
- 用户界面始终未看到计划进度,根因应聚焦前端历史消息重建、collab tool 合并或实时 child update 的 DOM 刷新。
- 旧的 `subagent-plan-progress` 任务已经实现服务端计划解析、状态持久化与基础卡片渲染,但真实使用仍暴露了覆盖不足的链路。
- 当前不重启;先用真实 session 数据构造复现和回归。
- codebase-memory 索引状态为 ready(4140 nodes / 8942 edges);关键前端入口仍为 `mergeCollabAgentTools`、`upsertCollabAgentToolCall`、`applyCcwebMcpChildAgentUpdate`。
- 历史重建通过 `renderToolCallsWithMergedCollab` 合并全部 collab tool;实时 child update 则先更新缓存和 `activeToolCalls`,随后只调用 `updateToolCall(toolUseId, tool.result, done)`,需要确认它是否用新 `activeToolCalls` 重新构造完整 tool,还是仅以 result 刷新旧 DOM。
- `upsertCollabAgentToolCall` 把每个 tool id 保存在 DOM 节点的 `__collabTools` Map 中,然后整体重新合并;若相同 tool 的旧快照和新快照以不同 key 并存,或刷新未走该函数,旧状态可能覆盖计划字段。
- `collabAgentStateEntries` 会显式规范化 `planProgress` / `planCurrentStep`,`mergeCollabAgentTaskState` 会 spread 保留这些字段,`createCollabAgentToolElement` 也会为存在的进度创建 `.collab-agent-item-plan`;基础数据/渲染链路从代码上看没有主动丢字段。
- `rememberCollabAgentStateFromChild` 通过 `{ ...child }` 把 child 计划字段写入缓存,所以实时事件接收阶段也能保留计划摘要。
- 高风险点:`updateToolCall` 的 collab 分支忽略已有 `domElement`,固定在 `getLatestAssistantToolScope()` 返回的最新助手消息中调用 `upsertCollabAgentToolCall`。如果子代理卡片属于较早的消息,实时进度会更新到错误作用域或无法命中原卡片。
- 独立计划审查已通过;建议的四个观测点(持久化、历史数据结构、实时 payload、DOM)已纳入定位。
- 真实 session 的 `plan_card_demo` 工具呈现出关键形状:`input.agentsStates` 仍是启动时快照(无计划),更新后的 `result` JSON 才包含最终 `planProgress: 3/3`;历史重建必须正确以 result 覆盖 input 才能显示。
- 同一工具的最新 result 还合并进了本轮计划审查子代理,说明长会话中的子代理状态持续复用旧 tool id;实时 DOM 更新如果只查最新消息,尤其容易错位。
- 现有 `subagent-card-metadata` 回归虽然调用 `applyCcwebMcpChildAgentUpdate`,但测试夹具中的 `updateToolCall()` 是空函数,完全没有验证实时更新是否命中原卡片 DOM;这是目前最明确的覆盖缺口。
- `rememberToolCallTarget` 能保存 tool 到 DOM 的引用,但 `applyCcwebMcpChildAgentUpdate` 会用新对象覆盖 `activeToolCalls` 条目,因此历史引用不会被保留。
- `buildMsgElement` 重建历史消息时直接用合并后的 tool 创建元素,既不调用 `rememberToolCallTarget`,也没有给元素设置 `data-collab-merged` / `__collabTools`;后续实时更新无法把它识别为可增量合并的原卡片。
- 修复需要同时处理两个点:实时更新优先按 tool id 找原卡片作用域;历史重建的 collab 卡片需保存合并标记和原始 tool Map,避免更新时重复插入或错位。
- 真实会话顺序为:消息 29 启动演示子代理并承载卡片,消息 30 才是用户反馈“没变化”;因此实时计划阶段原卡片确实位于最近的助手消息中。仅“最新作用域漂移”不足以解释第一次完全看不到,还需核对服务端是否逐次广播计划事件。
- 服务端关键路径已确认:`processCcwebMcpChildNotification` 解析计划,`sendCcwebMcpChildAgentUpdate` 同时更新活动工具、持久化工具并向父会话 WebSocket 发送 child update;下一步核验实际调用时机和 payload。
- 服务端对每个成功解析的计划通知都会立即返回 `changed: true`,随后直接调用 `sendCcwebMcpChildAgentUpdate`;代码中没有只在完成时发送的节流逻辑。
- 广播目标优先取当前活动 turn 的 WebSocket,否则取正在查看该 session 的 WebSocket;若事件已正确路由,前端理论上应逐次收到。
- `findCodexAppRouteByRuntime` 能根据 child thread id 从 `ccwebMcpChildThreads` 恢复父会话路由;还需用日志或回归验证真实 plan 通知没有在这一步变成 unrouted。
- PM2 当前 ccweb 状态为 online;现有 stdout/stderr 不记录具体 child plan 广播,也没有该演示线程或 plan notification 的可追溯日志,无法靠历史日志区分“未路由”与“前端未渲染”。因此回归必须直接覆盖 payload→DOM 全链路。
- 新回归使用真实快照形状:启动态 `input` 无计划,更新态 `result` 才有 `2/3` 与当前步骤;同时要求历史卡片保存 merged 标记和原始 tool Map。
- 修复前定向回归按预期失败在“实时子代理更新应先定位原卡片”,证明覆盖点有效。
- 历史消息重建现在给合并卡片写入 `data-collab-merged`、child ids 与原始 tool Map;result-only 计划快照可继续接收实时更新。
- 实时 `updateToolCall` 先按 tool id / tool Map 在全消息区查原卡片,并优先使用其父作用域;`applyCcwebMcpChildAgentUpdate` 同时保留已有 DOM 引用。
- 增强后的定向回归不仅断言代码契约,还实际把历史卡片挂入测试 DOM,验证能按原始 tool id 找回;当前已通过。
- 在线 `http://127.0.0.1:8002/app.js` 已包含 `markMergedCollabAgentElement` 和原卡片定位调用,返回 200 且 `Cache-Control: no-store, max-age=0`;静态文件修改无需重启进程。
- PM2 中 ccweb 仍为 online,pid 241273,整个修复过程未重启服务。
- 用户刷新后提供的真实截图显示:`plan_progress_live_demo` 卡片处于“进行中”,但没有任何进度计数、进度点或当前步骤;这是明确的失败验收,不能再以代理调用成功代替 UI 成功。
- 同一子代理 `019fab4a-e95c-7353-8a37-6cdaab89188e` 在 session 快照中同时出现在消息 33(旧 `plan_visibility_plan_review` tool,id `call_XLR...`)与消息 35(自身 tool,id `call_19L...`);两处最终都有 `4/4`,强烈指向实时 plan update 曾路由到旧 tool/card,而用户看到的是当前 tool/card。
- 计划最终更新时间 `00:35:45Z` 早于子代理返回时间 `00:37:10Z` 约 85 秒,排除“计划只在完成瞬间落盘、用户来不及看到”的解释。
- 当前环境没有可用 Playwright/浏览器控制工具;真实 UI 验收必须以用户截图为准,代码回归只能作为辅助证据。
- 服务端根因已定位:`syncCcwebMcpChildAgentsFromCollabItem` 对已有 child 仅在 `spawnToolId` 为空时赋 `item.id`。真实 `subAgentActivity started` 到达新卡片 `call_19L...` 时,child 已错误持有旧卡片 `call_XLR...`,因此不会重绑。
- 后续每次 plan notification 都按旧 `child.spawnToolId` 调用 `findCcwebMcpChildTargetToolInMessages`,于是 `4/4` 被合并到消息 33 的旧卡片;用户正在看的消息 35 当前卡片没有计划字段,和截图完全一致。
- `findCcwebMcpChildTargetToolInMessages` 在给定 spawn tool id 找不到精确匹配时仍回退到最新 collab tool,会进一步放大跨卡片污染风险;回归需确保有明确 id 时不更新其他卡片。
- 计划审查通过;建议明确 child id → tool call id → message/card 映射,并同时断言目标卡片更新、非目标卡片不变,真实验收只接受用户截图或明确确认。
- 父 rollout 证实 `call_19LK...` 就是 `plan_progress_live_demo` 的 `spawn_agent` / `sub_agent_activity started` 事件 ID,child id 为 `019fab4a...`;`call_XLR...` 明确属于更早的计划审查代理。两者不是同一卡片。
- 当前回归 `assertCodexAppChildToolFallbackContract` 明确断言“缺失的 spawnToolId 应回退到最新 collab tool”,这正是跨卡片污染的错误契约,需要反转为:给定非空 id 未精确命中时不得修改任何其他卡片。
- 新双卡片回归覆盖:正确 tool id 只更新当前卡片、旧卡片不出现当前 child、非空未知 id 返回 null 且两张卡片都不变、`subAgentActivity started` 将已有 child 从旧 id 重绑到当前 id。
- 修复前回归稳定失败在“非空未知 spawn tool id 不得回退”,修改服务端后已通过。
- 全量集成回归揭示嵌套 V2 场景:孙代理的内部 activity id 不在根会话可见 toolCalls 中,原先依赖模糊回退把孙代理合并进父子代理卡片。
- 最终路由规则改为:根子代理使用自己的 canonical activity/tool id;嵌套子代理继承最近可见父子代理的 `spawnToolId`,同时保留自己的 `parentThreadId`。这样可以精确命中可见卡片而无需跨卡回退。
- 更新后 `subagent-card-routing`、JS 语法、diff check 和全量 `npm run regression` 全部通过;全量回归也重新覆盖了 nested V2 child result。