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

+22 -15
View File
@@ -40,8 +40,8 @@ func voiceReplySinkFrom(ctx context.Context) VoiceReplyFunc {
return sink
}
// voiceTurn 配对一次语音回合与它的收尾回调(按代次配对,与 turnDocuments
// 同理):被打断或被新回合取代的陈旧回合在结算时不该替别人回音频。
// voiceTurn 配对一条**路**与它的语音收尾回调(按世代号配对,与 turnDocuments
// 同理):被中断 / 已失效的路在结算时不该替别人回音频。
//
// sinks 是切片,因为一个回合可能同时有**多条**语音请求在等音频:忙时入 pending
// 的语音消息可能被转向注入(design/15 §2.4,见 session_steer.go 的 consumePending)
@@ -60,37 +60,44 @@ func voiceSinksOf(sink VoiceReplyFunc) []VoiceReplyFunc {
return []VoiceReplyFunc{sink}
}
// rememberVoiceTurn 记下本回合的语音收尾回调。**每个新回合都必须调用它一次**
// rememberVoiceTurn 记下**本路**的语音收尾回调。**每条新路都必须调用它一次**
// (纯文本回合记 nil):否则一个陈旧的语音回调会被沿用到后面的文本回合上,
// 让一次文本问答无端合成一段音频。
//
// 它按路配对(design/15 §2.5.9):并行时两路各自持有自己的回调,一路结算
// 绝不会替另一路回音频。路已失效(被打断清空)时什么都不做。
func (o *Orchestrator) rememberVoiceTurn(sessionID string, gen uint64, sink VoiceReplyFunc) {
o.mu.Lock()
defer o.mu.Unlock()
if st := o.states[sessionID]; st != nil {
st.voiceTurn = voiceTurn{gen: gen, sinks: voiceSinksOf(sink)}
if leg := o.legLocked(o.states[sessionID], gen); leg != nil {
leg.voice = voiceTurn{gen: gen, sinks: voiceSinksOf(sink)}
}
}
// addVoiceSinkLocked 把一条**被转向注入**的语音请求的收尾回调追加到当前回合上。
// 必须在 o.mu 下调用;代次不符(那条请求不属于本回合)时什么都不做。
// addVoiceSinkLocked 把一条**被转向注入**的语音请求的收尾回调追加到该路(gen
// 所指的那一条)上。必须在 o.mu 下调用;该路已失效时什么都不做。
func (o *Orchestrator) addVoiceSinkLocked(st *sessionState, gen uint64, sink VoiceReplyFunc) {
if st == nil || sink == nil || st.voiceTurn.gen != gen {
if sink == nil {
return
}
st.voiceTurn.sinks = append(st.voiceTurn.sinks, sink)
leg := o.legLocked(st, gen)
if leg == nil {
return
}
leg.voice.sinks = append(leg.voice.sinks, sink)
}
// takeVoiceTurn 取走与 gen 配对的所有语音收尾回调并清空记录。第二个返回值为
// false 表示代次已陈旧(中断/取代):迟到的结算不该回音频。
// takeVoiceTurn 取走**本路**的全部语音收尾回调并清空记录。第二个返回值为 false
// 表示这条路已失效(被中断清空 / 被取代):迟到的结算不该回音频。
func (o *Orchestrator) takeVoiceTurn(sessionID string, gen uint64) ([]VoiceReplyFunc, bool) {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[sessionID]
if st == nil || st.voiceTurn.gen != gen {
leg := o.legLocked(o.states[sessionID], gen)
if leg == nil {
return nil, false
}
sinks := st.voiceTurn.sinks
st.voiceTurn = voiceTurn{}
sinks := leg.voice.sinks
leg.voice = voiceTurn{}
return sinks, true
}