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 §2.5 落地真并行后, // 需要这条护栏的仍只是串行链 —— 显式声明的并行路用**自己的 checkpoint 归属键** // 消除同一竞态,因此不接入这条链)。 // // 中断必须**立即**把会话交回空闲并接受新消息(design/15 §3),但被取消的 // 回合仍在收尾:loop 直到它自己的 Run 返回之前,才把被放弃回合的快照落定 // 为不可恢复态(`loop/run.go` 的 `abandonCheckpoint`;2026-10-04 起**所有非崩溃退出** // 都走它,不再只限取消)。若新回合此刻 // 就开跑,它可能在该落定之前读到那份**仍可恢复**的旧快照并续接它 —— 调用方 // 刚发出的那条消息会被整段顶掉,上下文既不最新也不完整(裁决 13.4.5 的 // 最新性 / 完整性)。 // // 围栏关掉这条竞态窗口,且不阻塞调用方:新消息照样被立即接受、Submit 立即 // 返回(非阻塞是硬约束),只是它的回合要等上一回合真正返回之后才开始。 // turnFence 是**一条路**的启动围栏。wait 是上一串行回合的收尾信号(nil 表示没有 // 在途的上一回合,无需等待 —— 并行路与续跑路都用 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) }