gastownhall/beads · error

legacy proxy record %s is protected by held lock %s; stop th

Error message

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

What it means

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.

Source

Thrown at internal/storage/dbproxy/proxy/endpoint.go:274

				lastSpawnErr = nil
				// This break exits the switch only; the outer discovery loop
				// continues with its normal bounded poll.
				break
			}

			var ep Endpoint
			ep, lastSpawnErr = spawnAndHandoff(rootDir, opts, deadline, stopEpoch, lock, discovery)
			if lastSpawnErr == nil {
				return ep, nil
			}
			if errors.Is(lastSpawnErr, errStartInterrupted) {
				return Endpoint{}, lastSpawnErr
			}
		case !lockfile.IsLocked(err):
			return Endpoint{}, fmt.Errorf("probe proxy lock: %w", err)
		case discovery.status == adoptionLegacy:
			recordPath := pidfile.Path(rootDir, PIDFileName)
			return Endpoint{}, fmt.Errorf(
				"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",
				recordPath,
				filepath.Join(rootDir, LockFileName),
				recordPath,
				recordPath,
			)
		}

		select {
		case <-timeout.C:
			if lastSpawnErr != nil {
				return Endpoint{}, lastSpawnErr
			}
			return Endpoint{}, fmt.Errorf("timeout waiting for proxy on %s", rootDir)
		case <-poll.C:
		}
	}
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Stop the old proxy: run the pre-upgrade bd binary once so it shuts down, or kill the old bd process (pgrep -f bd)
  2. Wait for the old proxy's idle exit timeout to elapse, then retry
  3. If the proxy is definitely gone, quarantine manually: mv .beads/proxy.pid .beads/proxy.pid.stale-$(date +%s) and retry
  4. Pin all tooling (CI + local) to the same bd version to avoid mixed-version proxies

Example fix

// before
error: legacy proxy record /repo/.beads/proxy.pid is protected by held lock /repo/.beads/proxy.lock; ...
// after
$ pgrep -af 'bd.*serve'   # find old proxy
$ kill <old-pid>          # or wait for idle exit
$ bd ready               # succeeds; or mv .beads/proxy.pid .beads/proxy.pid.stale-$(date +%s) first
Defensive patterns

Strategy: validation

Validate before calling

// Detect a legacy record + held lock before attempting open
if _, err := os.Stat(filepath.Join(rootDir, "proxy.pid")); err == nil {
    lf, err := os.Open(filepath.Join(rootDir, "proxy.lock"))
    if err == nil {
        defer lf.Close()
        if err := syscall.Flock(int(lf.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == syscall.EWOULDBLOCK {
            return errors.New("old proxy still running; upgrade blocked")
        }
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3a746a787ac49b25. Report an issue: GitHub.