gastownhall/beads · error · storage.ErrValidation
%w: invalid forced assignee transfer
Error message
%w: invalid forced assignee transfer
What it means
ValidateUpdateRequest returns this when ForceAssigneeTransfer is enabled but its preconditions are violated: the request also sets Claim, the patch does not actually set an assignee, or ExpectedAssignee is present. Forced transfer is meant to be an unambiguous, standalone reassignment, so any of those combinations is rejected before the row is modified. The error wraps storage.ErrValidation.
Source
Thrown at internal/storage/issueops/aggregate.go:77
{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)
}
}
if patch.Persistence.Set && !patch.Persistence.Value.IsValid() {View on GitHub (pinned to 71377f2769)
Solutions
- Ensure ForceAssigneeTransfer is combined with Patch.Assignee.Set=true and a target assignee value
- Remove Claim and ExpectedAssignee from the request when doing a forced transfer
- Send ForceAssigneeTransfer as the only guarded option on the request
Example fix
// before
req := publicops.UpdateRequest{ID: id, ForceAssigneeTransfer: true, Claim: true}
// after
req := publicops.UpdateRequest{
ID: id,
ForceAssigneeTransfer: true,
Patch: publicops.UpdatePatch{Assignee: publicops.SetField[string]{Set: true, Value: newOwner}},
} Defensive patterns
Strategy: validation
Validate before calling
func validForceTransfer(r publicops.UpdateRequest) bool {
return r.ForceAssigneeTransfer && !r.Claim && r.ExpectedAssignee == nil && r.Patch.Assignee.Set
}
if !validForceTransfer(req) { return errors.New("invalid forced assignee transfer request") } Type guard
func isForceTransferReady(r publicops.UpdateRequest) bool {
return r.ForceAssigneeTransfer && r.Patch.Assignee.Set && !r.Claim && r.ExpectedAssignee == nil
} Try / catch
if err := issueops.ValidateUpdateRequest(req); errors.Is(err, storage.ErrValidation) {
// fix request: ensure Assignee set, drop Claim/ExpectedAssignee
} Prevention
- Always set Patch.Assignee.Set=true with a value when forcing a transfer
- Keep forced transfers in a code path separate from claim logic
- Run ValidateUpdateRequest in a preflight test for every guarded-option combination you ship
When it happens
Trigger: ExecuteUpdate with request.ForceAssigneeTransfer=true plus any of: request.Claim=true, request.Patch.Assignee.Set=false (no assignee supplied), or request.ExpectedAssignee != nil.
Common situations: Admin tooling that force-reassigns issues but builds the patch conditionally so Assignee ends up unset; copying a claim-style request and flipping on ForceAssigneeTransfer; a UI sending stale ExpectedAssignee fields alongside a force transfer.
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/155b564ba379a58c.
Report an issue: GitHub.