{"record":{"id":"b0d229da7d4a8fe3","repo":"gastownhall/beads","slug":"failed-to-unclaim-issue-w","errorCode":null,"errorMessage":"failed to unclaim issue: %w","messagePattern":"failed to unclaim issue: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/unclaim.go","lineNumber":86,"sourceCode":"\t// and rewrite row_lock. The predicate CASes on row_lock rather than\n\t// assignee (ga-5ksp5): ownership was already authorized above (or bypassed\n\t// by force) against the row read into oldIssue, and row_lock is rewritten\n\t// by every path that mutates status/assignee/started_at (see the\n\t// freshRowLock invariant in lease.go) — so requiring it to still equal\n\t// oldIssue.RowVersion detects a claim that changed hands (or was released,\n\t// or closed) between that read and this write exactly as precisely as the\n\t// old `assignee = <actor>` predicate did, without embedding a\n\t// spelling-sensitive string comparison in SQL. force does not exempt this\n\t// check: force only widens WHO may unclaim, not whether the row is still\n\t// the one we read.\n\tresult, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\tUPDATE %s\n\t\tSET assignee = '', status = 'open', updated_at = ?,\n\t\t    started_at = NULL, row_lock = ?\n\t\tWHERE id = ? AND status IN ('open', 'in_progress') AND row_lock = ?\n\t`, issueTable), now, freshRowLock(), id, oldIssue.RowVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to unclaim issue: %w\", err)\n\t}\n\n\trowsAffected, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get rows affected: %w\", err)\n\t}\n\n\tif rowsAffected == 0 {\n\t\t// The pre-checks passed, so a 0-row result means the row changed\n\t\t// underneath us: re-read to disambiguate an ownership change from a\n\t\t// status change. actorMatches, not verbatim, mirrors the precheck above.\n\t\tcurrent, gerr := GetIssueInTx(ctx, tx, id)\n\t\tif gerr != nil {\n\t\t\treturn fmt.Errorf(\"failed to unclaim issue %s: no matching row\", id)\n\t\t}\n\t\tif !force && !actorMatches(current.Assignee, actor) {\n\t\t\treturn fmt.Errorf(\"%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)\",\n\t\t\t\tstorage.ErrNotOwner, id, current.Assignee)","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/unclaim.go#L68-L104","documentation":"UnclaimIssueInTx wraps the driver's UPDATE error with this message. The UPDATE atomically clears assignee, resets status to open, and bumps the row lock guarded by the previously-read row version; a failure means the database refused or failed the write, and the transaction should roll back leaving the claim intact.","triggerScenarios":"The UPDATE ... WHERE id = ? AND status IN ('open','in_progress') AND row_lock = ? statement fails: DB connection loss, aborted transaction, driver/constraint error, deadlock, or storage backend failure.","commonSituations":"Dolt/server connection dropped mid-transaction; lock contention causing deadlock aborts; disk-full or driver-level storage failures.","solutions":["Read the wrapped cause (errors.Unwrap/errors.Is) to identify the driver error class.","Retry the whole unclaim operation with a fresh transaction on transient errors.","Check database health/connectivity and storage backend resources.","If persistent, inspect driver logs; never partially retry inside the same transaction."],"exampleFix":"// before\nerr := store.ReleaseIssue(ctx, id) // one-shot, fails hard on transient DB error\n// after\nfor i := 0; i < 3; i++ {\n\terr = store.ReleaseIssue(ctx, id)\n\tif err == nil || !isTransientDBErr(err) { break }\n\ttime.Sleep(backoff(i))\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := store.ReleaseIssue(ctx, id); err != nil {\n\tif isTransientDBErr(err) { /* retry whole op with fresh tx */ }\n\treturn fmt.Errorf(\"release %s: %w\", id, err)\n}","preventionTips":["Keep transactions short to reduce deadlock/connection windows.","Use bounded retry with backoff around whole operations, not partial statements.","Monitor Dolt/database health and connection pool limits."],"tags":["database","transaction","write-failure","claim-management"],"backgroundTag":"db-write-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}