gastownhall/beads · error

mark is_blocked after add dependency %s -> %s: %w

Error message

mark is_blocked after add dependency %s -> %s: %w

What it means

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.

Source

Thrown at internal/storage/issueops/dependencies.go:353

	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) {
	if srcIsWisp {
		return issueIDs, removeID(wispIDs, source)
	}
	return removeID(issueIDs, source), wispIDs
}

func removeID(ids []string, remove string) []string {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause for the underlying SQL error and address it
  2. Confirm the affected issues/wisps rows exist before inserting the dependency
  3. Retry the add after connectivity/lock issues resolve; the tx is atomic so no partial edge remains
  4. Check table schema/version (migrations) if the is_blocked write consistently fails

Example fix

// before
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)
}
// after: surface the underlying driver error distinctly
if err := MarkIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
	if errors.Is(err, sql.ErrConnDone) || isRetryableDriverErr(err) {
		return false, fmt.Errorf("mark is_blocked after add dependency %s -> %s (retryable): %w", dep.IssueID, dep.DependsOnID, err)
	}
	return false, fmt.Errorf("mark is_blocked after add dependency %s -> %s: %w", dep.IssueID, dep.DependsOnID, err)
}
Defensive patterns

Strategy: retry

Validate before calling

// validate dependency targets before inserting scheduling edges
if dep.IssueID == dep.DependsOnID || !exists(ctx, db, dep.DependsOnID) {
	return errors.New("invalid dependency target")
}

Type guard

func isMarkBlockedFailure(err error) bool {
	return err != nil && strings.Contains(err.Error(), "mark is_blocked after add dependency")
}

Try / catch

err := store.AddDependencyInTx(ctx, tx, dep, opts)
if err != nil {
	if isMarkBlockedFailure(err) && isRetryable(err) {
		return retryWithBackoff(op)
	}
	return err
}

Prevention

When it happens

Trigger: 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).

Common situations: DB connection dropped mid-transaction; write conflicts with concurrent dependency mutations; schema mismatch causing the is_blocked update to fail.

Related errors


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