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 ownView on GitHub (pinned to 71377f2769)
Solutions
- Populate both request.Actor and request.IssueID before calling ExecuteClaim
- In CLI code, require the actor flag or derive it from config/user and fail fast with a usage message if empty
- 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
- Resolve the actor (config/env/flag) before constructing ClaimRequest
- Fail fast in CLI arg parsing on empty actor or issue ID
- Trim whitespace and reject empty results early
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
- %w: claim cannot use expected assignee or status
- ErrNotClaimable
- no store is open for this workspace
- not found
- db: ChildCounterSQLRepository.NextChildID: parentID must not
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7654a3c39935e327.
Report an issue: GitHub.