{"record":{"id":"9d8095d5919c7c75","repo":"gastownhall/beads","slug":"failed-to-get-current-claim-state-w","errorCode":null,"errorMessage":"failed to get current claim state: %w","messagePattern":"failed to get current claim state: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/claim.go","lineNumber":141,"sourceCode":"\t\t\tresult, err = tx.ExecContext(ctx, fmt.Sprintf(`\n\t\t\t\tUPDATE %s\n\t\t\t\tSET assignee = ?, status = 'in_progress', updated_at = ?, %s\n\t\t\t\tWHERE id = ? AND row_lock = ? AND status IN (%s)\n\t\t\t`, issueTable, rowLockClause, statusPlaceholders), args...)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to claim issue: %w\", err)\n\t\t}\n\t\trowsAffected, err = result.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get rows affected: %w\", err)\n\t\t}\n\t}\n\n\tif rowsAffected == 0 {\n\t\tassignee, currentStatus, err := readClaimStateInTx(ctx, tx, issueTable, id)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get current claim state: %w\", err)\n\t\t}\n\t\t// Idempotent: if already claimed in_progress by the same actor —\n\t\t// including a spelling difference across layers (ga-wzl83) — treat as\n\t\t// success. This supports agent retry workflows where claim may be\n\t\t// called multiple times after transient failures (GH#8).\n\t\tif actorMatches(assignee, actor) && currentStatus == types.StatusInProgress {\n\t\t\treturn &ClaimResult{OldIssue: oldIssue, IsWisp: isWisp}, nil\n\t\t}\n\t\t// The refusal carries the state that lost the CAS, read just above in\n\t\t// THIS transaction, so a caller learns who won without parsing the\n\t\t// message. The typed wrapper carries the fields; the PROSE is composed\n\t\t// here, because ClaimConflictError.Error() passes its wrapped refusal\n\t\t// through byte-for-byte — a bare sentinel would reach the caller as\n\t\t// \"issue already claimed\" with the holder dropped and\n\t\t// beads.ParseClaimConflict unable to recover it. The fragments are the\n\t\t// storage layer's exported ones, which is what keeps the parser and\n\t\t// this producer in step. The sentinel stays matchable through both\n\t\t// wraps, which errors.Is, ParseClaimConflict and the proxied batch","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/claim.go#L123-L159","documentation":"When the claim CAS affects 0 rows, ClaimIssueInTx reads the issue's current assignee/status inside the same transaction to decide between idempotent success and a refusal. This error means that state read (SELECT assignee, status) failed, so the claim cannot be disambiguated. The claim is abandoned and the underlying SQL error is wrapped.","triggerScenarios":"rowsAffected == 0 and readClaimStateInTx's SELECT fails — e.g. row deleted concurrently between the read and the CAS, connection error, or context canceled mid-transaction.","commonSituations":"Racing agents where one closes/deletes the issue while another claims it; a canceled CLI context (Ctrl-C) hitting the follow-up SELECT; DB connectivity loss during a burst of claims.","solutions":["Retry the claim; the row may just have been deleted/raced","Inspect the wrapped driver error for connection/context issues","If the issue no longer exists, refresh the issue list (bd ready) before claiming"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// confirm the issue still exists before claiming\niss, err := GetIssueInTx(ctx, tx, id)\nif err != nil || iss == nil { skip }","typeGuard":null,"tryCatchPattern":"res, err := ClaimIssueInTx(ctx, tx, id, actor)\nif err != nil {\n    var conflict *publicops.ClaimConflictError\n    if errors.As(err, &conflict) { return handleConflict(conflict) }\n    return retryClaim(ctx, id, actor) // transient read failure path\n}","preventionTips":["Keep claim transactions short to shrink the race window","Refresh issue lists (bd ready) after repeated failures — the row may be gone","Never claim in a loop with a canceled/expiring context"],"tags":["database","concurrency","storage","claim"],"backgroundTag":"claim-race-condition","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}