gastownhall/beads · error
slot held by %s, not %s
Error message
slot held by %s, not %s
What it means
The release was attempted by a holder whose name does not match the current holder recorded in the slot metadata. MergeSlotRelease only allows the actual holder (or an empty holder string meaning force-release) to release the slot, preventing one agent from releasing another's lock.
Source
Thrown at internal/storage/merge_slot.go:167
return &result, nil
}
// MergeSlotReleaseImpl is the shared implementation of Storage.MergeSlotRelease.
func MergeSlotReleaseImpl(ctx context.Context, s Storage, holder, actor string) error {
slotID := MergeSlotID(ctx, s)
return s.RunInTransaction(ctx,
fmt.Sprintf("bd: release merge slot %s", slotID),
func(tx Transaction) error {
slot, err := tx.GetIssue(ctx, slotID)
if err != nil || slot == nil {
return fmt.Errorf("merge slot not found: %s", slotID)
}
meta := parseSlotMeta(slot)
if holder != "" && meta.Holder != holder {
return fmt.Errorf("slot held by %s, not %s", meta.Holder, holder)
}
if slot.Status == types.StatusOpen {
// Already released; idempotent.
return nil
}
newMeta := slotMeta{Waiters: meta.Waiters}
metaStr, err := encodeSlotMeta(newMeta)
if err != nil {
return fmt.Errorf("failed to encode slot metadata: %w", err)
}
return tx.UpdateIssue(ctx, slot.ID, map[string]interface{}{
"status": types.StatusOpen,
"metadata": metaStr,
}, actor)
},
)View on GitHub (pinned to 71377f2769)
Solutions
- Confirm the holder name used at acquire matches the one passed at release (same hostname/container identity)
- Use bd merge-slot check to see the current holder before releasing
- If you are an admin, release with an empty holder string to force-release, then re-acquire
- If the holder crashed, force-release and let the new owner take the slot
Example fix
// before
err := store.MergeSlotRelease(ctx, "old-hostname", actor) // slot held by "new-hostname"
// after
status, _ := store.MergeSlotCheck(ctx)
if status.Holder == "old-hostname" {
err = store.MergeSlotRelease(ctx, "old-hostname", actor)
} Defensive patterns
Strategy: validation
Validate before calling
status, err := store.MergeSlotCheck(ctx)
if err != nil { return err }
if status.Holder != myHolder {
return fmt.Errorf("not the holder: slot held by %q", status.Holder)
} Type guard
func isHolder(status *MergeSlotStatus, holder string) bool {
return status != nil && status.Holder == holder
} Try / catch
if err := store.MergeSlotRelease(ctx, holder, actor); err != nil && strings.Contains(err.Error(), "slot held by") {
// Someone else owns the slot; log and skip release (or force-release with "" if authorized).
log.Printf("skip release: %v", err)
return nil
} Prevention
- Pass the exact same holder identity used at acquire (stable hostname/container name)
- Call MergeSlotCheck first to confirm current holder before releasing
- Pass holder="" only when an admin force-release is intended
- Handle re-acquisition by others: a late release after force-release will fail with this error — treat it as non-fatal
When it happens
Trigger: MergeSlotRelease called with holder != meta.Holder and holder != "" — e.g. two agents use the same holder name configured differently (hostname changed), a stale process releases after the holder was force-released and re-acquired by someone else, or a typo in the holder argument.
Common situations: Agent hostname/container name changed between acquire and release; slot re-acquired by another agent after a timeout/force release while the original holder still tries to clean up; manual force-release then legitimate release arriving late.
Related errors
- failed to acquire slot: %w
- ErrLockHeld
- server: ExternalDoltServer.Start: server already started
- acquiring lock (blocking): %w
- acquiring lock (non-blocking): %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6e1d872dd9c338da.
Report an issue: GitHub.