#8805: [Bug Fix][AI-assisted] Refresh Copilot token before expiry and retry on auth errors
agents
stale
Cluster:
GitHub Copilot Enhancements
## Summary
GitHub Copilot API tokens expire after ~30 minutes. When OpenClaw spawns a long-running subagent using Copilot as the provider, the token expires mid-session with no recovery mechanism, causing 401 auth errors and session failures.
Fixes #8808
## Problem
1. `resolveCopilotApiToken()` exchanges a GitHub token for a Copilot API token once at run start
2. The token is set via `authStorage.setRuntimeApiKey()` (runtime override, not persisted)
3. Unlike OAuth tokens in `auth-profiles.json`, runtime overrides bypass the normal refresh logic
4. Sessions running >30 minutes hit expired tokens with no automatic refresh or retry
## Solution
This PR adds:
- **Periodic token refresh**: Schedules refresh 5 minutes before token expiry (`COPILOT_REFRESH_MARGIN_MS`)
- **Auth error retry**: On 401/auth errors, refreshes token once and retries the attempt
- **Per-attempt retry reset**: Uses `authRetryPending` pattern to allow fresh retry after each successful request
- **Timer cleanup**: `copilotRefreshCancelled` flag prevents timer leaks on shutdown
## Testing
- [x] 3 new unit tests added:
- `refreshes copilot token after auth error and retries once`
- `allows another auth refresh after a successful retry`
- `does not reschedule copilot refresh after shutdown`
- [x] All 11 auth rotation tests pass
- [x] Build succeeds (`pnpm build`)
- [x] Lightly tested locally (token refresh logic verified via mocks)
## AI Disclosure 🤖
- [x] **AI-assisted**: This fix was developed with GitHub Copilot CLI assistance
- [x] **Testing level**: Fully tested with unit tests
- [x] **Understanding**: I confirm understanding of the code changes
The AI helped identify the root cause (runtime token injection bypassing OAuth refresh), designed the solution (proactive refresh + reactive retry), and wrote tests. Code was reviewed for correctness.
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR adds proactive and reactive refresh handling for the `github-copilot` provider inside the embedded PI runner.
- In `src/agents/pi-embedded-runner/run.ts`, a `CopilotTokenState` is introduced to track the GitHub token used for Copilot exchange, the Copilot token expiry time, and a scheduled refresh timer. The runner now schedules a refresh ~5 minutes before expiry, retries refresh after failure, and performs a one-time refresh+retry when an auth failure is detected (both for `promptError` and assistant-side auth errors).
- In `src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts`, new unit tests mock `resolveCopilotApiToken()` and validate: (1) auth-error retry behavior, (2) retry state resets after a successful request, and (3) refresh timers do not reschedule after shutdown.
Overall, the change fits into the existing auth-profile rotation loop by treating Copilot’s exchanged API token as a runtime override that needs its own refresh lifecycle.
<h3>Confidence Score: 4/5</h3>
- This PR is likely safe to merge; changes are localized and covered by new tests, with a few edge cases worth tightening.
- Token refresh logic is contained to the Copilot provider path and includes timer cleanup plus retry limiting via `authRetryPending`. Main risks are edge-case loops (near-expiry scheduling) and retry behavior in the presence of invalid GitHub credentials, which could degrade failover behavior rather than break correctness broadly.
- src/agents/pi-embedded-runner/run.ts (refresh scheduling and auth-retry conditions)
<!-- greptile_other_comments_section -->
<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>
**Context used:**
- Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=fd949e91-5c3a-4ab5-90a1-cbe184fd6ce8))
- Context from `dashboard` - AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=0d0c8278-ef8e-4d6c-ab21-f5527e322f13))
<!-- /greptile_comment -->
Most Similar PRs
#4364: fix(github-copilot): use gho_ tokens directly without exchange
by RebelSyntax · 2026-01-30
82.0%
#10492: fix(auth): store Anthropic setup-token as type:oauth for auto-refresh
by sparck75 · 2026-02-06
79.7%
#13505: feat(copilot): add GitHub Enterprise Cloud (GHE.com) support for Co...
by kryptus47 · 2026-02-10
79.4%
#11782: fix: resolve 403 auth error for GithubCopilot imageModel (#10277)
by adamkoncz · 2026-02-08
78.6%
#2123: fix(auth): sync from Claude CLI keychain before OAuth refresh
by jorge123255 · 2026-01-26
78.6%
#8467: fix(github-copilot): add configurable IDE headers + fix null filtering
by ericchansen · 2026-02-04
78.0%
#19267: fix: derive failover reason from timedOut flag to prevent unknown c...
by austenstone · 2026-02-17
78.0%
#11821: fix(auth): trigger failover on 401 status code from expired OAuth t...
by AnonO6 · 2026-02-08
77.5%
#14368: fix: skip auth profile cooldown on format errors to prevent provide...
by koatora20 · 2026-02-12
77.3%
#5027: fix(auth): use correct OAuth credentials for google-gemini-cli refresh
by shayan919293 · 2026-01-30
76.8%