Files
cc-web/.planning/session-form-switch-race/findings.md
2026-07-18 19:47:04 +08:00

48 lines
3.7 KiB
Markdown
Raw 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.

# 调研发现
## 用户侧证据
- 截图一:底部“表单”入口角标为 `1`,消息区只有普通助手消息,没有待处理表单卡片。
- 截图二:执行 F5 后,同一会话顶部出现“先定空条件规则”表单,角标仍为 `1`。
- 由此可排除“表单未持久化”这一主假设,更可能是 SPA 会话切换后的客户端状态同步或渲染失效。
## 待验证假设
1. 会话切换触发的消息请求存在竞态,旧会话响应晚到后覆盖当前消息列表。
2. 待处理表单计数按全局/会话摘要更新,但表单卡片依赖另一份未同步的消息状态。
3. 表单渲染使用了只在首次加载执行的派生缓存、去重集合或已处理 ID 集合,切换会话时未重置/重建。
## 代码定位(第一轮)
- `renderPendingCcwebPrompts()` 通过 `collectCurrentPendingCcwebPrompts()` 统计表单角标。
- `collectCurrentPendingCcwebPrompts()` 优先读取 `sessionCache.get(currentSessionId).snapshot.messages`,随后再扫描消息区 DOM;因此角标可以来自完整缓存,即使卡片尚未进入 DOM。
- `renderMessages()` 对超过 10 条的历史消息采用分批渲染:先渲染最后 10 条,再用 `setTimeout` 逐批前插;每个延迟批次用全局 `renderEpoch` 防止旧会话继续写 DOM。
- `renderMessages()` 首批完成后调用 `renderPendingNotes()`,后者会立即刷新表单角标;此时较旧的表单消息可能还未渲染,形成“角标为 1、卡片暂缺”的短暂状态。
- `applySessionSnapshot()` 是缓存展示和服务端 `session_info` 的共同入口;缓存展示强制 `immediate: true`,正常服务端响应默认分批渲染。
- 当前需要继续验证:快速切换过程中,哪个响应/早退分支使当前会话的剩余批次被 `renderEpoch` 永久取消,却没有再次对当前快照执行完整渲染。
## 已确认关键符号
- `public/app.js`: `openSession`、`showCachedSession`、`applySessionSnapshot`、`renderMessages`
- `public/app.js`: `collectCurrentPendingCcwebPrompts`、`renderPendingCcwebPrompts`、`renderPendingNotes`
- `scripts/regression.js`: `assertSessionSwitchResilienceContract`
## 根因结论
1. `beginSessionSwitch()` 在请求意图阶段执行 `renderEpoch++`;如果新快照尚未提交、请求被快速切换覆盖或失败,旧 DOM 仍展示但其剩余批次已永久失效。真正替换 DOM 的 `renderMessages()` 本身已经推进并校验 `renderEpoch`,所以前者属于重复且有害的失效动作。
2. `session_info` 的 `canSwitchToSessionInfo` 对当前 `sessionId` 无条件放行,没有阻止同一会话旧 `requestId` 响应重绘当前 DOM。
3. `handleLoadSession()` 给 `session_info` 附带了客户端 `requestId`,但 `session_history_chunk` 没有;前端无法区分 `A→B→A` 中两次 A 加载的历史块。
4. `finalizeLoadedSession()` 只按 `sessionId` 完成加载,旧 A 尾块可误完成新 A 请求。
## 测试策略
- 在 `scripts/regression.js` 新增无依赖、确定性的渲染 epoch 调度测试:发起 B 但不提交 B 时,A 的延迟表单批次必须继续;真正提交 B 后,A 批次必须停止。
- 扩展会话切换静态契约:`beginSessionSwitch` 不推进 epoch;带请求 ID 的旧 `session_info` 不得仅因 sessionId 当前而放行;历史块及 finalize 路径校验请求 ID。
- 扩展 WebSocket 集成断言:`session_info` 与同次 `session_history_chunk` 回传相同请求 ID。
## 仓库协作状态
- `.planning/.active_plan` 当前指向其他任务 `subagent-card-metadata`,本次不改动该指针。
- `.trellis/.current-task` 当前指向其他任务 `07-17-gilded-wasteland-theme`,本次不覆盖。
- 工作树已有无关未跟踪 CSV,必须保留。