gastownhall/beads · error
recompute is_blocked after add dependency %s -> %s: %w
Error message
recompute is_blocked after add dependency %s -> %s: %w
What it means
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.
Source
Thrown at internal/storage/issueops/dependencies.go:346
affectedIssues, affectedWisps, aerr = AffectedByDepChangeForWispInTx(ctx, tx, dep.IssueID, dep.DependsOnID, dep.Type)
} else {
affectedIssues, affectedWisps, aerr = AffectedByDepChangeInTx(ctx, tx, dep.IssueID, dep.DependsOnID, dep.Type)
}
if aerr != nil {
return false, fmt.Errorf("affected by add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, aerr)
}
if dep.Type == types.DepBlocks || dep.Type == types.DepConditionalBlocks {
if err := markDirectBlockingDependencySourceInTx(ctx, tx, dep.IssueID, srcIsWisp, dep.DependsOnID, kind, opts.PrecheckedTarget); err != nil {
return false, fmt.Errorf("mark direct is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
affectedIssues, affectedWisps = RemoveSourceFromAffected(dep.IssueID, srcIsWisp, affectedIssues, affectedWisps)
}
if dep.Type == types.DepParentChild {
// Parent-child adds are not monotonic: adding an already-closed child can
// satisfy an any-children waits-for gate and unblock the waiter.
recomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)
if err != nil {
return false, fmt.Errorf("recompute is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
mergeRecomputeIsBlockedResult(recomputeResult, recomputed)
// Snapshot only after all derived blocked-state maintenance has completed.
return eventWritten, RecordDepEventInTx(ctx, tx, EventDepAdd, dep.IssueID, string(dep.Type), dep.DependsOnID, metadata, actor)
}
if err := MarkIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
return false, fmt.Errorf("mark is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
// Snapshot only after all derived blocked-state maintenance has completed.
// The journal is never gated on opts.EmitEvent: a structurally-wired edge is
// as real to a replaying consumer as one added by an explicit dep verb.
return eventWritten, RecordDepEventInTx(ctx, tx, EventDepAdd, dep.IssueID, string(dep.Type), dep.DependsOnID, metadata, actor)
}
// RemoveSourceFromAffected drops the dep source from the affected-ID sets
// after a direct is_blocked mark, so the follow-up Mark/Recompute pass does
// not redo it. Shared with the domain/db dependency repository.
func RemoveSourceFromAffected(source string, srcIsWisp bool, issueIDs, wispIDs []string) ([]string, []string) {View on GitHub (pinned to 71377f2769)
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
Example fix
// before
recomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)
if err != nil {
return false, fmt.Errorf("recompute is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
// after: pre-validate targets exist so recompute cannot fail on dangling IDs
if err := ensureIssuesExistInTx(ctx, tx, affectedIssues); err != nil {
return false, fmt.Errorf("precheck affected issues before add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
recomputed, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)
if err != nil {
return false, fmt.Errorf("recompute is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check that all affected issues/wisps exist before adding parent-child edge
for _, id := range append(affectedIssues, affectedWisps...) {
if !exists(ctx, db, id) {
return fmt.Errorf("affected issue %s missing; aborting dep add", id)
}
}
if dep.IssueID == dep.DependsOnID {
return errors.New("refusing self parent-child edge")
} Type guard
func isRecomputeFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "recompute is_blocked after add dependency")
} Try / catch
err := store.AddDependencyInTx(ctx, tx, dep, opts)
var wrapped error
if err != nil && strings.HasPrefix(err.Error(), "recompute is_blocked") {
// transient/state issue: rollback happened; inspect cause and retry
wrapped = fmt.Errorf("retry dep add later: %w", err)
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- recompute is_blocked after close for %s: %w
- affected by delete for %s: %w
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/43eee7349d94be52.
Report an issue: GitHub.