fix(gateway,loop,tool): 解耦「工具互斥身份」与「checkpoint 归属」(修普通回合不受保护)
缺口:「1 普通回合 + 1 并行路」时普通回合既不登记也不被检查 —— 因为互斥身份取自 loop.ParallelLegFrom,而网关只给 parallel=true 那一路盖它。直接给普通回合盖 WithParallelLeg 会把它的 checkpoint 键从 sess 变成 sess#turn-x(改崩溃恢复语义),故改为两条独立通道: - 工具互斥身份:loop.WithToolLegID / ToolLegIDFrom(新增),网关在 beginLegLocked 内【无条件】盖, 取值 = 本路 turnID ⇒ 普通回合 / 队首续跑路 / 并行路 三条路都覆盖 - checkpoint 归属:WithParallelLeg 一行未改,仍只有真并行路 ⇒ 恢复语义零变化 守门断言 TestCheckpointOwnerIgnoresToolLegID:普通回合 ctx 下 AgentID 必须逐字仍是 sess。 变异 3 条:互斥身份回退只认 ParallelLegFrom(判忙失效)/ 顺手把 checkpoint 改读新通道 (守门断言红,证明确实解耦)/ 普通回合不盖身份(判忙失效)。 另修 race:-race -count=2 三包全绿;诚实边界含一个既有竞态(同一路并发登记共享,建议后续小切片)。
This commit is contained in:
1 parent
d64b26d499
commit
ef3dcb699b
9 files changed
+673
-53
No files matched your search
@@ -22,8 +22,10 @@ import (
|
||||
// kind:"turn"、并行路 kind:"parallel",两条路径都发。`steer_ack` 的帧壳
|
||||
// turnId 由 session_steer.go 的转向游标带出,与这里**取值同源**(leg.turnID)。
|
||||
//
|
||||
// 它**不做**(留给后续切片):写类工具的文件级互斥、第二路的进行态 system 摘要。
|
||||
// 预留点见 turnLeg 的字段。
|
||||
// 它**不做**(留给后续切片):第二路的进行态 system 摘要。写类工具的文件级互斥
|
||||
// 在**两个地方**落地:互斥**身份**由 beginLegLocked 对每一条在途路盖
|
||||
// (loop.WithToolLegID,取值 = 本路 turnID),登记 / 判忙 / 释放由工具侧
|
||||
// (internal/runtime/tool/fileclaims.go)在执行前后来做。
|
||||
|
||||
// 并发额度的取值口径(design/15 §2.5.7)。
|
||||
const (
|
||||
@@ -68,7 +70,9 @@ type turnLeg struct {
|
||||
// 展示口径)。除了发帧,日志也用它。
|
||||
leg int
|
||||
// parallel 报告本路是否为**显式声明的并行路**:它决定 checkpoint 归属键
|
||||
// (单路逐字等于 sessionID,并行路为 {sessionID}#{turnID})。
|
||||
// (单路逐字等于 sessionID,并行路为 {sessionID}#{turnID})。它**不**决定
|
||||
// 工具互斥身份 —— 那条身份每一路都有(loop.WithToolLegID),否则普通回合
|
||||
// 与并行路同文件写会并发。
|
||||
parallel bool
|
||||
// cancel 派生自提交时的 ctx,是**本路独占**的取消链:任一路的注入 / 取消
|
||||
// 都不影响另一路已发起的工具(I3′)。
|
||||
@@ -178,20 +182,32 @@ func (o *Orchestrator) startParallelLocked(ctx context.Context, st *sessionState
|
||||
}
|
||||
|
||||
// beginLegLocked 在状态锁下登记一条新路(design/15 §2.5.9)。它分配该路自己的
|
||||
// 世代号、派生**独立的**取消链(I3′),把 turnID 与(并行时的)路标识盖进 runCtx,
|
||||
// 并把 runCtx 交回调用方 —— 调用方解锁后再起 goroutine。
|
||||
// 世代号、派生**独立的**取消链(I3′),把 turnID 与(并行时的)**checkpoint 归属
|
||||
// 键**、以及(每一路的)**工具互斥身份**盖进 runCtx,并把 runCtx 交回调用方 ——
|
||||
// 调用方解锁后再起 goroutine。
|
||||
//
|
||||
// turnID 由**编排器**生成并由 loop 复用(design/15 §2.5.9 的 ⚠️ 必改项):
|
||||
// `turn_begin` 帧由编排器在调用 Run 之前发出,若 Run 内部再自己生成一个,
|
||||
// 客户端按 turnId 两级分桶会立即失配。单路时该 id 仍是每回合全新的随机值,
|
||||
// 因此对客户端**无可见差异**。
|
||||
//
|
||||
// 两条 ctx 通道在这里分道(见 loop/session_ctx.go):checkpoint 归属键
|
||||
// (loop.WithParallelLeg)**只给并行路**,工具互斥身份(loop.WithToolLegID)
|
||||
// **每一路都盖**。它们刻意不共用 —— 共用会把普通回合的 checkpoint 键从
|
||||
// sessionID 升成 `{sessionID}#{turnID}`,连带改变崩溃恢复语义。
|
||||
//
|
||||
// 它同时把会话标成忙(ModeThinking),使 mode 在任一路在途时恒非空闲。
|
||||
func (o *Orchestrator) beginLegLocked(ctx context.Context, st *sessionState, turnID string, parallel bool, fence turnFence) (*turnLeg, context.Context, context.CancelFunc) {
|
||||
st.gen++
|
||||
st.legSeq++
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
runCtx = loop.WithTurnID(runCtx, turnID)
|
||||
// 工具互斥身份:**每一条在途路都盖**(普通回合 / 并行路 / 队首续跑路)。
|
||||
// 漏掉普通回合的代价是真实的:「1 普通回合 + 1 并行路」时普通回合既不登记
|
||||
// 也不被检查,它与并行路同文件写仍会并发(design/15 §2.5.7 的文件级互斥
|
||||
// 因此形同虚设)。取值就是本路的 turnID:路内唯一且稳定,于是同一条路自己
|
||||
// 的多次工具调用不会互相判忙,而两条不同的路一定取到不同的值。
|
||||
runCtx = loop.WithToolLegID(runCtx, turnID)
|
||||
leg := &turnLeg{turnID: turnID, gen: st.gen, leg: st.legSeq, parallel: parallel, cancel: cancel, fence: fence}
|
||||
if parallel {
|
||||
runCtx = loop.WithParallelLeg(runCtx, turnID)
|
||||
@@ -305,6 +321,10 @@ func userMessageIDs(msgs []history.Message) []string {
|
||||
// **服务端权威的**消息 id(客户端靠它把这些气泡置为「已受理·回答中」并按 turnId
|
||||
// 分桶),因此它只能在落库之后发。
|
||||
//
|
||||
// ctx 上会先挂上本路的回合标识(design/15 §2.5.9 的两段式第一段):这一刻落库的
|
||||
// 用户消息随**写入**带上 turn_id,于是它与该路的助手回复同值 —— 客户端重连后靠
|
||||
// 这一列精确配对「回复 ↔ 提问」,而流帧早就丢了。
|
||||
//
|
||||
// 这里落库的只有**此刻到达**的那些消息(已播种的转录早就持久了,重追加会重复整个
|
||||
// 对话),且只写**引用** —— 抽取结论要等回合跑起来才有,由回合结算补进同一列
|
||||
// (两段式见 session_document_status.go)。
|
||||
@@ -312,6 +332,7 @@ func userMessageIDs(msgs []history.Message) []string {
|
||||
// 单路与并行路共用这一段,于是两条启动路径的帧序完全同形 —— 客户端只需要实现一
|
||||
// 遍「turn_begin → 灰态」。
|
||||
func (o *Orchestrator) publishLegStart(ctx context.Context, sessionID string, leg *turnLeg, messages []model.Message, deviceID, mode, personaID string, activeTurns int) {
|
||||
ctx = withTurnID(ctx, leg.turnID)
|
||||
persisted := o.persistUserMessages(ctx, sessionID, messages, deviceID, mode, personaID)
|
||||
o.rememberTurnDocuments(sessionID, leg.gen, persisted)
|
||||
o.rememberVoiceTurn(sessionID, leg.gen, voiceReplySinkFrom(ctx))
|
||||
|
||||
Reference in new issue
Block a user