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

+24 -4
View File
@@ -33,12 +33,29 @@ type checkpointState struct {
StopReason string `json:"stop_reason,omitempty"`
}
// checkpointAgentID 返回本回合 checkpoint 的归属键(design/15 §2.5.9)。
//
// 单路(ctx 上未挂并发路标识)时它逐字等于 sessionID —— 与加入并发路之前
// 完全一致,归属语义不变;真并行路(ctx 上挂了 legID)时升为
// `{sessionID}#{legID}`,使 LoadLatest 按路隔离:并行的两路各写各的窗口,
// 绝不会把对方的快照当成自己的续接(并发写同一个 key 会互相覆盖)。
//
// 归属键同时用作 checkpoint 行 id 的前缀,因此两路的行在 `checkpoints` 表里
// 天然不冲突。
func checkpointAgentID(ctx context.Context, sessionID string) string {
if leg := ParallelLegFrom(ctx); leg != "" {
return sessionID + "#" + leg
}
return sessionID
}
// saveCheckpoint 在 step 处持久化一个快照。它绝不让 run 失败:
// store 为 nil 时为空操作,marshal/save 出错则记录日志。
func (l *Loop) saveCheckpoint(ctx context.Context, sessionID string, step int, messages []model.Message, stopReason string) {
if l.checkpoint == nil {
return
}
owner := checkpointAgentID(ctx, sessionID)
state := checkpointState{
Step: step,
Messages: messages,
@@ -50,8 +67,8 @@ func (l *Loop) saveCheckpoint(ctx context.Context, sessionID string, step int, m
return
}
cp := &persist.Checkpoint{
ID: fmt.Sprintf("%s:%d", sessionID, step),
AgentID: sessionID,
ID: fmt.Sprintf("%s:%d", owner, step),
AgentID: owner,
Step: step,
State: buf,
}
@@ -79,10 +96,13 @@ func (l *Loop) loadCheckpoint(ctx context.Context, sessionID string, messages []
if l.checkpoint == nil {
return messages, 1, false
}
cp, err := l.checkpoint.LoadLatest(ctx, sessionID)
// 归属键按路取(design/15 §2.5.9):单路 = sessionID(逐字不变),真并行路
// = {sessionID}#{legID},因此两路的崩溃快照互不可见,绝不会互相顶掉。
owner := checkpointAgentID(ctx, sessionID)
cp, err := l.checkpoint.LoadLatest(ctx, owner)
if err != nil {
if !errors.Is(err, persist.ErrNotFound) {
l.log.Warn("checkpoint load", "session", sessionID, "error", err)
l.log.Warn("checkpoint load", "session", sessionID, "agent", owner, "error", err)
}
return messages, 1, false
}