{"record":{"id":"23107df6c0b18167","repo":"gastownhall/beads","slug":"idempotency-check-failed-w","errorCode":null,"errorMessage":"idempotency check failed: %w","messagePattern":"idempotency check failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/linear/client.go","lineNumber":956,"sourceCode":"// creating, queries Linear to see if an issue with that marker already exists.\n// If a match is found (e.g., from a prior interrupted sync), the existing\n// issue is returned without creating a duplicate.\n//\n// The create is performed as a single attempt (no internal retry) to avoid the\n// following race: if issueCreate reaches Linear but the HTTP response is lost\n// (network timeout, connection drop), a blind retry would create a second issue\n// with the same marker. Instead, after any create failure, this function\n// re-searches for the marker so that the caller can safely retry the entire\n// CreateIssueIdempotent call and get a consistent result.\n//\n// Note: concurrent creates from multiple sources (e.g., two sync processes\n// running simultaneously) cannot be made fully atomic without server-side\n// uniqueness enforcement, which Linear does not provide. The dedup window is\n// bounded by Linear's search-index propagation delay.\nfunc (c *Client) CreateIssueIdempotent(ctx context.Context, title, description string, priority int, stateID string, labelIDs []string, marker string) (*Issue, bool, error) {\n\texisting, err := c.FindIssueByDescriptionContains(ctx, marker)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"idempotency check failed: %w\", err)\n\t}\n\tif existing != nil {\n\t\treturn existing, true, nil\n\t}\n\n\tdescription = AppendIdempotencyMarker(description, marker)\n\tissue, err := c.createIssueSingleAttempt(ctx, title, description, priority, stateID, labelIDs)\n\tif err != nil {\n\t\t// The mutation may have reached Linear despite the error. Re-check for\n\t\t// the marker so callers retrying CreateIssueIdempotent get a consistent\n\t\t// result rather than creating a duplicate.\n\t\tif found, searchErr := c.FindIssueByDescriptionContains(ctx, marker); searchErr == nil && found != nil {\n\t\t\treturn found, true, nil\n\t\t}\n\t\treturn nil, false, err\n\t}\n\treturn issue, false, nil\n}","sourceCodeStart":938,"sourceCodeEnd":974,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/linear/client.go#L938-L974","documentation":"Before creating an issue, CreateIssueIdempotent calls FindIssueByDescriptionContains to detect an already-created issue with the same dedup marker. If that search itself fails (transport error, API error, GraphQL error), the idempotency guarantee cannot be established, so the library aborts with this wrapped error instead of risking a duplicate. The underlying cause is always wrapped and available via errors.As/Unwrap.","triggerScenarios":"Network failure during the search query; HTTP auth/rate-limit error from Linear during search; GraphQL error in the search query; search index propagation delay causing inconsistent results rather than an error (bounded window, per doc comment).","commonSituations":"Concurrent workers racing during a Linear outage; expired token surfacing first in the idempotency pre-check; hitting rate limits because every create now does an extra search call.","solutions":["Inspect the wrapped cause — it is one of the search-path errors (request failed / API error / GraphQL errors)","Retry the whole CreateIssueIdempotent call with backoff; the check-then-create is safe to rerun","Fix auth/rate-limit root causes (valid token, throttling) since the search amplifies API usage","Accept the documented dedup window: don't treat marker-based dedup as strictly atomic under concurrency"],"exampleFix":"// before\nissue, existed, err := client.CreateIssueIdempotent(ctx, ...) // transient search failure = hard abort\n// after\nvar issue *linear.Issue\nvar existed bool\nerr := retry.OnError(3, backoff, func() error {\n    var e error\n    issue, existed, e = client.CreateIssueIdempotent(ctx, title, desc, prio, stateID, labels, marker)\n    return e\n})","handlingStrategy":"retry","validationCode":"// validate search preconditions before idempotent create\nif marker == \"\" {\n    return errors.New(\"idempotency marker must be non-empty\")\n}\nif err := ctx.Err(); err != nil {\n    return err\n}","typeGuard":null,"tryCatchPattern":"var issue *linear.Issue\nerr := retry.Do(3, backoff, func() error {\n    var e error\n    issue, _, e = client.CreateIssueIdempotent(ctx, title, desc, prio, stateID, labels, marker)\n    return e\n})\nif err != nil && strings.Contains(err.Error(), \"idempotency check failed\") {\n    // inspect wrapped cause; do not blind-create without the marker search\n}","preventionTips":["Always retry the full idempotent create rather than falling back to plain create","Monitor rate limits: each create includes a search call","Never bypass the marker search manually — duplicates are the risk it guards","Accept the documented dedup window under high concurrency"],"tags":["idempotency","network","search"],"backgroundTag":"idempotency-check-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}