gastownhall/beads · error
affected by remove dependency %s -> %s: %w
Error message
affected by remove dependency %s -> %s: %w
What it means
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.
Source
Thrown at internal/storage/issueops/dependencies.go:969
// proxied repo and with the symmetric AddDependencyInTx EmitEvent gate).
eventWritten := false
if emitEvent {
if err := RecordEventInTable(ctx, tx, eventTable, issueID, types.EventDependencyRemoved, actor,
fmt.Sprintf("Removed dependency on %s", dependsOnID)); err != nil {
return false, fmt.Errorf("record dependency_removed event: %w", err)
}
eventWritten = true
}
var affectedIssues, affectedWisps []string
var aerr error
if isWisp {
affectedIssues, affectedWisps, aerr = AffectedByDepChangeForWispInTx(ctx, tx, issueID, dependsOnID, types.DependencyType(depType))
} else {
affectedIssues, affectedWisps, aerr = AffectedByDepChangeInTx(ctx, tx, issueID, dependsOnID, types.DependencyType(depType))
}
if aerr != nil {
return false, fmt.Errorf("affected by remove dependency %s -> %s: %w", issueID, dependsOnID, aerr)
}
recomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)
if err != nil {
return false, fmt.Errorf("recompute is_blocked after remove dependency %s -> %s: %w", issueID, dependsOnID, err)
}
mergeRecomputeIsBlockedResult(recomputeResult, recomputed)
// Snapshot only after all derived blocked-state maintenance has completed.
// Never gated on emitEvent — a structural removal is as real to a replaying
// consumer as one from an explicit dep verb.
return eventWritten, RecordDepEventInTx(ctx, tx, EventDepRemove, issueID, depType, dependsOnID, depMetadata, actor)
}
func mergeRecomputeIsBlockedResult(target *RecomputeIsBlockedResult, source RecomputeIsBlockedResult) {
if target == nil {
return
}
target.IssueRowsChanged = target.IssueRowsChanged || source.IssueRowsChanged
target.WispRowsChanged = target.WispRowsChanged || source.WispRowsChangedView on GitHub (pinned to 71377f2769)
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.
Example fix
// before: traversal times out on a large graph ok, err := store.RemoveDependencyInTx(ctx, tx, "bd-1", "bd-2", actor, true) // after: raise timeout and retry, then verify blocked state ctx, cancel := context.WithTimeout(ctx, 60*time.Second) ok, err = store.RemoveDependencyInTx(ctx, tx, "bd-1", "bd-2", actor, true) _ = store.RecomputeIsBlocked(ctx) // safety net after retries
Defensive patterns
Strategy: try-catch
Validate before calling
// sanity: dependency tables are queryable before removal so the affected-set traversal won't fail
if err := db.QueryRow("SELECT COUNT(*) FROM dependencies LIMIT 1").Err(); err != nil {
return fmt.Errorf("dependency graph unreadable; repair before removals: %w", err)
} Type guard
func isAffectedSetFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "affected by remove dependency")
} Try / catch
removed, err := store.RemoveDependency(ctx, issueID, dependsOnID, actor)
if isAffectedSetFailure(err) {
if isTransient(errors.Unwrap(err)) {
err = withBackoff(3, func() error { _, e := store.RemoveDependency(ctx, issueID, dependsOnID, actor); return e })
}
// after recovery, force full blocked-state repair
_ = store.RecomputeIsBlocked(ctx)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- count inbound dependencies from %s: %w
- retarget inbound dependencies to wisp in %s for %s: %w
- retarget inbound dependencies to issue in %s for %s: %w
- check retarget collision in %s: %w
- check rename collision in %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/39ab167d2c211fe3.
Report an issue: GitHub.