gastownhall/beads · error

remove relation %d: %w

Error message

remove relation %d: %w

What it means

During PushLinks, relations that exist in Azure DevOps but not in the desired bead dependency set are removed one by one via RemoveWorkItemLink (by relation index). Each failure is collected (not returned immediately) and wrapped as 'remove relation <idx>'. The sync reports all accumulated errors at the end.

Source

Thrown at internal/ado/links.go:313

	var removeIndices []int
	for _, cl := range current {
		if _, ok := desired[cl.key]; ok {
			continue
		}
		if !removableRelTypes[cl.key.Rel] {
			continue
		}
		if !managedTargets[cl.key.TargetID] {
			continue
		}
		removeIndices = append(removeIndices, cl.index)
	}
	// Sort descending so higher indices are removed first.
	sort.Sort(sort.Reverse(sort.IntSlice(removeIndices)))

	for _, idx := range removeIndices {
		if err := r.Client.RemoveWorkItemLink(ctx, workItemID, idx); err != nil {
			errs = append(errs, fmt.Errorf("remove relation %d: %w", idx, err))
		}
	}

	// Find relations to add (in desired but not current).
	for key, dep := range desired {
		if currentSet[key] {
			continue
		}
		targetURL := r.buildWorkItemURL(key.TargetID)
		rel := beadsDepToADORel(dep.Type)
		comment := ""
		if dep.Type == "discovered-from" {
			comment = discoveredFromComment
		}
		if err := r.Client.AddWorkItemLink(ctx, workItemID, targetURL, rel, comment); err != nil {
			errs = append(errs, fmt.Errorf("add link %s to %d: %w", rel, key.TargetID, err))
		}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped inner error to identify the HTTP status; fix permissions (PAT needs Work Items Read+Write) if 401/403.
  2. Re-run the sync: indices are recomputed from a fresh GET, which resolves stale-index (404/409) failures.
  3. Run a single sync process at a time to avoid concurrent link edits shifting relation indices.
  4. Retry transient network failures; RemoveWorkItemLink is idempotent per relation key on the next pass.

Example fix

// before: immediate bulk remove with stale indices
// after: re-fetch relations and remove by identity on failure
if err := r.Client.RemoveWorkItemLink(ctx, workItemID, idx); err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
        continue // already removed concurrently
    }
    errs = append(errs, fmt.Errorf("remove relation %d: %w", idx, err))
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check: confirm PAT has write scope and relation index is current
rel, resp, err := client.GetWorkItemRelations(ctx, workItemID)
if err != nil || resp == nil || idx >= len(rel) {
    return fmt.Errorf("relation index %d out of range; refresh before removing", idx)
}

Type guard

func isRemovableRelationErr(err error) bool {
    var apiErr *APIError
    if !errors.As(err, &apiErr) { return true } // transient/network: retry
    return apiErr.StatusCode != http.StatusForbidden
}

Try / catch

if err := r.Client.RemoveWorkItemLink(ctx, workItemID, idx); err != nil {
    var apiErr *APIError
    switch {
    case errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound:
        continue // already gone; next sync reconciles
    case errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden:
        return fmt.Errorf("insufficient PAT permissions to remove relations: %w", err)
    default:
        errs = append(errs, fmt.Errorf("remove relation %d: %w", idx, err))
    }
}

Prevention

When it happens

Trigger: Calling pushADOLinks/PushLinks when RemoveWorkItemLink fails for a given relation index — e.g. 401/403 from insufficient PAT permissions, 404 because the relation was deleted concurrently, 409/412 from a stale index after concurrent edits, or network errors.

Common situations: PAT lacking 'Write' work item scope; two sync jobs racing and shifting relation indices between list and remove; the relation already removed by a human in the ADO UI; transient network failures during bulk removal.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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