gastownhall/beads · warning
ErrAlreadyClaimed
ErrAlreadyClaimed
Error message
issue already claimed
What it means
ErrAlreadyClaimed is returned when attempting to claim an issue that is already claimed by another user. The error message contains the current assignee. ClaimConflictError wraps this sentinel while reporting the current assignee and status read inside the transaction that lost the compare-and-set, so errors.Is still matches.
Source
Thrown at issueops/errors.go:12
package issueops
import (
"errors"
"fmt"
"github.com/steveyegge/beads/beadserrors"
)
// ErrAlreadyClaimed is returned when attempting to claim an issue that is already
// claimed by another user. The error message contains the current assignee.
var ErrAlreadyClaimed = errors.New("issue already claimed")
// ErrNotClaimable is returned when attempting to claim an issue that is not in a
// claimable state, such as closed, deferred, or already in progress without the
// same actor owning the claim.
var ErrNotClaimable = errors.New("issue not claimable")
// ClaimConflictError reports the state that refused a claim — the current
// assignee and status, read inside the same transaction that lost the
// compare-and-set. It wraps the refusal rather than replacing it, so the
// sentinel still matches, the refusal's carefully-worded prose survives
// byte-for-byte, and a caller can classify the conflict from typed fields
// instead of parsing that prose.
//
// Err is set by every implementation that returns this type; a Claimer whose
// same-transaction re-read fails returns the bare refusal instead.
type ClaimConflictError struct {
// IssueID names the issue that refused the claim.
IssueID stringView on GitHub (pinned to 71377f2769)
Solutions
- Pick a different, unclaimed issue from the pool.
- Coordinate with the current assignee (named in the error/ClaimConflictError fields) to have them release the claim.
- Have the current holder unclaim, then retry the claim.
- Use errors.Is(err, issueops.ErrAlreadyClaimed) and read ClaimConflictError fields to surface who holds it.
Example fix
// before
err := claimer.Claim(ctx, req) // fails: already claimed by alice
// after
var conflict *issueops.ClaimConflictError
if errors.As(err, &conflict) {
fmt.Printf("issue held by %s (status %s)\n", conflict.Assignee, conflict.Status)
} else if errors.Is(err, issueops.ErrAlreadyClaimed) {
// choose another issue
} Defensive patterns
Strategy: type-guard
Validate before calling
iss, _ := store.GetIssue(ctx, id)
if iss.Assignee != "" && iss.Assignee != actor {
// skip: already claimed by someone else
} Type guard
var conflict *issueops.ClaimConflictError
if errors.As(err, &conflict) {
holder := conflict.Assignee
_ = holder
} Try / catch
if errors.Is(err, issueops.ErrAlreadyClaimed) {
var c *issueops.ClaimConflictError
errors.As(err, &c) // show current assignee from typed fields
} Prevention
- Fresh-read assignee before claiming in scripts.
- Handle claim races as normal in shared pools; pick another issue.
- Surface the current assignee from ClaimConflictError, not the raw message.
When it happens
Trigger: Calling the claimer (claim flow / issue operations update with claim semantics) on an issue whose assignee is set to someone other than the requesting actor; two actors racing to claim the same pool-assigned issue; RunClaimer test paths exercising foreign-holder refusals.
Common situations: Team members grabbing work from a shared pool simultaneously; a stale local view of an unassigned issue; automated agents competing for tasks.
Related errors
- ErrAlreadyClaimed
- failed to get current claim state: %w
- ErrNotClaimable
- get claimed issue: %w
- %w: %s is held by %q, expected %q
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/2e1b2ecec5b722aa.
Report an issue: GitHub.