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

  1. Confirm the holder name used at acquire matches the one passed at release (same hostname/container identity)
  2. Use bd merge-slot check to see the current holder before releasing
  3. If you are an admin, release with an empty holder string to force-release, then re-acquire
  4. 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

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


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