{"record":{"id":"43eee7349d94be52","repo":"gastownhall/beads","slug":"recompute-is-blocked-after-add-dependency-s-s","errorCode":null,"errorMessage":"recompute is_blocked after add dependency %s -> %s: %w","messagePattern":"recompute is_blocked after add dependency (.+?) -> (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":346,"sourceCode":"\t\taffectedIssues, affectedWisps, aerr = AffectedByDepChangeForWispInTx(ctx, tx, dep.IssueID, dep.DependsOnID, dep.Type)\n\t} else {\n\t\taffectedIssues, affectedWisps, aerr = AffectedByDepChangeInTx(ctx, tx, dep.IssueID, dep.DependsOnID, dep.Type)\n\t}\n\tif aerr != nil {\n\t\treturn false, fmt.Errorf(\"affected by add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, aerr)\n\t}\n\tif dep.Type == types.DepBlocks || dep.Type == types.DepConditionalBlocks {\n\t\tif err := markDirectBlockingDependencySourceInTx(ctx, tx, dep.IssueID, srcIsWisp, dep.DependsOnID, kind, opts.PrecheckedTarget); err != nil {\n\t\t\treturn false, fmt.Errorf(\"mark direct is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n\t\t}\n\t\taffectedIssues, affectedWisps = RemoveSourceFromAffected(dep.IssueID, srcIsWisp, affectedIssues, affectedWisps)\n\t}\n\tif dep.Type == types.DepParentChild {\n\t\t// Parent-child adds are not monotonic: adding an already-closed child can\n\t\t// satisfy an any-children waits-for gate and unblock the waiter.\n\t\trecomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"recompute is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n\t\t}\n\t\tmergeRecomputeIsBlockedResult(recomputeResult, recomputed)\n\t\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t\treturn eventWritten, RecordDepEventInTx(ctx, tx, EventDepAdd, dep.IssueID, string(dep.Type), dep.DependsOnID, metadata, actor)\n\t}\n\tif err := MarkIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {\n\t\treturn false, fmt.Errorf(\"mark is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n\t}\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// The journal is never gated on opts.EmitEvent: a structurally-wired edge is\n\t// as real to a replaying consumer as one added by an explicit dep verb.\n\treturn eventWritten, RecordDepEventInTx(ctx, tx, EventDepAdd, dep.IssueID, string(dep.Type), dep.DependsOnID, metadata, actor)\n}\n\n// RemoveSourceFromAffected drops the dep source from the affected-ID sets\n// after a direct is_blocked mark, so the follow-up Mark/Recompute pass does\n// not redo it. Shared with the domain/db dependency repository.\nfunc RemoveSourceFromAffected(source string, srcIsWisp bool, issueIDs, wispIDs []string) ([]string, []string) {","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L328-L364","documentation":"Wraps a failure from RecomputeIsBlockedInTxWithResult when a parent-child dependency is added inside addDependencyInTx. Parent-child adds are not monotonic (adding a closed child can unblock a waiter), so the full is_blocked state of affected issues/wisps must be recomputed; if that recompute fails the whole dependency-add transaction is aborted with this wrapped cause.","triggerScenarios":"Calling AddDependencyInTx or ApplyParentPatch with a Dependency whose Type is types.DepParentChild, where RecomputeIsBlockedInTxWithResult errors (underlying SQL failure in the cycle/state recomputation, lock contention, or a corrupted row for an affected issue/wisp).","commonSituations":"Database connectivity drops mid-transaction; Dolt transaction conflicts under concurrent parent-child edits; a dependency references an issue/wisp ID missing from its table so recompute queries fail.","solutions":["Inspect the wrapped cause (%w) to find the underlying SQL failure and fix that first","Verify all affectedIssues/affectedWisps rows exist in the issues/wisp tables before adding the edge","Retry the operation once the database is reachable; the whole tx rolled back so no partial state persists","Check for concurrent writers causing serialization/lock failures and retry with backoff"],"exampleFix":"// before\nrecomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\nif err != nil {\n\treturn false, fmt.Errorf(\"recompute is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n}\n// after: pre-validate targets exist so recompute cannot fail on dangling IDs\nif err := ensureIssuesExistInTx(ctx, tx, affectedIssues); err != nil {\n\treturn false, fmt.Errorf(\"precheck affected issues before add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n}\nrecomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\nif err != nil {\n\treturn false, fmt.Errorf(\"recompute is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n}","handlingStrategy":"try-catch","validationCode":"// pre-check that all affected issues/wisps exist before adding parent-child edge\nfor _, id := range append(affectedIssues, affectedWisps...) {\n\tif !exists(ctx, db, id) {\n\t\treturn fmt.Errorf(\"affected issue %s missing; aborting dep add\", id)\n\t}\n}\nif dep.IssueID == dep.DependsOnID {\n\treturn errors.New(\"refusing self parent-child edge\")\n}","typeGuard":"func isRecomputeFailure(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"recompute is_blocked after add dependency\")\n}","tryCatchPattern":"err := store.AddDependencyInTx(ctx, tx, dep, opts)\nvar wrapped error\nif err != nil && strings.HasPrefix(err.Error(), \"recompute is_blocked\") {\n\t// transient/state issue: rollback happened; inspect cause and retry\n\twrapped = fmt.Errorf(\"retry dep add later: %w\", err)\n}","preventionTips":["Ensure affectedIssues/affectedWisps rows exist before the add","Use errors.Is on the wrapped cause to classify SQL failures","Retry the whole transaction rather than partially applying edges","Monitor DB connectivity to catch mid-transaction drops"],"tags":["database","transaction","dependency-graph"],"backgroundTag":"dependency-add-transaction-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}