fix(loop): 所有非崩溃退出一律废弃可恢复快照(主人 2026-10-04 裁决「修」)
- 原状:只有「被取消」的回合把 checkpoint 落定为不可恢复;**模型/工具报错、预算耗尽**等未落定退出 同样留下可恢复快照 ⇒ 下一条消息被旧窗口整段顶掉(用户看到自己刚发的消息像丢了) - 修法:收尾 defer 改为「**回合未落定就废弃快照**」(不再只看 ctx 是否取消),新增 StopReasonAborted; checkpoint.resumableStopReason 收紧为「**只有 `tool_calls` 可恢复**」(空 stop reason 不再算); 终态快照改用 context.WithoutCancel 写(修「取消擦肩最终答案」时终态写失败、中途快照留任最新) - 语义边界(写进用例注释):**只有崩溃**(进程猝死、函数不返回)才允许恢复;控制流一旦回到调用方 就不得留可恢复态。原 resume_test 的用例改名为**崩溃恢复**用例并保留断言(含「新消息不被顶掉」的反面断言在新用例里) - 复核:build/vet 绿 + loop 包测试与 `-race` 全绿;新增 6 条(模型报错/工具失败+模型报错/预算耗尽/ 空 stop reason/取消擦肩最终答案)+ 端到端 TestInterruptThenChatKeepsLatestContextEndToEnd 绿; **4 条变异反证**(含把废弃快照写回 tool_calls → 精确复现真实劫持现场)均已还原 - 附:修 session_turn_fence.go 里引用旧函数名的陈旧注释
This commit is contained in:
1 parent
5c28ace70d
commit
88d7dd0026
5 files changed
+448
-47
No files matched your search
@@ -59,12 +59,13 @@ func (l *Loop) saveCheckpoint(ctx context.Context, sessionID string, step int, m
|
||||
}
|
||||
}
|
||||
|
||||
// loadCheckpoint 从该会话最新的持久 checkpoint 恢复一个被中断的
|
||||
// run(design §1 durable execution)。它返回恢复后的消息
|
||||
// loadCheckpoint 从该会话最新的持久 checkpoint 恢复一个**崩溃遗留**的
|
||||
// run(design/04 §1 durable execution)。它返回恢复后的消息
|
||||
// 窗口、下一个要运行的循环步(checkpoint 步 + 1),以及
|
||||
// 是否找到了可恢复的 checkpoint。不存在 checkpoint,或最新的一个
|
||||
// 反映的是已完成/被阻塞的 run 时,它返回调用方的消息并置步为 1,
|
||||
// 使循环从新开始。
|
||||
// 反映的是已完成/被阻塞的 run、或任何已落定的未完成退出(取消 / 报错 /
|
||||
// 预算耗尽 / 护栏拦截)时,它返回调用方的消息并置步为 1,使循环从新开始
|
||||
// (主人 2026-10-04 裁决:只有崩溃才允许恢复;见 resumableStopReason)。
|
||||
//
|
||||
// 恢复是幂等的:checkpoint 精确保存了上一个已完成步结束时
|
||||
// 的消息窗口(assistant 工具调用及其结果已融合在内),
|
||||
@@ -97,20 +98,20 @@ func (l *Loop) loadCheckpoint(ctx context.Context, sessionID string, messages []
|
||||
}
|
||||
|
||||
// resumableStopReason 报告最后一个步以上述 stop reason 结束的
|
||||
// checkpoint 是否代表一个值得恢复的在途 run。只有
|
||||
// 工具循环中途的步才真正算「被中断」:循环已把工具结果
|
||||
// 并入窗口并即将继续,因此从下一步接上
|
||||
// 会重建上下文而不重复工作。携带最终答案、
|
||||
// 预算耗尽、安全网终止或护栏拒绝的 checkpoint 是
|
||||
// 已完成/被阻塞的 run —— 恢复它会重新生成已产出的答案
|
||||
// (重复副作用)—— 因此这些一律落到全新开始(打开
|
||||
// 真正新会话的调用方使用不同的 sessionID,本就错过
|
||||
// LoadLatest)。
|
||||
// checkpoint 是否代表一个**崩溃遗留**的在途 run。只有落在工具循环中途的步
|
||||
// 算「崩溃遗留」:循环已把工具结果并入窗口并即将继续,进程却猝死了——
|
||||
// 此时收尾 defer 根本没执行,快照是进程最后留下的证据,从下一步接上会由
|
||||
// 历史重建上下文而不重复工作(design/04 §1 durable execution)。
|
||||
//
|
||||
// 其余一律落到全新开始(主人 2026-10-04 裁决:只有崩溃才允许恢复):
|
||||
// - 携带最终答案、护栏拒绝、预算耗尽或安全网终止的 checkpoint 是
|
||||
// 已完成/被阻塞的 run——恢复它会重新生成已产出的答案(重复副作用);
|
||||
// - canceled / aborted 是**已落定**的未完成退出,下一回合必须自己组
|
||||
// 上下文(裁决 13.4.5),不得被旧窗口顶掉;
|
||||
// - 空的 stop reason 无法证明是崩溃遗留(崩溃留下的快照必带
|
||||
// tool_calls),保守起见同样不恢复。
|
||||
//
|
||||
// 打开真正新会话的调用方使用不同的 sessionID,本就错过 LoadLatest。
|
||||
func resumableStopReason(stopReason string) bool {
|
||||
switch stopReason {
|
||||
case "", agent.StopReasonToolCalls:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return stopReason == agent.StopReasonToolCalls
|
||||
}
|
||||
Reference in new issue
Block a user