{"record":{"id":"05c7298a1c9d7779","repo":"gastownhall/beads","slug":"w-claim-cannot-use-expected-assignee-or-status","errorCode":null,"errorMessage":"%w: claim cannot use expected assignee or status","messagePattern":"%w: claim cannot use expected assignee or status","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":74,"sourceCode":"\t\t{patch.ClosedBySession.Set, \"closed_by_session\", patch.ClosedBySession.Value},\n\t\t{patch.EstimatedMinutes.Set, \"estimated_minutes\", patch.EstimatedMinutes.Value},\n\t\t{patch.ExternalRef.Set, \"external_ref\", patch.ExternalRef.Value},\n\t\t{patch.DueAt.Set, \"due_at\", patch.DueAt.Value},\n\t\t{patch.DeferUntil.Set, \"defer_until\", patch.DeferUntil.Value},\n\t} {\n\t\tif field.set {\n\t\t\tupdates[field.key] = field.val\n\t\t}\n\t}\n\treturn updates\n}\n\n// ValidateUpdateRequest checks mutually exclusive guarded-update options and\n// the canonical field values every backend must reject identically. Backends\n// call it before touching the row so an invalid patch cannot half-apply.\nfunc ValidateUpdateRequest(request publicops.UpdateRequest) error {\n\tif request.Claim && (request.ExpectedAssignee != nil || request.ExpectedStatus != nil) {\n\t\treturn fmt.Errorf(\"%w: claim cannot use expected assignee or status\", storage.ErrValidation)\n\t}\n\tif request.ForceAssigneeTransfer && (request.Claim || !request.Patch.Assignee.Set || request.ExpectedAssignee != nil) {\n\t\treturn fmt.Errorf(\"%w: invalid forced assignee transfer\", storage.ErrValidation)\n\t}\n\tpatch := request.Patch\n\tif patch.Title.Set {\n\t\tif err := types.ValidateIssueTitle(patch.Title.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update title: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.Priority.Set {\n\t\tif err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update priority: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.EstimatedMinutes.Set {\n\t\tif err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update estimated_minutes: %w\", storage.ErrValidation, err)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nreq := publicops.UpdateRequest{ID: id, Claim: true, ExpectedStatus: &st}\n_, err := issueops.ExecuteUpdate(ctx, tx, req)\n// after\nreq := publicops.UpdateRequest{ID: id, Claim: true}\n_, err := issueops.ExecuteUpdate(ctx, tx, req)","handlingStrategy":"validation","validationCode":"func validClaimRequest(r publicops.UpdateRequest) bool {\n  return r.Claim && r.ExpectedAssignee == nil && r.ExpectedStatus == nil\n}\nif !validClaimRequest(req) { return errors.New(\"claim cannot carry expected assignee/status\") }","typeGuard":"func isPureClaim(r publicops.UpdateRequest) bool {\n  return r.Claim && r.ExpectedAssignee == nil && r.ExpectedStatus == nil\n}","tryCatchPattern":"err := issueops.ValidateUpdateRequest(req)\nif errors.Is(err, storage.ErrValidation) {\n  // reject/repair request before ExecuteUpdate\n}","preventionTips":["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"],"tags":["go","validation","storage","update-request","claim"],"backgroundTag":"conflicting-update-options","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}