# Exclusive Lock Protocol The exclusive lock protocol allows external tools to claim exclusive management of a beads database, preventing the bd daemon from interfering with their operations. ## Use Cases - **Deterministic execution systems** (e.g., VibeCoder) that need full control over database state - **CI/CD pipelines** that perform atomic issue updates without daemon interference - **Custom automation tools** that manage their own git sync workflow ## How It Works ### Lock File Format The lock file is located at `.beads/.exclusive-lock` and contains JSON: ```json { "holder": "vc-executor", "pid": 12345, "hostname": "dev-machine", "started_at": "2025-10-25T12:00:00Z", "version": "1.0.0" } ``` **Fields:** - `holder` (string, required): Name of the tool holding the lock (e.g., "vc-executor", "ci-runner") - `pid` (int, required): Process ID of the lock holder - `hostname` (string, required): Hostname where the process is running - `started_at` (RFC3339 timestamp, required): When the lock was acquired - `version` (string, optional): Version of the lock holder ### Daemon Behavior The bd daemon checks for exclusive locks at the start of each sync cycle: 1. **No lock file**: Daemon proceeds normally with sync operations 2. **Valid lock (process alive)**: Daemon skips all operations for this database 3. **Stale lock (process dead)**: Daemon removes the lock and proceeds 4. **Malformed lock**: Daemon fails safe and skips the database ### Stale Lock Detection A lock is considered stale if: - The hostname matches the current machine (case-insensitive) AND - The PID does not exist on the local system (returns ESRCH) **Important:** The daemon only removes locks when it can definitively determine the process is dead (ESRCH error). If the daemon lacks permission to signal a PID (EPERM), it treats the lock as valid and skips the database. This fail-safe approach prevents accidentally removing locks owned by other users. **Remote locks** (different hostname) are always assumed to be valid since the daemon cannot verify remote processes. When a stale lock is successfully removed, the daemon logs: `Removed stale lock (holder-name), proceeding with sync` ## Usage Examples ### Creating a Lock (Go) ```go import ( "encoding/json" "os" "path/filepath" "github.com/steveyegge/beads/internal/types" ) func acquireLock(beadsDir, holder, version string) error { lock, err := types.NewExclusiveLock(holder, version) if err != nil { return err } data, err := json.MarshalIndent(lock, "", " ") if err != nil { return err } lockPath := filepath.Join(beadsDir, ".exclusive-lock") return os.WriteFile(lockPath, data, 0644) } ``` ### Releasing a Lock (Go) ```go func releaseLock(beadsDir string) error { lockPath := filepath.Join(beadsDir, ".exclusive-lock") return os.Remove(lockPath) } ``` ### Creating a Lock (Shell) ```bash #!/bin/bash BEADS_DIR=".beads" LOCK_FILE="$BEADS_DIR/.exclusive-lock" # Create lock cat > "$LOCK_FILE" <