Skip to content

GitHub Rate Limit Fallback -> Primary - #996

Merged
arul28 merged 12 commits into
mainfrom
ade/github-rate-limit-fallback
Aug 1, 2026
Merged

GitHub Rate Limit Fallback -> Primary#996
arul28 merged 12 commits into
mainfrom
ade/github-rate-limit-fallback

Conversation

@arul28

@arul28 arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

ADE   Open in ADE  ·  ade/github-rate-limit-fallback branch  ·  PR #996

Summary by CodeRabbit

  • New Features

    • Added detailed GitHub credential status with separate read/write connections, fallback sources, capability states, and refresh pauses.
    • Added automatic credential fallback and repository access handling.
    • Added GitHub relay health monitoring with retry backoff and server-provided retry timing.
    • Added clearer settings and integration notices for missing write access and temporary pauses.
  • Bug Fixes

    • Reduced unnecessary polling during relay cooldowns and GitHub rate-limit pauses.
    • Improved webhook authorization for installed repositories and linked events.
    • Improved Linear sign-in cleanup when sessions are interrupted or replaced.
  • Documentation

    • Documented GitHub status reporting, forced refreshes, and credential fallback behavior.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Aug 1, 2026 5:08pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

GitHub authentication now supports separate read, write, and Git transport credentials with fallback, cooldown, repository access tracking, and rate-limit pauses. Relay and PR polling use health-aware scheduling. The UI, CLI, token callers, OAuth service, and webhook authorization reflect the updated state model.

Changes

GitHub integration changes

