{"record":{"id":"5ded3e3d12b36f7c","repo":"gastownhall/beads","slug":"w-issue-s-is-assigned-to-q","errorCode":null,"errorMessage":"%w: issue %s is assigned to %q","messagePattern":"%w: issue (.+?) is assigned to %q","errorType":"validation","errorClass":"storage.ErrAlreadyClaimed","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":173,"sourceCode":"// because the caller named the holder under a different layer's spelling.\n//\n// Passing nil pools answers every question except pool membership, so a caller\n// that wants the config read only when it matters calls with nil first and\n// re-evaluates with the loaded aliases on refusal.\nfunc AuthorizeAssigneeTransferWithPools(before *types.Issue, request publicops.UpdateRequest, pools []string) error {\n\tif !request.Patch.Assignee.Set || actorMatches(request.Patch.Assignee.Value, before.Assignee) || request.ExpectedAssignee != nil || request.ForceAssigneeTransfer || before.Status != types.StatusInProgress || before.Assignee == \"\" || actorMatches(before.Assignee, request.Actor) {\n\t\treturn nil\n\t}\n\t// Exact-string membership, deliberately not actorMatches (ga-v2k49, same\n\t// reason as claim.go's identical pool checks): a pool alias is a literal\n\t// claim.pools config value, not a Gas Town identity that gets respelled\n\t// per layer, so there is no cross-spelling variant to reconcile.\n\tfor _, pool := range pools {\n\t\tif pool == before.Assignee {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"%w: issue %s is assigned to %q\", storage.ErrAlreadyClaimed, before.ID, before.Assignee)\n}\n\n// AuthorizeAssigneeTransfer protects an active assignment from unguarded transfer.\nfunc AuthorizeAssigneeTransfer(ctx context.Context, tx DBTX, before *types.Issue, request publicops.UpdateRequest) error {\n\tif err := AuthorizeAssigneeTransferWithPools(before, request, nil); err == nil {\n\t\treturn nil\n\t}\n\tpools, err := ClaimPoolAliasesInTx(ctx, tx)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn AuthorizeAssigneeTransferWithPools(before, request, pools)\n}\n\n// ApplyMetadataPatch returns the canonical metadata value and whether it changes.\nfunc ApplyMetadataPatch(current json.RawMessage, patch publicops.MetadataPatch) (json.RawMessage, bool, error) {\n\tif !patch.Replace.Set && !patch.Merge.Set && len(patch.Set) == 0 && len(patch.Unset) == 0 {\n\t\treturn current, false, nil","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L155-L191","documentation":"This error wraps storage.ErrAlreadyClaimed and is returned by AuthorizeAssigneeTransferWithPools when an issue has an assignee that is not covered by any allowed 'pool' value, blocking an unguarded transfer of an active assignment. The library throws it to protect active assignments from being silently reassigned through an update request without going through the proper transfer/authorization path.","triggerScenarios":"Calling AuthorizeAssigneeTransfer (or AuthorizeAssigneeTransferWithPools) with an UpdateRequest that would change or touch an issue whose before.Assignee is non-empty and does not match any entry in the pools slice (pools is nil or does not contain the current assignee).","commonSituations":"Agents or scripts run `bd update` on an issue that another agent already claimed; concurrent automation both assigning the same issue; passing the wrong pools list (or nil) when the caller only wants to allow reassignment within known agent pools.","solutions":["Check the issue's current assignee before updating; if set, use the proper assignment-release flow (clear the assignee with the correct authorization) instead of a plain update.","Pass the correct pools slice to AuthorizeAssigneeTransferWithPools so the current assignee's pool is included and the transfer is authorized.","If the assignment is stale (agent died), explicitly release/reclaim the issue through the supported claim workflow, then retry the update."],"exampleFix":"// before\nerr := issueops.AuthorizeAssigneeTransferWithPools(before, req, nil)\n// after\npools := []string{\"pool-alpha\", agentName} // include the pool that owns the current assignee\nerr := issueops.AuthorizeAssigneeTransferWithPools(before, req, pools)\nif errors.Is(err, storage.ErrAlreadyClaimed) {\n    // release or coordinate before updating\n}","handlingStrategy":"validation","validationCode":"func canTransfer(before *types.Issue, pools []string) bool {\n    return before == nil || before.Assignee == \"\" || slices.Contains(pools, before.Assignee)\n}","typeGuard":"func isAlreadyClaimed(err error) bool { return errors.Is(err, storage.ErrAlreadyClaimed) }","tryCatchPattern":"if err := issueops.AuthorizeAssigneeTransfer(ctx, tx, before, req); err != nil {\n    if errors.Is(err, storage.ErrAlreadyClaimed) {\n        return fmt.Errorf(\"issue %s held by %q; release or include its pool first\", before.ID, before.Assignee)\n    }\n    return err\n}","preventionTips":["Always fetch the current issue state and check Assignee before issuing an update.","Pass the full list of agent pools to AuthorizeAssigneeTransferWithPools rather than nil.","Use the claim/assign workflow for reassignment instead of raw updates.","Handle storage.ErrAlreadyClaimed explicitly in update paths used by automation."],"tags":["go","storage","assignee","conflict"],"backgroundTag":"issue-already-claimed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}