- 原状:只有「被取消」的回合把 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 里引用旧函数名的陈旧注释
69 lines
3.1 KiB
Go
69 lines
3.1 KiB
Go
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)
|
||
}
|