{"record":{"id":"4e750d42cf900586","repo":"gastownhall/beads","slug":"db-issuesqlrepository-unclaimissue-w","errorCode":null,"errorMessage":"db: IssueSQLRepository.UnclaimIssue: %w","messagePattern":"db: IssueSQLRepository\\.UnclaimIssue: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":1216,"sourceCode":"func (r *issueSQLRepositoryImpl) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {\n\tout, err := issueops.GetStaleIssuesInTx(ctx, r.runner, filter)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.GetStaleIssues: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {\n\tout, err := issueops.GetEpicsEligibleForClosureInTx(ctx, r.runner)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.GetEpicsEligibleForClosure: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) UnclaimIssue(ctx context.Context, id, actor string, force bool) error {\n\tif err := issueops.UnclaimIssueInTx(ctx, r.runner, id, actor, force); err != nil {\n\t\treturn fmt.Errorf(\"db: IssueSQLRepository.UnclaimIssue: %w\", err)\n\t}\n\treturn nil\n}\n\n// UnclaimIssueIfAssignee runs the classic compare-and-swap release against this\n// runner. Like UnclaimIssue it takes no IssueTableOpts: issueops routes the\n// write to the issues or wisps tables from the row itself, so a wisp's claim is\n// released against the wisp tables on both backends. The mismatch verdict\n// (storage.ErrAssigneeMismatch, nothing written) is produced by the shared\n// helper, not restated here.\nfunc (r *issueSQLRepositoryImpl) UnclaimIssueIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {\n\tif err := issueops.UnclaimIssueIfAssigneeInTx(ctx, r.runner, id, actor, expectedAssignee); err != nil {\n\t\treturn fmt.Errorf(\"db: IssueSQLRepository.UnclaimIssueIfAssignee: %w\", err)\n\t}\n\treturn nil\n}\n\n// HeartbeatIssue refreshes the lease on an issue actor holds in_progress,","sourceCodeStart":1198,"sourceCodeEnd":1234,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L1198-L1234","documentation":"Wraps a failure from issueops.UnclaimIssueInTx when releasing an issue/wisp claim unconditionally. The helper routes to issues or wisps tables automatically; failures can be lookup errors, SQL exec errors, or 'not claimed' style validation errors from finishUnclaimInTx. The wrapper adds repository context only.","triggerScenarios":"Calling UnclaimIssue(ctx, id, actor, force) when: the issue lookup fails (GetIssueInTx error), the UPDATE/DELETE of the lease row fails at the driver level, the ID is empty/invalid, or finishing the unclaim (event write, rows-affected check) errors.","commonSituations":"Concurrent modification (claim already released) surfacing as an unexpected rows-affected result, connection loss mid-transaction, or targeting an ID that was deleted between check and write.","solutions":["Read the wrapped cause to distinguish lookup/exec failures from validation failures.","Check the issue still exists and has an active claim before releasing (or handle idempotently).","Retry on transient driver errors; the transaction rolls back cleanly.","Verify the lease/claim tables exist and the schema is current."],"exampleFix":"// before\nif err := repo.UnclaimIssue(ctx, id, actor, false); err != nil { return err }\n// after\nif err := repo.UnclaimIssue(ctx, id, actor, false); err != nil {\n    if strings.Contains(err.Error(), \"not claimed\") { return nil } // already released\n    return fmt.Errorf(\"unclaim %s: %w\", id, err)\n}","handlingStrategy":"try-catch","validationCode":"// confirm the claim exists before releasing\nissue, err := repo.Get(ctx, id)\nif err != nil || issue.Assignee == \"\" { return nil } // nothing to unclaim","typeGuard":null,"tryCatchPattern":"if err := repo.UnclaimIssue(ctx, id, actor, false); err != nil {\n    if isAlreadyReleased(err) { return nil } // idempotent no-op\n    return fmt.Errorf(\"unclaim %s: %w\", id, err)\n}","preventionTips":["Make unclaim idempotent in callers (already-released is success).","Pass the same actor string used at Claim time.","Check issue existence before mutation.","Use force only when you intend to override staleness checks."],"tags":["database","claim","lease"],"backgroundTag":"lease-release-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}