fix(gateway): 回合围栏堵住「中断后立刻发新消息被旧快照吞掉」的时序竞态(13.4.5 最新性)
- 根因(已确定性复现,非测试问题):中断把会话立即置 idle ⇒ 紧随的新消息被当成空闲回合**立即开跑**, 而被取消回合的快照收尾在**它自己的 goroutine** 里、直到 Run 返回前才落定 ⇒ 新回合可能读到上一回合 中途留下的**可恢复**快照(stop_reason=tool_calls)并把调用方刚传进来的消息**整段替换** (现场证据:DB 里 web:1(step=1,tool_calls) 与 web:2(step=2,stop) 并存,LoadLatest 选中 web:1; 把收尾窗口拉长到 300ms ⇒ 旧码 3/3 必红) - 修法:新增 session_turn_fence.go —— 把「同一会话最多一个在途回合」从约定变成**可等待的围栏**: 新消息照旧被立即接受、Submit 立即返回(不阻塞硬约束不变),但其**回合启动**等上一回合真正返回 (那时被放弃的快照已落定);等待同时盯本回合 ctx,不收敛的上一回合不会永久挂住会话 - 复核:整包 `go test ./internal/services/gateway/ -count=1` 绿(隔离副本 0/12);`-shuffle=on` ×3 绿; **全树 `go test ./... -count=1` = 58 包 ok / 0 FAIL**;变异两处(HEAD 原码跑新用例 3/3 红、拿掉等待即红)已还原; 交叉验证:单独打回 loop 的「只有 tool_calls 可恢复」改动后,新用例仍 3/3 红 ⇒ 两侧修法互补 - 顺带:A2 的 `ws_route.go` 一行(定向回 bad_request 需要发送者的 conn;hub 级无按连接寻址入口) - 遗留(登记):loop 侧 `checkpoint.go` 的恢复会整段替换调用方消息 ⇒ 建议加一致性判据(checkpoint 窗口 最后一条用户消息 ≠ 调用方最新消息则不恢复);围栏未覆盖 orchestrator==nil 的 legacy 直连路径
This commit is contained in:
1 parent
5494c31474
commit
19f296a2ab
2 files changed
+91
-6
No files matched your search
@@ -48,9 +48,11 @@ func (c *wsConn) route(in wsInbound) {
|
||||
// 13.4.5:忙时入 pending、续接更新的旧上下文),不经 bus。
|
||||
c.submitChat(rawData(in.Data))
|
||||
case "interrupt":
|
||||
// design/15 §3: 用户说「停下/算了」→ 取消在飞回合并清空该会话的
|
||||
// 待处理队列,使下一条消息从干净状态开始。
|
||||
c.h.handleInterrupt(in.Data)
|
||||
// design/15 §3: 用户说「停下/算了」→ 取消在飞回合;待处理队列按
|
||||
// data.scope 决定去留(协同项 A2:省略/"all" = 连队列一起清,
|
||||
// "current" = 只取消当前回合并保留队列)。非法 scope 由连接级
|
||||
// 处理函数定向回 bad_request,因此这里调连接自己的方法。
|
||||
c.handleInterrupt(in.Data)
|
||||
case "voice_input":
|
||||
// 语音经注入的 STT→chat→TTS 管线处理。没有 voice service 时回一个
|
||||
// 明确的错误,而不是把帧丢掉。音频回复作为 voice_output 事件发在
|
||||
|
||||
Reference in new issue
Block a user