{"record":{"id":"cdffd9de5590c875","repo":"gastownhall/beads","slug":"w-claim-next-does-not-take-an-offset","errorCode":null,"errorMessage":"%w: claim next does not take an offset","messagePattern":"%w: claim next does not take an offset","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/claim_next.go","lineNumber":32,"sourceCode":"// what the CLI's own per-command commit wrote before the claim moved onto the\n// role. It is the only spelling: the id it names is the one the claim WON, so\n// no caller could have composed it before the call.\nfunc ClaimNextCommitMessage(issueID string) string {\n\treturn \"bd: claim ready \" + issueID\n}\n\n// ValidateClaimNextRequest applies the request rules every ReadyClaimer\n// implementation shares. It lives here rather than in each of them because a\n// rule enforced on one backend and not the other is not a contract.\nfunc ValidateClaimNextRequest(request publicops.ClaimNextRequest) error {\n\tif request.Actor == \"\" {\n\t\treturn fmt.Errorf(\"%w: claim next requires an actor\", storage.ErrValidation)\n\t}\n\tif request.Filter.Limit != nil {\n\t\treturn fmt.Errorf(\"%w: claim next does not take a limit\", storage.ErrValidation)\n\t}\n\tif request.Filter.Offset != 0 {\n\t\treturn fmt.Errorf(\"%w: claim next does not take an offset\", storage.ErrValidation)\n\t}\n\t// Brief is refused for the reason Limit and Offset are, and the reason is\n\t// sharper here: a claim does not read its row through the page's query at\n\t// all. ExecuteClaimNext refetches the winning row whole (GetIssueInTx) and\n\t// hydrates its counts itself, so the projection has nothing to apply to,\n\t// and a claim that accepted the field would answer a MUTATING request with\n\t// a fully-hydrated row carrying IsLitePartial=false — the caller's only\n\t// signal that it did not get what it asked for, saying it did.\n\tif request.Filter.Brief {\n\t\treturn fmt.Errorf(\"%w: claim next does not take a projection\", storage.ErrValidation)\n\t}\n\treturn nil\n}\n\n// ExecuteClaimNext claims the first ready issue matching filter in tx, hydrates\n// it in that same transaction, and reports the durable tables changed.\n//\n// It takes the filter rather than the public request because this package","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/claim_next.go#L14-L50","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfilter := lastReadyFilter // Offset: 25 from listing page 2\nreq := publicops.ClaimNextRequest{Actor: actor, Filter: filter}\n// after\nfilter := lastReadyFilter\nfilter.Limit = nil\nfilter.Offset = 0\nfilter.Brief = false\nreq := publicops.ClaimNextRequest{Actor: actor, Filter: filter}","handlingStrategy":"validation","validationCode":"func validForClaim(f types.WorkFilter) error {\n\tif f.Offset != 0 {\n\t\treturn fmt.Errorf(\"claim filter must not set Offset (got %d)\", f.Offset)\n\t}\n\tif f.Limit != nil {\n\t\treturn fmt.Errorf(\"claim filter must not set Limit\")\n\t}\n\treturn nil\n}","typeGuard":"func claimSafe(f types.WorkFilter) bool { return f.Offset == 0 && f.Limit == nil && !f.Brief }","tryCatchPattern":null,"preventionTips":["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"],"tags":["validation","claim-next","pagination"],"backgroundTag":"claim-next-pagination-not-allowed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}