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
@@ -57,8 +57,11 @@ func loopTurn(ctx context.Context) string {
|
||||
}
|
||||
|
||||
// parallelLegKey 是「本 Run 驱动的是某条并发路(leg)」的 context key。
|
||||
// 它只影响 checkpoint 归属(design/15 §2.5.9),不参与身份判定,也不改变
|
||||
// 循环的任何行为。
|
||||
// 它**只**影响 checkpoint 归属(design/15 §2.5.9),不参与身份判定,也不改变
|
||||
// 循环的任何行为。工具层的文件级互斥走**另一条独立通道**(toolLegIDKey,见
|
||||
// 本文件下半部分)—— 两条通道必须分开:这条只有真并行路才有,而互斥身份
|
||||
// 每条在途路都有。焊在一起会把普通回合的 checkpoint 键从 `sess` 改成
|
||||
// `sess#turn-x`,连带改变崩溃恢复语义(checkpoint.go 的 checkpointAgentID)。
|
||||
type parallelLegKey struct{}
|
||||
|
||||
// WithParallelLeg 把本回合所属**并发路**的标识挂到 ctx 上(design/15 §2.5.9):
|
||||
@@ -77,6 +80,9 @@ func WithParallelLeg(ctx context.Context, legID string) context.Context {
|
||||
|
||||
// ParallelLegFrom 返回 ctx 上挂的并发路标识;未挂载时返回 ""。它是
|
||||
// WithParallelLeg 的读侧,供 checkpoint 归属键与网关侧断言使用。
|
||||
//
|
||||
// ⚠️ 它**只**服务 checkpoint 归属,绝不是工具互斥身份 —— 工具侧要读
|
||||
// ToolLegIDFrom(那条通道每条在途路都有)。
|
||||
func ParallelLegFrom(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
@@ -84,3 +90,42 @@ func ParallelLegFrom(ctx context.Context) string {
|
||||
s, _ := ctx.Value(parallelLegKey{}).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// toolLegIDKey 是「本 Run 属于哪一条**在途路**」的 context key,**只**服务
|
||||
// 工具层的文件级互斥(design/15 §2.5.7、主人 2026-10-05 裁决 3)。
|
||||
//
|
||||
// 它与 parallelLegKey 刻意分成两条通道,因为二者的**受众与取舍完全不同**:
|
||||
// - 这条:每条在途路都挂(普通回合 / 并行路 / 队首续跑路),回答「此刻有没有
|
||||
// **另一条路**正在动这个文件」;它不能改变 checkpoint 归属,否则「1 普通回合
|
||||
// + 1 并行路」的会话里普通回合的快照键会被升成 `sess#turn-x`,崩溃恢复语义
|
||||
// 随之改变(硬要求:恢复语义零变化);
|
||||
// - 那条:只有真并行路才有,回答「这条路的 checkpoint 该写哪个键」。
|
||||
//
|
||||
// 把两者焊成一个(复用 ParallelLegFrom)是曾经的真实缺口:普通回合既不登记也不
|
||||
// 被检查,它与并行路同文件写仍可能并发。
|
||||
type toolLegIDKey struct{}
|
||||
|
||||
// WithToolLegID 把本路的互斥身份挂到 ctx 上(design/15 §2.5.7)。身份由**网关/
|
||||
// 编排器**在**每一条在途路**上挂载,取值就是该路的 turnID(编排器生成、唯一,
|
||||
// 普通回合也有)。它只回答「这条工具调用属于哪条在途路」,与 actor 元数据 /
|
||||
// 策略引擎无关(项目红线:身份识别绝不走这条通道),也不影响 checkpoint 归属。
|
||||
//
|
||||
// 空 legID 是空操作:子代理、后台思考、未接编排器的调用方不挂该键 ⇒ 工具层读到
|
||||
// 空串 ⇒ 完全不做互斥,行为与加入本特性之前逐字相同(兼容硬要求)。
|
||||
func WithToolLegID(ctx context.Context, legID string) context.Context {
|
||||
if ctx == nil || legID == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, toolLegIDKey{}, legID)
|
||||
}
|
||||
|
||||
// ToolLegIDFrom 返回 ctx 上挂的互斥身份;未挂载时返回 ""(⇒ 不登记也不检查)。
|
||||
// 它是 WithToolLegID 的读侧,只被循环的工具执行点读一次(tools.go 的 invokeOne
|
||||
// 把它桥接成 tool.WithLegID —— 工具侧凭 tool.LegIDFrom 取到)。
|
||||
func ToolLegIDFrom(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
s, _ := ctx.Value(toolLegIDKey{}).(string)
|
||||
return s
|
||||
}
|
||||
Reference in new issue
Block a user