{"record":{"id":"149dd0dcd29c18c0","repo":"gastownhall/beads","slug":"affected-by-status-change-for-s-w","errorCode":null,"errorMessage":"affected by status change for %s: %w","messagePattern":"affected by status change for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":529,"sourceCode":"\t\tvar newStatus string\n\t\tswitch v := rawStatus.(type) {\n\t\tcase string:\n\t\t\tnewStatus = v\n\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","sourceCodeStart":511,"sourceCodeEnd":547,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L511-L547","documentation":"After a status change, updateIssueInTx computes which dependent issues/wisps were affected (AffectedByStatusChangeInTx or its wisp variant) and wraps any failure as \"affected by status change for %s: %w\". This bookkeeping is required to recompute is_blocked on dependents, so a failure aborts the update transaction.","triggerScenarios":"A status-changing update triggers AffectedByStatusChangeInTx / AffectedByStatusChangeForWispInTx, which fails — typically a dependency-query SQL error, missing dependency table, or connection issue during the affected-rows query.","commonSituations":"Large dependency graphs making the affected-query slow or timing out; schema drift in dependency tables; Dolt connection drops mid-transaction; corrupt dependency rows referencing deleted issues.","solutions":["Inspect the wrapped error from the AffectedByStatusChange* call for the root SQL failure.","Check dependency table integrity (orphaned/deleted referenced ids) and clean up stale dependencies.","Retry the transaction if the error is transient (timeout, connection).","If the graph is very large, split the status change or verify dependency query performance/indexes."],"exampleFix":"// before\n_, err := storage.UpdateIssueInTx(ctx, tx, issueTable, eventTable, id, map[string]interface{}{\"status\": \"closed\"}, actor)\n// after (retry transient failures)\nfor i := 0; i < 3; i++ {\n    if _, err := storage.UpdateIssueInTx(ctx, tx, issueTable, eventTable, id, map[string]interface{}{\"status\": \"closed\"}, actor); err == nil {\n        break\n    }\n}","handlingStrategy":"retry","validationCode":"// pre-check dependency integrity before a status change\nif err := storage.ValidateDependencies(ctx, id); err != nil {\n    return fmt.Errorf(\"stale dependencies on %s: %w\", id, err)\n}","typeGuard":null,"tryCatchPattern":"if _, err := storage.UpdateIssue(ctx, id, map[string]interface{}{\"status\": \"closed\"}, actor); err != nil {\n    if strings.Contains(err.Error(), \"affected by status change\") && isTransient(err) {\n        // backoff and retry the whole transaction\n    }\n    return err\n}","preventionTips":["Clean up dependencies pointing at deleted issues before status changes.","Keep dependency graphs bounded; split very wide status-change fan-outs.","Use bounded retries for transient DB errors during the recompute step.","Monitor timeouts on the affected-issues query as the graph grows."],"tags":["storage","status-change","dependencies","transaction"],"backgroundTag":"status-change-recompute-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}