{"record":{"id":"3a746a787ac49b25","repo":"gastownhall/beads","slug":"legacy-proxy-record-s-is-protected-by-held-lock","errorCode":null,"errorMessage":"legacy proxy record %s is protected by held lock %s; stop the pre-upgrade proxy with the old bd binary or wait for its idle exit, then quarantine the record manually by renaming %s to %s.stale-<unix-timestamp> before retrying","messagePattern":"legacy proxy record (.+?) is protected by held lock (.+?); stop the pre-upgrade proxy with the old bd binary or wait for its idle exit, then quarantine the record manually by renaming (.+?) to (.+?)\\.stale-<unix-timestamp> before retrying","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/proxy/endpoint.go","lineNumber":274,"sourceCode":"\t\t\t\tlastSpawnErr = nil\n\t\t\t\t// This break exits the switch only; the outer discovery loop\n\t\t\t\t// continues with its normal bounded poll.\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tvar ep Endpoint\n\t\t\tep, lastSpawnErr = spawnAndHandoff(rootDir, opts, deadline, stopEpoch, lock, discovery)\n\t\t\tif lastSpawnErr == nil {\n\t\t\t\treturn ep, nil\n\t\t\t}\n\t\t\tif errors.Is(lastSpawnErr, errStartInterrupted) {\n\t\t\t\treturn Endpoint{}, lastSpawnErr\n\t\t\t}\n\t\tcase !lockfile.IsLocked(err):\n\t\t\treturn Endpoint{}, fmt.Errorf(\"probe proxy lock: %w\", err)\n\t\tcase discovery.status == adoptionLegacy:\n\t\t\trecordPath := pidfile.Path(rootDir, PIDFileName)\n\t\t\treturn Endpoint{}, fmt.Errorf(\n\t\t\t\t\"legacy proxy record %s is protected by held lock %s; stop the pre-upgrade proxy with the old bd binary or wait for its idle exit, then quarantine the record manually by renaming %s to %s.stale-<unix-timestamp> before retrying\",\n\t\t\t\trecordPath,\n\t\t\t\tfilepath.Join(rootDir, LockFileName),\n\t\t\t\trecordPath,\n\t\t\t\trecordPath,\n\t\t\t)\n\t\t}\n\n\t\tselect {\n\t\tcase <-timeout.C:\n\t\t\tif lastSpawnErr != nil {\n\t\t\t\treturn Endpoint{}, lastSpawnErr\n\t\t\t}\n\t\t\treturn Endpoint{}, fmt.Errorf(\"timeout waiting for proxy on %s\", rootDir)\n\t\tcase <-poll.C:\n\t\t}\n\t}\n}","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/proxy/endpoint.go#L256-L292","documentation":"The library found a legacy (pre-upgrade format) proxy record AND the proxy.lock is currently held by another process — almost certainly the old bd binary's proxy still running. The new code refuses to touch or replace a legacy record while a lock holder is alive, because the old binary may still be serving on it, so it returns this actionable message telling you how to drain and quarantine the record. It is a deliberate safety stop during binary upgrades, not a bug.","triggerScenarios":"GetCreateDatabaseProxyServerEndpoint sees discovery.status == adoptionLegacy while TryLock reports the lock is held (IsLocked). Happens when an older bd binary spawned a proxy, then the new binary tries to open the same workspace before the old proxy exits.","commonSituations":"Upgrading bd while a long-lived background proxy from the previous version is still running; idle-exit timeout of the old proxy has not elapsed; two different bd versions interleaved in the same clone (e.g. CI pinned to old version, dev on new).","solutions":["Stop the old proxy: run the pre-upgrade bd binary once so it shuts down, or kill the old bd process (pgrep -f bd)","Wait for the old proxy's idle exit timeout to elapse, then retry","If the proxy is definitely gone, quarantine manually: mv .beads/proxy.pid .beads/proxy.pid.stale-$(date +%s) and retry","Pin all tooling (CI + local) to the same bd version to avoid mixed-version proxies"],"exampleFix":"// before\nerror: legacy proxy record /repo/.beads/proxy.pid is protected by held lock /repo/.beads/proxy.lock; ...\n// after\n$ pgrep -af 'bd.*serve'   # find old proxy\n$ kill <old-pid>          # or wait for idle exit\n$ bd ready               # succeeds; or mv .beads/proxy.pid .beads/proxy.pid.stale-$(date +%s) first","handlingStrategy":"validation","validationCode":"// Detect a legacy record + held lock before attempting open\nif _, err := os.Stat(filepath.Join(rootDir, \"proxy.pid\")); err == nil {\n    lf, err := os.Open(filepath.Join(rootDir, \"proxy.lock\"))\n    if err == nil {\n        defer lf.Close()\n        if err := syscall.Flock(int(lf.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == syscall.EWOULDBLOCK {\n            return errors.New(\"old proxy still running; upgrade blocked\")\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Stop all background bd proxies before upgrading the binary","Pin the bd version across CI, IDE plugins, and local shells","After upgrades, run one command with the new binary and allow the idle-exit window to pass"],"tags":["upgrade","legacy","lock","proxy","migration"],"backgroundTag":"legacy-proxy-record-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}