gastownhall/beads · error

affected by status change for %s: %w

Error message

affected by status change for %s: %w

What it means

After a status change, updateIssueInTx computes which dependent issues/wisps were affected (AffectedByStatusChangeInTx or its wisp variant) and wraps any failure as "affected by status change for %s: %w". This bookkeeping is required to recompute is_blocked on dependents, so a failure aborts the update transaction.

Source

Thrown at internal/storage/issueops/update.go:529

		var newStatus string
		switch v := rawStatus.(type) {
		case string:
			newStatus = v
		case types.Status:
			newStatus = string(v)
		}
		oldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned
		newActive := newStatus != string(types.StatusClosed) && newStatus != string(types.StatusPinned)
		if oldActive != newActive {
			var affectedIssues, affectedWisps []string
			var aerr error
			if isWisp {
				affectedIssues, affectedWisps, aerr = AffectedByStatusChangeForWispInTx(ctx, tx, id)
			} else {
				affectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)
			}
			if aerr != nil {
				return nil, fmt.Errorf("affected by status change for %s: %w", id, aerr)
			}
			recompute, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)
			if err != nil {
				return nil, fmt.Errorf("recompute is_blocked after status change for %s: %w", id, err)
			}
			updateResult.IssueRowsChanged = !isWisp || recompute.IssueRowsChanged
			updateResult.WispRowsChanged = isWisp || recompute.WispRowsChanged
		}
	}

	// Snapshot only after all derived blocked-state maintenance has completed,
	// so the journal row carries the settled bead. recordEvent controls the
	// human-facing audit event only: the journal is a machine replay feed and
	// must never have a hole punched in it by an audit-suppressing caller.
	if err := RecordEventInTx(ctx, tx, EventUpdate, id, actor); err != nil {
		return nil, err
	}
	return updateResult, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped error from the AffectedByStatusChange* call for the root SQL failure.
  2. Check dependency table integrity (orphaned/deleted referenced ids) and clean up stale dependencies.
  3. Retry the transaction if the error is transient (timeout, connection).
  4. If the graph is very large, split the status change or verify dependency query performance/indexes.

Example fix

// before
_, err := storage.UpdateIssueInTx(ctx, tx, issueTable, eventTable, id, map[string]interface{}{"status": "closed"}, actor)
// after (retry transient failures)
for i := 0; i < 3; i++ {
    if _, err := storage.UpdateIssueInTx(ctx, tx, issueTable, eventTable, id, map[string]interface{}{"status": "closed"}, actor); err == nil {
        break
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// pre-check dependency integrity before a status change
if err := storage.ValidateDependencies(ctx, id); err != nil {
    return fmt.Errorf("stale dependencies on %s: %w", id, err)
}

Try / catch

if _, err := storage.UpdateIssue(ctx, id, map[string]interface{}{"status": "closed"}, actor); err != nil {
    if strings.Contains(err.Error(), "affected by status change") && isTransient(err) {
        // backoff and retry the whole transaction
    }
    return err
}

Prevention

When it happens

Trigger: A status-changing update triggers AffectedByStatusChangeInTx / AffectedByStatusChangeForWispInTx, which fails — typically a dependency-query SQL error, missing dependency table, or connection issue during the affected-rows query.

Common situations: Large dependency graphs making the affected-query slow or timing out; schema drift in dependency tables; Dolt connection drops mid-transaction; corrupt dependency rows referencing deleted issues.

Related errors


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