{"record":{"id":"bee555c8ebba3805","repo":"gastownhall/beads","slug":"failed-after-d-retries-w","errorCode":null,"errorMessage":"failed after %d retries: %w","messagePattern":"failed after (.+?) retries: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/compact/haiku.go","lineNumber":204,"sourceCode":"\n\t\tlastErr = err\n\n\t\tif ctx.Err() != nil {\n\t\t\treturn \"\", ctx.Err()\n\t\t}\n\n\t\tif !isRetryable(err) {\n\t\t\tspan.RecordError(err)\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\t\t\treturn \"\", fmt.Errorf(\"non-retryable error: %w\", err)\n\t\t}\n\t}\n\n\tif lastErr != nil {\n\t\tspan.RecordError(lastErr)\n\t\tspan.SetStatus(codes.Error, lastErr.Error())\n\t}\n\treturn \"\", fmt.Errorf(\"failed after %d retries: %w\", h.maxRetries+1, lastErr)\n}\n\nfunc isRetryable(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\treturn false\n\t}\n\n\tvar netErr net.Error\n\tif errors.As(err, &netErr) && netErr.Timeout() {\n\t\treturn true\n\t}\n\n\tvar apiErr *anthropic.Error\n\tif errors.As(err, &apiErr) {","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/compact/haiku.go#L186-L222","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Wait for the rate-limit window to reset and re-run compaction (respect Retry-After from the wrapped 429)","Throttle batch compaction — process fewer issues concurrently or add delay between calls","Increase maxRetries/backoff if transient outages are common in your environment","Use a dedicated or higher-tier API key to raise rate limits; check status.anthropic.com for outages"],"exampleFix":"// before\nclient, err := compact.New(key) // default retries\n// after\n// configure larger retry budget / backoff via client options if the API exposes it,\n// or serialize calls:\nfor _, iss := range issues {\n\tsum, err := client.SummarizeTier1(ctx, iss)\n\tif err != nil {\n\t\ttime.Sleep(30 * time.Second) // back off on 429 before continuing\n\t}\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"summary, err := client.SummarizeTier1(ctx, issue)\nif err != nil && strings.Contains(err.Error(), \"failed after\") {\n\tselect {\n\tcase <-time.After(backoff):\n\t\tsummary, err = client.SummarizeTier1(ctx, issue)\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}","preventionTips":["Serialize or rate-limit batch compaction to stay under quotas","Use a dedicated API key with sufficient rate limits","Add generous timeouts and exponential backoff around bulk runs","Monitor provider status pages during known incident windows"],"tags":["retry","rate-limit","network","anthropic"],"backgroundTag":"api-rate-limit-exceeded","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}