{"record":{"id":"6d77b03b82957b6e","repo":"gastownhall/beads","slug":"failed-to-remove-work-item-link-w","errorCode":null,"errorMessage":"failed to remove work item link: %w","messagePattern":"failed to remove work item link: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/ado/client.go","lineNumber":561,"sourceCode":"\t_, err := c.doRequest(ctx, http.MethodPatch, urlStr, \"application/json-patch+json\", ops)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add work item link: %w\", err)\n\t}\n\treturn nil\n}\n\n// RemoveWorkItemLink removes a relation link by index from the given work item.\nfunc (c *Client) RemoveWorkItemLink(ctx context.Context, sourceID, relationIndex int) error {\n\tops := []PatchOperation{\n\t\t{\n\t\t\tOp:   \"remove\",\n\t\t\tPath: fmt.Sprintf(\"/relations/%d\", relationIndex),\n\t\t},\n\t}\n\turlStr := addAPIVersion(fmt.Sprintf(\"%s/wit/workitems/%d\", c.apiBase(), sourceID))\n\t_, err := c.doRequest(ctx, http.MethodPatch, urlStr, \"application/json-patch+json\", ops)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to remove work item link: %w\", err)\n\t}\n\treturn nil\n}\n\n// ListProjects returns all team projects in the organization.\n// This is an org-level endpoint, not project-scoped.\nfunc (c *Client) ListProjects(ctx context.Context) ([]Project, error) {\n\turlStr := addAPIVersion(c.orgBase() + \"/projects\")\n\trespBody, err := c.doRequest(ctx, http.MethodGet, urlStr, \"\", nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list projects: %w\", err)\n\t}\n\n\tvar envelope listResponse\n\tif err := json.Unmarshal(respBody, &envelope); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse projects response: %w\", err)\n\t}\n","sourceCodeStart":543,"sourceCodeEnd":579,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/ado/client.go#L543-L579","documentation":"This error wraps any failure from the HTTP PATCH that deletes a relation (JSON-Patch 'remove /relations/{index}') from a work item in RemoveWorkItemLink. Like its sibling add-link error, it wraps the original doRequest error with %w so status codes and ADO messages remain inspectable. It marks the failure at the 'remove link' call boundary.","triggerScenarios":"Calling Client.RemoveWorkItemLink(ctx, sourceID, relationIndex) when doRequest fails: 404 because sourceID or the relation index no longer exists (relations shift when other links are removed), 401 invalid PAT, network error, or 400 for a negative/out-of-range index.","commonSituations":"Removing a link by a stale index captured before another process modified relations; concurrent edits to the work item causing index drift; expired PAT after long-running jobs; typo in the work item ID.","solutions":["Re-fetch the work item's current relations and recompute relationIndex before retrying (indices shift on concurrent changes)","Check the wrapped cause (errors.As) for ADO's status code — 404 usually means stale index or wrong ID","Verify the PAT has Work Items write scope and has not expired","Confirm sourceID exists in the configured organization/project","Retry transient network/5xx failures; ADO occasionally throttles writes (429)"],"exampleFix":"// before\nitem, _ := client.GetWorkItem(ctx, id)\nidx := findRel(item, relURL)\nclient.RemoveWorkItemLink(ctx, id, idx) // idx may be stale later\n// after\nitem, _ := client.GetWorkItem(ctx, id)\nidx := findRel(item, relURL)\nif idx < 0 {\n  return nil // nothing to remove\n}\nif err := client.RemoveWorkItemLink(ctx, id, idx); err != nil {\n  return fmt.Errorf(\"remove link from %d (idx %d): %w\", id, idx, err)\n}","handlingStrategy":"validation","validationCode":"// validate the index against freshly fetched relations immediately before removal\nwi, err := client.GetWorkItem(ctx, sourceID)\nif err != nil {\n  return err\n}\nif relationIndex < 0 || relationIndex >= len(wi.Relations) {\n  return fmt.Errorf(\"relation index %d out of range (have %d)\", relationIndex, len(wi.Relations))\n}","typeGuard":"func validRelationIndex(n, total int) bool { return n >= 0 && n < total }","tryCatchPattern":"if err := client.RemoveWorkItemLink(ctx, sourceID, idx); err != nil {\n  if strings.Contains(err.Error(), \"404\") {\n    // stale index: refetch relations and recompute\n    return retryWithFreshIndex(ctx, sourceID, relURL)\n  }\n  return err\n}","preventionTips":["Re-fetch relations and compute the index immediately before removing","Remove links one at a time rather than batching index-based removals (indices shift)","Identify relations by URL/id, not by position, wherever possible","Handle 404 as 'already removed' in idempotent workflows"],"tags":["azure-devops","http","work-items","concurrency"],"backgroundTag":"http-request-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}