gastownhall/beads · error

failed after %d retries: %w

Error message

failed after %d retries: %w

What it means

When every attempt in callWithRetry fails with a retryable error, the client gives up after h.maxRetries retries (maxRetries+1 total attempts) and wraps the final error with this message. It signals sustained unavailability or rate limiting of the AI endpoint, not a client bug.

Source

Thrown at internal/compact/haiku.go:204

		lastErr = err

		if ctx.Err() != nil {
			return "", ctx.Err()
		}

		if !isRetryable(err) {
			span.RecordError(err)
			span.SetStatus(codes.Error, err.Error())
			return "", fmt.Errorf("non-retryable error: %w", err)
		}
	}

	if lastErr != nil {
		span.RecordError(lastErr)
		span.SetStatus(codes.Error, lastErr.Error())
	}
	return "", fmt.Errorf("failed after %d retries: %w", h.maxRetries+1, lastErr)
}

func isRetryable(err error) bool {
	if err == nil {
		return false
	}

	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return false
	}

	var netErr net.Error
	if errors.As(err, &netErr) && netErr.Timeout() {
		return true
	}

	var apiErr *anthropic.Error
	if errors.As(err, &apiErr) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Wait for the rate-limit window to reset and re-run compaction (respect Retry-After from the wrapped 429)
  2. Throttle batch compaction — process fewer issues concurrently or add delay between calls
  3. Increase maxRetries/backoff if transient outages are common in your environment
  4. Use a dedicated or higher-tier API key to raise rate limits; check status.anthropic.com for outages

Example fix

// before
client, err := compact.New(key) // default retries
// after
// configure larger retry budget / backoff via client options if the API exposes it,
// or serialize calls:
for _, iss := range issues {
	sum, err := client.SummarizeTier1(ctx, iss)
	if err != nil {
		time.Sleep(30 * time.Second) // back off on 429 before continuing
	}
}
Defensive patterns

Strategy: retry

Try / catch

summary, err := client.SummarizeTier1(ctx, issue)
if err != nil && strings.Contains(err.Error(), "failed after") {
	select {
	case <-time.After(backoff):
		summary, err = client.SummarizeTier1(ctx, issue)
	case <-ctx.Done():
		return ctx.Err()
	}
}

Prevention

When it happens

Trigger: All maxRetries+1 attempts of the API call return retryable errors (429 rate limit, 5xx server errors, transient network timeouts) so lastErr is wrapped after the loop exits.

Common situations: Hitting Anthropic rate limits during batch compaction of many issues; provider outage (5xx); corporate proxy or flaky network dropping connections; per-minute token quota exhausted on a shared key.

Related errors


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