Layer / File(s) Summary
Credential contracts, health, and service flow
apps/desktop/src/shared/*, apps/desktop/src/main/services/github/*, apps/ade-cli/src/headlessLinearServices.*
GitHub credentials now have read, write, and transport capabilities. Services track health, cooldowns, repository access, fallback state, rate limits, and background pauses.
Relay health and PR polling
apps/desktop/src/main/services/automations/*, apps/desktop/src/main/services/prs/*, apps/desktop/src/main/main.ts, apps/ade-cli/src/bootstrap.ts
Relay polling tracks lifecycle state and retry cooldowns. PR polling responds to relay health and GitHub background request pauses.
Integration lifecycles and presentation
apps/desktop/src/main/services/ipc/*, apps/desktop/src/main/services/cto/*, apps/desktop/src/main/services/projects/*, apps/desktop/src/renderer/*, apps/ade-cli/src/*, apps/webhook-relay/*
Callers use operation-specific credentials. The UI and CLI show credential state. Linear OAuth coordinates concurrent sessions and cleanup. Webhook authorization validates account and installed-repository bindings.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: desktop, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the pull request's GitHub rate-limit fallback changes and is concise enough for repository history.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/github-rate-limit-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from 54f15a5 to b57addc Compare August 1, 2026 08:23
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b57addc1d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tokenStored: true,
userLogin: validated.userLogin,
authSource: tokenLookup.source,
authSource: candidate.source,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep repo-capable classic tokens connected for reads

When a classic token has repo access but lacks the write-only workflow scope, /user succeeds and this candidate becomes active, but computeConnected still applies the old combined read/write scope requirement and returns false. prService.requireGithubSnapshotAuth then rejects every PR snapshot before the new request-level fallback can run, even though this change now defines connected as read usability and reports writes separately through writeAuthSource; evaluate read capability independently or continue to the next read candidate.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

Comment on lines +1259 to +1260
const canTryNext = !args.token
&& (response.status === 401 || response.status === 403 || response.status === 429);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fall back on inaccessible private-repository 404s

When a higher-precedence credential authenticates successfully but cannot see the target private repository, GitHub commonly returns 404 rather than 403; this condition immediately throws instead of trying a healthy App, CLI, or PAT candidate. The mirrored headless implementation has the same exclusion, so both desktop and socket-backed CLI paths lose the promised fallback for private-repository reads; retry repository-scoped 404s through the credential chain and report not-found only after the candidates are exhausted.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +1558 to +1559
const activeWriteCandidate = writeCandidates.find((candidate) => !statusCooldown(candidate))
?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify write credentials before advertising them

When the GitHub App succeeds as the preferred read credential, the loop stops without probing the lower gh or PAT tokens, yet this selection labels the first non-cooled write token as active. If that token is expired or under-scoped, writeAuthSource still hides the connection banner and enables write-dependent UI until the user's first write fails with 401/403; validate the write candidate separately, in both desktop and headless services, before reporting it as active.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from b57addc to b64b653 Compare August 1, 2026 09:27
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b64b653251

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +166 to +167
const permissionDenied = errorTypes.includes("FORBIDDEN")
|| /forbidden|resource not accessible|not accessible by integration|does not have access/i.test(message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retry GraphQL repository NOT_FOUND with fallback credentials

When a higher-priority credential cannot see a private repository, GitHub can return HTTP 200 with a GraphQL NOT_FOUND error such as “Could not resolve to a Repository”; this classifier recognizes only rate-limit and FORBIDDEN variants, so apiRequest returns the payload and prService.graphqlRequest throws before trying the App, CLI, or PAT fallback. The new REST 404 handling therefore still leaves GraphQL-backed PR status and merge reads broken in both desktop and headless paths; treat ambiguous repository NOT_FOUND responses as credential failures and exhaust the candidate chain.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines 2212 to +2213
async getTokenOrThrowAsync(): Promise<string> {
const token = (await readAuthToken()).token;
const token = (await readAuthToken("write")).token;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve App-only credentials for direct read callers

When the read-only GitHub App is the only healthy credential, forcing this getter to select a write credential makes it report missing auth even though status.connected is true. Direct read callers such as projectScaffoldService.listMyGitHubRepos and prService.downloadJobLogTail call this getter before performing repository or Actions reads, so those surfaces bypass the new App-first read chain and cannot use request fallback; expose a read capability for these callers or route them through the capability-aware request path, including the mirrored headless service.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (5)
apps/desktop/src/shared/githubOperationCredential.test.ts (1)

19-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for the degraded acceptance path.

The suite covers fallback when a later candidate works. It does not cover the branch where a repository-access failure is the only outcome. That branch sets active from a failed probe and calls onAcceptedProbe with validated: false, which decides whether the credential is reported as connected and whether probe success is recorded. Add a case with readCandidates: [app] only, and assert active.candidate.source === "app", activeWriteSource === null, and onAcceptedProbe receives false as the third argument.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/shared/githubOperationCredential.test.ts` around lines 19 -
56, Add a test alongside “resolves read fallback and validates the selected
writer once” covering a single read candidate whose probe reports repository
access failure. Use readCandidates: [app] with no writable candidate, then
assert active.candidate.source is "app", activeWriteSource is null, and
onAcceptedProbe is called with false as its third argument.
apps/desktop/src/main/services/github/githubService.ts (1)

1269-1277: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle a 304 response that has no cache entry.

inFlightConditionalGetKeys protects the key only until the finally block at Lines 1265-1267 runs. A concurrent request can evict the entry between that point and the lookup at Line 1270. Control then falls through to response.text(), and !response.ok reports the 304 as GitHub API request failed (HTTP 304).

Keep the key protected until the cached value is read, or repeat the request without if-none-match when the entry is gone. The headless implementation in apps/ade-cli/src/headlessLinearServices.ts has the same gap in a wider form.

♻️ Proposed change
-      } finally {
-        if (sentConditionalGet) inFlightConditionalGetKeys.delete(cacheKey);
-      }
-
-      if (response.status === 304) {
-        const cached = etagCache.get(cacheKey);
-        if (cached) {
-          recordGithubCredentialSuccess(candidate, response.headers);
-          repositoryFallback.recordSuccess(candidate);
-          releaseGitHubResponse(response);
-          return { data: cached.data as T, response, linkHeader: cached.linkHeader };
-        }
-      }
+      } catch (error) {
+        if (sentConditionalGet) inFlightConditionalGetKeys.delete(cacheKey);
+        throw error;
+      }
+
+      if (response.status === 304) {
+        const cached = etagCache.get(cacheKey);
+        if (sentConditionalGet) inFlightConditionalGetKeys.delete(cacheKey);
+        if (cached) {
+          recordGithubCredentialSuccess(candidate, response.headers);
+          repositoryFallback.recordSuccess(candidate);
+          releaseGitHubResponse(response);
+          return { data: cached.data as T, response, linkHeader: cached.linkHeader };
+        }
+        releaseGitHubResponse(response);
+        etagCache.delete(cacheKey);
+        continue;
+      }
+      if (sentConditionalGet) inFlightConditionalGetKeys.delete(cacheKey);

The continue retries the same candidate list from the start of the loop only if you re-enter it; if you prefer a direct retry, extract the request into a local function and call it once without the conditional header.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/github/githubService.ts` around lines 1269 -
1277, Update the conditional GitHub request flow around
inFlightConditionalGetKeys and the 304 cache lookup so the key remains protected
through reading etagCache. If no cached entry exists for a 304 response, retry
the same request without the if-none-match header (or otherwise re-enter the
candidate loop) instead of passing 304 to response.text() and the HTTP error
path; apply the equivalent fix in the headless implementation.
apps/desktop/src/main/services/automations/automationIngressService.test.ts (1)

1218-1224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the failure cooldown.

The new tests cover relay health for the configured and unconfigured paths. They do not cover the new cooldown logic: the exponential backoff at lines 1019-1027 of automationIngressService.ts, the Retry-After extension, and pollNow() clearing the cooldown at line 1105. Those branches gate all background polling, so a regression there stops relay ingest silently.

A test that returns 500 with a Retry-After header, asserts that the next pollGithubRelayOnce issues no fetch, and then asserts that pollNow() does issue one would cover the whole path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/automations/automationIngressService.test.ts`
around lines 1218 - 1224, Add tests for the failure cooldown in the relay
polling suite around pollGithubRelayOnce and pollNow: mock a 500 response with a
Retry-After header, verify the initial failure sets cooldown and the next
pollGithubRelayOnce performs no fetch, then call pollNow() and verify it clears
the cooldown and performs exactly one fetch.
apps/desktop/src/main/services/github/githubCredentialHealth.test.ts (1)

35-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting this test into focused cases.

This single it asserts shared cooldowns, resource-scoped cooldowns, background pause reserve, account filtering, health reset, and ignoreNonRateLimit. It calls clearGithubCredentialHealth() four times mid-test to reset state between phases. Each clearGithubCredentialHealth() boundary marks a natural test boundary. Separate it blocks would identify which behavior regressed when the suite fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/github/githubCredentialHealth.test.ts` around
lines 35 - 146, Split the oversized test around the existing
clearGithubCredentialHealth() boundaries into focused it blocks covering shared
cooldowns, background reserve and resource scoping, account filtering, and
non-rate-limit recovery. Move each phase’s setup and assertions into its own
test, retaining the relevant fake-timer setup and cleanup so each case remains
isolated and failures identify the affected behavior.
apps/desktop/src/main/services/prs/prService.ts (1)

4215-4215: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make mutation capability explicit in graphqlRequest.

graphqlRequest currently infers "write" by matching mutation at the start of the operation text. Add options.capability?: "read" | "write" and pass "write" from the mutation call sites so the credential routing does not depend on comment, fragment, or naming edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.ts` at line 4215, Update
graphqlRequest to accept an optional options.capability value of "read" or
"write", and use it when determining credential routing. Change mutation call
sites to pass capability: "write", while preserving read as the default for
other requests and removing reliance on the query-text mutation regex.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/headlessLinearServices.test.ts`:
- Around line 881-906: Update the test around the environment setup and cleanup
in the headless 404 fallback case to isolate credential sources: save and clear
ADE_GITHUB_TOKEN, GITHUB_TOKEN, and GH_TOKEN, set GH_CONFIG_DIR to an isolated
value, and restore all original values in the existing finally block alongside
ADE_HOME and globalThis.fetch. Preserve the test’s strict authorization
assertions.

In `@apps/ade-cli/src/headlessLinearServices.ts`:
- Around line 1087-1103: Protect conditional GET cache entries in the GitHub
request flow around fetchGitHub by tracking cache keys currently awaiting
responses, excluding those keys from the eviction loop, and removing them when
the request completes. When a 304 response has no cached entry, treat it as a
retryable cache miss rather than allowing it to fall through as an HTTP error,
matching the existing inFlightConditionalGetKeys behavior used by the desktop
implementation.

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`:
- Around line 1110-1113: Update the stop() method to reset
relayPollCooldownUntilMs and relayPollFailureCount alongside githubRelayHealthy,
ensuring a subsequent start() performs its initial poll immediately rather than
honoring stale failure state.
- Around line 1019-1030: Clamp the server-provided retry timestamp in the
cooldown calculation around relayPollFailureCount and relayPollCooldownUntilMs
so it cannot extend the cooldown beyond GITHUB_RELAY_POLL_BACKOFF_CAP_MS from
the current time. Preserve the local exponential backoff and ensure both
scheduled polling and the github_delivery path through pollGithubRelayOnce
observe the capped cooldown.

In `@apps/desktop/src/main/services/github/githubRateLimit.ts`:
- Around line 127-176: Update the GraphQL payload guard in
classifyGitHubGraphqlCredentialFailure to return null when the response contains
usable data, while preserving classification for error-only payloads. Validate
the data field as non-null/usable before extracting errors so neither the
permissionDenied nor rateLimited branches can downgrade a successful partial
response.

In `@apps/desktop/src/main/services/prs/prPollingService.ts`:
- Around line 252-254: Update the background pause selection in the PR polling
flow to choose the project-scoped callback whenever
getGithubBackgroundPauseUntilMs is provided, even when it resolves to null; only
call githubBackgroundRequestPauseUntilMs when the callback is absent. Preserve
the existing Promise resolution and ensure unrelated credential cooldowns cannot
affect project-scoped polling.
- Around line 306-310: Update the relay safety sweep in the polling flow around
prService.refresh() to record lastRelaySafetySweepAtMs before awaiting the
refresh, ensuring failed attempts advance the interval. Also adjust the final
delay selection so accumulated consecutiveFailures backoff is preferred over the
relay safety-sweep delay, while preserving the existing minimum delay and
healthy-relay behavior.

In `@apps/desktop/src/renderer/lib/githubIntegrationStatus.ts`:
- Around line 263-270: Update the status handling around writeAuthSource so a
connected GitHub App with writeAuthSource omitted is treated the same as
writeAuthSource === "none", returning the "no-write-credential" state and its
existing write-access messaging. Add a test covering authSource: "app",
connected: true, and an omitted writeAuthSource.

In `@apps/webhook-relay/src/relay.ts`:
- Around line 1525-1530: The account-token authorization path must terminate
when the account has no matching repository binding. In
apps/webhook-relay/src/relay.ts lines 1525-1530, update the flow after
authenticateAccount and githubRepositoryAccountMatches so a present accountId
with no match returns an authorization failure before
assertGitHubRepoAuthorized. In apps/webhook-relay/test/account.test.ts lines
674-679, include a valid GitHub bearer token alongside the ADE account token and
assert a 401 response with no provider calls.

---

Nitpick comments:
In `@apps/desktop/src/main/services/automations/automationIngressService.test.ts`:
- Around line 1218-1224: Add tests for the failure cooldown in the relay polling
suite around pollGithubRelayOnce and pollNow: mock a 500 response with a
Retry-After header, verify the initial failure sets cooldown and the next
pollGithubRelayOnce performs no fetch, then call pollNow() and verify it clears
the cooldown and performs exactly one fetch.

In `@apps/desktop/src/main/services/github/githubCredentialHealth.test.ts`:
- Around line 35-146: Split the oversized test around the existing
clearGithubCredentialHealth() boundaries into focused it blocks covering shared
cooldowns, background reserve and resource scoping, account filtering, and
non-rate-limit recovery. Move each phase’s setup and assertions into its own
test, retaining the relevant fake-timer setup and cleanup so each case remains
isolated and failures identify the affected behavior.

In `@apps/desktop/src/main/services/github/githubService.ts`:
- Around line 1269-1277: Update the conditional GitHub request flow around
inFlightConditionalGetKeys and the 304 cache lookup so the key remains protected
through reading etagCache. If no cached entry exists for a 304 response, retry
the same request without the if-none-match header (or otherwise re-enter the
candidate loop) instead of passing 304 to response.text() and the HTTP error
path; apply the equivalent fix in the headless implementation.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Line 4215: Update graphqlRequest to accept an optional options.capability
value of "read" or "write", and use it when determining credential routing.
Change mutation call sites to pass capability: "write", while preserving read as
the default for other requests and removing reliance on the query-text mutation
regex.

In `@apps/desktop/src/shared/githubOperationCredential.test.ts`:
- Around line 19-56: Add a test alongside “resolves read fallback and validates
the selected writer once” covering a single read candidate whose probe reports
repository access failure. Use readCandidates: [app] with no writable candidate,
then assert active.candidate.source is "app", activeWriteSource is null, and
onAcceptedProbe is called with false as its third argument.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d0e94491-0e9e-4929-a1d4-7a24099cd853

📥 Commits

Reviewing files that changed from the base of the PR and between 43ea118 and b64b653.

⛔ Files ignored due to path filters (3)
  • docs/features/automations/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
📒 Files selected for processing (31)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/webhook-relay/src/relay.ts
  • apps/webhook-relay/test/account.test.ts

Comment thread apps/ade-cli/src/headlessLinearServices.test.ts
Comment thread apps/ade-cli/src/headlessLinearServices.ts
Comment on lines +127 to +176
if (
!payload
|| typeof payload !== "object"
|| !("errors" in payload)
|| !Array.isArray(payload.errors)
) return null;
const errors: unknown[] = payload.errors;
const messages = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const message = "message" in error ? error.message : null;
return typeof message === "string" && message.trim() ? [message.trim()] : [];
});
const errorTypes = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const extensions = "extensions" in error
&& error.extensions
&& typeof error.extensions === "object"
? error.extensions
: null;
return [
"type" in error ? error.type : null,
extensions && "code" in extensions ? extensions.code : null,
extensions && "type" in extensions ? extensions.type : null,
]
.filter((value): value is string => typeof value === "string")
.map((value) => value.toUpperCase());
});
const message = messages.join("; ") || "GitHub GraphQL request failed.";
const rateLimit = readGitHubRateLimitState(headers);
const rateLimited = rateLimit?.remaining === 0
|| errorTypes.some((type) => type === "RATE_LIMITED" || type === "RATE_LIMIT")
|| /rate limit|too many requests|abuse detection/i.test(message);
if (rateLimited) {
return {
status: 429,
message,
...classifyGitHubAuthFailure({ status: 429, message, headers }),
};
}
const permissionDenied = errorTypes.includes("FORBIDDEN")
|| /forbidden|resource not accessible|not accessible by integration|does not have access/i.test(message);
if (permissionDenied) {
return {
status: 403,
message,
...classifyGitHubAuthFailure({ status: 403, message, headers }),
};
}
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Ignore GraphQL payloads that still carry usable data.

The guard only requires an errors array. GitHub returns partial responses that contain both data and per-field errors, for example a FORBIDDEN error on one node of an otherwise successful query. In that case:

  • The permissionDenied branch matches on FORBIDDEN, so classifyGitHubGraphqlCredentialFailure returns a non-null result.
  • githubService.ts (Lines 1341-1362) then calls recordGithubCredentialFailure, which places a working credential on the 5-minute FALLBACK_COOLDOWN_MS, retries the next credential, and finally throws even though the query returned data.

The same applies to the rateLimited branch when x-ratelimit-remaining is 0 on a successful query that carries a single field error.

Restrict classification to payloads without usable data.

🛠️ Proposed guard for partial responses
   if (
     !payload
     || typeof payload !== "object"
     || !("errors" in payload)
     || !Array.isArray(payload.errors)
   ) return null;
+  // GitHub returns partial GraphQL responses that carry both `data` and
+  // per-field `errors`. Those responses are usable, so they must not put the
+  // credential on cooldown or trigger a fallback retry.
+  const data = "data" in payload ? (payload as { data?: unknown }).data : null;
+  if (data != null) return null;
   const errors: unknown[] = payload.errors;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
!payload
|| typeof payload !== "object"
|| !("errors" in payload)
|| !Array.isArray(payload.errors)
) return null;
const errors: unknown[] = payload.errors;
const messages = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const message = "message" in error ? error.message : null;
return typeof message === "string" && message.trim() ? [message.trim()] : [];
});
const errorTypes = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const extensions = "extensions" in error
&& error.extensions
&& typeof error.extensions === "object"
? error.extensions
: null;
return [
"type" in error ? error.type : null,
extensions && "code" in extensions ? extensions.code : null,
extensions && "type" in extensions ? extensions.type : null,
]
.filter((value): value is string => typeof value === "string")
.map((value) => value.toUpperCase());
});
const message = messages.join("; ") || "GitHub GraphQL request failed.";
const rateLimit = readGitHubRateLimitState(headers);
const rateLimited = rateLimit?.remaining === 0
|| errorTypes.some((type) => type === "RATE_LIMITED" || type === "RATE_LIMIT")
|| /rate limit|too many requests|abuse detection/i.test(message);
if (rateLimited) {
return {
status: 429,
message,
...classifyGitHubAuthFailure({ status: 429, message, headers }),
};
}
const permissionDenied = errorTypes.includes("FORBIDDEN")
|| /forbidden|resource not accessible|not accessible by integration|does not have access/i.test(message);
if (permissionDenied) {
return {
status: 403,
message,
...classifyGitHubAuthFailure({ status: 403, message, headers }),
};
}
return null;
}
if (
!payload
|| typeof payload !== "object"
|| !("errors" in payload)
|| !Array.isArray(payload.errors)
) return null;
// GitHub returns partial GraphQL responses that carry both `data` and
// per-field `errors`. Those responses are usable, so they must not put the
// credential on cooldown or trigger a fallback retry.
const data = "data" in payload ? (payload as { data?: unknown }).data : null;
if (data != null) return null;
const errors: unknown[] = payload.errors;
const messages = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const message = "message" in error ? error.message : null;
return typeof message === "string" && message.trim() ? [message.trim()] : [];
});
const errorTypes = errors.flatMap((error) => {
if (!error || typeof error !== "object") return [];
const extensions = "extensions" in error
&& error.extensions
&& typeof error.extensions === "object"
? error.extensions
: null;
return [
"type" in error ? error.type : null,
extensions && "code" in extensions ? extensions.code : null,
extensions && "type" in extensions ? extensions.type : null,
]
.filter((value): value is string => typeof value === "string")
.map((value) => value.toUpperCase());
});
const message = messages.join("; ") || "GitHub GraphQL request failed.";
const rateLimit = readGitHubRateLimitState(headers);
const rateLimited = rateLimit?.remaining === 0
|| errorTypes.some((type) => type === "RATE_LIMITED" || type === "RATE_LIMIT")
|| /rate limit|too many requests|abuse detection/i.test(message);
if (rateLimited) {
return {
status: 429,
message,
...classifyGitHubAuthFailure({ status: 429, message, headers }),
};
}
const permissionDenied = errorTypes.includes("FORBIDDEN")
|| /forbidden|resource not accessible|not accessible by integration|does not have access/i.test(message);
if (permissionDenied) {
return {
status: 403,
message,
...classifyGitHubAuthFailure({ status: 403, message, headers }),
};
}
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/github/githubRateLimit.ts` around lines 127 -
176, Update the GraphQL payload guard in classifyGitHubGraphqlCredentialFailure
to return null when the response contains usable data, while preserving
classification for error-only payloads. Validate the data field as
non-null/usable before extracting errors so neither the permissionDenied nor
rateLimited branches can downgrade a successful partial response.

Comment thread apps/desktop/src/main/services/prs/prPollingService.ts
Comment thread apps/desktop/src/main/services/prs/prPollingService.ts
Comment thread apps/desktop/src/renderer/lib/githubIntegrationStatus.ts Outdated
Comment thread apps/webhook-relay/src/relay.ts
@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from b64b653 to 0bd05d3 Compare August 1, 2026 10:36
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bd05d3dc7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2230 to 2232
async getGitTransportTokenOrThrowAsync(): Promise<string> {
const token = (await readAuthToken("read", "non-rate-limit-only")).token;
if (!token) throw new Error("GitHub auth missing. Run `gh auth login -h github.com -s repo -s workflow` or add a personal access token in Settings.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude App credentials from Git transport

When the user has authorized the ADE GitHub App and also has a healthy gh/PAT credential, this read-capability lookup selects the App before those credentials. The configured App permissions in apps/webhook-relay/README.md:289-295 omit Contents, so authenticated private-repository clones, fetches, remote-clone preflights, and cross-machine handoffs receive an unusable token instead of the Git-capable fallback. Use a transport-specific candidate set that excludes the read-only App (and mirror the correction in the headless implementation at apps/ade-cli/src/headlessLinearServices.ts:1795-1796).

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +1526 to +1533
if (accountId) {
if (await githubRepositoryAccountMatches(env, repo, accountId)) {
return { authorized: true, accountId };
}
return {
authorized: false,
response: json({ ok: false, error: "unauthorized" }, { status: 401 }),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind or fall back for unassociated account repositories

For a signed-in headless runtime whose pre-existing installed-repository row still has account_id = null, startup goes directly to /events and sends both the ADE account token and a valid GitHub App bearer token, but this branch returns 401 before either validating the bearer or associating the row. Since headless bootstrap does not call /status first, every retry follows the same path, so relay-backed PR updates and automation events remain disabled indefinitely; authorize and bind this unassociated case, or fall back to the supplied GitHub credential.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +778 to +779
if (!run.isCurrent()) return;
if (Date.now() < relayPollCooldownUntilMs) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Schedule a relay retry when the cooldown expires

When an HTTP drain fails while the WebSocket remains connected, the interval is still the five-minute connected safety interval, but entering cooldown only stores a timestamp and schedules no one-shot retry. Wake-up frames arriving during the nominal 30-second backoff hit this early return and are discarded, so a transient 5xx can delay automation delivery for nearly five minutes instead of retrying when the advertised cooldown expires.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
apps/desktop/src/main/services/prs/prAsync.test.ts (1)

284-288: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explain the jump from one to three listAll calls.

The assertion expects listAll to reach 3 immediately after the 10-second boundary, which means two calls occur in the same tick. That count encodes an internal detail of the list-check path. A reader cannot tell whether 3 is intended or incidental, and a refactor that merges the two calls would fail this test for no functional reason.

Add a short comment that states which two code paths call listAll at that boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prAsync.test.ts` around lines 284 - 288,
Add a concise comment immediately before the post-10-second listAll call-count
assertion explaining that the count increases from one to three because both
relevant list-check code paths invoke listAll in the same timer tick. Keep the
existing timing and assertions unchanged.
apps/desktop/src/main/services/automations/automationIngressService.ts (1)

941-960: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Truncate the relay error body before it becomes an error message.

responseMessage takes the whole response text when the body is not JSON. A relay behind a proxy can return a large HTML error page. That string then flows into logger.warn at line 1076 and into lastError in the relay status, which the renderer displays.

Cap the length before building the error.

♻️ Proposed truncation
-          let responseMessage = responseText.trim();
+          let responseMessage = responseText.trim().slice(0, 500);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`
around lines 941 - 960, In the non-OK response handling around the GitHub relay
poll error construction, truncate responseMessage to a bounded length before
interpolating it into GithubRelayPollError. Apply the cap after selecting either
the parsed error/message or plain-text body, while preserving the status and
retry metadata and ensuring logger.warn and relay status receive only the
truncated message.
apps/ade-cli/src/headlessLinearServices.test.ts (1)

1106-1181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Temp directories from fs.mkdtempSync are never removed.

Line 1112 creates a temp ADE_HOME and the finally block only restores the environment variable. Each run of this file leaves several directories in the OS temp directory, each holding an encrypted credential file. Remove the directory in the finally block, or add the cleanup to isolateHeadlessGithubAuth.restore so every test benefits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/headlessLinearServices.test.ts` around lines 1106 - 1181,
Clean up the temporary ADE_HOME directory created by fs.mkdtempSync in the
“drops a cached headless writer when that credential disappears” test. Track the
created directory and remove it during finally, or extend
isolateHeadlessGithubAuth.restore to perform this cleanup for all applicable
tests while preserving environment restoration.
apps/desktop/src/shared/githubConditionalRequestCache.ts (1)

43-55: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Eviction uses insertion order, not recency.

store evicts the first non-active key in insertion order. get and begin do not move a key to the end, and entries.set on an existing key keeps its original position. A frequently reused ETag entry can therefore be evicted before a cold one, which costs extra unconditional requests against the rate limit.

To make this an LRU, delete the key before re-inserting it and refresh the position on a cache hit.

♻️ Optional LRU adjustment
     store(key: string, entry: GithubConditionalRequestCacheEntry): void {
       while (entries.size >= maxSize && !entries.has(key)) {
         let evictable: string | null = null;
         for (const candidate of entries.keys()) {
           if (activeConditionalRequests.has(candidate)) continue;
           evictable = candidate;
           break;
         }
         if (!evictable) break;
         entries.delete(evictable);
       }
+      entries.delete(key);
       entries.set(key, entry);
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/shared/githubConditionalRequestCache.ts` around lines 43 -
55, Update the cache methods around store, get, and begin to maintain true LRU
ordering: delete an existing key before re-inserting it in store, and refresh a
key’s insertion position on cache hits in get and begin. Preserve active-request
eviction behavior while ensuring frequently reused entries move to the end and
cold entries are evicted first.
apps/desktop/src/shared/githubOperationCredential.ts (1)

216-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider recording the write probe value only for successful probes.

At Line 220 the write loop reuses successfulProbes. The read loop at Line 201 also stores a probe value that came from a failed probe (the repository-access fallback acceptance). A later write candidate with the same token then takes the ok: true path with a probe value whose repoAccessOk is false. For fine-grained and App credentials capabilities() still returns write: false, so the current effect is bounded, but the map name and the reuse contract no longer match.

Store the accepted-but-unvalidated probe in a separate map, or record the validated flag with the value so the write phase can distinguish the two cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/shared/githubOperationCredential.ts` around lines 216 - 236,
Separate repository-access fallback probes from validated entries in the shared
probe cache used by the read and write loops. Update the read-phase recording
and write-phase lookup around successfulProbes so a write candidate only reuses
probe values from genuinely successful probes, while accepted-but-unvalidated
values are tracked separately or marked with their validation state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/services/github/githubService.ts`:
- Around line 760-816: Memoize the inventory produced by readCredentialInventory
with a short TTL or credential-change signal, and invalidate that memoized value
wherever cachedStatusCredentialInventoryKey is reset: setToken, clearToken,
clearAppUserAuth, and pollAppUserDeviceAuth. In
apps/desktop/src/main/services/github/githubService.ts lines 760-816, update the
inventory builder and invalidation paths; in lines 2148-2155, read the memoized
inventory rather than rebuilding it on each polling tick.
- Around line 1393-1412: Update the exhaustion selection in the
credential-attempt flow around `lastAttemptError` and `firstUnavailable` to
prefer a recorded rate-limit failure over a later non-rate-limit error. Preserve
the existing `GitHubRateLimitError` construction and fallback behavior, ensuring
callers receive the rate-limit signal whenever any credential was unavailable
due to cooldown.
- Around line 2165-2203: Update the credential-reset paths in setToken,
clearToken, clearAppUserAuth, and pollAppUserDeviceAuth so
clearGithubCredentialHealth receives the affected token instead of clearing all
credential health state. Preserve the existing cache invalidation behavior, and
pass the token associated with each changed authentication source, including the
authorized device-auth result.
- Around line 1021-1033: Restrict the negative repository access recording in
the authFailure handling near classifyGitHubAuthFailure to credential failures
that indicate this specific repository is inaccessible: response status 404 or a
repository-specific access-denial message. Do not call
recordGithubCredentialRepositoryAccess(candidate, repo, false) for generic 403
cases such as scope, SAML/SSO, or IP allow-list failures, while preserving the
existing permission_denied classification for other handling.

In `@apps/desktop/src/main/services/prs/prAsync.test.ts`:
- Around line 15-18: Move the clearGithubCredentialHealth cleanup out of the
prPollingService-scoped afterEach into a file-level teardown in prAsync.test.ts,
ensuring it uses the same credential token as recordGithubCredentialSuccess() so
module-level health state is reset for every test.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 4209-4218: Update graphqlRequest to accept an optional repo
argument and prefer it when constructing the GraphQL request context, while
retaining the owner/name-derived behavior for existing callers. Update the
review-thread reply, thread resolve/unresolve, and reaction mutation callers to
pass their owning repository alongside node IDs, and add coverage for
repository-scoped write-token failure followed by a writable fallback token.
- Line 5750: Update downloadJobLogTail to use a GitHub-service request helper
instead of obtaining one token and issuing raw fetch calls directly. The helper
must preserve the existing manual redirect behavior, classify 401, 403, and 429
responses, cool down the failed credential, and retry with another read-capable
credential before returning null.

---

Nitpick comments:
In `@apps/ade-cli/src/headlessLinearServices.test.ts`:
- Around line 1106-1181: Clean up the temporary ADE_HOME directory created by
fs.mkdtempSync in the “drops a cached headless writer when that credential
disappears” test. Track the created directory and remove it during finally, or
extend isolateHeadlessGithubAuth.restore to perform this cleanup for all
applicable tests while preserving environment restoration.

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`:
- Around line 941-960: In the non-OK response handling around the GitHub relay
poll error construction, truncate responseMessage to a bounded length before
interpolating it into GithubRelayPollError. Apply the cap after selecting either
the parsed error/message or plain-text body, while preserving the status and
retry metadata and ensuring logger.warn and relay status receive only the
truncated message.

In `@apps/desktop/src/main/services/prs/prAsync.test.ts`:
- Around line 284-288: Add a concise comment immediately before the
post-10-second listAll call-count assertion explaining that the count increases
from one to three because both relevant list-check code paths invoke listAll in
the same timer tick. Keep the existing timing and assertions unchanged.

In `@apps/desktop/src/shared/githubConditionalRequestCache.ts`:
- Around line 43-55: Update the cache methods around store, get, and begin to
maintain true LRU ordering: delete an existing key before re-inserting it in
store, and refresh a key’s insertion position on cache hits in get and begin.
Preserve active-request eviction behavior while ensuring frequently reused
entries move to the end and cold entries are evicted first.

In `@apps/desktop/src/shared/githubOperationCredential.ts`:
- Around line 216-236: Separate repository-access fallback probes from validated
entries in the shared probe cache used by the read and write loops. Update the
read-phase recording and write-phase lookup around successfulProbes so a write
candidate only reuses probe values from genuinely successful probes, while
accepted-but-unvalidated values are tracked separately or marked with their
validation state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd1bf6a8-c38b-4357-b5d5-ae3726465875

📥 Commits

Reviewing files that changed from the base of the PR and between b64b653 and 0bd05d3.

⛔ Files ignored due to path filters (3)
  • docs/features/automations/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
📒 Files selected for processing (37)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.test.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/webhook-relay/src/relay.ts
  • apps/webhook-relay/test/account.test.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • apps/ade-cli/src/bootstrap.ts
  • apps/webhook-relay/test/account.test.ts
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/ade-cli/README.md
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/webhook-relay/src/relay.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/main.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.ts

Comment thread apps/desktop/src/main/services/github/githubService.ts Outdated
Comment thread apps/desktop/src/main/services/github/githubService.ts
Comment thread apps/desktop/src/main/services/github/githubService.ts Outdated
Comment thread apps/desktop/src/main/services/github/githubService.ts Outdated
Comment thread apps/desktop/src/main/services/prs/prAsync.test.ts
Comment thread apps/desktop/src/main/services/prs/prService.ts Outdated
Comment thread apps/desktop/src/main/services/prs/prService.ts Outdated
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27a0cc1101

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +209 to +212
active = { candidate, value: result.value };
successfulProbes.set(candidate.token, result.value);
args.onAcceptedProbe(candidate, result.value, true);
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Continue past probes that lack read capability

When a higher-precedence classic/OAuth credential authenticates at /user but lacks the repo scope, its probe is ok, so this assignment stops the read loop even though capabilities(candidate, result.value).read is false. computeStatus then reports connected: false and never selects a healthy App, CLI, or PAT later in the chain; check read capability before accepting the probe in the shared resolver so both desktop and headless status paths fall through.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +88 to +96
if (args.status === 403) {
return {
rateLimit,
authFailure: {
kind: "permission_denied",
message,
retryAt: null,
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Distinguish operation-level 403s before cooling credentials

When a valid credential receives an operation-level 403, such as a repository or organization policy rejecting a particular write, this unconditional classification makes apiRequest record a credential failure and place the token in a five-minute core cooldown. With one credential—or when fallback credentials receive the same policy response—unrelated PR reads, writes, and Git transport are then treated as unavailable even though authentication remains valid; only credential-specific permission failures should poison credential health. This shared classifier affects both desktop and headless request paths.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from 27a0cc1 to 7703ee2 Compare August 1, 2026 12:27
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 7703ee2c46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4de66e057d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end("<!doctype html><html><body style=\"font-family:Geist,-apple-system,BlinkMacSystemFont,sans-serif;padding:24px\">Linear connected. You can close this window and return to ADE.</body></html>");
} catch (error) {
if (session.status !== "pending") return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge End superseded OAuth callback responses

When two callback requests reach the listener concurrently, the first successful exchange marks the session completed and aborts the shared controller; the second handler then reaches this guard and returns without calling res.end() or destroying the response. Because completed sessions only invoke graceful server.close() rather than closeAllConnections(), that request remains open indefinitely and prevents the old callback server from fully closing; explicitly finish or destroy the response before returning from terminal-session paths.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/main/services/cto/linearOAuthService.ts (1)

505-521: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

dispose() releases the fixed callback port asynchronously without an awaitable signal. dispose() starts every server close with void and returns synchronously. The callback server binds the fixed port OAUTH_PORT, and the port is released only after the close callback runs on a later event-loop turn. Any caller that disposes the service and then creates a replacement can fail its first startSession with EADDRINUSE.

  • apps/desktop/src/main/services/cto/linearOAuthService.ts#L505-L521: collect the closeServerAndWait and finalizeSession promises and return Promise.all(...) from dispose(), then update the LinearOAuthService type and every call site to await it.
  • apps/desktop/src/main/services/cto/linearAuth.test.ts#L722-L724: the replacement service binds port 19836 immediately after the rejection, so the assertion depends on the old listener unbinding within a few microtask turns. Await the new dispose() promise before you create the replacement service, or poll until the port is free.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/cto/linearOAuthService.ts` around lines 505 -
521, The synchronous dispose flow leaves the fixed OAuth callback port
unavailable until asynchronous cleanup completes. In
apps/desktop/src/main/services/cto/linearOAuthService.ts#L505-L521, update
LinearOAuthService.dispose and its implementation to collect closeServerAndWait
and finalizeSession promises, return Promise.all, and update every caller to
await it. In apps/desktop/src/main/services/cto/linearAuth.test.ts#L722-L724,
await the replacement service’s new dispose promise before creating the
replacement, or poll until port 19836 is released.
apps/desktop/src/main/services/automations/automationIngressService.ts (1)

791-822: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Supersession thrown before the try block escapes as an unhandled rejection.

Line 805 calls run.wait(...) outside the try block that starts at line 839. If stop() runs while the account token is being retrieved, run.wait throws GithubRelayPollSupersededError, and no local handler catches it. pollGithubRelayOnce awaits pollGithubRelay without a catch, so the rejection reaches void pollGithubRelayOnce() call sites in the poll timer, the retry timer, and the socket handlers as an unhandled rejection. It also rejects start() and pollNow() for the superseded run.

Catch supersession in pollGithubRelayOnce so every entry path stays quiet.

🛡️ Proposed guard
     pollInFlight = (async () => {
       do {
         pollRerunRequested = false;
-        await pollGithubRelay(relayPollGeneration);
+        try {
+          await pollGithubRelay(relayPollGeneration);
+        } catch (error) {
+          if (!(error instanceof GithubRelayPollSupersededError)) throw error;
+        }
       } while (pollRerunRequested && !stopped && Date.now() >= relayPollCooldownUntilMs);
     })().finally(() => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/automations/automationIngressService.ts`
around lines 791 - 822, Update pollGithubRelayOnce to catch and silently handle
GithubRelayPollSupersededError from pollGithubRelay, including failures from
run.wait during account-token retrieval. Ensure all callers—including start(),
pollNow(), timers, and socket handlers—remain non-rejecting for superseded runs
while preserving propagation of unrelated errors.
🧹 Nitpick comments (4)
apps/desktop/src/main/services/cto/linearAuth.test.ts (1)

711-715: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the microtask-count dependency with an explicit synchronization point.

await Promise.resolve() yields exactly one microtask turn. The test relies on startSessionOnce reaching server.listen in that turn. startSessionOnce currently reaches listen after a single await Promise.all([]), so the ordering holds today. If anyone adds or removes an await before listen, the interleaving changes and the test either tests a different code path or fails for an unrelated reason.

Make the intent explicit. Wait until the callback port is bound, then dispose.

♻️ Suggested test change
     const interruptedStart = service.startSession();
-    await Promise.resolve();
+    // Dispose only after the callback port is bound, so the test exercises the
+    // post-listen disposal path rather than a specific microtask ordering.
+    await waitForPortBound(19836);
     service.dispose();

Add a helper that polls the port:

async function waitForPortBound(port: number, timeoutMs = 2_000): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const bound = await new Promise<boolean>((resolve) => {
      const probe = net.createConnection({ host: "127.0.0.1", port });
      probe.once("connect", () => { probe.destroy(); resolve(true); });
      probe.once("error", () => resolve(false));
    });
    if (bound) return;
    await waitMs(5);
  }
  throw new Error(`Timed out waiting for port ${port} to bind`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/cto/linearAuth.test.ts` around lines 711 -
715, Replace the single Promise.resolve synchronization in the interrupted
startSession test with an explicit waitForPortBound helper that probes the
callback port until it accepts a connection or times out. Invoke it after
service.startSession() and before service.dispose(), preserving the existing
rejection assertion and using the test’s configured callback port.
apps/desktop/src/main/services/github/githubRawRequest.ts (1)

169-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the error-selection priority.

The exhausted chain picks between firstRateLimitError, a rate-limited unavailableError, lastAttemptError, and a fallback unavailableError. This order is correct, but it is dense and this exact logic decides which message surfaces to the user when multiple credentials fail differently. Add a short comment stating the precedence so a future change to this rate-limit-fallback path does not accidentally invert it.

📝 Proposed comment addition
+  // Precedence: an in-flight rate-limit failure first, then a cooldown that
+  // was itself caused by a rate limit, then the last live attempt failure,
+  // then any remaining cooldown reason.
   const exhausted = firstRateLimitError
     ?? (unavailableError?.authFailure.kind === "rate_limited" ? unavailableError : null)
     ?? lastAttemptError
     ?? unavailableError;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/github/githubRawRequest.ts` around lines 169 -
183, Add a concise comment immediately above the `exhausted` assignment
documenting its precedence: `firstRateLimitError`, then a rate-limited
`unavailableError`, then `lastAttemptError`, and finally the general
`unavailableError`. Leave the selection logic unchanged.
apps/ade-cli/src/headlessLinearServices.test.ts (1)

934-1014: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two tests still use the manual environment setup that isolateHeadlessGithubAuth replaces. Both sites repeat the same save, delete, and restore sequence for ADE_HOME, ADE_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN, GH_CONFIG_DIR, and globalThis.fetch, and neither removes the temporary directories it creates. The new helper already performs both the isolation and the cleanup.

  • apps/ade-cli/src/headlessLinearServices.test.ts#L934-L1014: replace the manual setup with isolateHeadlessGithubAuth("ade-headless-github-graphql-", { emptyGhConfig: true }) plus storeHeadlessAppUserToken(), and replace the finally body with environment.restore().
  • apps/ade-cli/src/headlessLinearServices.test.ts#L1375-L1448: replace the manual setup with isolateHeadlessGithubAuth("ade-headless-github-app-only-", { emptyGhConfig: true }) plus storeHeadlessAppUserToken(), and replace the finally body with environment.restore().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/headlessLinearServices.test.ts` around lines 934 - 1014,
Replace the repeated environment and fetch setup in
apps/ade-cli/src/headlessLinearServices.test.ts lines 934-1014 with
isolateHeadlessGithubAuth("ade-headless-github-graphql-", { emptyGhConfig: true
}) and storeHeadlessAppUserToken(), then use environment.restore() in finally.
Apply the same replacement at lines 1375-1448 using the
"ade-headless-github-app-only-" prefix; remove the duplicated manual cleanup in
both tests.
apps/desktop/src/main/services/prs/prService.ts (1)

10885-10967: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate thread-ownership checks and support synthetic PR ids consistently.

replyToReviewThread and resolveReviewThread each repeat the same thread-ownership check inline: requireRow + repoFromRow + fetchReviewThreads + a manual some() check. The same logic already exists as assertThreadBelongsToPr, and postReviewComment and setReviewThreadResolved already use it.

assertThreadBelongsToPr resolves the target through resolvePrThreadTarget, which supports both mapped PR rows and synthetic (unmapped GitHub-tab) PR ids. requireRow only supports mapped rows and throws for a synthetic id. As a result, replyToReviewThread and resolveReviewThread cannot operate on unmapped GitHub-tab PRs, while postReviewComment, setReviewThreadResolved, and reactToComment can. Since this PR already touches all four methods to add the new { repo } argument, switch replyToReviewThread and resolveReviewThread to call assertThreadBelongsToPr as well. This removes the duplicated logic and makes unmapped-PR support consistent across all review-thread mutations.

♻️ Proposed refactor for `replyToReviewThread`
     async replyToReviewThread(args: ReplyToPrReviewThreadArgs): Promise<PrReviewThreadComment> {
-      const row = requireRow(args.prId);
-      const repo = repoFromRow(row);
-      const threads = await fetchReviewThreads(repo, Number(row.github_pr_number));
-      if (!threads.some((t) => t.id === args.threadId)) {
-        throw new Error(`Thread ${args.threadId} does not belong to PR ${args.prId}`);
-      }
+      const { repo } = await assertThreadBelongsToPr(args.prId, args.threadId);
       const data = await graphqlRequest<{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 10885 - 10967,
Replace the duplicated requireRow/repoFromRow/fetchReviewThreads ownership
checks in replyToReviewThread and resolveReviewThread with the existing
assertThreadBelongsToPr helper, passing the PR id and thread id as required. Use
the helper’s resolved target for the subsequent GraphQL request so both mapped
and synthetic PR ids remain supported, while preserving the existing reply and
resolve mutations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/services/cto/linearOAuthService.ts`:
- Around line 73-81: Update finalizeSession so every terminal status closes the
OAuth server’s connections, not only the expired status. After the completed or
failed response is written, use closeIdleConnections() before
closeServerAndWait(server), or consistently use closeAllConnections() after
writing the response; preserve the existing expired-session behavior and ensure
dispose() cannot remain blocked by keep-alive connections.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 2066-2069: Update the stale comment inside markHotRefresh’s
uniquePrIds loop to describe re-arming the 15-second loop instead of the
obsolete 5-second interval, while preserving the existing quota-avoidance
rationale.

---

Outside diff comments:
In `@apps/desktop/src/main/services/automations/automationIngressService.ts`:
- Around line 791-822: Update pollGithubRelayOnce to catch and silently handle
GithubRelayPollSupersededError from pollGithubRelay, including failures from
run.wait during account-token retrieval. Ensure all callers—including start(),
pollNow(), timers, and socket handlers—remain non-rejecting for superseded runs
while preserving propagation of unrelated errors.

In `@apps/desktop/src/main/services/cto/linearOAuthService.ts`:
- Around line 505-521: The synchronous dispose flow leaves the fixed OAuth
callback port unavailable until asynchronous cleanup completes. In
apps/desktop/src/main/services/cto/linearOAuthService.ts#L505-L521, update
LinearOAuthService.dispose and its implementation to collect closeServerAndWait
and finalizeSession promises, return Promise.all, and update every caller to
await it. In apps/desktop/src/main/services/cto/linearAuth.test.ts#L722-L724,
await the replacement service’s new dispose promise before creating the
replacement, or poll until port 19836 is released.

---

Nitpick comments:
In `@apps/ade-cli/src/headlessLinearServices.test.ts`:
- Around line 934-1014: Replace the repeated environment and fetch setup in
apps/ade-cli/src/headlessLinearServices.test.ts lines 934-1014 with
isolateHeadlessGithubAuth("ade-headless-github-graphql-", { emptyGhConfig: true
}) and storeHeadlessAppUserToken(), then use environment.restore() in finally.
Apply the same replacement at lines 1375-1448 using the
"ade-headless-github-app-only-" prefix; remove the duplicated manual cleanup in
both tests.

In `@apps/desktop/src/main/services/cto/linearAuth.test.ts`:
- Around line 711-715: Replace the single Promise.resolve synchronization in the
interrupted startSession test with an explicit waitForPortBound helper that
probes the callback port until it accepts a connection or times out. Invoke it
after service.startSession() and before service.dispose(), preserving the
existing rejection assertion and using the test’s configured callback port.

In `@apps/desktop/src/main/services/github/githubRawRequest.ts`:
- Around line 169-183: Add a concise comment immediately above the `exhausted`
assignment documenting its precedence: `firstRateLimitError`, then a
rate-limited `unavailableError`, then `lastAttemptError`, and finally the
general `unavailableError`. Leave the selection logic unchanged.

In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 10885-10967: Replace the duplicated
requireRow/repoFromRow/fetchReviewThreads ownership checks in
replyToReviewThread and resolveReviewThread with the existing
assertThreadBelongsToPr helper, passing the PR id and thread id as required. Use
the helper’s resolved target for the subsequent GraphQL request so both mapped
and synthetic PR ids remain supported, while preserving the existing reply and
resolve mutations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 61c6daf8-dcd6-476a-b382-5d9d56bcd67b

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd05d3 and 4de66e0.

⛔ Files ignored due to path filters (3)
  • docs/features/automations/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
📒 Files selected for processing (41)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearOAuthService.ts
  • apps/desktop/src/main/services/github/githubAppUserAuthService.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/main/services/github/githubRawRequest.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.test.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/webhook-relay/src/relay.ts
  • apps/webhook-relay/test/account.test.ts
🚧 Files skipped from review as they are similar to previous changes (27)
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ade-cli/README.md
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.test.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/webhook-relay/src/relay.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts

Comment thread apps/desktop/src/main/services/cto/linearOAuthService.ts
Comment thread apps/desktop/src/main/services/prs/prService.ts
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b5d44693a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +248 to +251
const requestedResource = options.resource?.trim().toLowerCase() || null;
const entries = requestedResource
? [health.resources.get(requestedResource), health.resources.get("unknown")]
: [...health.resources.values()];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat invalid-token cooldowns as credential-wide

When a revoked credential returns 401 from GraphQL or Search with an x-ratelimit-resource header, its invalid_token cooldown is stored under that resource, but lookups for core inspect only core and unknown. Both desktop and headless Git-transport selectors consequently choose the known-invalid higher-priority token instead of the healthy fallback, causing clones, fetches, and handoffs to fail until another core request happens to diagnose the token; keep rate-limit cooldowns resource-specific, but apply invalid-token health across every resource.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +309 to +311
if (Date.now() - lastRelaySafetySweepAtMs >= RELAY_SAFETY_SWEEP_INTERVAL_MS) {
lastRelaySafetySweepAtMs = Date.now();
await prService.refresh();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record relay safety sweeps only after a successful refresh

When the relay is healthy and this periodic full refresh fails transiently, advancing lastRelaySafetySweepAtMs before awaiting the request suppresses the refresh on the backoff tick. That next tick processes stale cached PRs as a success, resets consecutiveFailures, and then waits out the remainder of the 15-minute safety interval, so missed relay events are not reconciled promptly; update the timestamp only after prService.refresh() succeeds. This shared poller is wired into both desktop and headless runtime paths.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f30b29508

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

finalizeSession(session, { status: "completed" });
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end("<!doctype html><html><body style=\"font-family:Geist,-apple-system,BlinkMacSystemFont,sans-serif;padding:24px\">Linear connected. You can close this window and return to ADE.</body></html>");
await exchangeCode(session, code, session.abortController.signal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize duplicate OAuth callback exchanges

When two callbacks for the same session overlap, both observe status === "pending" and exchange the same single-use authorization code concurrently. If the duplicate exchange fails before the valid exchange completes, its catch path marks the session failed and aborts the shared controller, preventing the successful request from completing the sign-in; reserve the session synchronously before this await or share one in-flight exchange among callbacks.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/services/prs/prService.ts (1)

11047-11063: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add an ownership check before reacting to args.commentId.

reactToComment only maps args.prId to repo for credential failover, then sends args.commentId directly to addReaction. Comment/reaction subject IDs are global GitHub node IDs, so a caller can add a reaction to any subject the credential can target, independent of the PR the caller claims. Add a check that the comment belongs to args.prId before calling addReaction, or document why reactions are intentionally allowed outside that scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 11047 - 11063,
Add an ownership validation step in reactToComment to confirm args.commentId
belongs to the pull request identified by args.prId before invoking addReaction.
Reuse the existing PR/comment lookup or validation mechanism, and reject or
return on mismatches while preserving the current repository-based credential
failover behavior.

Source: Path instructions

🧹 Nitpick comments (1)
apps/desktop/src/main/services/prs/prService.ts (1)

10885-10923: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Every review-thread mutation re-fetches the full thread list to check ownership.

replyToReviewThread (line 10886), resolveReviewThread (line 10942), postReviewComment (line 10960), and setReviewThreadResolved (line 11006) each call assertThreadBelongsToPr first. That helper calls fetchReviewThreads, which pages through up to 10 pages of 100 threads, each with up to 50 nested comments, before the actual mutation runs. This adds one heavy GraphQL round trip to every single reply, resolve, comment, or resolve-toggle action.

Use a targeted lookup instead, for example a node(id: $threadId) { ... on PullRequestReviewThread { pullRequest { number repository { owner { login } name } } } } query, to confirm ownership without listing every thread on the PR.

Also applies to: 10941-10957, 10959-11003, 11005-11045

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 10885 - 10923,
Replace the full-list ownership check used by replyToReviewThread,
resolveReviewThread, postReviewComment, and setReviewThreadResolved with a
targeted thread-ID lookup via node, selecting the PullRequestReviewThread
pullRequest repository identity and number. Validate that the returned thread
belongs to args.prId and preserve the existing rejection behavior for missing or
mismatched ownership, without calling fetchReviewThreads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Line 1640: Update the Linear OAuth service resolution flow around
linearOAuthServiceTransition so the same mutex serializes the complete check,
disposal, and creation sequence for each ctx.adeDir. Ensure callers wait before
evaluating or returning linearCredentialService, then re-check the requested
directory after acquiring the mutex, preventing one project from receiving
another project's service.

---

Outside diff comments:
In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 11047-11063: Add an ownership validation step in reactToComment to
confirm args.commentId belongs to the pull request identified by args.prId
before invoking addReaction. Reuse the existing PR/comment lookup or validation
mechanism, and reject or return on mismatches while preserving the current
repository-based credential failover behavior.

---

Nitpick comments:
In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 10885-10923: Replace the full-list ownership check used by
replyToReviewThread, resolveReviewThread, postReviewComment, and
setReviewThreadResolved with a targeted thread-ID lookup via node, selecting the
PullRequestReviewThread pullRequest repository identity and number. Validate
that the returned thread belongs to args.prId and preserve the existing
rejection behavior for missing or mismatched ownership, without calling
fetchReviewThreads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03375040-2560-461c-a93a-afd664fc296a

📥 Commits

Reviewing files that changed from the base of the PR and between 4de66e0 and 5f30b29.

📒 Files selected for processing (14)
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearOAuthService.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/github/githubRawRequest.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/ade-cli/src/bootstrap.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/github/githubRawRequest.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/desktop/src/main/services/cto/linearOAuthService.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts

Comment thread apps/desktop/src/main/services/ipc/registerIpc.ts Outdated
@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from 5f30b29 to e1be85e Compare August 1, 2026 14:56
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: e1be85ed30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@arul28
arul28 force-pushed the ade/github-rate-limit-fallback branch from e1be85e to 789b069 Compare August 1, 2026 15:32
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/desktop/src/main/services/cto/linearOAuthService.ts (1)

86-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Settle writeResponse when the write throws.

writeResponse resolves only on finish or close. If response.writeHead or response.end throws, for example because headers were already sent, the returned promise never settles. respondAndFinalizeSession assigns that promise to session.closePromise, so dispose() and startSessionOnce would then await forever and the port would stay bound.

Guard the write and resolve on a synchronous failure.

♻️ Proposed fix
     response.once("finish", finish);
     response.once("close", finish);
-    response.writeHead(status, { "content-type": contentType });
-    response.end(body);
+    try {
+      response.writeHead(status, { "content-type": contentType });
+      response.end(body);
+    } catch {
+      finish();
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/cto/linearOAuthService.ts` around lines 86 -
106, Update writeResponse to wrap response.writeHead and response.end in
synchronous error handling, invoking the existing finish settlement path when
either operation throws so the returned promise always resolves. Preserve the
current finish/close listeners and idempotent settled guard.
apps/desktop/src/main/services/ipc/runtimeBridge.test.ts (1)

1937-1941: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both Linear OAuth queue tests depend on the internal await count. Each site flushes exactly one microtask with await Promise.resolve(), then asserts that the queued operation has not created a second service yet. The number of awaits inside the IPC queue path is an implementation detail, so both assertions are timing-fragile.

  • apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L1937-L1941: replace the single flush with a vi.waitFor on an observable condition, or gate the second request on a signal the first operation controls.
  • apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L2022-L2024: apply the same state-based wait before asserting the creation count.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/ipc/runtimeBridge.test.ts` around lines 1937 -
1941, Replace the timing-dependent single microtask flush in the Linear OAuth
queue test around the first site
(apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L1937-L1941) with
vi.waitFor on an observable state condition, or gate the second request using a
signal controlled by the first operation; preserve the assertion that only one
service is created. Apply the same state-based synchronization to the sibling
site (apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L2022-L2024)
before asserting the creation count, so both tests avoid relying on internal IPC
queue await counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/desktop/src/main/services/cto/linearOAuthService.ts`:
- Around line 86-106: Update writeResponse to wrap response.writeHead and
response.end in synchronous error handling, invoking the existing finish
settlement path when either operation throws so the returned promise always
resolves. Preserve the current finish/close listeners and idempotent settled
guard.

In `@apps/desktop/src/main/services/ipc/runtimeBridge.test.ts`:
- Around line 1937-1941: Replace the timing-dependent single microtask flush in
the Linear OAuth queue test around the first site
(apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L1937-L1941) with
vi.waitFor on an observable state condition, or gate the second request using a
signal controlled by the first operation; preserve the assertion that only one
service is created. Apply the same state-based synchronization to the sibling
site (apps/desktop/src/main/services/ipc/runtimeBridge.test.ts#L2022-L2024)
before asserting the creation count, so both tests avoid relying on internal IPC
queue await counts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 098d3582-2245-46f9-9370-6b95b28559ee

📥 Commits

Reviewing files that changed from the base of the PR and between 5f30b29 and 789b069.

⛔ Files ignored due to path filters (3)
  • docs/features/automations/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
📒 Files selected for processing (42)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/cto/linearAuth.test.ts
  • apps/desktop/src/main/services/cto/linearOAuthService.ts
  • apps/desktop/src/main/services/github/githubAppUserAuthService.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/main/services/github/githubRawRequest.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.ts
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.test.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/webhook-relay/src/relay.ts
  • apps/webhook-relay/test/account.test.ts
🚧 Files skipped from review as they are similar to previous changes (39)
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.test.tsx
  • apps/desktop/src/shared/githubApiPath.test.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/desktop/src/shared/githubOperationCredential.test.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.test.ts
  • apps/desktop/src/shared/githubConditionalRequestCache.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/projects/projectScaffoldService.ts
  • apps/ade-cli/README.md
  • apps/desktop/src/main/services/prs/prAsync.test.ts
  • apps/desktop/src/renderer/lib/githubIntegrationStatus.ts
  • apps/desktop/src/shared/githubApiPath.ts
  • apps/desktop/src/main/services/github/githubAppUserAuthService.ts
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/github/githubRawRequest.ts
  • apps/webhook-relay/test/account.test.ts
  • apps/desktop/src/main/services/github/githubCredentialHealth.ts
  • apps/ade-cli/src/headlessLinearServices.test.ts
  • apps/webhook-relay/src/relay.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/prs/prPollingService.ts
  • apps/desktop/src/main/services/automations/automationIngressService.test.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/desktop/src/main/services/github/githubRateLimit.ts
  • apps/desktop/src/renderer/components/app/IntegrationBannerHost.tsx
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/main/services/automations/automationIngressService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/ade-cli/src/headlessLinearServices.ts
  • apps/desktop/src/shared/githubOperationCredential.ts
  • apps/desktop/src/main/services/github/githubService.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 789b069014

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const appStatus = appUserAuth.getAuthStatus();
const [appToken, gh] = await Promise.all([
appStatus.tokenStored
? appUserAuth.getValidTokenForRelay().catch(() => null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report GitHub App refresh failures instead of dropping them

When the stored App access token enters its refresh window and refreshing fails transiently (for example, a network timeout), this catch silently removes the App from the candidate inventory. With no CLI/PAT fallback, computeStatus consequently reports tokenStored: false and authFailure: null, so Settings tells an already-authorized user to reconnect rather than exposing a retryable failure; the headless mirror has the same behavior. Preserve and classify the refresh error so status remains accurate while the credential is temporarily unavailable.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb74e95fc0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1781 to +1782
authSource: candidate.source,
writeAuthSource: activeWriteSource ?? "none",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Attribute writes to the credential that performed them

When read and write credentials belong to different users—for example, the App reads as Alice while an environment token writes as Bob—userLogin remains Alice even though mutations use writeAuthSource. After land() merges as Bob, resolveViewerLoginForMerge() reads this status login and recordMergeOutcome() permanently coalesces Alice into merged_by_login, corrupting the merge history attribution. Preserve the selected mutation credential's identity and use it for write attribution.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

});
}
const connected = computeConnected({
const connected = validatedCredentialCapabilities(candidate, value, repo).read;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate PAT saves against write capability

When the App provides healthy reads but a newly saved PAT is invalid or under-scoped, this aggregate read flag is still true while writeAuthSource is none. GitHubSection.handleSaveToken() checks only status.connected, hides the PAT setup, and reports “saved and verified,” even though the new token failed validation and no writes can run; make the save confirmation require the PAT/write credential to be usable.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

Address review comments 3696023035 and 3696023038.
@arul28

arul28 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af19b33564

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1917 to +1919
onRejectedProbe: (entry, result, repositoryAccessFailure) => {
if (!repositoryAccessFailure) {
recordGithubCredentialFailure(entry, result.authFailure, result.rateLimit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve read access after rejecting PAT writes

When a saved classic PAT has repo but not workflow scope and no other read credential is available, verifyStoredPat synthesizes a permission_denied result for the missing write capability and this callback records a five-minute core cooldown. Subsequent apiRequest and getStatus calls then skip the same PAT for reads even though the preceding status probe established usable read access. Fresh evidence beyond the earlier scope issue is that the new save-verification path now poisons credential health after read status succeeds; avoid recording a read cooldown for a write-only capability failure, including in the mirrored headless callback.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines +104 to +108
if (args.tokenType === "fine-grained") {
// GitHub does not expose fine-grained token permissions during validation.
// A successful repository probe establishes that the user-selected token
// can target this repo; actual write requests still fail over on 403.
return { read: repositoryReadAvailable, write: repositoryReadAvailable };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop treating fine-grained read probes as write verification

When a newly saved fine-grained PAT has only repository read permissions, the repository metadata probe succeeds and this return marks write: true; verifyStoredPat consequently reports a writable credential and Settings says the token was saved and verified, although the first PR mutation will receive 403. Fresh evidence for the previously reported verification problem is that the current shared evaluator explicitly equates repository-read success with write capability, affecting both desktop and headless status paths; keep write capability unverified or validate it independently instead of advertising it as active.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Comment on lines 263 to 264
const targetedPrIds = Array.from(pendingTargetedPrIds);
pendingTargetedPrIds.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain targeted PR IDs when reconciliation fails

When the relay is healthy and a webhook-triggered targeted refresh fails transiently, these IDs are cleared before prService.refresh completes and the catch path never restores them. The backoff tick then sees no target and, if the latest relay safety sweep was recent, performs no refresh and resets the failure count, leaving derived PR state stale until the next 15-minute safety sweep; remove IDs only after a successful targeted refresh or requeue them on failure. This poller is used by both desktop and headless runtime paths.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

@arul28
arul28 merged commit 75672cd into main Aug 1, 2026
4 checks passed
@arul28
arul28 deleted the ade/github-rate-limit-fallback branch August 1, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant