Skip to content

fix(integrations): retry linearGraphQl on a transient Linear 429 - #9385

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
kai392:fix/critical-issue-linear-429-retry
Jul 27, 2026
Merged

fix(integrations): retry linearGraphQl on a transient Linear 429#9385
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
kai392:fix/critical-issue-linear-429-retry

Conversation

@kai392

@kai392 kai392 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Closes #9319

What

linearGraphQl (the raw POST helper shared by LinearAdapter.listOpenProjects and findLinearNativeLink) threw immediately on any non-OK response, so a transient Linear 429 rate-limit aborted the call — unlike src/github/client.ts, which already bounds its rate-limit responses with a small retry.

This adds a bounded retry mirroring that shape, but with Linear's own status/header semantics and file-local constants (this module's convention, like LINEAR_FETCH_TIMEOUT_MS):

  • LINEAR_RATE_LIMIT_MAX_RETRIES = 2, LINEAR_RATE_LIMIT_BASE_BACKOFF_MS = 500, LINEAR_RATE_LIMIT_MAX_BACKOFF_MS = 4000
  • linearRateLimitDelayMs(retryAfterHeader, attempt) — honors a valid non-negative Retry-After (seconds), else capped exponential backoff, always bounded by the max.
  • On 429 with retries left: sleep then retry; any other non-OK status, or an exhausted 429, throws exactly as before (the existing error contract is preserved).

Tests

4 new cases in test/unit/linear-adapter.test.ts (100% branch on the new code): 429→Retry-After: 0→200 succeeds; 429 exhausted throws HTTP 429 after exactly 3 calls; 429 with no Retry-After backs off then succeeds; negative Retry-After falls back to backoff. Full file: 22 passed.

@kai392
kai392 requested a review from JSONbored as a code owner July 27, 2026 15:29
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

linearGraphQl (the sole HTTP entry point for every LinearAdapter method) did one fetch and threw on
any non-OK response, so a transient Linear 429 (rate limit) hard-failed on the first attempt — unlike
the GitHub-backed sibling adapters, which retry via the shared GitHub client. Add a bounded retry for
429 only: honor a valid non-negative Retry-After header when present, else a capped exponential
backoff, up to a small fixed cap. Any other non-OK status still throws immediately, and an exhausted
429 falls through to the same throw, preserving the existing error contract. Bounded so the caller
never hangs (same intent as LINEAR_FETCH_TIMEOUT_MS).

Closes JSONbored#9319

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.40%. Comparing base (83b6867) to head (4ee0ad8).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9385      +/-   ##
==========================================
+ Coverage   75.38%   75.40%   +0.01%     
==========================================
  Files         275      276       +1     
  Lines       58023    58069      +46     
  Branches     6181     6193      +12     
==========================================
+ Hits        43739    43785      +46     
  Misses      14014    14014              
  Partials      270      270              
Flag Coverage Δ
backend 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/integrations/linear-adapter.ts 100.00% <100.00%> (ø)

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-27 15:39:35 UTC

2 files · 1 AI reviewer · no blockers · readiness 95/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This mirrors the existing github/client.ts and http-retry.ts rate-limit-retry pattern for the Linear GraphQL POST helper: a bounded loop retries a 429 up to LINEAR_RATE_LIMIT_MAX_RETRIES times, honoring a valid non-negative Retry-After header (capped) or falling back to capped exponential backoff, and preserves the exact prior error contract (`Linear API HTTP <status>`) for exhausted retries and all other non-OK statuses. The logic is correct: response.ok short-circuits to the existing success path, only 429 with attempts remaining loops, everything else throws immediately as before. Test coverage is thorough — success-after-retry, exhausted-retry-throws with exact call count, no-Retry-After backoff, and negative/invalid Retry-After treated as absent — covering both branches of every new conditional.

Nits — 3 non-blocking
  • src/integrations/linear-adapter.ts:57 uses the bare literal `429` inline where the sibling github client pattern names rate-limit statuses via a helper — a small `LINEAR_RATE_LIMIT_STATUS = 429` constant would match that convention, though it's a minor readability point given the file already documents the retry rationale in the comment above it.
  • The PR closes fix(integrations): linearGraphQl has no retry on a transient Linear rate limit #9319 per the description, satisfying the issue-link requirement for this repo.
  • src/integrations/linear-adapter.ts: consider extracting a small shared `computeRetryDelayMs(retryAfterHeader, attempt, base, max)` helper between this file and github/client.ts's `rateLimitRetryMs` (and packages/loopover-miner/lib/http-retry.ts's `retryDelayMs`) since the three now duplicate near-identical Retry-After/backoff logic — not blocking, since each host has slightly different semantics (this one doesn't take the max-of-backoff-vs-Retry-After the miner does).

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9319
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 96 registered-repo PR(s), 50 merged, 6 issue(s).
Contributor context ✅ Confirmed Gittensor contributor kai392; Gittensor profile; 96 PR(s), 6 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff adds a bounded retry loop in linearGraphQl that retries only on 429 (honoring Retry-After or falling back to capped exponential backoff), leaves non-429 non-OK responses throwing immediately, and preserves the final throw once retries are exhausted, matching all stated requirements.

Review context
  • Author: kai392
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, Cuda, JavaScript, Kotlin, Perl, TypeScript, Vue
  • Official Gittensor activity: 96 PR(s), 6 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Triage stale or unlinked PRs.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 7dee222 into JSONbored:main Jul 27, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants