fix(daemon): add cross-process locking to registry (bd-5bj)

The global daemon registry (~/.beads/registry.json) could be corrupted
when multiple daemons from different workspaces wrote simultaneously.

Changes:
- Add file locking (flock) for cross-process synchronization
- Use atomic writes (temp file + rename) to prevent partial writes
- Keep entire read-modify-write cycle under single lock
- Add FlockExclusiveBlocking and FlockUnlock to lockfile package

This prevents race conditions that caused JSON corruption like `]]`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-27 14:22:42 -08:00
parent c196a554cc
commit 1d5fd64383
4 changed files with 206 additions and 75 deletions

View File

@@ -19,3 +19,14 @@ func flockExclusive(f *os.File) error {
}
return err
}
// FlockExclusiveBlocking acquires an exclusive blocking lock on the file.
// This will wait until the lock is available.
func FlockExclusiveBlocking(f *os.File) error {
return unix.Flock(int(f.Fd()), unix.LOCK_EX)
}
// FlockUnlock releases a lock on the file.
func FlockUnlock(f *os.File) error {
return unix.Flock(int(f.Fd()), unix.LOCK_UN)
}

View File

@@ -4,7 +4,6 @@ package lockfile
import (
"errors"
"fmt"
"os"
)
@@ -13,5 +12,17 @@ var errDaemonLocked = errors.New("daemon lock already held by another process")
func flockExclusive(f *os.File) error {
// WASM doesn't support file locking
// In a WASM environment, we're typically single-process anyway
return fmt.Errorf("file locking not supported in WASM")
return nil // No-op in WASM
}
// FlockExclusiveBlocking acquires an exclusive blocking lock on the file.
// In WASM, this is a no-op since we're single-process.
func FlockExclusiveBlocking(f *os.File) error {
return nil
}
// FlockUnlock releases a lock on the file.
// In WASM, this is a no-op.
func FlockUnlock(f *os.File) error {
return nil
}

View File

@@ -36,3 +36,34 @@ func flockExclusive(f *os.File) error {
return err
}
// FlockExclusiveBlocking acquires an exclusive blocking lock on the file.
// This will wait until the lock is available.
func FlockExclusiveBlocking(f *os.File) error {
// LOCKFILE_EXCLUSIVE_LOCK only (no FAIL_IMMEDIATELY = blocking)
const flags = windows.LOCKFILE_EXCLUSIVE_LOCK
ol := &windows.Overlapped{}
return windows.LockFileEx(
windows.Handle(f.Fd()),
flags,
0,
0xFFFFFFFF,
0xFFFFFFFF,
ol,
)
}
// FlockUnlock releases a lock on the file.
func FlockUnlock(f *os.File) error {
ol := &windows.Overlapped{}
return windows.UnlockFileEx(
windows.Handle(f.Fd()),
0,
0xFFFFFFFF,
0xFFFFFFFF,
ol,
)
}