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

@@ -269,6 +269,10 @@ func TestPersistDocumentStatusIsNotSilent(t *testing.T) {
writer := &fakeDocumentWriter{fakeHistory: &fakeHistory{}}
o := NewOrchestrator(OrchestratorOptions{Run: fixedRunner(res), Log: slog.New(cap), History: writer})
o.SetMode("web", ModeIdle)
// 文档结论回写现在是**按路配对**的(design/15 §2.5.9:并行时两路各写自己
// 那几行),因此白盒构造必须把那条路也登记进去 —— 真实路径上
// rememberTurnDocuments 紧跟 beginLegLocked 调用,恒有路在。
registerTestLeg(o, "web", 1)
o.rememberTurnDocuments("web", 1, []history.Message{{ID: "m1", SessionID: "web", Role: history.RoleUser, Content: "坏列", Document: "{not json"}})
o.persistDocumentStatus("web", 1, res)
if !cap.has(`document status merge`, "") {
@@ -280,6 +284,23 @@ func TestPersistDocumentStatusIsNotSilent(t *testing.T) {
})
}
// registerTestLeg 直接把一条在途路登记进会话状态(不存在时顺带建会话状态),
// 使白盒用例能在不驱动真实回合的前提下断言**按路配对**的收尾路径(文档结论
// 回写、语音回调)。它只服务测试:生产路径上的路一律由 beginLegLocked 登记。
func registerTestLeg(o *Orchestrator, sessionID string, gen uint64) {
o.mu.Lock()
defer o.mu.Unlock()
st := o.states[sessionID]
if st == nil {
st = &sessionState{mode: ModeIdle}
o.states[sessionID] = st
}
if st.legs == nil {
st.legs = make(map[uint64]*turnLeg, 1)
}
st.legs[gen] = &turnLeg{gen: gen, leg: 1, fence: newTurnFence(nil)}
}
// TestHistoryEndpointReturnsDocumentStatusArray 钉住线上契约(§4.5):历史接口
// 按 `documents` 数组回传引用与结论,且没有附件的消息**省略**该字段(省略 =
// 没有附件,与引入文档之前逐字一致)。