gastownhall/beads · error · storage.ErrValidation

%w: claim cannot use expected assignee or status

Error message

%w: claim cannot use expected assignee or status

What it means

This error is returned by ValidateUpdateRequest in internal/storage/issueops/aggregate.go when an update request sets Claim=true while also supplying ExpectedAssignee or ExpectedStatus. Claim is a guarded fast-path that asserts the issue is claimable, so combining it with optimistic-concurrency expectation fields is ambiguous and rejected before any row is touched. It wraps storage.ErrValidation so callers can classify it with errors.Is.

Source

Thrown at internal/storage/issueops/aggregate.go:74

		{patch.ClosedBySession.Set, "closed_by_session", patch.ClosedBySession.Value},
		{patch.EstimatedMinutes.Set, "estimated_minutes", patch.EstimatedMinutes.Value},
		{patch.ExternalRef.Set, "external_ref", patch.ExternalRef.Value},
		{patch.DueAt.Set, "due_at", patch.DueAt.Value},
		{patch.DeferUntil.Set, "defer_until", patch.DeferUntil.Value},
	} {
		if field.set {
			updates[field.key] = field.val
		}
	}
	return updates
}

// ValidateUpdateRequest checks mutually exclusive guarded-update options and
// the canonical field values every backend must reject identically. Backends
// call it before touching the row so an invalid patch cannot half-apply.
func ValidateUpdateRequest(request publicops.UpdateRequest) error {
	if request.Claim && (request.ExpectedAssignee != nil || request.ExpectedStatus != nil) {
		return fmt.Errorf("%w: claim cannot use expected assignee or status", storage.ErrValidation)
	}
	if request.ForceAssigneeTransfer && (request.Claim || !request.Patch.Assignee.Set || request.ExpectedAssignee != nil) {
		return fmt.Errorf("%w: invalid forced assignee transfer", storage.ErrValidation)
	}
	patch := request.Patch
	if patch.Title.Set {
		if err := types.ValidateIssueTitle(patch.Title.Value); err != nil {
			return fmt.Errorf("%w: update title: %w", storage.ErrValidation, err)
		}
	}
	if patch.Priority.Set {
		if err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {
			return fmt.Errorf("%w: update priority: %w", storage.ErrValidation, err)
		}
	}
	if patch.EstimatedMinutes.Set {
		if err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {
			return fmt.Errorf("%w: update estimated_minutes: %w", storage.ErrValidation, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove ExpectedAssignee and ExpectedStatus from the request when Claim is true — the claim guard does its own concurrency checking
  2. Split the operation: if you need expectation-based checks, drop Claim and use a plain update with ExpectedAssignee/ExpectedStatus
  3. Inspect the request-assembly code path and only set expectation fields on non-claim updates

Example fix

// before
req := publicops.UpdateRequest{ID: id, Claim: true, ExpectedStatus: &st}
_, err := issueops.ExecuteUpdate(ctx, tx, req)
// after
req := publicops.UpdateRequest{ID: id, Claim: true}
_, err := issueops.ExecuteUpdate(ctx, tx, req)
Defensive patterns

Strategy: validation

Validate before calling

func validClaimRequest(r publicops.UpdateRequest) bool {
  return r.Claim && r.ExpectedAssignee == nil && r.ExpectedStatus == nil
}
if !validClaimRequest(req) { return errors.New("claim cannot carry expected assignee/status") }

Type guard

func isPureClaim(r publicops.UpdateRequest) bool {
  return r.Claim && r.ExpectedAssignee == nil && r.ExpectedStatus == nil
}

Try / catch

err := issueops.ValidateUpdateRequest(req)
if errors.Is(err, storage.ErrValidation) {
  // reject/repair request before ExecuteUpdate
}

Prevention

When it happens

Trigger: Calling ExecuteUpdate (or building a publicops.UpdateRequest) with request.Claim=true and a non-nil ExpectedAssignee pointer, or with Claim=true and a non-nil ExpectedStatus pointer. Either combination triggers it immediately.

Common situations: Code that programmatically builds update requests and reuses a struct with leftover expectation fields; migrating from an explicit expected-assignee claim flow to the new Claim shortcut while still populating the old fields; UI or CLI layers that always set expected status for CAS updates then add claim.

Related errors


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