{"record":{"id":"94911f4d970bd430","repo":"gastownhall/beads","slug":"recompute-is-blocked-after-status-change-for-s","errorCode":null,"errorMessage":"recompute is_blocked after status change for %s: %w","messagePattern":"recompute is_blocked after status change for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":533,"sourceCode":"\t\tcase types.Status:\n\t\t\tnewStatus = string(v)\n\t\t}\n\t\toldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned\n\t\tnewActive := newStatus != string(types.StatusClosed) && newStatus != string(types.StatusPinned)\n\t\tif oldActive != newActive {\n\t\t\tvar affectedIssues, affectedWisps []string\n\t\t\tvar aerr error\n\t\t\tif isWisp {\n\t\t\t\taffectedIssues, affectedWisps, aerr = AffectedByStatusChangeForWispInTx(ctx, tx, id)\n\t\t\t} else {\n\t\t\t\taffectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)\n\t\t\t}\n\t\t\tif aerr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"affected by status change for %s: %w\", id, aerr)\n\t\t\t}\n\t\t\trecompute, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"recompute is_blocked after status change for %s: %w\", id, err)\n\t\t\t}\n\t\t\tupdateResult.IssueRowsChanged = !isWisp || recompute.IssueRowsChanged\n\t\t\tupdateResult.WispRowsChanged = isWisp || recompute.WispRowsChanged\n\t\t}\n\t}\n\n\t// Snapshot only after all derived blocked-state maintenance has completed,\n\t// so the journal row carries the settled bead. recordEvent controls the\n\t// human-facing audit event only: the journal is a machine replay feed and\n\t// must never have a hole punched in it by an audit-suppressing caller.\n\tif err := RecordEventInTx(ctx, tx, EventUpdate, id, actor); err != nil {\n\t\treturn nil, err\n\t}\n\treturn updateResult, nil\n}\n\nfunc cloneUpdateFields(updates map[string]interface{}) map[string]interface{} {\n\tcloned := make(map[string]interface{}, len(updates))","sourceCodeStart":515,"sourceCodeEnd":551,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L515-L551","documentation":"This error wraps a failure while recomputing the is_blocked flag for issues that depend on an issue whose status just changed. After a status update inside a transaction, issueops recalculates blocked state for all affected issues/wisps via RecomputeIsBlockedInTxWithResult; if that recompute query fails, the whole update transaction is rolled back and this wrapped error is returned.","triggerScenarios":"Calling UpdateIssueInTx or UpdateIssueWithoutEventInTx with a status-changing update (e.g. closing an issue) while its dependency graph rows are in a state the recompute SQL cannot handle, or when the underlying database errors during the recompute statement.","commonSituations":"Database connectivity drops mid-transaction, dependency rows reference issues in unexpected states, schema/lock contention during bulk status changes, or corrupted dependency edges left from a prior failed migration.","solutions":["Inspect the wrapped %w cause to identify the underlying SQL/DB error and fix that first","Retry the update transaction; the recompute runs inside the same tx so a transient DB error is safe to retry","Verify dependency rows (blocked-by relations) are consistent, e.g. via a data-integrity check or re-sync","If reproducible, capture the failing query from RecomputeIsBlockedInTxWithResult and check schema/version compatibility"],"exampleFix":"// before\nbd update bd-42 --status closed  // fails: recompute is_blocked after status change\n\n// after\n// 1. check the wrapped cause\nbd doctor  # or inspect storage logs for the underlying SQL error\n// 2. repair dangling dependency references, then retry the status change","handlingStrategy":"retry","validationCode":"// pre-flight: verify dependency rows are consistent before a status change\nrows, err := db.Query(\"SELECT issue_id FROM dependencies WHERE depends_on_id = ?\", issueID)\nif err != nil {\n    return fmt.Errorf(\"cannot read dependencies, update would fail recompute: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"// wrap in retry with backoff for transient DB errors\nfor i := 0; i < 3; i++ {\n    _, err := issueops.UpdateIssueInTx(ctx, tx, id, updates)\n    if err == nil || !strings.Contains(err.Error(), \"recompute is_blocked\") {\n        break\n    }\n    time.Sleep(backoff(i))\n}","preventionTips":["Keep DB connectivity healthy; recompute runs in the same tx as the update","Periodically validate dependency edges for dangling references","Treat any wrapped cause in this error as the real bug and fix it, not the wrapper"],"tags":["storage","database","transaction","dependencies"],"backgroundTag":"dependency-graph-recompute-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}