docs/EXCLUSIVE_LOCK.md
The exclusive lock protocol allows external tools to claim exclusive management of a beads database, preventing the Dolt server from interfering with their operations.
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 holderThe Dolt server checks for exclusive locks at the start of each sync cycle:
A lock is considered stale if:
Important: The server only removes locks when it can definitively determine the process is dead (ESRCH error). If the server 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 server cannot verify remote processes.
When a stale lock is successfully removed, the server logs: Removed stale lock (holder-name), proceeding with sync
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)
}
func releaseLock(beadsDir string) error {
lockPath := filepath.Join(beadsDir, ".exclusive-lock")
return os.Remove(lockPath)
}
#!/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 --claim
# Release lock
rm "$LOCK_FILE"
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...
}
The exclusive lock protocol only prevents Dolt server interference. It does NOT provide:
If you need coordination between multiple tools, implement your own locking mechanism.
Dolt handles git worktrees natively. The exclusive lock protocol is separate from worktree support.
Locks from remote hosts are always assumed valid because the server cannot verify remote PIDs. This means:
If the lock file becomes corrupted (invalid JSON), the server fails safe and skips the database. You must manually fix or remove the lock file.
The Dolt server 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 server logs (.beads/dolt/sql-server.log) to troubleshoot lock issues.
Note: The server 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.
bd dolt start.beads/.exclusive-lock.beads/.exclusive-lock// 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
For integration help, see:
File issues at: https://github.com/gastownhall/beads/issues