// pathlock.go — Path-based read/write lock coordination for tool execution. // // All foreground tool calls acquire a lock before execution based on the // tool's declared scope and the file path from its arguments: // // - scope "read": no lock (reads never conflict) // - scope "write": exclusive lock on the file path (serializes writes to same path) // - scope "global": exclusive global lock (serializes with everything) // // This ensures correctness across all agents in a session (and eventually // across sessions sharing the same toolsrv), without the agents needing // to coordinate explicitly. package server import "sync" // pathLockTable provides path-granularity serialization for tool execution. type pathLockTable struct { mu sync.Mutex // protects the paths map global sync.RWMutex // global barrier: write-locked by "global" scope tools paths map[string]*sync.Mutex // per-path exclusive locks for "write" scope tools } // acquire locks the appropriate resources for a tool call and returns a // release function. The caller MUST call release when execution completes. // // Semantics: // - "read": returns immediately (no-op release) // - "write": holds global.RLock + exclusive path lock // - "global" (or empty): holds global.Lock (exclusive — blocks all writes and other globals) func (t *pathLockTable) acquire(scope, path string) func() { switch scope { case "read": return func() {} // reads never conflict case "write": if path == "" { // No path extractable — fall through to global barrier. t.global.Lock() return func() { t.global.Unlock() } } // Shared global lock (allows concurrent writes to different paths, // blocks while a global-scope tool is running). t.global.RLock() pl := t.pathLock(path) pl.Lock() return func() { pl.Unlock() t.global.RUnlock() } default: // "global", "", or unknown t.global.Lock() return func() { t.global.Unlock() } } } // pathLock returns (or creates) the mutex for a given path. func (t *pathLockTable) pathLock(path string) *sync.Mutex { t.mu.Lock() defer t.mu.Unlock() if t.paths == nil { t.paths = make(map[string]*sync.Mutex) } l, ok := t.paths[path] if !ok { l = &sync.Mutex{} t.paths[path] = l } return l }