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 packageView on GitHub (pinned to 71377f2769)
Solutions
- Set request.Filter.Offset to 0 (the zero value) before calling ClaimNext
- Build a fresh types.WorkFilter for the claim instead of reusing a paginated listing filter
- 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
- Construct claim filters fresh rather than reusing listing filters
- Add a helper that zeroes pagination fields before any claim call
- Cover ValidateClaimNextRequest rejections in unit tests when building filters dynamically
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
- %w: claim next does not take a limit
- %w: claim next does not take a projection
- invalid limit %d: a query limit must be zero or greater; 0 m
- invalid offset %d: a query offset must be zero or greater
- invalid offset: an offset cannot be combined with a display
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/cdffd9de5590c875.
Report an issue: GitHub.