feat(gateway): 忙时并行帧层(turn_begin + steer_ack.turnId + interrupt.turnId)

新下行帧 turn_begin:编排器每启动一路恰好一帧(单路 kind:"turn" / 并行路 "parallel"),
data{kind, messageIds, leg, activeTurns};角色门 fail-closed 仅 owner+admin,未进 frameReachesAll。
steer_ack 增帧壳 turnId(与该路 turn_begin 同源)—— 并行下没有它配不上正确的 turn.end。
上行 interrupt.turnId(仅 scope:"current" 生效):精确停某一路;目标不存在 ⇒ 定向
{type:"error",data:{code:"bad_request"}},不广播、不动任何会话;省略 turnId = v0.65 原口径(全部在途路)。
帧序按实现:session_busy / pending_drain → message_persisted(user) → turn_begin
(规范草案 §2.9 的时序图需按实现更正:messageIds 必须是已落库的权威 id,故只能在落库之后)。
测试:编排器侧 + 真 WS/JWT 端到端;变异 5 条(每路一帧 / 帧壳 turnId / 目标化打断 / 取消筛选)。
Docs 待升 v0.66(由监工登记)。
This commit is contained in:
AskaEth committed 2026-10-05 09:38:11 +08:00
1 parent e5cc9c6054
commit c0dc435770
13 files changed
+800 -45

No files matched your search

+33 -5
View File
@@ -56,6 +56,33 @@ func (o *Orchestrator) InterruptScope(sessionID string, scope InterruptScope) in
return o.interruptLocked(sessionID, o.states[sessionID], scope)
}
// InterruptTurn 只取消 `turnId` 指定的那**一路**(忙时并行规范草案 §2.3:
// `interrupt{scope:"current", turnId}`),其余在途路与待处理队列一概不受影响 ——
// 每路各有自己的取消链,因此停一路绝不牵动另一路已发起的工具(I3′)。
//
// 返回 false 表示该会话此刻没有这条路(不存在 / 已收尾 / 已被取消)。调用方据此
// 定向回一条显式错误帧,**绝不**静默按「全部在途路」处理:把「只停这一路」悄悄
// 执行成「停掉所有路」,正是这类定向能力最不该有的失败方式。
//
// 它与 InterruptScope(Current) 的差别只有「目标集」:没有目标就是每一路(现状),
// 有目标就只取消它。帧与队列口径完全一致(dropped 恒为 0、mode 如实报告当时的
// 忙碌通道、被取消的路仍在 st.legs 里收尾到自然结算)。
func (o *Orchestrator) InterruptTurn(sessionID, turnID string) bool {
if turnID == "" {
return false
}
sessionID = o.normalize(sessionID)
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[sessionID]
if o.legByTurnIDLocked(st, turnID) == nil {
return false
}
canceled := o.cancelLegsLocked(st, turnID)
o.emitState(sessionID, EventSessionInterrupt, interruptPayload(sessionID, st.mode, 0, canceled, len(st.legs)))
return true
}
// InterruptAll 取消每一个在飞会话并清空所有待处理队列。它在网关关闭时使用,
// 使没有后台回合比 hub 活得更久。它返回被丢弃的待处理回合总数。
func (o *Orchestrator) InterruptAll() int {
@@ -73,15 +100,16 @@ func (o *Orchestrator) InterruptAll() int {
// Current 只取消在途路,队列与路登记原样留下,使被取消回合的结算仍会续跑队列。
// 它返回真的被丢弃的待处理回合数(Current 恒为 0)。
//
// 多路口径(design/15 §2.5.4):无论哪个 scope,被取消的都是**当前全部在途路**
// —— 省略 turnId 的 scope 就是「作用于每一路」的意思(精确停某一路是后续切片的
// `interrupt.turnId`,本切片不做)。解除在途登记的方式与旧口径的 `st.gen++` 等价:
// 把路从 st.legs 里摘掉,被取消路的迟到结算因此找不到自己的路、成为空操作。
// 多路口径(design/15 §2.5.4):这里取消的是**当前全部在途路**(目标传空串)
// —— 省略 turnId 的 scope 就是「作用于每一路」的意思;「只停某一路」走
// InterruptTurn(同一套取消链,只是带上目标)。解除在途登记的方式与旧口径的
// `st.gen++` 等价:把路从 st.legs 里摘掉,被取消路的迟到结算因此找不到自己的
// 路、成为空操作。
func (o *Orchestrator) interruptLocked(sessionID string, st *sessionState, scope InterruptScope) int {
if st == nil || st.mode == ModeIdle {
return 0
}
canceled := o.cancelLegsLocked(st)
canceled := o.cancelLegsLocked(st, "")
if scope == InterruptScopeCurrent {
// 只取消在途路:不碰 pending、不解除路的登记(它们仍在收尾)。mode 如实
// 报告为当时的忙碌通道(回合仍在收尾、队列还在续跑),而不是 idle。