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
- 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
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
- 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
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
- batch create failed and recovery search also failed: %w (bat
- fetch parent %s: %w
- failed to import config %q: %w
- transient error %d (attempt %d/%d)
- max retries (%d) exceeded: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ed2b142deaed5d29.
Report an issue: GitHub.