feat: improve conversation timeline and agent plans

This commit is contained in:
shiyue
2026-07-29 16:50:12 +08:00
parent 4d97446a6f
commit 33c9783079
16 changed files with 663 additions and 59 deletions

View File

@@ -0,0 +1,50 @@
# 发现记录
## 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 索引状态为 ready4140 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 Mapresult-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 仍为 onlinepid 241273整个修复过程未重启服务。
- 用户刷新后提供的真实截图显示:`plan_progress_live_demo` 卡片处于“进行中”,但没有任何进度计数、进度点或当前步骤;这是明确的失败验收,不能再以代理调用成功代替 UI 成功。
- 同一子代理 `019fab4a-e95c-7353-8a37-6cdaab89188e` 在 session 快照中同时出现在消息 33`plan_visibility_plan_review` toolid `call_XLR...`)与消息 35自身 toolid `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` 事件 IDchild 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。

View File

@@ -0,0 +1,44 @@
# 进度日志
## 2026-07-29
- 恢复上下文并核对旧计划、真实 rollout 与会话持久化证据。
- 确认子代理确实调用 `update_plan`,问题属于 ccweb 显示链路缺陷。
- 建立六步修复计划,当前定位真实数据断点。
- 使用 codebase-memory 定位并读取历史重建、collab 合并、实时 child update 与 DOM upsert 的关键实现。
- 计划审查子代理确认计划可执行,无阻断问题。
- 完成关键实现交叉核对,初步锁定实时更新错误选择“最新助手消息作用域”的风险。
- 抽取真实 session 工具数据,确认计划摘要仅存在于 result 快照,并发现现有实时更新回归把 DOM 更新函数 stub 掉了。
- 进一步确认历史重建元素缺少 collab 增量合并元数据,实时更新也会覆盖已有 DOM 引用。
- 对照真实消息顺序后保留前端作用域缺陷,但不把它当作唯一根因;转向核验服务端计划事件逐次广播。
- 核对服务端计划处理与发送代码,确认实现意图是每次计划变化都实时广播,不是只持久化最终状态。
- 检查 PM2 历史日志;日志粒度不足以还原该事件,决定用真实 payload 驱动的前端回归直接验证。
- 阶段 1 完成:真实数据、历史重建、实时 payload 与 DOM 四个观测点已经核验;开始补失败回归。
- 新增真实 result-only 快照与历史卡片增量元数据回归;修复前定向回归按预期失败。
- 阶段 2 完成,开始实现历史卡片标记与实时原卡片定位。
- 完成历史卡片增量元数据与实时原卡片定位修复,并保留 active tool DOM 引用。
- 真实 result-only 快照、历史恢复标记、tool id 查找行为回归均已通过;进入语法和定向检查。
- `node --check` 检查 `public/app.js``scripts/regression.js` 通过。
- `subagent-card-metadata``plan-list-progress` 两组定向回归通过;进入全量回归和差异审查。
- 全量 `npm run regression` 在 60 秒限制内通过,`git diff --check` 通过。
- 审查相关差异:修改仅围绕历史 collab 卡片元数据、实时目标定位和对应回归;未改服务端协议与 Plan List 渲染逻辑。
- 阶段 5 完成;开始验证当前在线服务是否直接提供最新静态资源,从而避免重启。
- 在线 `/app.js` 已命中修复代码且禁用缓存PM2 保持 online确认无需重启。
- 六个阶段全部完成,准备清理本任务临时 TODO CSV 并交付。
- `check-complete.sh` 首次因计划采用表格格式报告 0/0已补充机器可读阶段标记不影响任务实际完成状态。
- 用户真实截图证明功能仍未生效;撤销“已完成”结论并重新打开计划。
- 已核对本次演示子代理的 session 快照:同一 child 同时落入消息 33/35 两个不同 collab tool开始定位 spawn tool id 路由错误。
- 读取服务端 child 同步与工具查找实现,确认已有 child 不会在新的 `subAgentActivity started` 事件中重绑 spawn tool id导致计划持续写入旧卡片。
- 独立计划审查通过,阶段 7 继续收敛精确映射并准备失败回归。
- 用父 rollout 精确确认 child `019fab4a...` 的正确 tool call id 是 `call_19LK...`,旧 `call_XLR...` 属于另一代理。
- 找到现有回归还在保护“错误 id 回退到最新卡片”的旧行为,准备先把契约改成防污染失败回归。
- 阶段 7 完成:真实 child、tool call 与消息卡片映射已经闭环;开始编写双卡片防污染回归。
- 新增 `subagent-card-routing` 定向回归,修复前按预期失败。
- 服务端已改为 canonical `subAgentActivity` 强制重绑当前 tool id非空 id 只允许精确匹配;定向回归通过,进入全量验证。
- 语法、路由定向回归、卡片定向回归和 diff check 通过。
- 首次全量回归失败于嵌套 V2 child 可见卡片合并;严格禁回退方案过窄,进入按 child/parent 归属的替代设计。
- 实现分层可见 tool id根子代理重绑自身 tool嵌套子代理继承父级可见 tool新增嵌套断言。
- 路由定向回归、语法检查、diff check 和全量回归均通过;准备执行服务端生效前的重启安全门禁。
- 重启门禁确认只有当前对话 running执行 PM2 重启后新 PID 178501 onlineHTTP 返回 200。
- 阶段 9 完成;启动新的真实演示代理,等待用户实际画面确认。
- 新演示代理 `plan_progress_route_fix_demo` 已完成 4 步计划并返回;阶段 10 仍保持 in_progress等待用户确认卡片是否实际出现 `x/4`,不以后台完成代替 UI 验收。

