gastownhall/beads · error

remove labels: %s: %w

Error message

remove labels: %s: %w

What it means

Wraps an error from labelRepo.Delete while removing one label in removeMany (RemoveLabels / RemoveWispLabels). The failing label is named in the message. Because removal proceeds label-by-label, earlier successful deletions are not rolled back, so the batch can end partially applied.

Source

Thrown at internal/storage/domain/label.go:135

func (u *labelUseCaseImpl) RemoveLabels(ctx context.Context, issueID string, labels []string, actor string) error {
	return u.removeMany(ctx, issueID, labels, actor, false)
}

func (u *labelUseCaseImpl) RemoveWispLabels(ctx context.Context, wispID string, labels []string, actor string) error {
	return u.removeMany(ctx, wispID, labels, actor, true)
}

func (u *labelUseCaseImpl) removeMany(ctx context.Context, id string, labels []string, actor string, useWisp bool) error {
	if id == "" {
		return fmt.Errorf("remove labels: id must not be empty")
	}
	opts := LabelOpts{UseWispsTable: useWisp}
	for _, label := range labels {
		if label == "" {
			continue
		}
		if err := u.labelRepo.Delete(ctx, id, label, actor, opts); err != nil {
			return fmt.Errorf("remove labels: %s: %w", label, err)
		}
	}
	return nil
}

func (u *labelUseCaseImpl) SetLabels(ctx context.Context, issueID string, labels []string, actor string) error {
	return u.setMany(ctx, issueID, labels, actor, false)
}

func (u *labelUseCaseImpl) SetWispLabels(ctx context.Context, wispID string, labels []string, actor string) error {
	return u.setMany(ctx, wispID, labels, actor, true)
}

func (u *labelUseCaseImpl) setMany(ctx context.Context, id string, labels []string, actor string, useWisp bool) error {
	if id == "" {
		return fmt.Errorf("set labels: id must not be empty")
	}
	opts := LabelOpts{UseWispsTable: useWisp}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped error for the root cause (driver, constraint, not-found)
  2. Re-list labels (GetLabels) to see the post-failure state and reconcile
  3. Retry idempotently: Delete of an already-removed label can be treated as success by filtering labels from GetLabels output first
  4. Check for concurrent writers if errors appear intermittently

Example fix

// before
store.RemoveLabels(ctx, id, labels, actor) // may partially fail
// after
current, err := store.GetLabels(ctx, id)
if err != nil { return err }
var want []string
for _, l := range labels {
	if slices.Contains(current, l) { want = append(want, l) }
}
return store.RemoveLabels(ctx, id, want, actor)
Defensive patterns

Strategy: try-catch

Validate before calling

current, err := store.GetLabels(ctx, id)
if err != nil { return err }
want := slices.DeleteFunc(slices.Clone(labels), func(l string) bool {
	return !slices.Contains(current, l)
})

Try / catch

if err := store.RemoveLabels(ctx, id, labels, actor); err != nil {
	log.Printf("label removal partially failed on %s: %v", id, err)
	// reconcile
	cur, gerr := store.GetLabels(ctx, id)
	if gerr != nil { return gerr }
	log.Printf("remaining labels: %v", cur)
	return err
}

Prevention

When it happens

Trigger: RemoveLabels/RemoveWispLabels where labelRepo.Delete fails: storage driver error, record missing, constraint or permissions issue, or the underlying row was concurrently deleted.

Common situations: Concurrent agents mutating the same issue's labels; database connectivity drops mid-loop; removing labels from a nonexistent or cross-database ID; Dolt transaction conflicts.

Related errors


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