gastownhall/beads · error

a ready count does not take a limit

Error message

a ready count does not take a limit

What it means

BuildReadyCountFilter refuses a ReadyRequest that carries a Limit, because a COUNT of the ready set must be unbounded — any limit would make the count answer 'how many up to N' rather than the set size. The refusal wraps issueops.ErrValidation with the message as a %w suffix.

Source

Thrown at internal/workapi/ready.go:118

		return filter, fmt.Errorf("invalid sort policy '%s'. Valid values: hybrid, priority, oldest%.0w", in.Sort, issueops.ErrValidation)
	}
	return filter, nil
}

// BuildReadyCountFilter turns a ready request into the storage-level filter a
// COUNT of the ready set runs against: BuildReadyFilter's filter with the page
// removed, and the single definition of what `bd ready`'s published total means.
//
// It refuses a request carrying a page. issueops.ReadyCounter.CountReady
// promises its answer equals len(Reader.Ready(r with Limit=0).Items), and a
// Limit would make that "how many of the first N" while an Offset would
// subtract the rows it skipped from the size of a set that still holds them.
//
// The zeroed limit is set on a LOCAL copy: a nil Limit means the shared ready
// default at BuildReadyFilter, so an unlimited count has to say so explicitly.
func BuildReadyCountFilter(in issueops.ReadyRequest) (types.WorkFilter, error) {
	if in.Limit != nil {
		return types.WorkFilter{}, fmt.Errorf("%w: a ready count does not take a limit", issueops.ErrValidation)
	}
	if in.Offset != 0 {
		return types.WorkFilter{}, fmt.Errorf("%w: a ready count does not take an offset", issueops.ErrValidation)
	}
	unlimited := 0
	counted := in
	counted.Limit = &unlimited
	// Brief is CARRIED, not refused and not cleared, which is the opposite of
	// what ClaimNext does with it. A count reads no field of any row, so the
	// projection cannot make the number wrong; and the count is not always
	// cheap enough for that to be the end of it. The unit-of-work seam has no
	// COUNT(*) over the ready predicate and sizes the set by running the
	// unbounded page and taking its length (uow/ready_counter.go), so clearing
	// the field here would hydrate every heavy column of the whole ready set to
	// answer `bd ready --brief`, which is the cost the projection exists to
	// avoid and larger than the page it was asked for. Carrying it also keeps
	// the count filter what this builder says it is: the listing's filter with
	// the PAGE removed, and nothing else removed.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Nil out Limit on the request before calling BuildReadyCountFilter
  2. Build a separate ReadyRequest for counting with no Limit set
  3. At the handler layer, ignore/reject limit params for the count route

Example fix

// before
req.Limit = &pageLimit
countFilter, err := BuildReadyCountFilter(req)
// after
req.Limit = nil
countFilter, err := BuildReadyCountFilter(req)
Defensive patterns

Strategy: validation

Validate before calling

if req.Limit != nil {
	return errors.New("count requests must not set a limit")
}

Try / catch

filter, err := BuildReadyCountFilter(req)
if err != nil && errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "does not take a limit") {
	// clear Limit and rebuild
}

Prevention

When it happens

Trigger: Calling BuildReadyCountFilter with in.Limit != nil, e.g. a client reusing the same ReadyRequest it built for a paged `bd ready` call for the count endpoint.

Common situations: Shared request builders feeding both the list and count paths; an HTTP handler binding the page limit from the query string into the count request; copying a ReadyRequest struct wholesale without clearing Limit.

Related errors


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