View File

@@ -0,0 +1,78 @@
# 修复子代理计划进度不可见
## 目标
基于真实子代理 rollout 与会话快照,定位 `update_plan` 已执行但父会话子代理卡片不显示进度的断点,补齐回归并修复历史重建与实时更新链路;优先避免不必要的 ccweb 重启。
## 阶段
| 阶段 | 状态 | 验收标准 |
|---|---|---|
| 1. 核验真实通知与会话快照,确定显示链路断点 | complete | 真实数据能稳定复现字段被吞或 DOM 未刷新的具体路径 |
| 2. 补充真实快照驱动的失败回归 | complete | 回归在修复前失败,且覆盖已确认断点 |
| 3. 修复卡片计划字段的合并与实时更新 | complete | 历史消息和实时 child update 均保留并展示计划摘要 |
| 4. 运行定向回归和语法检查 | complete | 相关回归与 JS 语法检查通过 |
| 5. 运行全量回归并审查差异 | complete | 全量回归、diff check 通过且无无关改动 |
| 6. 验证线上静态资源并确认无需重启 | complete | 当前服务可读取修复资源;仅在确需且满足安全门禁时重启 |
| 7. 对齐真实计划事件与卡片工具 ID | complete | 找到 `plan_progress_live_demo` 计划更新实际写入哪张 tool/card以及为何当前卡片未命中 |
| 8. 补充真实工具路由失败回归 | complete | 使用本次消息 33/35 的双工具形状稳定复现错误卡片被更新 |
| 9. 修复工具路由并完成回归 | complete | 实时计划仅更新当前子代理所属卡片,定向与全量回归通过 |
| 10. 启动新演示并以实际画面验收 | in_progress | 新代理运行时卡片实际出现 `x/4`;未经用户画面确认不宣告完成 |
## 机器可读阶段状态
### Phase 1核验真实链路
**Status:** complete
### Phase 2补充失败回归
**Status:** complete
### Phase 3修复历史与实时更新
**Status:** complete
### Phase 4定向验证
**Status:** complete
### Phase 5全量回归与差异审查
**Status:** complete
### Phase 6在线资源验证
**Status:** complete
### Phase 7对齐真实工具路由
**Status:** complete
### Phase 8补充真实路由回归
**Status:** complete
### Phase 9修复并验证
**Status:** complete
### Phase 10真实画面验收
**Status:** in_progress
## 关键决策
- 以真实 rollout 中 4 次 `update_plan` 和 session 中最终 `3/3` 为事实基线,不再把问题归因于子代理未调用。
- 先验证历史消息重建,再验证实时 `ccweb_mcp_child_agent_update` DOM 更新,避免凭 UI 现象猜测。
- 使用现有 child update 事件与卡片结构,不新增协议字段或消息类型。
- 保留工作区既有脏改动和用户的其他 TODO CSV只修改本任务相关区域。
## 错误记录
| 错误 | 尝试 | 处理 |
|---|---|---|
| 定向回归报错:实时子代理更新未先定位原卡片 | 1 | 预期失败,证明回归已覆盖真实断点;进入实现修复 |
| 完成检查器报告 0/0 phases | 1 | 计划原为表格格式,补充机器可读阶段标题和完成状态标记 |
| 用户真实截图仍无 `x/4` | 2 | 前一回归只覆盖单工具历史 DOM未覆盖同一 child 同时落入消息 33/35 两个 tool 的真实路由;重新打开任务 |
| 全量回归Nested V2 child result 未合并到可见卡片 | 1 | 严格禁止非精确回退破坏嵌套子代理;改为按 child/parent 归属回退,不恢复“最新卡片”策略 |