Skip to content

fix(auth): fix GitHub device flow polling and add --wait flag for WSL…#46698

Open
chadmoore wants to merge 6 commits intoopenclaw:mainfrom
chadmoore:fix/device-flow-slow-down-backoff
Open

fix(auth): fix GitHub device flow polling and add --wait flag for WSL…#46698
chadmoore wants to merge 6 commits intoopenclaw:mainfrom
chadmoore:fix/device-flow-slow-down-backoff

Conversation

@chadmoore
Copy link
Copy Markdown

…/remote terminals

The device flow polling loop had two bugs:

  1. When GitHub returns slow_down, the response includes the new minimum polling interval in seconds. The old code ignored this and only added a fixed 2000ms to the original interval, causing continued rate limiting. Now intervalMs is updated to the server-specified value (seconds * 1000).

  2. The loop polled immediately on the first iteration before the user had a chance to authorize, triggering authorization_pending which in turn caused slow_down cascades. Moving the sleep to the top of the loop fixes this.

Adds a --wait flag to login-github-copilot that prompts the user to confirm they've authorized before polling begins. Also prints the URL and code as plain text so they're easy to copy in terminals (e.g. WSL, SSH) where the clack box rendering prevents ctrl+click link detection.

Usage:
openclaw models auth login-github-copilot --wait

Summary

  • Problem: GitHub device flow polling fired immediately on the first iteration, causing an authorization_pending
    response that triggered GitHub's slow_down backoff, making logins unnecessarily slow.
  • Why it matters: WSL/remote terminal users can't auto-open a browser, so they need extra time to manually navigate
    to the URL — and the backoff made the flow even more painful.
  • What changed: Poll delay moved to the top of the loop (always wait first); slow_down now respects GitHub's
    returned interval value; new --wait flag prints plain-text URL+code and waits for user confirmation before polling
    begins.
  • What did NOT change: No changes to token storage, auth profile logic, or any non-GitHub-Copilot auth paths.

Change Type (select all)

  • Bug fix
  • Feature

Scope (select all touched areas)

  • Auth / tokens
  • UI / DX

Linked Issue/PR

  • Closes #
  • Related #

User-visible / Behavior Changes

  • openclaw models login --github-copilot now waits one polling interval before the first token request (previously
    polled immediately).
  • New --wait flag: prints verification URL and code as plain text, then prompts for confirmation before polling.
    Intended for WSL/remote terminals where the browser doesn't auto-open.
  • slow_down responses now use GitHub's returned interval value rather than adding a fixed 2000ms offset.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (same endpoints, adjusted timing only)
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux (WSL2 / Ubuntu)
  • Runtime/container: Node.js
  • Model/provider: GitHub Copilot
  • Integration/channel: CLI device flow login
  • Relevant config: N/A

Steps

  1. Run openclaw models login --github-copilot --wait
  2. Copy the printed URL and code, authorize in browser
  3. Confirm prompt in terminal

Expected

  • Login completes without slow_down backoff errors; polling begins only after the initial interval has elapsed.

Actual

  • Previously: immediate first poll → authorization_pending → slow_down backoff triggered.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: Device flow login with --wait flag on WSL2; confirmed URL/code printed as plain text;
    confirmed polling does not begin until user confirms.
  • Edge cases checked: slow_down response with and without interval field in body; expired token during polling.
  • What you did not verify: Non-WSL native terminal auto-open behavior; Windows native terminal.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert src/providers/github-copilot-auth.ts and src/cli/models-cli.ts
    to prior state.
  • Files/config to restore: src/providers/github-copilot-auth.ts, src/cli/models-cli.ts
  • Known bad symptoms: Login hangs indefinitely or never completes polling.

Risks and Mitigations

  • Risk: Adding an initial delay before the first poll could slow down logins on fast connections where the user
    authorizes immediately.
    - Mitigation: The delay equals the server-specified interval (minimum 1s), which is GitHub's own recommended
    polling cadence — no change to the happy path timing in practice.

