gastownhall/beads · warning
ErrBusy
ErrBusy
Error message
workspace gate busy
What it means
ErrBusy is returned when a workspace gate cannot be acquired within the configured wait budget. The wrapped error carries holder diagnostics when available, and errors.Is should be used for classification. The public alias for external consumers is beads.ErrGateBusy.
Source
Thrown at internal/workspacegate/gate.go:95
// Exclusive is held by maintenance operations. It conflicts with
// every other holder, shared or exclusive. See the starvation note
// on Shared: nothing here prevents shared holders from starving an
// Exclusive acquirer.
Exclusive
)
func (m Mode) String() string {
if m == Exclusive {
return "exclusive"
}
return "shared"
}
// ErrBusy is returned when the gate cannot be acquired within the
// configured wait budget. Use errors.Is; the wrapped error carries holder
// diagnostics when available. The public alias for external consumers is
// beads.ErrGateBusy.
var ErrBusy = errors.New("workspace gate busy")
// Options tunes acquisition. The zero value means a single non-blocking
// attempt with no diagnostics callback.
type Options struct {
// Wait bounds how long Acquire keeps retrying after the first busy
// attempt. Zero or negative means exactly one non-blocking try.
// The underlying blocking lock primitives have no deadline support,
// so waiting is implemented as timed polling of the non-blocking
// primitive. This is not a fairness guarantee: see the starvation
// note on Exclusive — a Wait on an Exclusive acquisition can still
// exhaust its budget against a rolling sequence of Shared holders
// that each individually released before it, none of which had any
// signal that an exclusive acquirer was waiting.
Wait time.Duration
// PollInterval is the retry cadence while waiting (default 100ms).
PollInterval time.Duration
// Reason is recorded in the advisory holder-info sidecar on
// exclusive acquisition so blocked commands can say who is holdingView on GitHub (pinned to 71377f2769)
Solutions
- Wait for the other holder to finish and retry; the wrapped error names the holder for diagnosis.
- Increase Options.Wait so Acquire retries within a longer budget instead of a single non-blocking try.
- Investigate stale holders (crashed processes) and clear them before retrying.
- Classify with errors.Is(err, workspacegate.ErrBusy) / beads.ErrGateBusy and implement bounded retry with backoff in the caller.
Example fix
// before
g, err := gate.Acquire(ctx) // zero Wait: single try, busy
// after
g, err := gate.Acquire(ctx, workspacegate.Options{Wait: 5 * time.Second})
if errors.Is(err, workspacegate.ErrBusy) {
// inspect holder diagnostics in wrapped error, retry or report
} Defensive patterns
Strategy: retry
Try / catch
g, err := gate.Acquire(ctx, workspacegate.Options{Wait: 5 * time.Second})
if errors.Is(err, workspacegate.ErrBusy) {
// read holder diagnostics from wrapped error, back off and retry
} Prevention
- Set a nonzero Options.Wait budget for interactive commands.
- Serialize long-running daemon work away from CLI commands.
- Alert on stale holders after crashes and clean them up.
- Use the public alias beads.ErrGateBusy for classification.
When it happens
Trigger: Calling Acquire (directly or via acquireCommandWorkspaceGates) while another process holds the gate and the wait budget (Options.Wait) expires; a single non-blocking try (zero/negative Wait) hitting a held gate; streaming events (streamEvents) contending for the same gate.
Common situations: Two bd commands running concurrently in the same workspace (e.g. a background daemon and a CLI command); a crashed process leaving stale holder state; CI jobs racing on a shared workspace checkout.
Related errors
- lock busy: held by another process
- lock already held by another process
- lock already held by another process
- timeout waiting for cache lock on %s
- %w for %s
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c1bf2eca74c56946.
Report an issue: GitHub.