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
- Inspect the wrapped cause for the underlying SQL error and address it
- Confirm the affected issues/wisps rows exist before inserting the dependency
- Retry the add after connectivity/lock issues resolve; the tx is atomic so no partial edge remains
- 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
- Verify both endpoints of the edge exist before adding
- Keep transactions short to avoid lock timeouts on the is_blocked write
- Retry atomically — the tx rollbacks prevent partial state
- Keep schema migrations current so the is_blocked update path is valid
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
- db: Update %s: recompute is_blocked: %w
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/88173a18b75da165.
Report an issue: GitHub.