Files
AskaEth 89e13cf53d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,loop): 忙时并行回合基础管线(I1′ 有条件放宽)
- turnId 由编排器生成、loop Run 复用(硬前提:否则 turn_begin 与流帧各铸一个,客户端两级分桶立即失配)
- StateSink 增带 turnId 的变体(未接线时退化为现签名,不丢状态帧)
- 上行 chat.parallel(WS chat 帧 + REST 三入口同形;省略 = 逐字不变)
- 单会话额度默认 2 / 硬上限 3 / <2 关闭;额度满或未声明 ⇒ 落回既有排队语义
- I1′:checkpoint 归属、历史配对、忙态计数三处从会话级单例升为路级
- session_idle 收紧为「所有在途路收尾且 pending 空」(仅并行时生效;单路逐字不变)
- 预留接口(形状定死、无实现无接线):tool/legfile.go 的 FileClaimer / ErrFileBusy / WithLegID
- 顺手修一个真 bug:并行路 ctx 派生自提交请求作用域,REST 同步入口一写响应就把它连坐杀死(改 context.WithoutCancel 派生)
- 23 个新用例(含 -race -count=3);变异 6 条(turnId 复用 / idle 时点 / 并行升级 / checkpoint 键 / 编排器交 id / pending_ack reason)
- 契约变更点 10 条待监工升 v0.65(本人未改 Docs/**)
2026-10-05 09:18:50 +08:00

71 lines
3.3 KiB
Go
Raw Permalink 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 §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)
}