{"record":{"id":"619573f27dc566d6","repo":"gastownhall/beads","slug":"refusing-backend-cleanup-s-is-held-while-proxy-l","errorCode":null,"errorMessage":"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","messagePattern":"refusing backend cleanup: (.+?) is held while proxy lock is free \\(recorded pid (.+?) at (.+?)\\); stop the process holding the child lock or remove the stale lock owner before retrying","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/proxy/endpoint.go","lineNumber":761,"sourceCode":"\t}\n\treturn errors.Join(errs...)\n}\n\nfunc cleanupOrphanBackend(rootDir string) error {\n\trecordPath := pidfile.Path(rootDir, server.PIDFileName)\n\tpf, readErr := pidfile.Read(rootDir, server.PIDFileName)\n\n\tchildLockPath := filepath.Join(rootDir, server.LockFileName)\n\tchildLock, lockErr := util.TryLock(childLockPath)\n\tswitch {\n\tcase lockErr == nil:\n\t\tchildLock.Unlock()\n\tcase lockfile.IsLocked(lockErr):\n\t\tpid := 0\n\t\tif pf != nil {\n\t\t\tpid = pf.Pid\n\t\t}\n\t\treturn fmt.Errorf(\n\t\t\t\"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\",\n\t\t\tchildLockPath, pid, recordPath,\n\t\t)\n\tdefault:\n\t\treturn fmt.Errorf(\"probe backend lock %s: %w\", childLockPath, lockErr)\n\t}\n\n\tif readErr != nil {\n\t\tif isMalformedPIDFileError(readErr) {\n\t\t\treturn unverifiableProcessError(\n\t\t\t\t\"backend cleanup\",\n\t\t\t\trecordPath,\n\t\t\t\t0,\n\t\t\t\treadErr,\n\t\t\t\tunverifiableProcessChecks{},\n\t\t\t)\n\t\t}\n\t\treturn fmt.Errorf(\"read backend record %s: %w\", recordPath, readErr)","sourceCodeStart":743,"sourceCodeEnd":779,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/proxy/endpoint.go#L743-L779","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (shell)\nbd doctor --fix\n// after: confirm and clear the stale holder first\nlsof /path/to/workspace/.beads/server.lock   # identify holder\nps -fp <pid>                                  # confirm it is stale or another session\n# if stale/foreign, remove the lock and retry\nrm -f /path/to/workspace/.beads/server.lock\nbd doctor --fix","handlingStrategy":"validation","validationCode":"// check the holder before asking for backend cleanup\npf, _ := pidfile.Read(root, server.PIDFileName)\nif pf != nil && pidAlive(pf.Pid) {\n    return fmt.Errorf(\"backend pid %d still alive; stop it before cleanup\", pf.Pid)\n}\nout, _ := exec.Command(\"lsof\", \"-wn\", root+\"/.beads/server.lock\").Output()\nfmt.Println(string(out)) // confirm nobody holds the lock","typeGuard":null,"tryCatchPattern":"err := cleanupOrphanBackend(root)\nvar held *lockHeldError\nif err != nil && strings.Contains(err.Error(), \"refusing backend cleanup\") {\n    // parse recorded pid from the message, verify liveness, then stop holder or clear stale lock\n}","preventionTips":["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"],"tags":["file-lock","concurrency","backend"],"backgroundTag":"file-lock-held","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}