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:
1 parent
e5cc9c6054
commit
c0dc435770
13 files changed
+800
-45
No files matched your search
@@ -143,7 +143,7 @@ type sessionState struct {
|
||||
// 单路时它至多一条,且其行为与引入多路之前逐字相同;len(legs) 就是
|
||||
// 忙态帧里的 activeTurns,也是「所有路都收尾了吗」的判据。
|
||||
legs map[uint64]*turnLeg
|
||||
// legSeq 是路序号(1 基)的分配源,供后续切片的 `turn_begin.data.leg` 使用。
|
||||
// legSeq 是路序号(1 基)的分配源,即 `turn_begin.data.leg`。
|
||||
legSeq int
|
||||
pending []pendingMsg
|
||||
tail []model.Message // 用于上下文续跑的有界对话尾部
|
||||
@@ -313,20 +313,21 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
turn := o.seedTurnLocked(ctx, st, sessionID, messages)
|
||||
leg, runCtx, cancel := o.beginLegLocked(ctx, st, newTurnID(), false, newTurnFence(st.fence.done))
|
||||
st.fence = leg.fence
|
||||
// 本次启动后的在途路数:`turn_begin.activeTurns` 与忙态帧**同源**(正在收尾的路
|
||||
// 也计入),因此它只能在锁内读一次 —— 解锁之后再读数会随另一路的收尾漂移。
|
||||
activeTurns := len(st.legs)
|
||||
o.mu.Unlock()
|
||||
|
||||
// 本回合的转向源(design/15 §2.4):loop 在**回合内边界**回调它重读转录,把忙时
|
||||
// 新到、已入 pending 的用户消息并入**同一回合**的下一次模型调用(I1:绝不新开
|
||||
// 第二回合)。它挂在 runCtx 上,因此只作用于本回合;回合结算或被打断后随 ctx 消亡。
|
||||
runCtx = loop.WithSteerSource(runCtx, o.beginSteer(sessionID, leg.gen))
|
||||
// 它同时带上本路的 turnId,使 `steer_ack` 的帧壳能指明「被哪一路吃进」。
|
||||
runCtx = loop.WithSteerSource(runCtx, o.beginSteer(sessionID, leg.gen, leg.turnID))
|
||||
|
||||
o.emitState(sessionID, EventSessionBusy, busyPayload(sessionID, ModeThinking, 1))
|
||||
// 只有此刻到达的消息会被持久化(已播种的转录已持久,重追加会重复整个对话),
|
||||
// 且落库只写**引用** —— 抽取还发生在后面,结论由回合结算时补进同一列
|
||||
// (两段式见 session_document_status.go)。
|
||||
o.rememberTurnDocuments(sessionID, leg.gen, o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID))
|
||||
// 语音收尾回调同样按**路**配对(协同 C1,见 session_voice.go)。
|
||||
o.rememberVoiceTurn(sessionID, leg.gen, voiceReplySinkFrom(ctx))
|
||||
o.emitState(sessionID, EventSessionBusy, busyPayload(sessionID, ModeThinking, activeTurns))
|
||||
// 落库 → 按路配对 → 下发 `turn_begin`:与并行路的启动共用同一段,两条启动
|
||||
// 路径的帧序因此同形(见 publishLegStart)。
|
||||
o.publishLegStart(ctx, sessionID, leg, messages, deviceID, msgMode, env.PersonaID, activeTurns)
|
||||
|
||||
if sync {
|
||||
return o.runImmediateSync(runCtx, sessionID, leg, turn, env, deviceID, msgMode, a.hint.MessageID)
|
||||
@@ -410,7 +411,7 @@ func (o *Orchestrator) completeTurn(sessionID string, leg *turnLeg, res *loop.Re
|
||||
// 任何在途路:围栏只负责放行它后面的那一个。
|
||||
runLeg, runCtx, cancel := o.beginLegLocked(context.Background(), st, newTurnID(), false, newTurnFence(nil))
|
||||
st.fence = runLeg.fence
|
||||
runCtx = loop.WithSteerSource(runCtx, o.beginSteer(sessionID, runLeg.gen))
|
||||
runCtx = loop.WithSteerSource(runCtx, o.beginSteer(sessionID, runLeg.gen, runLeg.turnID))
|
||||
// 续跑的这一轮同样有「回合前已落库的用户消息」,因此把它们的文档状态
|
||||
// 记到**这一路**上(design/24 §6.1);语音回调同理(协同 C1:忙时排队
|
||||
// 不许把音频丢掉)。
|
||||
@@ -423,6 +424,10 @@ func (o *Orchestrator) completeTurn(sessionID string, leg *turnLeg, res *loop.Re
|
||||
o.mu.Unlock()
|
||||
|
||||
o.emitState(sessionID, EventPendingDrain, map[string]any{"sessionId": sessionID, "mode": ModeThinking, "queueDepth": depth, "activeTurns": activeTurns})
|
||||
// 续跑也是一条**新路**,因此同样发一次 turn_begin(kind:"turn"):它的
|
||||
// 归属消息就是刚从队首弹出的那条(它在入队时就已落库,id 取自
|
||||
// next.persisted)——没有这一帧,客户端就不知道为什么气泡突然开始回答了。
|
||||
o.emitTurnBegin(sessionID, runLeg, userMessageIDs(next.persisted), activeTurns)
|
||||
go o.goTurn(sessionID, runLeg, resume, env, runCtx, cancel, next.deviceID, next.mode)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user