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.WispRowsChanged

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped error from the AffectedByDepChange* helper; retry the transaction if transient (lock/timeout).
  2. Run migrations to restore expected dependency-table schema and indexes.
  3. Reduce graph size or increase query timeout if the traversal times out on huge graphs.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/39ab167d2c211fe3. Report an issue: GitHub.