gastownhall/beads · error

%w: claim next does not take a projection

Error message

%w: claim next does not take a projection

What it means

ValidateClaimNextRequest rejects ClaimNextRequest.Filter.Brief=true. A claim does not read its row through the paged query at all: ExecuteClaimNext refetches the winning row whole and hydrates counts itself, so a 'brief' projection has nothing to apply to. Accepting it would return a fully-hydrated row with IsLitePartial=false, falsely signaling the caller got the projection it asked for.

Source

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

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
// speaks WorkFilter: the request-to-filter translation is internal/workapi's,
// and the same builder that answers Reader.Ready is what the accessor
// implementations run before calling in here. That is what keeps the claim's
// predicate and the listing's predicate the same predicate.
//
// A nil claim with a nil error is the ordinary empty-front outcome. It reports
// no changed tables, so the caller's transaction commits nothing and no
// history entry is recorded.
func ExecuteClaimNext(ctx context.Context, tx *sql.Tx, actor string, filter types.WorkFilter) (publicops.ClaimNextResult, ChangedTables, error) {
	claimed, err := ClaimReadyIssueInTx(ctx, tx, filter, actor)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set request.Filter.Brief = false before calling ClaimNext
  2. Use a dedicated WorkFilter for claims, separate from listing filters
  3. If the caller wants a light row, strip fields from the returned ClaimNextResult client-side instead of asking the storage layer to project

Example fix

// before
req.Filter = readyListFilter // Brief: true
result, err := store.ClaimNext(ctx, req)
// after
req.Filter = readyListFilter
req.Filter.Brief = false // claims always return fully hydrated rows
result, err := store.ClaimNext(ctx, req)
Defensive patterns

Strategy: validation

Validate before calling

if req.Filter.Brief {
	return errors.New("ClaimNext always returns a fully hydrated row; Brief is not supported")
}

Type guard

func noProjection(f types.WorkFilter) bool { return !f.Brief }

Prevention

When it happens

Trigger: Calling ClaimNext with request.Filter.Brief set to true, usually by reusing a filter from a brief/lite ready listing.

Common situations: Code paths that request brief rows to save bandwidth for listings then reuse the same filter struct for claiming; CLI flags like --brief or a lite-mode toggle left enabled when claiming work.

Related errors


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