gastownhall/beads · error

set parent of %s → %s: %w

Error message

set parent of %s → %s: %w

What it means

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.

Source

Thrown at internal/linear/parent_reconcile.go:227

		}

		if dryRun {
			// Dry-run: record the intended mutation but skip the API call.
			// All read-only state (fetch results, idempotency check above)
			// matched wet-run, so the preview is trustworthy.
			stats.Mutations = append(stats.Mutations, link)
			stats.WouldUpdate++
			continue
		}

		// Use the child's host client (resolved during fetch) so the
		// update goes to the correct team in multi-team setups.
		updated, err := childE.client.UpdateIssue(ctx, childE.issue.ID, map[string]interface{}{
			"parentId": parentE.issue.ID,
		})
		if err != nil {
			if isRateLimitExhausted(err) {
				return stats, fmt.Errorf("set parent of %s → %s: %w",
					link.ChildIdentifier, link.ParentIdentifier, err)
			}
			stats.Errors = append(stats.Errors,
				fmt.Errorf("set parent of %s → %s: %w",
					link.ChildIdentifier, link.ParentIdentifier, err))
			continue
		}
		// Refresh cache with the post-update issue (Linear returns the
		// updated record), so a later link that references this child
		// as a parent sees the freshest state. Host client is unchanged.
		if updated != nil {
			fetched[link.ChildIdentifier] = entry{issue: updated, client: childE.client}
		}
		// Record the mutation only AFTER the API call succeeds, so
		// Mutations reflects actual state propagated to Linear (callers
		// can trust the list for accurate post-sync reporting).
		stats.Mutations = append(stats.Mutations, link)
		stats.Updated++

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pause and wait for the Linear rate-limit window to reset, then re-run reconciliation
  2. Throttle UpdateIssue calls (add per-request delay or a token bucket) inside ReconcileParents
  3. Reduce concurrent writers sharing the same Linear API key
  4. Persist reconciliation progress so aborted runs resume at the failed link

Example fix

// before
stats, err := ReconcileParents(ctx, allLinks)
// after: chunked with pause on rate limit
for chunk := range chunks(allLinks, 50) {
    stats, err = ReconcileParents(ctx, chunk)
    if isRateLimitExhausted(err) { time.Sleep(rateLimitWait) }
}
Defensive patterns

Strategy: retry

Validate before calling

// pace writes before calling
for i := range links {
    if i > 0 { time.Sleep(200 * time.Millisecond) }
    _ = i
}

Type guard

null

Try / catch

stats, err := ReconcileParents(ctx, links)
if err != nil && isRateLimitExhausted(err) {
    wait := retryAfterFrom(err)
    if wait == 0 { wait = time.Minute }
    time.Sleep(wait)
    // resume from persisted progress
}

Prevention

When it happens

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

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

Related errors


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