gastownhall/beads · error

claim: actor and issue ID must not be empty

Error message

claim: actor and issue ID must not be empty

What it means

Claim validates that the ClaimRequest carries both an actor identity and an issue ID before opening a transaction. The unit-of-work seam refuses to run ClaimIssue with empty identifiers because such a call can never match a row and would waste a transaction. It is thrown as a validationError, i.e. an input-shape problem, not a storage problem.

Source

Thrown at internal/storage/uow/issue_claimer.go:54

func NewIssueClaimer(provider UnitOfWorkProvider) (publicops.Claimer, error) {
	if isNilUnitOfWorkProvider(provider) {
		return nil, fmt.Errorf("new issue claimer: unit-of-work provider must not be nil")
	}
	return &issueClaimer{provider: provider}, nil
}

var _ publicops.Claimer = (*issueClaimer)(nil)

// Claim runs the compare-and-set in a retried unit-of-work transaction.
//
// RETRY LIVES HERE, not in the caller. RunTxResult redoes the WHOLE attempt in
// a FRESH unit of work when one loses Dolt's commit-time merge, because
// re-committing a session the server already rolled back is a lost write. That
// is the same place every other verb on this seam keeps it, and it is what
// lets the role promise that a lost merge is retried rather than surfaced.
func (c *issueClaimer) Claim(ctx context.Context, request publicops.ClaimRequest) (publicops.ClaimResult, error) {
	if request.Actor == "" || request.IssueID == "" {
		return publicops.ClaimResult{}, validationError(fmt.Errorf("claim: actor and issue ID must not be empty"))
	}
	return RunTxResult(ctx, c.provider, func(ctx context.Context, uw UnitOfWork) (publicops.ClaimResult, string, error) {
		uc := uw.IssueUseCase()
		claimed, err := uc.ClaimIssue(ctx, request.IssueID, request.Actor)
		if err != nil {
			return publicops.ClaimResult{}, "", classifyClaimError(ctx, uc, request.IssueID, err)
		}
		// Read back INSIDE this transaction, so the result describes the row
		// this CAS wrote and not a later writer's.
		issue, err := uc.GetIssue(ctx, request.IssueID)
		if err != nil {
			return publicops.ClaimResult{}, "", err
		}
		if issue == nil {
			// A miss with a nil error is the other shape a not-found takes at
			// this seam; normalize it rather than dereferencing nil.
			return publicops.ClaimResult{}, "", fmt.Errorf("%w: issue %s", publicops.ErrNotFound, request.IssueID)
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set both request.Actor and request.IssueID before calling Claim.
  2. If the issue ID comes from user input, validate non-empty at the CLI/API boundary.
  3. Verify the struct literal constructing ClaimRequest actually assigns both fields, not just the issue ID.

Example fix

// before
res, err := uow.Claim(ctx, publicops.ClaimRequest{IssueID: id})
// after
res, err := uow.Claim(ctx, publicops.ClaimRequest{Actor: actor, IssueID: id})
Defensive patterns

Strategy: validation

Validate before calling

if req.Actor == "" || req.IssueID == "" {
    return fmt.Errorf("claim requires actor and issue ID")
}

Type guard

func validClaimRequest(r publicops.ClaimRequest) bool {
    return r.Actor != "" && r.IssueID != ""
}

Prevention

When it happens

Trigger: Calling Claim(ctx, publicops.ClaimRequest{}) with Actor unset, IssueID unset, or both empty strings.

Common situations: Forgotten request wiring when building ClaimRequest programmatically; a variable holding the issue ID that was never populated; refactors that renamed Actor fields and left the assignment behind.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/549b4ac66db7ebab. Report an issue: GitHub.