{"record":{"id":"bb8e32f24ca6898f","repo":"gastownhall/beads","slug":"recompute-is-blocked-after-close-for-s-w","errorCode":null,"errorMessage":"recompute is_blocked after close for %s: %w","messagePattern":"recompute is_blocked after close for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":382,"sourceCode":"\t\t\treturn &CloseResult{IsWisp: isWisp, AlreadyClosed: true}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to close issue: %s\", id)\n\t}\n\n\t// A closed issue holds no lease (no-op for wisps, which are never leased).\n\tif err := DeleteLeaseInTx(ctx, tx, id); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif recordEvent {\n\t\tif err := RecordEventInTable(ctx, tx, eventTable, id, types.EventClosed, actor, reason); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to record event: %w\", err)\n\t\t}\n\t}\n\n\trecompute, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"recompute is_blocked after close for %s: %w\", id, err)\n\t}\n\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// recordEvent gates the human audit event, never the journal.\n\tif err := RecordEventInTx(ctx, tx, EventClose, id, actor); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &CloseResult{IsWisp: isWisp, IssueRowsChanged: !isWisp || recompute.IssueRowsChanged}, nil\n}\n","sourceCodeStart":364,"sourceCodeEnd":393,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L364-L393","documentation":"This error wraps a failure of RecomputeIsBlockedInTxWithResult, which maintains derived is_blocked state on dependent issues after this issue closes. Since blocked-state is derived data, the library refuses to commit a close with a stale/incorrect recompute and rolls back the transaction. The issue ID and underlying error are included for diagnosis.","triggerScenarios":"RecomputeIsBlockedInTxWithResult returns an error while updating is_blocked for affectedIssues/affectedWisps inside the close transaction: large dependency graphs timing out, lock contention on dependent rows, missing columns, or connection failure mid-recompute.","commonSituations":"Closing a hub issue with hundreds of dependents in one transaction under a tight context deadline; deadlocks with other writers updating the same dependent rows; schema drift on the issues table; slow Dolt server under load.","solutions":["Read the wrapped error (errors.Is/As) — deadlocks/timeouts are the usual cause; retry with a fresh transaction.","Increase the context timeout when closing issues with large dependency fan-out.","Close issues in smaller batches to reduce transaction size and lock scope.","Check schema/version consistency (bd doctor) and server health if failures persist."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := store.CloseIssue(ctx, id, actor, reason)\nif err != nil && strings.Contains(err.Error(), \"recompute is_blocked after close\") {\n    if isDeadlockOrTimeout(err) {\n        return retryClose(ctx, id, actor, reason, 1) // whole tx rolled back\n    }\n    return err\n}","preventionTips":["Give large fan-out closes a generous context timeout.","Close high-dependency issues in small batches, not one giant transaction.","Stagger concurrent closes of issues sharing dependents to reduce lock contention.","Keep schema current; verify with bd doctor if errors persist."],"tags":["database","transaction","dependency-graph","recompute"],"backgroundTag":"blocked-state-recompute-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}