gastownhall/beads · error
refusing backend cleanup: %s is held while proxy lock is fre
Error message
refusing backend cleanup: %s is held while proxy lock is free (recorded pid %d at %s); stop the process holding the child lock or remove the stale lock owner before retrying
What it means
cleanupOrphanBackend refuses to clean up the orphaned backend when its child lock file (server lock) is still held while the proxy lock is free. This is a safety guard: some process may still be using the backend, so removing its record or stopping it would be unsafe. The error names the lock path, the recorded pid, and the record path.
Source
Thrown at internal/storage/dbproxy/proxy/endpoint.go:761
}
return errors.Join(errs...)
}
func cleanupOrphanBackend(rootDir string) error {
recordPath := pidfile.Path(rootDir, server.PIDFileName)
pf, readErr := pidfile.Read(rootDir, server.PIDFileName)
childLockPath := filepath.Join(rootDir, server.LockFileName)
childLock, lockErr := util.TryLock(childLockPath)
switch {
case lockErr == nil:
childLock.Unlock()
case lockfile.IsLocked(lockErr):
pid := 0
if pf != nil {
pid = pf.Pid
}
return fmt.Errorf(
"refusing backend cleanup: %s is held while proxy lock is free (recorded pid %d at %s); stop the process holding the child lock or remove the stale lock owner before retrying",
childLockPath, pid, recordPath,
)
default:
return fmt.Errorf("probe backend lock %s: %w", childLockPath, lockErr)
}
if readErr != nil {
if isMalformedPIDFileError(readErr) {
return unverifiableProcessError(
"backend cleanup",
recordPath,
0,
readErr,
unverifiableProcessChecks{},
)
}
return fmt.Errorf("read backend record %s: %w", recordPath, readErr)View on GitHub (pinned to 71377f2769)
Solutions
- Check whether pid <pid> is alive and what it is (ps -fp <pid>); if it's a live bd session, let it finish before restarting the proxy
- If the recorded pid is dead but the lock seems held, remove the stale lock owner per the message — verify no live holder with lsof <childLockPath> or fuser, then delete the lock file
- Ensure only one bd workspace session is active (close other terminals/tools using the same beads workspace)
- Re-run bd after the holder exits — cleanup will succeed once the child lock is free
Example fix
// before (shell) bd doctor --fix // after: confirm and clear the stale holder first lsof /path/to/workspace/.beads/server.lock # identify holder ps -fp <pid> # confirm it is stale or another session # if stale/foreign, remove the lock and retry rm -f /path/to/workspace/.beads/server.lock bd doctor --fix
Defensive patterns
Strategy: validation
Validate before calling
// check the holder before asking for backend cleanup
pf, _ := pidfile.Read(root, server.PIDFileName)
if pf != nil && pidAlive(pf.Pid) {
return fmt.Errorf("backend pid %d still alive; stop it before cleanup", pf.Pid)
}
out, _ := exec.Command("lsof", "-wn", root+"/.beads/server.lock").Output()
fmt.Println(string(out)) // confirm nobody holds the lock Try / catch
err := cleanupOrphanBackend(root)
var held *lockHeldError
if err != nil && strings.Contains(err.Error(), "refusing backend cleanup") {
// parse recorded pid from the message, verify liveness, then stop holder or clear stale lock
} Prevention
- Ensure bd processes exit cleanly so OS locks release (avoid kill -9 during proxy operations)
- Run only one bd session per workspace at a time
- Check lsof/fuser on the lock file before forcing a cleanup
- In containers, run cleanup inside the same PID namespace that created the lock
When it happens
Trigger: During spawnAndHandoff's backend cleanup, util.TryLock(childLockPath) returns lockfile.IsLocked while the proxy lock could be acquired — a live or lock-leaking process holds the backend's lock.
Common situations: A crashed bd child leaked an OS-level file lock that the OS hasn't released (rare; flock is released on process death, so more likely another live process); a second bd session is actively using the same workspace; container PID-namespace confusion making the recorded pid unresolvable; an unrelated process holding the lock file open with an exclusive lock.
Related errors
- lock busy: held by another process
- lock already held by another process
- ErrLockHeld
- acquiring lock (blocking): %w
- acquiring lock (non-blocking): %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/619573f27dc566d6.
Report an issue: GitHub.