gastownhall/beads · error · storage.ErrValidation

storage.ErrValidation

storage.ErrValidation

Error message

%w: claim requires actor and issue ID

What it means

ExecuteClaim validates its ClaimRequest before doing any work: both Actor and IssueID must be non-empty, otherwise it returns storage.ErrValidation wrapped with this message. This is a pre-condition guard so an unattributed or untargeted claim never reaches the transaction.

Source

Thrown at internal/storage/issueops/public_claim.go:26

	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/types"
	publicops "github.com/steveyegge/beads/issueops"
)

// ClaimCommitMessage names the claimed issue and its claimant in the storage
// commit. It lives here so every implementation of the claim role spells it
// identically and `bd dolt log` reads the same on all of them. The actor is in
// it because that line IS the audit trail — which is why every surface
// reaching the role validates the actor before calling it.
func ClaimCommitMessage(issueID, actor string) string {
	return fmt.Sprintf("bd: claim %s by %s", issueID, actor)
}

// ExecuteClaim applies a guarded claim in tx and reports durable tables changed.
func ExecuteClaim(ctx context.Context, tx *sql.Tx, request publicops.ClaimRequest) (publicops.ClaimResult, ChangedTables, error) {
	if request.Actor == "" || request.IssueID == "" {
		return publicops.ClaimResult{}, nil, fmt.Errorf("%w: claim requires actor and issue ID", storage.ErrValidation)
	}
	// ClaimIssueInTx routes a wisp id to the wisp tables and claims it there.
	// The role deliberately does not: the wisp plane is not claimable through
	// it, and the refusal lands here — before the pre-image read, and before
	// any write the enclosing transaction would have to roll back.
	if IsActiveWispInTx(ctx, tx, request.IssueID) {
		return publicops.ClaimResult{}, nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, request.IssueID)
	}
	claimed, err := ClaimIssueInTx(ctx, tx, request.IssueID, request.Actor)
	if err != nil {
		return publicops.ClaimResult{}, nil, classifyClaimRefusalInTx(ctx, tx, request.IssueID, err)
	}
	// The CAS matches no row when the actor already holds the issue in
	// progress, and ClaimIssueInTx reports that as success. The pre-image is
	// what tells the two apart, and staging nothing for the idempotent case is
	// what keeps a polling caller from minting empty version-control commits.
	// Judged under actorMatches, not verbatim (ga-v2k49): a caller re-claiming
	// its own in-progress issue under a different layer's spelling of its own

View on GitHub (pinned to 71377f2769)

Solutions

  1. Populate both request.Actor and request.IssueID before calling ExecuteClaim
  2. In CLI code, require the actor flag or derive it from config/user and fail fast with a usage message if empty
  3. Trim and validate user-supplied IDs before constructing ClaimRequest

Example fix

// before
res, err := ExecuteClaim(ctx, tx, publicops.ClaimRequest{IssueID: id})
// after
if actor == "" || id == "" {
    return fmt.Errorf("claim needs --actor and an issue ID")
}
res, err := ExecuteClaim(ctx, tx, publicops.ClaimRequest{Actor: actor, IssueID: id})
Defensive patterns

Strategy: validation

Validate before calling

func claimRequestOK(r publicops.ClaimRequest) bool { return r.Actor != "" && r.IssueID != "" }

Try / catch

if err != nil {
  if errors.Is(err, storage.ErrValidation) {
    // user-input problem: fix the request, do not retry
    return fmt.Errorf("invalid claim request: %w", err)
  }
  return err
}

Prevention

When it happens

Trigger: Calling ExecuteClaim with ClaimRequest{Actor: ""} or ClaimRequest{IssueID: ""}, e.g. an empty actor from an unset env/config value or an issue ID variable that was never populated.

Common situations: Missing BD_ACTOR-like config, a CLI flag not passed (--actor), an issue ID parsed from user input that came back empty after trimming.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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