gastownhall/beads · error

%w: claim next does not take an offset

Error message

%w: claim next does not take an offset

What it means

ValidateClaimNextRequest rejects a ClaimNextRequest whose WorkFilter carries a non-zero Offset. Claim-next is a mutating operation that claims the FIRST ready issue; a paged offset has no meaning because there is no page — the winner is selected and then refetched whole via GetIssueInTx. Any non-zero Offset is therefore treated as an invalid request and wrapped in storage.ErrValidation.

Source

Thrown at internal/storage/issueops/claim_next.go:32

// what the CLI's own per-command commit wrote before the claim moved onto the
// role. It is the only spelling: the id it names is the one the claim WON, so
// no caller could have composed it before the call.
func ClaimNextCommitMessage(issueID string) string {
	return "bd: claim ready " + issueID
}

// ValidateClaimNextRequest applies the request rules every ReadyClaimer
// implementation shares. It lives here rather than in each of them because a
// rule enforced on one backend and not the other is not a contract.
func ValidateClaimNextRequest(request publicops.ClaimNextRequest) error {
	if request.Actor == "" {
		return fmt.Errorf("%w: claim next requires an actor", storage.ErrValidation)
	}
	if request.Filter.Limit != nil {
		return fmt.Errorf("%w: claim next does not take a limit", storage.ErrValidation)
	}
	if request.Filter.Offset != 0 {
		return fmt.Errorf("%w: claim next does not take an offset", storage.ErrValidation)
	}
	// Brief is refused for the reason Limit and Offset are, and the reason is
	// sharper here: a claim does not read its row through the page's query at
	// all. ExecuteClaimNext refetches the winning row whole (GetIssueInTx) and
	// hydrates its counts itself, so the projection has nothing to apply to,
	// and a claim that accepted the field would answer a MUTATING request with
	// a fully-hydrated row carrying IsLitePartial=false — the caller's only
	// signal that it did not get what it asked for, saying it did.
	if request.Filter.Brief {
		return fmt.Errorf("%w: claim next does not take a projection", storage.ErrValidation)
	}
	return nil
}

// ExecuteClaimNext claims the first ready issue matching filter in tx, hydrates
// it in that same transaction, and reports the durable tables changed.
//
// It takes the filter rather than the public request because this package

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set request.Filter.Offset to 0 (the zero value) before calling ClaimNext
  2. Build a fresh types.WorkFilter for the claim instead of reusing a paginated listing filter
  3. Use the Limit field's absence as a checklist: if the filter came from a paging context, reset Limit and Offset and clear Brief together

Example fix

// before
filter := lastReadyFilter // Offset: 25 from listing page 2
req := publicops.ClaimNextRequest{Actor: actor, Filter: filter}
// after
filter := lastReadyFilter
filter.Limit = nil
filter.Offset = 0
filter.Brief = false
req := publicops.ClaimNextRequest{Actor: actor, Filter: filter}
Defensive patterns

Strategy: validation

Validate before calling

func validForClaim(f types.WorkFilter) error {
	if f.Offset != 0 {
		return fmt.Errorf("claim filter must not set Offset (got %d)", f.Offset)
	}
	if f.Limit != nil {
		return fmt.Errorf("claim filter must not set Limit")
	}
	return nil
}

Type guard

func claimSafe(f types.WorkFilter) bool { return f.Offset == 0 && f.Limit == nil && !f.Brief }

Prevention

When it happens

Trigger: Calling ClaimNext (directly or via ValidateCloseBatchRequest) with request.Filter.Offset set to any non-zero value, typically reusing a WorkFilter built for a paged Ready/Read listing.

Common situations: Developers reuse the same WorkFilter struct for both listing ready issues (where Offset pages) and claiming one; an offset left over from a previous page of 'bd ready' results, or code copying pagination state from a UI/cursor, triggers this.

Related errors


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