{"record":{"id":"20ede56ac67f05d9","repo":"gastownhall/beads","slug":"fetch-parent-s-w","errorCode":null,"errorMessage":"fetch parent %s: %w","messagePattern":"fetch parent (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/linear/parent_reconcile.go","lineNumber":194,"sourceCode":"\t\tif err != nil {\n\t\t\t// Rate-limit circuit breaker tripped — stop now rather than\n\t\t\t// hammer the API for every remaining link.\n\t\t\tif isRateLimitExhausted(err) {\n\t\t\t\treturn stats, fmt.Errorf(\"fetch child %s: %w\", link.ChildIdentifier, err)\n\t\t\t}\n\t\t\tstats.Errors = append(stats.Errors,\n\t\t\t\tfmt.Errorf(\"fetch child %s: %w\", link.ChildIdentifier, err))\n\t\t\tcontinue\n\t\t}\n\t\tif childE.issue == nil {\n\t\t\tstats.NotFound = append(stats.NotFound, link.ChildIdentifier)\n\t\t\tcontinue\n\t\t}\n\n\t\tparentE, err := fetchIssue(link.ParentIdentifier)\n\t\tif err != nil {\n\t\t\tif isRateLimitExhausted(err) {\n\t\t\t\treturn stats, fmt.Errorf(\"fetch parent %s: %w\", link.ParentIdentifier, err)\n\t\t\t}\n\t\t\tstats.Errors = append(stats.Errors,\n\t\t\t\tfmt.Errorf(\"fetch parent %s: %w\", link.ParentIdentifier, err))\n\t\t\tcontinue\n\t\t}\n\t\tif parentE.issue == nil {\n\t\t\tstats.NotFound = append(stats.NotFound, link.ParentIdentifier)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Idempotency: skip if remote parent already matches by UUID.\n\t\tif childE.issue.Parent != nil && childE.issue.Parent.ID == parentE.issue.ID {\n\t\t\tstats.Skipped++\n\t\t\tcontinue\n\t\t}\n\n\t\tif dryRun {\n\t\t\t// Dry-run: record the intended mutation but skip the API call.","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/linear/parent_reconcile.go#L176-L212","documentation":"ReconcileParents fetches each link's parent issue from Linear via fetchIssue. When the fetch fails AND isRateLimitExhausted(err) reports the Linear API rate limit is fully exhausted, the function aborts the entire reconciliation immediately by returning this wrapped error instead of merely recording it in stats.Errors. The %w wrapping preserves the underlying rate-limit error for errors.Is/errors.As inspection.","triggerScenarios":"Calling ReconcileParents (via reconcileLinearParents) when a link.ParentIdentifier fetch hits Linear's API rate limit that is already exhausted (retries depleted), causing the early return path at the isRateLimitExhausted(err) branch.","commonSituations":"Bulk parent-link reconciliation over many issues in a short window; multiple sync workers sharing one Linear API key; a cron/daemon loop running ReconcileParents more often than the Linear rate budget allows.","solutions":["Wait for the Linear rate-limit window to reset (inspect Retry-After / the underlying error) and re-run reconciliation","Reduce request volume: batch or throttle fetchIssue calls, and run ReconcileParents less frequently","Process links in smaller batches and persist progress so a rate-limit abort resumes rather than restarts","Check for other processes or jobs sharing the same Linear API key and stagger them"],"exampleFix":"// before: tight loop aborting on limit\nfor _, link := range links { stats, err := ReconcileParents(ctx, link) }\n// after: backoff between batches\nif err != nil && isRateLimitExhausted(err) {\n    time.Sleep(backoff)\n    continue // resume from saved progress\n}","handlingStrategy":"retry","validationCode":"// no pre-call validation possible; budget requests ahead of time\nlimiter := rate.NewLimiter(rate.Every(time.Second), 5)\nif !limiter.Allow() { time.Sleep(time.Second) }","typeGuard":"null","tryCatchPattern":"stats, err := ReconcileParents(ctx, links)\nif err != nil && isRateLimitExhausted(err) {\n    wait, ok := retryAfterFrom(err) // inspect wrapped error\n    if !ok { wait = time.Minute }\n    time.Sleep(wait)\n    stats, err = ReconcileParents(ctx, remaining(links))\n}","preventionTips":["Throttle Linear API calls with a client-side rate limiter sized below Linear's documented limits","Persist reconciliation progress so aborted runs resume instead of restarting from zero","Stagger jobs that share the same Linear API key"],"tags":["rate-limit","linear-api","network"],"backgroundTag":"api-rate-limit-exhausted","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}