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
- Read the wrapped inner error to identify the HTTP status; fix permissions (PAT needs Work Items Read+Write) if 401/403.
- Re-run the sync: indices are recomputed from a fresh GET, which resolves stale-index (404/409) failures.
- Run a single sync process at a time to avoid concurrent link edits shifting relation indices.
- 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
- Ensure the PAT has Work Items Read+Write scope.
- Run only one link-sync process at a time to avoid shifting relation indices.
- Re-fetch relations immediately before removal so indices are fresh.
- Treat 404 on removal as success (relation already deleted).
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
- add link %s to %d: %w
- failed to remove work item link: %w
- cannot extract work item ID from URL: %s
- work item %d: %w
- transitioning to %q: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/56a463332a800b66.
Report an issue: GitHub.