fix(gateway,loop,tool): 解耦「工具互斥身份」与「checkpoint 归属」(修普通回合不受保护)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

缺口:「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:
AskaEth committed 2026-10-05 09:53:08 +08:00
1 parent d64b26d499
commit ef3dcb699b
9 files changed
+673 -53

No files matched your search

+4 -4
View File
@@ -151,8 +151,8 @@ func NewFileClaimer() FileClaimer {
// 返回带文件状态的 ErrFileBusy(直接回灌给模型)。它**绝不等待**:判忙即返回,
// 由模型稍后重试(主人裁决 3 的「不静默、不排队死等」)。
//
// 空 legID 或空目标是空操作:单路 / 子代理 / 后台思考(没有并发路)不登记也不
// 检查,行为与加入本特性之前逐字相同(兼容硬要求)。
// 空 legID 或空目标是空操作:子代理 / 后台思考 / 未接编排器的调用方(ctx 上没有
// 互斥身份)不登记也不检查,行为与加入本特性之前逐字相同(兼容硬要求)。
//
// 同一路重复 Claim 同一目标幂等;一路自己先读后写同一文件不会被自己挡住
// (否则一条路自己的多步工具调用会互相锁死)。
@@ -318,8 +318,8 @@ func safeFileTargets(log *slog.Logger, name string, t sdk.Tool, args any) (out [
// 在执行后 defer 调用它:成功 / 报错 / 超时 / panic 都要释放,否则一个中途报错
// 的路会把文件永久锁住)。
//
// 返回 nil 释放函数表示本次调用不参与互斥:未装占用表、未挂路标识(单路 /
// 子代理 / 未接线,兼容硬要求)、或工具没有声明目标文件。
// 返回 nil 释放函数表示本次调用不参与互斥:未装占用表、ctx 上没有互斥身份
// (子代理 / 后台思考 / 未接编排器,兼容硬要求)、或工具没有声明目标文件。
//
// 被另一条路占住时**不等待、不排队**,直接把带文件状态的 ErrFileBusy 交给模型
// (主人裁决 3)。注意它**不包成 ToolError**:占用判定发生在执行之前,调用根本