- G2:`stream:true` 原经**有损总线**且**先回 200** ⇒ 总线饱和时客户端拿到 200 却什么都没发生。
改为**直达编排器**(与 ws chat 帧既有先例同一原则):新文件 chat_stream_dispatch.go;200 变成有意义的
承诺(同步穿过「编排器接收 + 用户消息落库」才回);顺带对齐 `202 {queued,queueDepth}` / `409 chat_busy`;
兜底路径(未接编排器才走总线)现在**返回错误**、ctx 与请求脱钩(WithoutCancel + 30min 上界)→ `503 chat_dispatch_failed`;
未接 loop 由 200 改为 `500 chat_unavailable`(与同步路径同口径,已同步改既有用例并新增钉住用例)
- G4:`chat`(WS 帧 + REST 体)新增**可选** `clientMsgId` ⇒ 幂等。
原子性两层,均非「先查再插」:① 同进程在 `o.mu` 同一临界区内完成「查认领表→查存储→预分配 id→认领」;
② 跨重启靠 `(session_id, client_msg_id)` **部分**唯一索引 + `INSERT … ON CONFLICT DO NOTHING`(命中则读回既有行)。
重复语义:REST `200 {duplicate:true, messageId}`;WS 新帧 `chat_duplicate`(owner/admin 档);重复**不落库、不起回合**。
`clientMsgId` 回填进 DTO ⇒ `message_persisted` 与读接口都可见(客户端可据此对齐本地气泡,不再靠文本相等)
- 迁移 `0013_chat_client_msg_id`(up:加列 + 部分唯一索引 `idx_chat_messages_client_msg_id`;down:先删索引再删列),
已在 `data/cyrene.db` **副本**上验 up → 判重 → down → 再 up
- 拆分:store.go 647→498 行(`log_reads.go` 抽走四个读路径);七个 SELECT 清单抽成 `messageColumns` 一处
- 复核:**全树 `go test ./... -count=1` 0 FAIL** + build/vet 绿 + linux/darwin 交叉编译绿;
四条真断言(总线饱和仍落库出帧且 `ws.chat` 零发布 / 同键两次仅一回合+并发两条也仅一回合 / 省略键逐字不变 /
迁移副本 up-down-up);**3 条变异反证**均已还原
- 越界说明(均已报备):`history.go` DTO 字段(否则「读接口可见」不可能)、`policy_store_test.go`
(0013 使「回滚最后一步 == 0012」前提失效,改为按 schema_migrations 现算)、`extra_test.go`(补 runner)
- 顺带补交 P0-A 遗漏的新测试文件 `loop/memory_writeback_test.go`
28 lines
1.7 KiB
SQL
28 lines
1.7 KiB
SQL
-- 0013_chat_client_msg_id: 客户端消息幂等键(Android 协同 §G4)。
|
|
--
|
|
-- 背景:chat 全链路此前**没有幂等** —— 同一段文本发两次就是两条用户消息、
|
|
-- 两个回合(history.insert 每次都新铸 id)。弱网/切后台下的客户端超时重发
|
|
-- 因此既留下重复气泡,也白烧一次模型调用。
|
|
--
|
|
-- client_msg_id 是**客户端生成**的去重键(例如一条乐观气泡的本地 id)。
|
|
-- 服务端据此在同一会话内只承认第一条:重复到达返回既有消息,不再落库、
|
|
-- 不再起回合。它**可选**:'' 意味着「这个客户端不带幂等键」,行为与加入本列
|
|
-- 之前逐字相同(老客户端零影响,历史行也全是 '')。
|
|
--
|
|
-- 判重由**唯一索引原子裁决**,而不是「先查再插」——后者是两个并发重发都
|
|
-- 查不到、都插入的经典竞态:
|
|
-- (session_id, client_msg_id) 上建**部分**唯一索引,只覆盖
|
|
-- client_msg_id != '' 的行。部分索引是必须的:若约束覆盖全表,所有空值行
|
|
-- 会在同一会话里互相冲突,等于禁止一个会话出现第二条消息。空值行因此
|
|
-- 完全不参与约束。
|
|
--
|
|
-- 0003 的 chat_messages 表在此已存在,故 ALTER ADD COLUMN 是非破坏性的正确
|
|
-- 一步;全新库由 internal/runtime/history.NewStore 幂等地前滚(含该列与该
|
|
-- 索引)。索引命名沿用 idx_{表}_{列} 口径,表名复数(援引 AGENTS §10 已登记
|
|
-- 的例外)。
|
|
|
|
ALTER TABLE chat_messages ADD COLUMN client_msg_id TEXT NOT NULL DEFAULT '';
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_client_msg_id
|
|
ON chat_messages (session_id, client_msg_id) WHERE client_msg_id != '';
|