feat(tool): 写类工具文件级互斥(读共享 / 写独占)
新增 tool/fileclaims.go:FileTargeter 可选接口,由工具声明本次调用要碰的文件 (动态声明、不猜正文 —— 同一工具的 path 在读/写语义不同)。 语义:读共享 / 写独占;两个读允许并行(主人「优先性能」口径);未知操作/裸路径按写处理(保守)。 占用键含操作类别(read/write,NUL 分隔),路径 Abs+Clean+Windows 大小写折叠;随最后占用者离场删键(有界)。 Dispatcher 默认装配进程内占用表(无需 cmd 侧接线);Invoke 登记 + defer 释放(成功/报错/超时/panic 四条路径)。 loop/tools.go 唯一接线:tool.WithLegID(ParallelLegFrom(ctx)) 桥接网关路标识。 判忙不阻塞、不排队死等;显式文案回灌(含文件名与状态)让 LLM 稍后重试。 测试 3 个新文件(占用表语义 / 接线确定性并发 / loop 桥与端到端);变异 5 条,另揪出并修一个 真隐患(空串既当「无人占用」哨兵又是合法路标识 ⇒ 改 (leg,busy) 返回)。 诚实边界:①「1 普通回合 + 1 并行路」时普通回合不受保护(网关只给 parallel 路盖 legID; 修法须与 checkpoint 归属一起裁决);② 目前无真实工具实现 FileTargeter ⇒ 机制就绪但占用表恒空, 真实保护待下一片给 file 等工具加声明。
This commit is contained in:
1 parent
c0dc435770
commit
354b08a45f
7 files changed
+1367
-12
No files matched your search
@@ -6,10 +6,13 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 本文件是**接口预留**(design/15 §2.5.7 与主人 2026-10-05 裁决 3),本切片
|
||||
// **不实现也不接线**:忙时并行(design/15 §2.5)落地后,同一会话可以有多条在途
|
||||
// 回合,而写类工具(执行命令 / 修改文件 / 对外发消息 / 改配置,即 design/11 §4
|
||||
// 的敏感清单)在两路之间会真正争抢同一个副作用目标。
|
||||
// 本文件钉住**文件级**互斥(design/15 §2.5.7 与主人 2026-10-05 裁决 3)的对外
|
||||
// 形状:通道(WithLegID / LegIDFrom)、接口(FileClaimer)与错误(ErrFileBusy /
|
||||
// FileBusyError / FileBusyStatusError)。**实现与接线在 fileclaims.go**(占用表、
|
||||
// 目标声明接口 FileTargeter、Dispatcher 的登记/释放)。忙时并行(design/15 §2.5)
|
||||
// 落地后同一会话可以有多条在途回合,而写类工具(执行命令 / 修改文件 / 对外发
|
||||
// 消息 / 改配置,即 design/11 §4 的敏感清单)在两路之间会真正争抢同一个副作用
|
||||
// 目标。
|
||||
//
|
||||
// 主人口径(裁决 3)是**文件级**互斥,而不是会话级串行:
|
||||
// - 工具先检测目标文件此刻是否正被**别的在途路**操作;
|
||||
@@ -17,11 +20,6 @@ import (
|
||||
// LLM,并提示稍后再试(**不静默、不排队死等**);
|
||||
// - **不是同一个文件** ⇒ 允许并行操作。
|
||||
//
|
||||
// 为什么只留接口:写类工具的「目标文件」需要逐工具声明(命令、补丁、配置写入
|
||||
// 各自的目标语义不同),且占用登记要挂在**路**的生命周期上(路一收尾就必须释放,
|
||||
// 否则一个中途报错的路会把那个文件永久锁住)。这两件都属于后续切片,本切片只把
|
||||
// 通道与判定口径钉死,使实现方不必再改工具接口。
|
||||
//
|
||||
// 身份与权限**不走这条通道**:它只回答「此刻有没有另一路正在动这个文件」,与
|
||||
// actor 元数据 / 策略引擎无关(项目红线)。
|
||||
|
||||
@@ -39,8 +37,22 @@ func FileBusyError(target string) error {
|
||||
return fmt.Errorf("%w(目标:%s)", ErrFileBusy, target)
|
||||
}
|
||||
|
||||
// FileBusyStatusError 在 FileBusyError 之上补一句**文件状态**(该文件此刻被
|
||||
// 另一路读还是写、本次想做的操作因此做不了),使回灌给 LLM 的结果说清「为什么
|
||||
// 现在动不了它、该等什么」,而不是只丢一个「忙」。status 为空时退化为
|
||||
// FileBusyError(形状不变)。
|
||||
//
|
||||
// 与 target 一样,status 只描述**状态**:绝不带另一条路的身份、正文或任何文件
|
||||
// 内容(红线:不泄露另一路的内容)。
|
||||
func FileBusyStatusError(target, status string) error {
|
||||
if status == "" {
|
||||
return FileBusyError(target)
|
||||
}
|
||||
return fmt.Errorf("%w(目标:%s;%s)", ErrFileBusy, target, status)
|
||||
}
|
||||
|
||||
// FileClaimer 是按**文件级**粒度登记在途操作的接口(主人裁决 3)。实现方
|
||||
// (后续切片)负责:
|
||||
// (进程内实现见 fileclaims.go 的 NewFileClaimer)负责:
|
||||
// - 以规范化后的目标为键,记录「哪一条路正在操作它」;
|
||||
// - Claim 在目标已被**另一条路**占用时返回 ErrFileBusy(同一路重复 Claim
|
||||
// 同一目标必须是幂等的,否则一条路自己的多步工具调用会互相挡);
|
||||
@@ -48,6 +60,9 @@ func FileBusyError(target string) error {
|
||||
// 占用者时是空操作。
|
||||
//
|
||||
// 实现必须是**有界的**:占用表随路收尾清空,绝不随会话长度增长。
|
||||
//
|
||||
// target 的拼法由调用方与本包的 fileClaimKey 统一给出(操作类别 + 规范化路径);
|
||||
// 直接传裸路径的调用方按「写」处理(见 parseFileClaimKey)。
|
||||
type FileClaimer interface {
|
||||
Claim(ctx context.Context, legID, target string) error
|
||||
Release(ctx context.Context, legID, target string)
|
||||
|
||||
Reference in new issue
Block a user