{"record":{"id":"39ab167d2c211fe3","repo":"gastownhall/beads","slug":"affected-by-remove-dependency-s-s-w","errorCode":null,"errorMessage":"affected by remove dependency %s -> %s: %w","messagePattern":"affected by remove dependency (.+?) -> (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependencies.go","lineNumber":969,"sourceCode":"\t// proxied repo and with the symmetric AddDependencyInTx EmitEvent gate).\n\teventWritten := false\n\tif emitEvent {\n\t\tif err := RecordEventInTable(ctx, tx, eventTable, issueID, types.EventDependencyRemoved, actor,\n\t\t\tfmt.Sprintf(\"Removed dependency on %s\", dependsOnID)); err != nil {\n\t\t\treturn false, fmt.Errorf(\"record dependency_removed event: %w\", err)\n\t\t}\n\t\teventWritten = true\n\t}\n\n\tvar affectedIssues, affectedWisps []string\n\tvar aerr error\n\tif isWisp {\n\t\taffectedIssues, affectedWisps, aerr = AffectedByDepChangeForWispInTx(ctx, tx, issueID, dependsOnID, types.DependencyType(depType))\n\t} else {\n\t\taffectedIssues, affectedWisps, aerr = AffectedByDepChangeInTx(ctx, tx, issueID, dependsOnID, types.DependencyType(depType))\n\t}\n\tif aerr != nil {\n\t\treturn false, fmt.Errorf(\"affected by remove dependency %s -> %s: %w\", issueID, dependsOnID, aerr)\n\t}\n\trecomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"recompute is_blocked after remove dependency %s -> %s: %w\", issueID, dependsOnID, err)\n\t}\n\tmergeRecomputeIsBlockedResult(recomputeResult, recomputed)\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// Never gated on emitEvent — a structural removal is as real to a replaying\n\t// consumer as one from an explicit dep verb.\n\treturn eventWritten, RecordDepEventInTx(ctx, tx, EventDepRemove, issueID, depType, dependsOnID, depMetadata, actor)\n}\n\nfunc mergeRecomputeIsBlockedResult(target *RecomputeIsBlockedResult, source RecomputeIsBlockedResult) {\n\tif target == nil {\n\t\treturn\n\t}\n\ttarget.IssueRowsChanged = target.IssueRowsChanged || source.IssueRowsChanged\n\ttarget.WispRowsChanged = target.WispRowsChanged || source.WispRowsChanged","sourceCodeStart":951,"sourceCodeEnd":987,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependencies.go#L951-L987","documentation":"This error wraps a failure from the affected-set computation (AffectedByDepChangeInTx or AffectedByDepChangeForWispInTx) after a dependency edge was deleted. These queries determine which issues' is_blocked flags must be recomputed following the removal; when they fail the blocked-state cache can no longer be trusted, so the transaction aborts.","triggerScenarios":"removeDependencyInTx (via RemoveDependencyInTx or ApplyParentPatch) deletes an edge, then the affected-set traversal query fails — dependency-table scan error, missing columns used by the traversal, connection loss, or timeout on a very large dependency graph.","commonSituations":"Very deep/wide dependency graphs making the traversal slow enough to hit timeouts; schema drift after partial migration; DB lock contention right after the DELETE; corrupted dependency table indexes.","solutions":["Read the wrapped error from the AffectedByDepChange* helper; retry the transaction if transient (lock/timeout).","Run migrations to restore expected dependency-table schema and indexes.","Reduce graph size or increase query timeout if the traversal times out on huge graphs.","After any rollback, force a full is_blocked recompute (bd doctor / recompute command) to repair cached blocked state."],"exampleFix":"// before: traversal times out on a large graph\nok, err := store.RemoveDependencyInTx(ctx, tx, \"bd-1\", \"bd-2\", actor, true)\n// after: raise timeout and retry, then verify blocked state\nctx, cancel := context.WithTimeout(ctx, 60*time.Second)\nok, err = store.RemoveDependencyInTx(ctx, tx, \"bd-1\", \"bd-2\", actor, true)\n_ = store.RecomputeIsBlocked(ctx) // safety net after retries","handlingStrategy":"try-catch","validationCode":"// sanity: dependency tables are queryable before removal so the affected-set traversal won't fail\nif err := db.QueryRow(\"SELECT COUNT(*) FROM dependencies LIMIT 1\").Err(); err != nil {\n    return fmt.Errorf(\"dependency graph unreadable; repair before removals: %w\", err)\n}","typeGuard":"func isAffectedSetFailure(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"affected by remove dependency\")\n}","tryCatchPattern":"removed, err := store.RemoveDependency(ctx, issueID, dependsOnID, actor)\nif isAffectedSetFailure(err) {\n    if isTransient(errors.Unwrap(err)) {\n        err = withBackoff(3, func() error { _, e := store.RemoveDependency(ctx, issueID, dependsOnID, actor); return e })\n    }\n    // after recovery, force full blocked-state repair\n    _ = store.RecomputeIsBlocked(ctx)\n}","preventionTips":["Bound dependency-graph depth in your workflow; very deep graphs slow the affected-set traversal.","Keep indexes on dependency table target columns so traversal queries stay fast.","After any failed removal transaction, run a full is_blocked recompute to be safe.","Retry the whole removal on transient errors — the transaction is atomic."],"tags":["storage","dependencies","blocked-state","sql"],"backgroundTag":"blocked-state-recompute-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}