{"record":{"id":"ed2b142deaed5d29","repo":"gastownhall/beads","slug":"set-parent-of-s-s-w","errorCode":null,"errorMessage":"set parent of %s → %s: %w","messagePattern":"set parent of (.+?) → (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/linear/parent_reconcile.go","lineNumber":227,"sourceCode":"\t\t}\n\n\t\tif dryRun {\n\t\t\t// Dry-run: record the intended mutation but skip the API call.\n\t\t\t// All read-only state (fetch results, idempotency check above)\n\t\t\t// matched wet-run, so the preview is trustworthy.\n\t\t\tstats.Mutations = append(stats.Mutations, link)\n\t\t\tstats.WouldUpdate++\n\t\t\tcontinue\n\t\t}\n\n\t\t// Use the child's host client (resolved during fetch) so the\n\t\t// update goes to the correct team in multi-team setups.\n\t\tupdated, err := childE.client.UpdateIssue(ctx, childE.issue.ID, map[string]interface{}{\n\t\t\t\"parentId\": parentE.issue.ID,\n\t\t})\n\t\tif err != nil {\n\t\t\tif isRateLimitExhausted(err) {\n\t\t\t\treturn stats, fmt.Errorf(\"set parent of %s → %s: %w\",\n\t\t\t\t\tlink.ChildIdentifier, link.ParentIdentifier, err)\n\t\t\t}\n\t\t\tstats.Errors = append(stats.Errors,\n\t\t\t\tfmt.Errorf(\"set parent of %s → %s: %w\",\n\t\t\t\t\tlink.ChildIdentifier, link.ParentIdentifier, err))\n\t\t\tcontinue\n\t\t}\n\t\t// Refresh cache with the post-update issue (Linear returns the\n\t\t// updated record), so a later link that references this child\n\t\t// as a parent sees the freshest state. Host client is unchanged.\n\t\tif updated != nil {\n\t\t\tfetched[link.ChildIdentifier] = entry{issue: updated, client: childE.client}\n\t\t}\n\t\t// Record the mutation only AFTER the API call succeeds, so\n\t\t// Mutations reflects actual state propagated to Linear (callers\n\t\t// can trust the list for accurate post-sync reporting).\n\t\tstats.Mutations = append(stats.Mutations, link)\n\t\tstats.Updated++","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/linear/parent_reconcile.go#L209-L245","documentation":"After resolving both child and parent issues, ReconcileParents calls childE.client.UpdateIssue to set parentId. If that mutation fails with an exhausted rate limit, the function aborts immediately, returning this wrapped error (child and parent identifiers included) rather than recording it in stats.Errors. This prevents hammering Linear once the quota is gone.","triggerScenarios":"The UpdateIssue(ctx, childE.issue.ID, {\"parentId\": parentE.issue.ID}) call fails and isRateLimitExhausted(err) is true — i.e. the write to Linear is rejected because the API rate budget is exhausted.","commonSituations":"Large reconciliation runs issuing one UpdateIssue per link; several agents or CI jobs writing to Linear concurrently with the same key; immediately following a run that already drained the quota via fetch calls.","solutions":["Pause and wait for the Linear rate-limit window to reset, then re-run reconciliation","Throttle UpdateIssue calls (add per-request delay or a token bucket) inside ReconcileParents","Reduce concurrent writers sharing the same Linear API key","Persist reconciliation progress so aborted runs resume at the failed link"],"exampleFix":"// before\nstats, err := ReconcileParents(ctx, allLinks)\n// after: chunked with pause on rate limit\nfor chunk := range chunks(allLinks, 50) {\n    stats, err = ReconcileParents(ctx, chunk)\n    if isRateLimitExhausted(err) { time.Sleep(rateLimitWait) }\n}","handlingStrategy":"retry","validationCode":"// pace writes before calling\nfor i := range links {\n    if i > 0 { time.Sleep(200 * time.Millisecond) }\n    _ = i\n}","typeGuard":"null","tryCatchPattern":"stats, err := ReconcileParents(ctx, links)\nif err != nil && isRateLimitExhausted(err) {\n    wait := retryAfterFrom(err)\n    if wait == 0 { wait = time.Minute }\n    time.Sleep(wait)\n    // resume from persisted progress\n}","preventionTips":["Add server-aware backoff (Retry-After) to the Linear client wrapper","Keep reconciliation batches small to bound quota consumption per run","Monitor quota consumption across all processes using the API key"],"tags":["rate-limit","linear-api","write-failure"],"backgroundTag":"api-rate-limit-exhausted","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}