Update all documentation to use the new subcommand syntax: - `bd daemon --start` → `bd daemon start` - `bd daemon --stop` → `bd daemon stop` - `bd daemon --status` → `bd daemon status` - `bd daemon --health` → `bd daemon status --all` - `--global=false` → `--local` The old flag syntax is deprecated but still works with warnings. Closes: bd-734vd Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.8 KiB
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:
{
"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 holderhostname(string, required): Hostname where the process is runningstarted_at(RFC3339 timestamp, required): When the lock was acquiredversion(string, optional): Version of the lock holder
Daemon Behavior
The bd daemon checks for exclusive locks at the start of each sync cycle:
- No lock file: Daemon proceeds normally with sync operations
- Valid lock (process alive): Daemon skips all operations for this database
- Stale lock (process dead): Daemon removes the lock and proceeds
- 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)
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)
func releaseLock(beadsDir string) error {
lockPath := filepath.Join(beadsDir, ".exclusive-lock")
return os.Remove(lockPath)
}
Creating a Lock (Shell)
#!/bin/bash
BEADS_DIR=".beads"
LOCK_FILE="$BEADS_DIR/.exclusive-lock"
# Create lock
cat > "$LOCK_FILE" <<EOF
{
"holder": "my-tool",
"pid": $$,
"hostname": "$(hostname)",
"started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"version": "1.0.0"
}
EOF
# Do work...
bd create "My issue" -p 1
bd update bd-42 --status in_progress
# Release lock
rm "$LOCK_FILE"
Recommended Pattern
Always use cleanup handlers to ensure locks are released:
func main() {
beadsDir := ".beads"
// Acquire lock
if err := acquireLock(beadsDir, "my-tool", "1.0.0"); err != nil {
log.Fatal(err)
}
// Ensure lock is released on exit
defer func() {
if err := releaseLock(beadsDir); err != nil {
log.Printf("Warning: failed to release lock: %v", err)
}
}()
// Do work with beads database...
}
Edge Cases and Limitations
Multiple Writers Without Daemon
The exclusive lock protocol only prevents daemon interference. It does NOT provide:
- ❌ Mutual exclusion between multiple external tools
- ❌ Transaction isolation or ACID guarantees
- ❌ Protection against direct file system manipulation
If you need coordination between multiple tools, implement your own locking mechanism.
Git Worktrees
The daemon already has issues with git worktrees (see AGENTS.md). The exclusive lock protocol doesn't solve this—use --no-daemon mode in worktrees instead.
Remote Hosts
Locks from remote hosts are always assumed valid because the daemon cannot verify remote PIDs. This means:
- Stale locks from remote hosts will not be automatically cleaned up
- You must manually remove stale remote locks
Lock File Corruption
If the lock file becomes corrupted (invalid JSON), the daemon fails safe and skips the database. You must manually fix or remove the lock file.
Daemon Logging
The daemon logs lock-related events:
Skipping database (locked by vc-executor)
Removed stale lock (vc-executor), proceeding with sync
Skipping database (lock check failed: malformed lock file: unexpected EOF)
Check daemon logs (default: .beads/daemon.log) to troubleshoot lock issues.
Note: The daemon checks for locks at the start of each sync cycle. If a lock is created during a sync cycle, that cycle will complete, but subsequent cycles will skip the database.
Testing Your Integration
- Start the daemon:
bd daemon start --interval 1m - Create a lock: Use your tool to create
.beads/.exclusive-lock - Verify daemon skips: Check daemon logs for "Skipping database" message
- Release lock: Remove
.beads/.exclusive-lock - Verify daemon resumes: Check daemon logs for normal sync cycle
Security Considerations
- Lock files are not secure. Any process can create, modify, or delete them.
- PID reuse could theoretically cause issues (very rare, especially with hostname check)
- This is a cooperative protocol, not a security mechanism
API Reference
Go Types
// ExclusiveLock represents the lock file format
type ExclusiveLock struct {
Holder string `json:"holder"`
PID int `json:"pid"`
Hostname string `json:"hostname"`
StartedAt time.Time `json:"started_at"`
Version string `json:"version"`
}
// NewExclusiveLock creates a lock for the current process
func NewExclusiveLock(holder, version string) (*ExclusiveLock, error)
// Validate checks if the lock has valid field values
func (e *ExclusiveLock) Validate() error
// ShouldSkipDatabase checks if database should be skipped due to lock
func ShouldSkipDatabase(beadsDir string) (skip bool, holder string, err error)
// IsProcessAlive checks if a process is running
func IsProcessAlive(pid int, hostname string) bool
Questions?
For integration help, see:
- AGENTS.md - General workflow guidance
- README.md - Daemon configuration
- examples/ - Sample integrations
File issues at: https://github.com/steveyegge/beads/issues