Skip to content

fix(integrations): linearGraphQl has no retry on a transient Linear rate limit #9319

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

linearGraphQl (src/integrations/linear-adapter.ts:24-36) is the sole HTTP entry point for
every LinearAdapter method — it does one raw fetch() with a 10s timeout
(LINEAR_FETCH_TIMEOUT_MS) and throws immediately on any non-OK response:

async function linearGraphQl<T>(apiKey: string, query: string, variables: Record<string, unknown>): Promise<T> {
  const response = await fetch(LINEAR_API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: apiKey },
    body: JSON.stringify({ query, variables }),
    signal: AbortSignal.timeout(LINEAR_FETCH_TIMEOUT_MS),
  });
  if (!response.ok) throw new Error(`Linear API HTTP ${response.status}`);
  ...
}

There is no retry of any kind — a transient Linear 429 hard-fails on the very first attempt.

LinearAdapter implements the same ProjectTrackerAdapter interface as the GitHub-backed
adapters in src/integrations/project-tracker-adapter.ts, which are meant to be interchangeable
(project-tracker-adapter.ts:27-31's doc comment), and resolveTrackerMatches treats both paths
symmetrically (.catch(() => []) fail-open). But every GitHub-backed call in that file goes
through makeInstallationOctokittimeoutFetchfetchWithGitHubRetry
(src/github/client.ts:457-486), which explicitly detects a rate limit
(isRateLimitedResponse, src/github/client.ts:411-419 — a 403/429 with a Retry-After header,
an exhausted x-ratelimit-remaining, or a secondary-limit/abuse response body) and retries with
Retry-After-aware exponential backoff (rateLimitRetryMs, src/github/client.ts:426-433,
capped via GITHUB_RATE_LIMIT_MAX_RETRIES/GITHUB_RATE_LIMIT_MAX_DELAY_MS) before surfacing the
failure. src/gittensor/api.ts (a separate raw-fetch third-party API module in this same
codebase) also has its own bounded timeout/retry handling for this reason.

As a result, a transient Linear rate-limit degrades straight to "no suggestion" for the whole
adapter on the very first blip, while the equivalent transient GitHub rate limit is absorbed and
still produces a correct suggestion in the common case — a real, mechanically verifiable asymmetry
between two adapters of the same interface family.

test/unit/linear-adapter.test.ts:85-91 ("listOpenProjects throws on an HTTP-level failure")
currently asserts an immediate-throw-on-503 as correct — that assertion is fine and must keep
passing (503 is not a rate-limit signal); the gap is that no test anywhere exercises a
429-then-success retry path, confirming this is untested as well as unhandled.

Requirements

  • Add a bounded retry to linearGraphQl for a 429 response (Linear's GraphQL API returns 429
    with a standard Retry-After header for rate limiting): on 429, wait per Retry-After if
    present and valid, else an exponential backoff, then retry the same request — capped at a fixed
    small number of retries (mirror the shape of GITHUB_RATE_LIMIT_MAX_RETRIES/
    rateLimitRetryMs in src/github/client.ts:411-433, adapted to Linear's own status/header
    semantics; you do not need to reuse GitHub's exact functions, since Linear is a different host —
    a local, adapted equivalent in this file is correct, matching this file's own convention of
    local per-adapter constants like LINEAR_FETCH_TIMEOUT_MS).
  • Any non-429 non-OK response (e.g. the existing 503 test case) must continue to throw
    immediately on the first attempt — no behavior change for a genuine error.
  • Once retries are exhausted, the existing throw new Error(\Linear API HTTP ${response.status}`)`
    behavior must still apply.
  • Total added latency must stay bounded (mirroring LINEAR_FETCH_TIMEOUT_MS's existing "never
    hang the caller" intent) — pick a small retry cap and a capped backoff ceiling, not unbounded
    retry.

Deliverables

  • linearGraphQl retries a 429 response up to a small, fixed cap, honoring Retry-After
    when present and falling back to a capped exponential backoff otherwise.
  • A non-429 non-OK response (e.g. 503) still throws immediately with no retry — verify the
    existing test at test/unit/linear-adapter.test.ts:85-91 still passes unmodified.
  • A new regression test asserting a 429-then-200 sequence succeeds (i.e. the adapter method
    returns the successful result, not a thrown error) after exactly one retry.
  • A new regression test asserting 429 exhausting the retry cap still throws
    Linear API HTTP 429 (or an equivalent error), matching this file's existing thrown-error
    shape.

All of the above Deliverables are required in the same PR.

Test Coverage Requirements

99%+ Codecov patch coverage, branch-counted, on every changed/added line in
src/integrations/linear-adapter.ts, including both the retry-then-succeed and
retry-exhausted-then-throw branches.

Expected Outcome

A transient Linear 429 self-heals via a bounded retry with backoff, matching the reliability bar
src/github/client.ts's fetchWithGitHubRetry already sets for the GitHub-backed adapters this
module is meant to be interchangeable with — instead of the whole LinearAdapter degrading to "no
suggestion" on the very first rate-limit blip.

Links & Resources

  • src/integrations/linear-adapter.ts:24-36 (linearGraphQl, the function to fix)
  • src/github/client.ts:411-433 (isRateLimitedResponse/rateLimitRetryMs), :457-486
    (fetchWithGitHubRetry) — the precedent for the retry shape
  • src/integrations/project-tracker-adapter.ts:27-31 (the interchangeable-adapter contract this
    fix brings LinearAdapter in line with)
  • test/unit/linear-adapter.test.ts:85-91 (existing non-retry coverage, must keep passing)

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions