Files
Cyrene_v2/internal/services/gateway/session_turn_fence.go
T
AskaEth 88d7dd0026
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(loop): 所有非崩溃退出一律废弃可恢复快照(主人 2026-10-04 裁决「修」)
- 原状:只有「被取消」的回合把 checkpoint 落定为不可恢复;**模型/工具报错、预算耗尽**等未落定退出
  同样留下可恢复快照 ⇒ 下一条消息被旧窗口整段顶掉(用户看到自己刚发的消息像丢了)
- 修法:收尾 defer 改为「**回合未落定就废弃快照**」(不再只看 ctx 是否取消),新增 StopReasonAborted;
  checkpoint.resumableStopReason 收紧为「**只有 `tool_calls` 可恢复**」(空 stop reason 不再算);
  终态快照改用 context.WithoutCancel 写(修「取消擦肩最终答案」时终态写失败、中途快照留任最新)
- 语义边界(写进用例注释):**只有崩溃**(进程猝死、函数不返回)才允许恢复;控制流一旦回到调用方
  就不得留可恢复态。原 resume_test 的用例改名为**崩溃恢复**用例并保留断言(含「新消息不被顶掉」的反面断言在新用例里)
- 复核:build/vet 绿 + loop 包测试与 `-race` 全绿;新增 6 条(模型报错/工具失败+模型报错/预算耗尽/
  空 stop reason/取消擦肩最终答案)+ 端到端 TestInterruptThenChatKeepsLatestContextEndToEnd 绿;
  **4 条变异反证**(含把废弃快照写回 tool_calls → 精确复现真实劫持现场)均已还原
- 附:修 session_turn_fence.go 里引用旧函数名的陈旧注释
2026-10-04 14:17:58 +08:00

69 lines
3.1 KiB
Go
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.
package gateway
import (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件把「同一会话最多一个在途回合」从一句约定变成可等待的进程内围栏
// (design/15 §3 与裁决 13.4.5 的组合要求)。
//
// 中断必须**立即**把会话交回空闲并接受新消息(design/15 §3),但被取消的
// 回合仍在收尾:loop 直到它自己的 Run 返回之前,才把被放弃回合的快照落定
// 为不可恢复态(`loop/run.go` 的 `abandonCheckpoint`;2026-10-04 起**所有非崩溃退出**
// 都走它,不再只限取消)。若新回合此刻
// 就开跑,它可能在该落定之前读到那份**仍可恢复**的旧快照并续接它 —— 调用方
// 刚发出的那条消息会被整段顶掉,上下文既不最新也不完整(裁决 13.4.5 的
// 最新性 / 完整性)。
//
// 围栏关掉这条竞态窗口,且不阻塞调用方:新消息照样被立即接受、Submit 立即
// 返回(非阻塞是硬约束),只是它的回合要等上一回合真正返回之后才开始。
// turnFence 是一次回合的启动围栏。wait 是上一回合的收尾信号(nil 表示没有
// 在途的上一回合,无需等待);done 是本回合的收尾信号,在本回合返回时关闭,
// 从而放行等它的下一回合。
type turnFence struct {
wait <-chan struct{}
done chan struct{}
}
// newTurnFence 以会话当前的收尾信号为先驱建一道新围栏;prev 为 nil 时
// wait 也为 nil(本回合可立即开始)。
func newTurnFence(prev chan struct{}) turnFence {
var wait <-chan struct{}
if prev != nil {
wait = prev
}
return turnFence{wait: wait, done: make(chan struct{})}
}
// release 关闭本回合的收尾信号,放行正在等它的下一回合。每个回合恰好调用
// 一次(它在本回合的唯一出口上被 defer)。
func (f turnFence) release() { close(f.done) }
// awaitTurnFence 等到上一回合真正返回,使被取消回合的 checkpoint 收尾先
// 落定。等待同时盯着本回合自己的 ctx,因此一个迟迟不收敛的上一回合不会把
// 本回合永远挂住:用户再按一次「停下」即可让它立即结算。
//
// 它只负责**等待**,绝不因此跳过本回合的驱动:本回合若在等到之前就已被
// 取消(连续两次中断 / 网关关闭),驱动本身会以 ctx 错误立即返回。取消因此
// 始终被 runner 观察到 —— 被取消的回合绝不会在无人知晓的情况下消失。
func awaitTurnFence(ctx context.Context, f turnFence) {
if f.wait == nil {
return
}
select {
case <-f.wait:
case <-ctx.Done():
}
}
// runTurnFenced 在上一回合收尾之后驱动本回合(编排器其余部分仍只认识
// TurnRunner,围栏只在启动处生效)。
func (o *Orchestrator) runTurnFenced(ctx context.Context, sessionID string, messages []model.Message, env loop.SessionEnv, fence turnFence) (*loop.Result, error) {
awaitTurnFence(ctx, fence)
return o.run(ctx, sessionID, messages, env)
}