@openclaw-barnacle openclaw-barnacle bot added cli CLI command changes size: XS labels Mar 15, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR fixes two genuine bugs in the GitHub device-flow polling loop and adds a --wait flag to improve the experience in WSL/remote terminals. The fixes are well-targeted and the overall approach is sound.

Key changes:

  • Sleep moved to the top of the polling loop so the first HTTP request is no longer fired before the user has a chance to authorize — eliminating the authorization_pendingslow_down cascade.
  • slow_down handling now respects GitHub's server-returned interval value (converted from seconds to ms) instead of applying a fixed 2000 ms offset. The defensive fallback of +5000 ms when the field is absent is reasonable, though note it accumulates on each successive fieldless slow_down (unlike the old code which always based the delay on the original params.intervalMs).
  • New --wait flag prints the verification URL and code as plain text and pauses for an explicit user confirmation before polling begins — useful for WSL/SSH where the browser can't auto-open.

Minor suggestions:

  • When --wait is active and the user confirms they've already authorized, the polling loop still sleeps the full intervalMs (typically 5 s) before the first request. Since the user just signalled readiness, skipping that initial delay would be a small but appreciated UX improvement.
  • With --wait, the URL and code are displayed twice: once in the note() box (which WSL users can't easily click) and again in the plain-text block below. Skipping or conditionally suppressing the note() call when --wait is set would reduce duplication.

Confidence Score: 4/5

  • Safe to merge — both bug fixes are correct, the new flag is purely additive, and no auth logic or token storage is changed.
  • The two polling bugs are fixed correctly and the slow_down interval handling now matches GitHub's spec. The --wait feature is well-contained. The only concerns are minor UX nits (redundant display, unnecessary initial delay after confirmation) that do not affect correctness or security.
  • No files require special attention.

Comments Outside Diff (1)

  1. src/providers/github-copilot-auth.ts, line 159-167 (link)

    URL and code are displayed twice when --wait is used

    The note() box on lines 159–162 unconditionally renders the verification URL and code for all users. When --wait is also set, the plain-text block immediately below (line 167) prints the same information again. The resulting output in a WSL terminal looks like:

    ┌─ Authorize ─────────────────────────────────────────
    │  Visit: https://github.com/login/device
    │  Code: XXXX-YYYY
    └──────────────────────────────────────────────────────
    
      URL:  https://github.com/login/device
      Code: XXXX-YYYY
    

    The note() box is the one WSL users can't easily click — so when --wait is active, the note() call can be omitted (or skipped with a conditional) to avoid the duplication. This is not a functional bug, but it may confuse users into thinking they have two different codes.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/providers/github-copilot-auth.ts
    Line: 159-167
    
    Comment:
    **URL and code are displayed twice when `--wait` is used**
    
    The `note()` box on lines 159–162 unconditionally renders the verification URL and code for all users. When `--wait` is also set, the plain-text block immediately below (line 167) prints the same information again. The resulting output in a WSL terminal looks like:
    
    ```
    ┌─ Authorize ─────────────────────────────────────────
    │  Visit: https://github.com/login/device
    │  Code: XXXX-YYYY
    └──────────────────────────────────────────────────────
    
      URL:  https://github.com/login/device
      Code: XXXX-YYYY
    ```
    
    The `note()` box is the one WSL users can't easily click — so when `--wait` is active, the `note()` call can be omitted (or skipped with a conditional) to avoid the duplication. This is not a functional bug, but it may confuse users into thinking they have two different codes.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/providers/github-copilot-auth.ts
Line: 80-86

Comment:
**Initial delay still applies after `--wait` confirmation**

When the user passes `--wait` and confirms "Have you authorized the code in your browser?", they've already indicated the token should be ready. However, `pollForAccessToken` unconditionally sleeps `intervalMs` (minimum 1 s, typically 5 s from GitHub) before the first request — regardless of whether the caller told the user to go authorize or the user just confirmed they already did.

This means after the user types "yes", the CLI silently waits up to 5 seconds before making any attempt. It won't cause a functional problem, but it's a small UX cliff that could be avoided by accepting an optional `skipFirstDelay` parameter and only sleeping when the user hasn't pre-confirmed.

```typescript
async function pollForAccessToken(params: {
  deviceCode: string;
  intervalMs: number;
  expiresAt: number;
  skipFirstDelay?: boolean;  // skip the initial sleep when user already confirmed
}): Promise<string> {
  // ...
  let intervalMs = params.intervalMs;
  let first = true;

  while (Date.now() < params.expiresAt) {
    if (!first || !params.skipFirstDelay) {
      await new Promise((r) => setTimeout(r, intervalMs));
      if (Date.now() >= params.expiresAt) break;
    }
    first = false;
    // ... rest of loop unchanged
  }
}
```

Then pass `skipFirstDelay: true` from `githubCopilotLoginCommand` when `opts.wait` is `true` and the user confirmed.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/providers/github-copilot-auth.ts
Line: 159-167

Comment:
**URL and code are displayed twice when `--wait` is used**

The `note()` box on lines 159–162 unconditionally renders the verification URL and code for all users. When `--wait` is also set, the plain-text block immediately below (line 167) prints the same information again. The resulting output in a WSL terminal looks like:

```
┌─ Authorize ─────────────────────────────────────────
│  Visit: https://github.com/login/device
│  Code: XXXX-YYYY
└──────────────────────────────────────────────────────

  URL:  https://github.com/login/device
  Code: XXXX-YYYY
```

The `note()` box is the one WSL users can't easily click — so when `--wait` is active, the `note()` call can be omitted (or skipped with a conditional) to avoid the duplication. This is not a functional bug, but it may confuse users into thinking they have two different codes.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 04a921b

Comment on lines +80 to +86

while (Date.now() < params.expiresAt) {
// Always wait before polling — avoids immediate `authorization_pending`
// on the first request which triggers GitHub's slow_down backoff.
await new Promise((r) => setTimeout(r, intervalMs));

if (Date.now() >= params.expiresAt) break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Initial delay still applies after --wait confirmation

When the user passes --wait and confirms "Have you authorized the code in your browser?", they've already indicated the token should be ready. However, pollForAccessToken unconditionally sleeps intervalMs (minimum 1 s, typically 5 s from GitHub) before the first request — regardless of whether the caller told the user to go authorize or the user just confirmed they already did.

This means after the user types "yes", the CLI silently waits up to 5 seconds before making any attempt. It won't cause a functional problem, but it's a small UX cliff that could be avoided by accepting an optional skipFirstDelay parameter and only sleeping when the user hasn't pre-confirmed.

async function pollForAccessToken(params: {
  deviceCode: string;
  intervalMs: number;
  expiresAt: number;
  skipFirstDelay?: boolean;  // skip the initial sleep when user already confirmed
}): Promise<string> {
  // ...
  let intervalMs = params.intervalMs;
  let first = true;

  while (Date.now() < params.expiresAt) {
    if (!first || !params.skipFirstDelay) {
      await new Promise((r) => setTimeout(r, intervalMs));
      if (Date.now() >= params.expiresAt) break;
    }
    first = false;
    // ... rest of loop unchanged
  }
}

Then pass skipFirstDelay: true from githubCopilotLoginCommand when opts.wait is true and the user confirmed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/providers/github-copilot-auth.ts
Line: 80-86

Comment:
**Initial delay still applies after `--wait` confirmation**

When the user passes `--wait` and confirms "Have you authorized the code in your browser?", they've already indicated the token should be ready. However, `pollForAccessToken` unconditionally sleeps `intervalMs` (minimum 1 s, typically 5 s from GitHub) before the first request — regardless of whether the caller told the user to go authorize or the user just confirmed they already did.

This means after the user types "yes", the CLI silently waits up to 5 seconds before making any attempt. It won't cause a functional problem, but it's a small UX cliff that could be avoided by accepting an optional `skipFirstDelay` parameter and only sleeping when the user hasn't pre-confirmed.

```typescript
async function pollForAccessToken(params: {
  deviceCode: string;
  intervalMs: number;
  expiresAt: number;
  skipFirstDelay?: boolean;  // skip the initial sleep when user already confirmed
}): Promise<string> {
  // ...
  let intervalMs = params.intervalMs;
  let first = true;

  while (Date.now() < params.expiresAt) {
    if (!first || !params.skipFirstDelay) {
      await new Promise((r) => setTimeout(r, intervalMs));
      if (Date.now() >= params.expiresAt) break;
    }
    first = false;
    // ... rest of loop unchanged
  }
}
```

Then pass `skipFirstDelay: true` from `githubCopilotLoginCommand` when `opts.wait` is `true` and the user confirmed.

How can I resolve this? If you propose a fix, please make it concise.

@chadmoore chadmoore force-pushed the fix/device-flow-slow-down-backoff branch from 85ffe67 to e127ff5 Compare March 15, 2026 01:40
Copy link
Copy Markdown

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

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: e127ff5956

ℹ️ 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".

let first = true;

while (Date.now() < params.expiresAt) {
if (!first || !params.skipFirstDelay) {
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 Always honor initial device-flow polling interval

When --wait is used, skipFirstDelay makes the first token poll fire immediately, so users who confirm quickly (before GitHub’s initial interval elapses) still hit the token endpoint too early and can receive authorization_pending/slow_down again. This reintroduces the backoff behavior the change is trying to prevent for the new --wait flow; the first poll should still respect the provider’s minimum interval (or only skip the already-elapsed portion).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

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

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: 81222a0e43

ℹ️ 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".

chadmoore and others added 4 commits March 14, 2026 22:00
…/remote terminals

The device flow polling loop had two bugs:

1. When GitHub returns slow_down, the response includes the new minimum
   polling interval in seconds. The old code ignored this and only added
   a fixed 2000ms to the original interval, causing continued rate limiting.
   Now intervalMs is updated to the server-specified value (seconds * 1000).

2. The loop polled immediately on the first iteration before the user had
   a chance to authorize, triggering authorization_pending which in turn
   caused slow_down cascades. Moving the sleep to the top of the loop
   fixes this.

Adds a --wait flag to login-github-copilot that prompts the user to
confirm they've authorized before polling begins. Also prints the URL
and code as plain text so they're easy to copy in terminals (e.g. WSL,
SSH) where the clack box rendering prevents ctrl+click link detection.

Usage:
  openclaw models auth login-github-copilot --wait

Co-authored-by: Copilot <[email protected]>
- Skip initial poll delay when --wait is used and user has confirmed
  authorization, avoiding a silent 1-5s pause after the user types 'yes'
- Show note() box only when --wait is not set; when --wait is set show
  plain-text URL/code only, preventing the info from appearing twice

Co-authored-by: Copilot <[email protected]>
Replace skipFirstDelay with firstPollAfter timestamp so the first poll
respects GitHub's interval relative to when the device code was issued.
If the user takes longer than the interval to confirm, we poll immediately;
otherwise we wait only the remaining portion — no backoff risk either way.

Co-authored-by: Copilot <[email protected]>
…quest

Capturing the timestamp before awaiting requestDeviceCode meant network
latency counted as elapsed polling time, potentially making firstPollAfter
already past on slow links and triggering an immediate poll.

Co-authored-by: Copilot <[email protected]>
@chadmoore chadmoore force-pushed the fix/device-flow-slow-down-backoff branch from de579cd to db0e97d Compare March 15, 2026 02:00
Copy link
Copy Markdown

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

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: c3e99c60ac

ℹ️ 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 372 to +375
provider: "github-copilot",
method: "device",
yes: Boolean(opts.yes),
wait: Boolean(opts.wait),
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 Wire the new --wait flag into the GitHub login path

--wait is advertised here, but it never reaches githubCopilotLoginCommand. modelsAuthLoginCommand only accepts provider/method/setDefault/yes (src/commands/models/auth.ts:494-499), runProviderAuthMethod does not forward arbitrary CLI flags, and the GitHub plugin still invokes githubCopilotLoginCommand({ yes: true, profileId: "github-copilot:github" }, ...) (extensions/github-copilot/index.ts:75-90). In practice, openclaw models auth login-github-copilot --wait behaves exactly like before, so the WSL/SSH users this change targets still won't get the plain-text URL plus confirmation flow.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant