gastownhall/beads · error

step %s already claimed (status: %s)

Error message

step %s already claimed (status: %s)

What it means

The same step-advance transaction only allows claiming a step whose status is `open`. If the issue exists but is already `in_progress`, `closed`, or otherwise non-open, the port returns `step %s already claimed (status: %s)` to signal a concurrent/duplicate claim rather than silently overwriting state.

Source

Thrown at cmd/bd/mol_port.go:110

// transaction rather than seeing the last committed value.
func (w storeMolWriter) GetConfig(ctx context.Context, key string) (string, error) {
	if w.tx != nil {
		return w.tx.GetConfig(ctx, key)
	}
	return w.DoltStorage.GetConfig(ctx, key)
}

func (w storeMolWriter) ClaimStepIfOpen(ctx context.Context, id, actor string) error {
	return w.DoltStorage.RunInTransaction(ctx, fmt.Sprintf("bd: advance to step %s", id), func(tx storage.Transaction) error {
		current, err := tx.GetIssue(ctx, id)
		if err != nil {
			return err
		}
		if current == nil {
			return fmt.Errorf("step %s not found", id)
		}
		if current.Status != types.StatusOpen {
			return fmt.Errorf("step %s already claimed (status: %s)", id, current.Status)
		}
		return tx.UpdateIssue(ctx, id, map[string]interface{}{"status": types.StatusInProgress}, actor)
	})
}

func newStandaloneStoreMolWriter(store storage.DoltStorage) storeMolWriter {
	return storeMolWriter{DoltStorage: store}
}

type uowMolReader struct {
	uw uow.UnitOfWork
}

func (r uowMolReader) GetIssue(ctx context.Context, id string) (*types.Issue, error) {
	issue, isWisp, rerr := workapi.GetIssueOrWisp(ctx, workapi.NewUOWDetailSource(r.uw), id)
	if errors.Is(rerr, storage.ErrNotFound) {
		return nil, fmt.Errorf("issue %s not found", id)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the step's status first (`bd show <id>`) and skip the claim if it is already in_progress.
  2. Coordinate claim work so only one worker claims a step (the claim itself is transactional — treat this error as 'someone else got it' and move on).
  3. If the step is genuinely stuck, reset its status to open (`bd update <id> --status open`) before re-claiming.

Example fix

// before
_ = writer.AdvanceToStep(ctx, id, actor) // blind claim, fails if already claimed
// after
if step, _ := reader.GetIssue(ctx, id); step != nil && step.Status == types.StatusOpen {
    err = writer.AdvanceToStep(ctx, id, actor)
}
Defensive patterns

Strategy: validation

Validate before calling

// before claiming, check the step is still open
STATUS=$(bd show "$STEP_ID" --json | jq -r '.status')
[[ "$STATUS" == "open" ]] || { echo "step $STEP_ID is $STATUS, skipping"; exit 0; }

Type guard

func claimable(iss *types.Issue) bool {
	return iss != nil && iss.Status == types.StatusOpen
}

Try / catch

if err := advance(ctx, stepID, actor); err != nil && strings.Contains(err.Error(), "already claimed") {
	log.Printf("step %s taken by another worker; skipping", stepID)
	return nil // expected in concurrent claiming
}

Prevention

When it happens

Trigger: Two agents or invocations advancing the same molecule step concurrently; re-running a claim script without checking status first; a step that was previously claimed and never reset. `current.Status != types.StatusOpen` triggers the error inside the `RunInTransaction` callback.

Common situations: CI retries after a partial failure; parallel workers racing on the same molecule; a human manually started the step (`bd update <id> --status in_progress`) and automation then tries to claim it.

Related errors


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