{"record":{"id":"e20485c6d0d0223a","repo":"gastownhall/beads","slug":"db-claim-s-read-current-state-w","errorCode":null,"errorMessage":"db: Claim %s: read current state: %w","messagePattern":"db: Claim (.+?): read current state: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":500,"sourceCode":"\t\t\t`, table, rowLockClause, statusPredicate), args...)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn domain.ClaimRowResult{}, fmt.Errorf(\"db: Claim %s: %w\", id, err)\n\t\t}\n\t\trows, err = res.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn domain.ClaimRowResult{}, fmt.Errorf(\"db: Claim %s: rows affected: %w\", id, err)\n\t\t}\n\t}\n\n\tif rows == 0 {\n\t\tvar currentAssignee sql.NullString\n\t\tvar currentStatus types.Status\n\t\t//nolint:gosec // G201: table is one of two hardcoded constants\n\t\tif err := r.runner.QueryRowContext(ctx,\n\t\t\tfmt.Sprintf(\"SELECT assignee, status FROM %s WHERE id = ?\", table), id,\n\t\t).Scan(&currentAssignee, &currentStatus); err != nil {\n\t\t\treturn domain.ClaimRowResult{}, fmt.Errorf(\"db: Claim %s: read current state: %w\", id, err)\n\t\t}\n\t\tassignee := \"\"\n\t\tif currentAssignee.Valid {\n\t\t\tassignee = currentAssignee.String\n\t\t}\n\t\treturn domain.ClaimRowResult{\n\t\t\tUpdated:               false,\n\t\t\tCurrentAssignee:       assignee,\n\t\t\tCurrentAssigneeIsPool: slices.Contains(pools, assignee),\n\t\t\tCurrentStatus:         currentStatus,\n\t\t\tStartedAtWasZero:      startedWasZero,\n\t\t\tOldIssue:              oldIssue,\n\t\t}, nil\n\t}\n\n\t// Grant the lease in the ephemeral leases table, mirroring\n\t// issueops.ClaimIssueInTx. Wisps are never leased. This dual must stay in\n\t// lockstep with the primary path (see the row_lock comment above).","sourceCodeStart":482,"sourceCodeEnd":518,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L482-L518","documentation":"This error wraps a failure to re-read the issue's current assignee/status after the claim CAS matched zero rows (the lost-race path). Claim uses this SELECT to build a ClaimRowResult describing who actually holds the issue; if the read fails, Claim aborts with this wrapped error.","triggerScenarios":"Calling Claim where another actor won the race (rows == 0) and the follow-up SELECT assignee, status ... WHERE id = ? fails — connection loss, context cancellation, or the issue being deleted between the UPDATE and SELECT.","commonSituations":"Two agents claiming the same issue concurrently; issue deleted by another process between CAS and re-read; transient network errors to a remote database.","solutions":["Inspect the wrapped cause: if ErrNotFound, the issue was deleted concurrently — refresh the issue list.","Retry the claim against current state.","Check for concurrent deletion workflows colliding with claims.","Verify connection stability if the cause is a network error."],"exampleFix":"// before\nres, err := store.Claim(ctx, id, actor, opts) // lost race; opaque read failure\n// after\nres, err := store.Claim(ctx, id, actor, opts)\nif err != nil && res.CurrentAssignee != \"\" {\n    log.Printf(\"issue %s held by %s\", id, res.CurrentAssignee)\n}","handlingStrategy":"try-catch","validationCode":"issue, err := store.Get(ctx, id, opts)\nif err != nil { return err }\nif issue.Assignee != \"\" && issue.Assignee != actor { return fmt.Errorf(\"taken by %s\", issue.Assignee) }","typeGuard":"func claimLostRace(err error, res domain.ClaimRowResult) bool {\n    return res.CurrentAssignee != \"\" && res.CurrentAssignee != actor\n}","tryCatchPattern":"res, err := store.Claim(ctx, id, actor, opts)\nif err != nil && strings.Contains(err.Error(), \"read current state\") {\n    // lost race or concurrent delete; re-fetch and decide\n    fresh, gerr := store.Get(ctx, id, opts)\n    if gerr != nil { return gerr }\n    _ = fresh\n}","preventionTips":["Treat claims as CAS: always handle the lost-race branch.","Avoid deleting issues while claim workflows are active.","Re-fetch current state instead of retrying the same claim blindly.","Log the current holder from ClaimRowResult for diagnostics."],"tags":["database","claim","race-condition"],"backgroundTag":"claim-lost-race","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}