gastownhall/beads · error

fetch parent %s: %w

Error message

fetch parent %s: %w

What it means

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.

Source

Thrown at internal/linear/parent_reconcile.go:194

		if err != nil {
			// Rate-limit circuit breaker tripped — stop now rather than
			// hammer the API for every remaining link.
			if isRateLimitExhausted(err) {
				return stats, fmt.Errorf("fetch child %s: %w", link.ChildIdentifier, err)
			}
			stats.Errors = append(stats.Errors,
				fmt.Errorf("fetch child %s: %w", link.ChildIdentifier, err))
			continue
		}
		if childE.issue == nil {
			stats.NotFound = append(stats.NotFound, link.ChildIdentifier)
			continue
		}

		parentE, err := fetchIssue(link.ParentIdentifier)
		if err != nil {
			if isRateLimitExhausted(err) {
				return stats, fmt.Errorf("fetch parent %s: %w", link.ParentIdentifier, err)
			}
			stats.Errors = append(stats.Errors,
				fmt.Errorf("fetch parent %s: %w", link.ParentIdentifier, err))
			continue
		}
		if parentE.issue == nil {
			stats.NotFound = append(stats.NotFound, link.ParentIdentifier)
			continue
		}

		// Idempotency: skip if remote parent already matches by UUID.
		if childE.issue.Parent != nil && childE.issue.Parent.ID == parentE.issue.ID {
			stats.Skipped++
			continue
		}

		if dryRun {
			// Dry-run: record the intended mutation but skip the API call.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Wait for the Linear rate-limit window to reset (inspect Retry-After / the underlying error) and re-run reconciliation
  2. Reduce request volume: batch or throttle fetchIssue calls, and run ReconcileParents less frequently
  3. Process links in smaller batches and persist progress so a rate-limit abort resumes rather than restarts
  4. Check for other processes or jobs sharing the same Linear API key and stagger them

Example fix

// before: tight loop aborting on limit
for _, link := range links { stats, err := ReconcileParents(ctx, link) }
// after: backoff between batches
if err != nil && isRateLimitExhausted(err) {
    time.Sleep(backoff)
    continue // resume from saved progress
}
Defensive patterns

Strategy: retry

Validate before calling

// no pre-call validation possible; budget requests ahead of time
limiter := rate.NewLimiter(rate.Every(time.Second), 5)
if !limiter.Allow() { time.Sleep(time.Second) }

Type guard

null

Try / catch

stats, err := ReconcileParents(ctx, links)
if err != nil && isRateLimitExhausted(err) {
    wait, ok := retryAfterFrom(err) // inspect wrapped error
    if !ok { wait = time.Minute }
    time.Sleep(wait)
    stats, err = ReconcileParents(ctx, remaining(links))
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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