{"record":{"id":"88173a18b75da165","repo":"gastownhall/beads","slug":"mark-is-blocked-after-add-dependency-s-s-w","errorCode":null,"errorMessage":"mark is_blocked after add dependency %s -> %s: %w","messagePattern":"mark is_blocked after add dependency (.+?) -> (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":353,"sourceCode":"\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) {\n\tif srcIsWisp {\n\t\treturn issueIDs, removeID(wispIDs, source)\n\t}\n\treturn removeID(issueIDs, source), wispIDs\n}\n\nfunc removeID(ids []string, remove string) []string {","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L335-L371","documentation":"Wraps a failure from MarkIsBlockedInTx after adding a non-parent-child (scheduling) dependency. Unlike parent-child adds, plain blocking edges only need the affected issues flagged as blocked via a cheap mark; if that write fails, the dependency-add transaction is aborted with this wrapped cause so blocked-state and graph stay consistent.","triggerScenarios":"Calling AddDependencyInTx or ApplyParentPatch with a scheduling-edge dependency (blocks/conditional) where MarkIsBlockedInTx fails on the affected issues/wisps (SQL error, connection loss, lock timeout).","commonSituations":"DB connection dropped mid-transaction; write conflicts with concurrent dependency mutations; schema mismatch causing the is_blocked update to fail.","solutions":["Inspect the wrapped cause for the underlying SQL error and address it","Confirm the affected issues/wisps rows exist before inserting the dependency","Retry the add after connectivity/lock issues resolve; the tx is atomic so no partial edge remains","Check table schema/version (migrations) if the is_blocked write consistently fails"],"exampleFix":"// before\nif err := MarkIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {\n\treturn false, fmt.Errorf(\"mark is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n}\n// after: surface the underlying driver error distinctly\nif err := MarkIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {\n\tif errors.Is(err, sql.ErrConnDone) || isRetryableDriverErr(err) {\n\t\treturn false, fmt.Errorf(\"mark is_blocked after add dependency %s -> %s (retryable): %w\", dep.IssueID, dep.DependsOnID, err)\n\t}\n\treturn false, fmt.Errorf(\"mark is_blocked after add dependency %s -> %s: %w\", dep.IssueID, dep.DependsOnID, err)\n}","handlingStrategy":"retry","validationCode":"// validate dependency targets before inserting scheduling edges\nif dep.IssueID == dep.DependsOnID || !exists(ctx, db, dep.DependsOnID) {\n\treturn errors.New(\"invalid dependency target\")\n}","typeGuard":"func isMarkBlockedFailure(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"mark is_blocked after add dependency\")\n}","tryCatchPattern":"err := store.AddDependencyInTx(ctx, tx, dep, opts)\nif err != nil {\n\tif isMarkBlockedFailure(err) && isRetryable(err) {\n\t\treturn retryWithBackoff(op)\n\t}\n\treturn err\n}","preventionTips":["Verify both endpoints of the edge exist before adding","Keep transactions short to avoid lock timeouts on the is_blocked write","Retry atomically — the tx rollbacks prevent partial state","Keep schema migrations current so the is_blocked update path is valid"],"tags":["database","transaction","blocked-state"],"backgroundTag":"dependency-add-transaction-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}