gastownhall/beads · error

a ready count does not take an offset

Error message

a ready count does not take an offset

What it means

BuildReadyCountFilter likewise refuses a non-zero Offset: skipping rows would require subtracting them from a set that still holds them, which a count cannot express. It wraps issueops.ErrValidation so callers classify via errors.Is.

Source

Thrown at internal/workapi/ready.go:121

}

// 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.
	return BuildReadyFilter(counted)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set Offset to 0 on the request passed to BuildReadyCountFilter
  2. Construct the count request separately from the paged listing request
  3. If you need 'remaining count after skip', fetch the unbounded count and subtract the offset client-side

Example fix

// before
req.Offset = 20
countFilter, err := BuildReadyCountFilter(req)
// after
req.Offset = 0
countFilter, err := BuildReadyCountFilter(req) // subtract 20 from the result if needed
Defensive patterns

Strategy: validation

Validate before calling

if req.Offset != 0 {
	return errors.New("count requests must not set an offset")
}

Try / catch

filter, err := BuildReadyCountFilter(req)
if err != nil && errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "does not take an offset") {
	// zero Offset and rebuild; subtract manually if needed
}

Prevention

When it happens

Trigger: Calling BuildReadyCountFilter with in.Offset != 0, e.g. forwarding the offset of the current page from a paged ready listing into the count request.

Common situations: Handler code copying the list request (offset included) into the count call; clients computing total-after-skip counts; pagination middleware injecting Offset into every request uniformly.

Related errors


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