diff --git a/docs/superpowers/specs/2026-09-11-chat-turn-idempotency-and-session-isolation-design.md b/docs/superpowers/specs/2026-09-11-chat-turn-idempotency-and-session-isolation-design.md new file mode 100644 index 00000000..a1e679ed --- /dev/null +++ b/docs/superpowers/specs/2026-09-11-chat-turn-idempotency-and-session-isolation-design.md @@ -0,0 +1,218 @@ +# 聊天回合幂等与项目切换隔离 — 设计规格 + +> 日期:2026-09-11 +> 状态:已批准,进入实现 +> 关联 RCA:重复消息写入、项目切换重复展示、全局流状态冲突 + +--- + +## 一、问题背景 + +三个缺陷叠加导致重复消息与错误行为: + +1. **后端无幂等门禁**:同一 POST 可多次 INSERT 用户消息并独立执行 Recognizer/Coordinator。 +2. **前端身份对账不完整**:本地 `msg-{timestamp}` 与历史 `msg-{dbId}` 无法识别为同一条。 +3. **流运行态全局共享**:`streamAbortRef` / `isThinking` 跨 session,切项目会中断后台 turn。 + +数据库已证实:`session-6010-张工-private` 中相同正文在 15.86s 内写入两条用户消息(ID 24994/24995),无 `client_turn_id`。 + +--- + +## 二、目标 + +- 同一用户意图 → 一个 `chat_turn`、一条用户消息、一条助手回复。 +- 项目切换只改展示上下文,不取消其他 session 的后台 turn。 +- 弱网重试/断线重连复用同一 `client_turn_id`,不重复执行 Agent。 + +--- + +## 三、核心协议 + +### 3.1 客户端回合 ID + +- 用户首次点击发送时生成 `client_turn_id = crypto.randomUUID()`。 +- 用户消息与助手占位共享同一 turn。 +- 自动/手动重试、断线重连必须复用,只有新输入才生成新 turn。 + +### 3.2 HTTP 请求 + +```http +POST /api/v1/chat/stream +Idempotency-Key: {client_turn_id} +Content-Type: application/json +``` + +```json +{ + "session_id": "...", + "client_turn_id": "...", + "message": "...", + "user_id": 1, + "project_id": 6010 +} +``` + +- 同 session 内同 key 不同正文 hash → `409 IDEMPOTENCY_KEY_REUSED`。 +- 新客户端 **必须** 携带 `client_turn_id`。 + +### 3.3 chat_turns 权威表 + +```text +chat_turns ( + id BIGSERIAL PRIMARY KEY, + session_id TEXT NOT NULL, + client_turn_id TEXT NOT NULL, + user_id BIGINT, + project_id BIGINT, + user_message_id BIGINT, + assistant_message_id BIGINT, + request_body_sha256 TEXT NOT NULL, + status TEXT NOT NULL, -- accepted|processing|completed|failed|cancelled + owner_token TEXT, + lease_expires_at TIMESTAMPTZ, + first_request_id TEXT, + last_request_id TEXT, + attempt_count INT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + completed_at TIMESTAMPTZ, + UNIQUE(session_id, client_turn_id) +) +``` + +**执行权门禁(必须包住 Agent):** + +1. `INSERT ... ON CONFLICT DO NOTHING` 赢得执行权 → 写用户消息 → 串行执行 Agent → 写助手消息 → `completed`。 +2. 已 `completed` → 返回既有 IDs,不重跑 Agent。 +3. `processing` → 返回状态或附着已有流。 +4. `failed` → 同 turn 恢复,不新建用户消息。 + +### 3.4 SSE 事件 + +**turn_ack(首确认):** + +```json +{ + "type": "turn_ack", + "turn": { + "client_turn_id": "...", + "user_message_id": 24994, + "status": "processing", + "duplicate": false + } +} +``` + +**done(完成):** + +```json +{ + "type": "done", + "done": { + "client_turn_id": "...", + "user_message_id": 24994, + "message_id": 25001, + "status": "completed" + } +} +``` + +### 3.5 断线续接 + +- `GET /api/v1/chat/turns/{client_turn_id}?session_id=...` 查询 turn 状态与 message IDs。 +- 前端断线不新建 turn;先查状态再决定附着/展示完成。 + +--- + +## 四、前端身份合并 + +优先级: + +1. `clientTurnId` 相同 +2. `serverId` 相同 +3. 迁移期弱兜底:同 session + role + content hash + 极近时间(非永久主键) + +收到 `turn_ack` 后立即绑定用户消息 `serverId`;`done` 绑定助手 `serverId`。 + +--- + +## 五、Session 隔离 + +### 5.1 窗口拉取竞态 + +- `generationRef` + `AbortController` +- 响应校验 generation / sessionId / !aborted +- session 切换立即展示该 session 缓存骨架 + +### 5.2 流运行态 + +```text +Map> +``` + +- 切项目不 abort 其他 session +- `isThinking` 按当前 session 维度 +- 停止仅针对指定 session + turn + +### 5.3 持久化 + +- **废弃** 全量 `MESSAGES` localStorage +- 保留 `chat.window.{sessionId}` + `chat.draft.{sessionId}` +- 进行中 turn 只持久化 `clientTurnId` + 状态引用 + +--- + +## 六、同 session 串行 + +- 每 Session 内 turn 串行执行(mutex/queue) +- 不同 session 可并行 +- 防止上下文乱序与数据竞争 + +--- + +## 七、止血(Phase 1) + +幂等落地前: + +- **关闭** chat stream 无幂等自动重发 +- 瞬态错误只展示手动「重试」按钮 +- 手动重试提示确认上一请求状态 + +--- + +## 八、验收标准 + +```text +发送项目 A 消息 +→ A 一条用户消息 + 一个助手占位 +→ 切到 B,A 后台继续 +→ 回到 A:仍一条用户消息、一条助手回复 +→ DB:一个 chat_turn、一条用户消息、一条助手消息 +→ Coordinator/工作流副作用只执行一次 +``` + +--- + +## 九、测试矩阵 + +| 场景 | 期望 | +| :--- | :--- | +| 本地 tmp vs 服务端 msg-dbId | 合并为一条 | +| 同 client_turn_id 并发 POST | 1 user + 1 assistant | +| 同正文不同 client_turn_id | 允许两轮 | +| 断线同 turn 重连 | 不新增用户消息、不重跑 Agent | +| A→B→A 窗口竞态 | B 晚到不覆盖 A | +| 切项目后台 turn | A 继续;回 A 无重复 | + +--- + +## 十、实施顺序 + +1. 关闭无幂等自动重发 +2. user_message_id + 前端身份合并 +3. useChatWindow 竞态保护 +4. chat_turns + 端到端幂等 +5. session 串行 + turn_ack/续接 +6. per-session 流运行态 +7. 停用全量 messages 持久化 +8. 测试矩阵 + 真实 Agent 验收报告