{"record":{"id":"a03a3f0466a1c54e","repo":"gastownhall/beads","slug":"w-issue-s-a03a3f","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":80,"sourceCode":"// transaction rolls back with the issue unchanged (a true compare-and-swap).\n// row_lock only tracks lifecycle/ownership writes (status, assignee, started_at),\n// so this guards against a concurrent lifecycle change — not against concurrent\n// label, dependency, rename, or is_blocked writes that leave row_lock untouched\n// (see the freshRowLock invariant in lease.go).\nfunc CloseIssueCheckedInTx(ctx context.Context, tx DBTX, id, reason, actor, session string, force bool, expectedVersion *int64) (*CloseResult, error) {\n\tif expectedVersion != nil {\n\t\tif err := CheckVersionInTx(ctx, tx, id, *expectedVersion); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// Read the target before policy checks. A closed target can still have open\n\t// children, which refuses without Force just like an open target.\n\tclosed, targetColumn, found, err := isClosedInTx(ctx, tx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !found {\n\t\treturn nil, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t}\n\t// domain/db also supplies a *sql.DB Runner, whose independently pooled\n\t// statements cannot retain a savepoint. The shared UOW uses a pinned\n\t// *sql.Conn after START TRANSACTION, while embedded callers use *sql.Tx.\n\tif !closeCheckedSavepointEligible(tx) {\n\t\treturn closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)\n\t}\n\tsavepoint, err := createCloseCheckedSavepoint(ctx, tx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult, scopedErr := closeIssueCheckedAfterSavepoint(ctx, tx, id, reason, actor, session, force, closed, targetColumn)\n\tif scopedErr != nil {\n\t\tif cleanupErr := rollbackAndReleaseCloseCheckedSavepoint(ctx, tx, savepoint); cleanupErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"discard checked close savepoint after %v: %w\", scopedErr, cleanupErr)\n\t\t}\n\t\treturn nil, scopedErr\n\t}","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L62-L98","documentation":"CloseIssueCheckedInTx wraps storage.ErrNotFound when the target id exists in neither the issues nor wisps table (isClosedInTx probes both and found=false). The checked close cannot apply policy to a nonexistent target, so it refuses before any write.","triggerScenarios":"Calling CloseIssueCheckedInTx (via ExecuteClose / bd close) with an id that is misspelled, already fully deleted, or present only in a table this database does not contain.","commonSituations":"Stale ids from an old export or another database; typo in a shell script; an issue deleted by a concurrent actor between listing and closing; compacted/purged wisps referenced by id.","solutions":["Verify the id with bd show <id> or a listing before closing","Check you are operating against the intended database (right repo/path, right remote)","Handle errors.Is(err, storage.ErrNotFound) in the caller and skip or report the id rather than retrying"],"exampleFix":"err := store.CloseIssue(ctx, id, reason, actor)\nif errors.Is(err, storage.ErrNotFound) {\n\tlog.Printf(\"skipping %s: not found in this database\", id)\n\treturn nil // do not retry\n}","handlingStrategy":"try-catch","validationCode":"func issueExists(ctx context.Context, r storage.Reader, id string) bool {\n\t_, err := r.GetIssue(ctx, id)\n\treturn !errors.Is(err, storage.ErrNotFound)\n}","typeGuard":null,"tryCatchPattern":"err := store.CloseIssue(ctx, id, reason, actor)\nswitch {\ncase errors.Is(err, storage.ErrNotFound):\n\tlog.Printf(\"%s not found; skipping\", id)\n\treturn nil\ncase err != nil:\n\treturn err\n}","preventionTips":["Fetch ids from the same database you close against","Use errors.Is(err, storage.ErrNotFound) rather than string matching","Skip-not-fail in batch close scripts so one stale id does not abort the run"],"tags":["not-found","close","validation"],"backgroundTag":"issue-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}