feat(gateway,loop): 忙时并行回合基础管线(I1′ 有条件放宽)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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/**)
This commit is contained in:
AskaEth committed 2026-10-05 09:18:50 +08:00
1 parent 7d2ff74cfc
commit 89e13cf53d
23 files changed
+1921 -275

No files matched your search

+11 -6
View File
@@ -186,13 +186,13 @@ func candidateSeqs(cs []steerCandidate) map[uint64]bool {
}
// pendingSnapshot 在状态锁下取该会话待处理队列的一份副本(世代不符时返回 nil:
// 那份队列已不属于本回合,例如被中断清空或被更新的顶层回合取代)。取副本而不是
// 那条路已失效,例如被打断、被取代或会话已清空)。取副本而不是
// 持锁做转录读取,是为了不让有界存储读取堵住 Submit。
func (o *Orchestrator) pendingSnapshot(sessionID string, gen uint64) []pendingMsg {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[sessionID]
if st == nil || st.gen != gen || len(st.pending) == 0 {
if o.legLocked(st, gen) == nil || len(st.pending) == 0 {
return nil
}
return append([]pendingMsg(nil), st.pending...)
@@ -202,9 +202,14 @@ func (o *Orchestrator) pendingSnapshot(sessionID string, gen uint64) []pendingMs
// 被第二个回合再答一遍)。它按 pendingMsg.seq 认领,因此即便队列在同一时刻被追加
// (只会加在队尾)或被中断清空,也绝不会误删别的消息。
//
// 返回 false 表示这次认领**没有生效**(队列里已无这些序号,或世代已被中断/更新的
// 顶层回合取代)——调用方此时必须放弃注入:否则这条消息会被本回合与那条新路径各答
// 一次。它必须在把消息交给循环**之前**调用。
// 返回 false 表示这次认领**没有生效**(队列里已无这些序号,或本路已失效
// ——被打断、被取代)。调用方此时必须放弃注入:否则这条消息会被本回合与那条
// 新路径各答一次。它必须在把消息交给循环**之前**调用。
//
// 多路口径(design/15 §2.5.2 I2):认领仍只走这一把锁与这一套序号,因此
// 「新起的第 2 路」与「在飞路的下一个 steer 边界」抢同一条消息时也只有一个赢家
// ——升级路径本就不把消息放进 pending(它直接登记为一条新路),而 steer 只能
// 认领 pending 里的条目,两者不可能同时拿走同一条。
func (o *Orchestrator) consumePending(sessionID string, gen uint64, seqs map[uint64]bool) bool {
if len(seqs) == 0 {
return false
@@ -212,7 +217,7 @@ func (o *Orchestrator) consumePending(sessionID string, gen uint64, seqs map[uin
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[sessionID]
if st == nil || st.gen != gen {
if o.legLocked(st, gen) == nil {
return false
}
kept := make([]pendingMsg, 0, len(st.pending))