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
- Remove ExpectedAssignee and ExpectedStatus from the request when Claim is true — the claim guard does its own concurrency checking
- Split the operation: if you need expectation-based checks, drop Claim and use a plain update with ExpectedAssignee/ExpectedStatus
- 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
- Construct claim requests through a dedicated constructor that zeroes expectation fields
- Add a unit test asserting Claim requests never set ExpectedAssignee/ExpectedStatus
- Never reuse a single UpdateRequest struct across claim and CAS update flows
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
- no store is open for this workspace
- not found
- db: ChildCounterSQLRepository.NextChildID: parentID must not
- db: DependencySQLRepository.Insert: dep must not be nil
- db: DependencySQLRepository.Insert: IssueID must not be empt
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/05c7298a1c9d7779.
Report an issue: GitHub.