# 失败插入卡片链路与实现契约 ## 现状链路 ```text sendMessage → 创建本地用户卡片 + clientMessageId → WebSocket type=message → handleMessage → 活动 Codex App turn 时进入 handleCodexAppSteerMessage → codex_app_steer_status(pending / inserted / failed) → handleServerMessage → updateCodexAppSteerMessage → setCodexAppSteerStatusElement ``` 关键位置: - `public/app.js`:`sendMessage`、`setCodexAppSteerStatusElement`、`updateCodexAppSteerMessage`、`createMsgElement`、`clearUserMessageIndex`。 - `public/style.css`:`.codex-steer-*` 状态样式。 - `server.js`:`handleMessage`、`handleCodexAppSteerMessage`。 - `scripts/regression.js`:`runCodexAppRuntimeImageSteerRegression`、`runCodexAppStaleRunningRegression`。 ## 已确认事实 1. 本地失败卡片以 `clientMessageId` / `data-message-id` 稳定定位,不应按文件名或 DOM 顺序匹配。 2. 当前 `userMessageIndex` 只保存内容与时间;重试还需要保存附件、会话、模式和 agent。 3. 当前状态渲染只有文本,没有操作容器、忙碌语义和并发保护。 4. 当前服务端在调用 `turn/steer` 前持久化用户消息,最终失败时不回滚,会留下模型未接收的幽灵消息。 5. 同步前置失败不会持久化;两类失败必须统一为“失败卡片仅存在于当前客户端”的语义。 6. 本地临时卡片当前提前增加 `currentSessionMessageCount`,失败后会造成消息索引漂移。 ## 推荐实现 ### 前端 - 新增独立的运行中插入记录 Map,以 `clientMessageId` 保存:元素、文本、附件克隆、sessionId、mode、agent、pending/committed 状态。 - `clearUserMessageIndex` 同时清理该 Map,防止切换会话后重试旧请求。 - `setCodexAppSteerStatusElement` 统一维护: - `role=status`、`aria-live=polite`; - 卡片 `aria-busy`; - 失败态的“重试”“删除”按钮; - 非失败态移除操作容器; - 按钮使用 `type=button`、明确 `aria-label`,并阻止重复绑定。 - `retryCodexAppSteerMessage` 只在记录存在、仍属于当前会话、当前为 Codex App 运行中且 WebSocket 可用时发送;先切到 pending 并禁用操作,发送失败则恢复 failed。 - `deleteCodexAppSteerMessage` 只允许删除 failed 临时项,移除 DOM、两个索引记录并刷新轮廓/滚动条。 - 临时插入不提前递增 `currentSessionMessageCount`;收到 `inserted` 后才标记 session message 并递增,失败/删除不改变持久化索引。 ### 服务端 - 给待持久化的 steer 用户消息写入稳定 `id`(优先使用 `clientMessageId`)。 - 最终 `turn/steer` 失败时,在发送 failed 状态前,从最新会话中按该 id 精确移除本次用户消息并保存。 - stale no-active-turn 自动恢复成功分支不得回滚;同步前置失败和 ready 超时本就没有持久化项。 - 异步错误消息补带 `clientMessageId`,便于前端和回归关联。 ## 测试契约 - 前端独立单测或可执行契约测试: - failed 创建两个按钮,pending/inserted 不展示; - 快速双击重试只发送一次,载荷复用原文本与附件; - retry 时 `aria-busy=true` 且操作禁用/移除; - failed 恢复操作; - delete 只移除目标项及对应索引; - turn 已结束或 WebSocket 断开时不发送并保留可恢复失败态。 - 服务端定向回归: - 现有同步附件失败仍不持久化; - mismatch 异步失败后同名用户消息为 0; - stale recovery / 成功 steer 用户消息仍恰好为 1。 - 静态契约:按钮文本、CSS 作用域和 status handler 保持存在。 ## 已知限制 - 附件已经从服务端过期时,单纯重试仍会失败;按钮主要恢复临时 turn/server 错误。再次失败后必须恢复可操作状态和明确错误提示。 - 失败项按现有持久化边界保持客户端临时态,刷新后会消失;不新增任意历史消息删除协议。