{"record":{"id":"5126dd5f9a837c40","repo":"gastownhall/beads","slug":"w-s-has-status-q-expected-q","errorCode":null,"errorMessage":"%w: %s has status %q, expected %q","messagePattern":"%w: (.+?) has status %q, expected %q","errorType":"error_code","errorClass":"storage.ErrStatusMismatch","httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/update_cas.go","lineNumber":56,"sourceCode":"\tisWisp := IsActiveWispInTx(ctx, tx, id)\n\tissueTable, _, _, _ := WispTableRouting(isWisp)\n\n\tvar assignee sql.NullString\n\tvar status string\n\terr := tx.QueryRowContext(ctx,\n\t\tfmt.Sprintf(\"SELECT assignee, status FROM %s WHERE id = ?\", issueTable), id,\n\t).Scan(&assignee, &status)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read assignee/status for %s: %w\", id, err)\n\t}\n\tif expectedAssignee != nil && !actorMatches(assignee.String, *expectedAssignee) {\n\t\treturn fmt.Errorf(\"%w: %s is held by %q, expected %q\", storage.ErrAssigneeMismatch, id, assignee.String, *expectedAssignee)\n\t}\n\tif expectedStatus != nil && status != *expectedStatus {\n\t\treturn fmt.Errorf(\"%w: %s has status %q, expected %q\", storage.ErrStatusMismatch, id, status, *expectedStatus)\n\t}\n\treturn nil\n}\n","sourceCodeStart":38,"sourceCodeEnd":60,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update_cas.go#L38-L60","documentation":"The status-mismatch branch of the CAS check: the caller declared an expected status but the row currently has a different one. storage.ErrStatusMismatch is returned with actual vs expected so state-machine transitions (e.g. close only if open) can fail safely instead of performing an invalid transition.","triggerScenarios":"ExecuteUpdate with ExpectedFields.Status (commonly 'open' before a close) while the issue is already closed/reopened/in another status — usually because another writer changed it first.","commonSituations":"Double-close attempts from two agents; retry after a partially-completed close; scripts assuming 'open' on issues already resolved; workflow automation racing human edits.","solutions":["Check errors.Is(err, storage.ErrStatusMismatch) and treat the current status (shown in the message) as authoritative","Re-fetch the issue and skip the transition if it already reached the target state (idempotent handling)","Retry only after reconciling with the actual status, never in a tight loop","Use CheckVersionInTx/row-version CAS when the whole row, not just status, must be unchanged"],"exampleFix":"// before\ncloseIssue(id) // fails on second run\n// after\nerr := closeIssue(id)\nif errors.Is(err, storage.ErrStatusMismatch) {\n    iss, _ := store.GetIssue(ctx, id)\n    if iss.Status == \"closed\" { return nil } // already closed; idempotent\n}","handlingStrategy":"try-catch","validationCode":"iss, _ := store.GetIssue(ctx, id)\nif iss.Status != \"open\" { return fmt.Errorf(\"issue %s is %s; refusing transition\", id, iss.Status) }","typeGuard":null,"tryCatchPattern":"err := closeIssueChecked(ctx, tx, id, ver)\nif errors.Is(err, storage.ErrStatusMismatch) {\n    return nil // already in/checked past target status; idempotent no-op\n}","preventionTips":["Make state transitions idempotent: treat already-done as success","Pre-check status before CAS, but still handle the race","Use row-version CAS when multiple fields must be unchanged"],"tags":["go","cas","concurrency","status","conflict"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}