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

@@ -0,0 +1,46 @@
package gateway
import (
"context"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// 本文件归属会话编排器的**待处理队列**(design/15 §2.1/§2.2):忙时到达、且
// 未声明并行(或并行额度已满)的消息怎么入队、怎么落库、怎么回 ack。它从
// session_orchestrator.go 拆出,使那个文件继续只留状态机骨架(并守住
// 「函数 ≤80 行」的硬约束)。
// enqueueLocked 把一条忙时到达的用户消息放进有界待处理队列,并返回本次提交的
// 判定。它**必须在 o.mu 下调用**,并在返回前解锁:落库是慢操作,绝不能在状态
// 锁下做,因此它自己解锁后再做落库与状态帧广播(与 startParallelLocked 同一
// 约定,调用方直接 return 它的返回值)。
//
// 入队时携带该消息到达时的设备 / 模式 / 语音回调,使它的续跑回合与「它就是当前
// 回合」时逐项一致(design/16 §4.5、§5.1;协同 C1)。落库两段式的第一段在这里:
// 用户消息立刻进历史(抽取结论等回合跑起来才有,由续跑结算回写,见
// session_document_status.go)。
func (o *Orchestrator) enqueueLocked(ctx context.Context, st *sessionState, sessionID string, messages []model.Message, env loop.SessionEnv, deviceID, msgMode string, hint clientMsgHint) SubmitResult {
st.pendingSeq++
seq := st.pendingSeq
st.pending = append(st.pending, pendingMsg{
messages: messages,
env: env,
deviceID: deviceID,
mode: msgMode,
at: time.Now(),
seq: seq,
voice: voiceReplySinkFrom(ctx),
})
depth := len(st.pending)
mode := st.mode
o.mu.Unlock()
o.attachPendingDocuments(sessionID, seq, o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID))
// 声明了并行却落回排队时如实给出 reason,让客户端把气泡渲染成「排队中」而不是
// 「被拒」(design/15 §2.6);未声明时该键缺席,帧与今天逐字相同。
o.emitState(sessionID, EventPendingAck, pendingAckPayload(sessionID, mode, depth, parallelRequested(ctx)))
return SubmitResult{Accepted: true, Pending: true, QueueDepth: depth, MessageID: hint.MessageID}
}