{"record":{"id":"5df46d19f0805972","repo":"gastownhall/beads","slug":"w-issue-s-5df46d","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"error_code","errorClass":"storage.ErrNotFound","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update_cas.go","lineNumber":47,"sourceCode":"// fix already shipped for UnclaimIssueInTx's SQL-CAS predicate and\n// AuthorizeAssigneeTransferWithPools; this was the third, previously-split\n// verbatim-comparison surface (ga-5ksp5, gate review on #5439).\n//\n//nolint:gosec // G201: table name comes from WispTableRouting (hardcoded constants)\nfunc CheckExpectedFieldsInTx(ctx context.Context, tx DBTX, id string, expectedAssignee, expectedStatus *string) error {\n\tif expectedAssignee == nil && expectedStatus == nil {\n\t\treturn nil\n\t}\n\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":29,"sourceCodeEnd":60,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update_cas.go#L29-L60","documentation":"CheckExpectedFieldsInTx performs compare-and-swap style validation of expected assignee/status before an update. When the issue row does not exist (sql.ErrNoRows on the SELECT), it returns storage.ErrNotFound wrapped with the issue id so callers can branch on a sentinel via errors.Is.","triggerScenarios":"ExecuteUpdate is called with ExpectedFields (expected assignee or status) for an issue id that has no row in the issues table — the issue was deleted, never created, or the ID is wrong.","commonSituations":"Stale client cache referencing a deleted issue; optimistic-concurrency CAS flows racing with a concurrent delete; typo'd or truncated ID passed to a scripted update.","solutions":["Confirm the issue ID exists before the CAS update","Handle storage.ErrNotFound explicitly via errors.Is and surface 'issue not found' rather than a generic CAS failure","Re-sync local state (bd sync / refetch) if the issue was expected to exist","Fix the ID source — scripts that parse IDs from output may pick up stale values"],"exampleFix":"// before\nerr := ExecuteUpdate(ctx, tx, id, updates, &ExpectedFields{Status: &s}) // generic failure\n// after\nif err := CheckExpectedFieldsInTx(ctx, tx, id, expStatus, expAssignee); err != nil {\n    if errors.Is(err, storage.ErrNotFound) { return fmt.Errorf(\"issue %s was deleted; aborting CAS update\", id) }\n}","handlingStrategy":"try-catch","validationCode":"if _, err := store.GetIssue(ctx, id); err != nil {\n    return fmt.Errorf(\"cannot CAS-update missing issue %s\", id)\n}","typeGuard":null,"tryCatchPattern":"err := ExecuteUpdate(ctx, tx, id, updates, exp)\nif errors.Is(err, storage.ErrNotFound) {\n    return fmt.Errorf(\"issue %s no longer exists; aborting CAS\", id)\n}","preventionTips":["Refresh issue IDs from a live query, not stale caches","Expect deletion races in multi-agent setups and code for ErrNotFound","Validate ID format/length before issuing CAS calls"],"tags":["go","not-found","cas","concurrency"],"backgroundTag":"issue-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}