Pi logins fail with invalid_grant after T3 stops a discovery or text-generation Pi mid token refresh#30182
What happened
Pi threads fail with OAuth refresh failed for openai: OpenAI OAuth token request failed (400): {"error": "invalid_grant"} on the first openai/* (Sign in with ChatGPT) call after an idle hour while a T3 client is connected. Signing in again only lasts until the next idle hour. Pi's anthropic OAuth login fails the same way, less often.
Diagnosis
- T3 starts
pi --mode rpcfor jobs it ends as soon as Pi answers: the provider status probe (discoverPiViaRpc, every 5 min while a client holds a lease), per-workspace command discovery (discoverPiCommandsForCwd), text generation and thread forks. It stops each one right away (packages/provider-pi/src/server/rpc.ts): SIGTERM to the process group and SIGKILL after 1 s (terminatePiProcess), ortaskkill /PID … /T /Fon Windows (terminateWindowsTree). - In RPC mode, Pi starts a background model-catalog refresh at launch unless
--offlineis set, and that refresh renews any expired OAuth credential. Pi's shutdown, whether from SIGTERM or from stdin closing, waits for extensionsession_shutdownhandlers but not for the refresh, andtaskkill /Fruns no JavaScript. Pi 1.5.0 behaves the same. - ChatGPT refresh tokens are single-use. If the stop lands after the token request has left Pi but before the response is saved,
auth.jsonkeeps the spent token and the next refresh getsinvalid_grant. - T3's traces put the stop inside that window:
- On Windows, probes are hard-killed 245–490 ms after Pi is ready, while the refresh holds Pi's auth lock.
- On one Mac, SIGTERM lands about 100 ms into the refresh.
- Locks left by killed probes stall the next Pi start for up to 30 s. We saw
discoverPiCommandsForCwdruns of 27 s and 30 s.
- Text generation and forks run with
--no-extensions, so a Pi extension cannot work around this. The Pi side is reported separately: earendil-works/pi#11692.
Steps to reproduce
Hermetic run of the installed Pi. It uses fake credentials, denies outbound network, and stands in for OpenAI's token endpoint with single-use rotation and 150 ms of latency each way.
- Put an expired
openaiOAuth credential in a scratchPI_CODING_AGENT_DIR. - Start T3's probe command,
pi --mode rpc --no-session, and sendget_state,get_available_modelsandget_commands. - Stop it the way T3 does, 100–200 ms after it answers.
auth.jsonnow holds the spent refresh token, andpi -p --model openai/gpt-6-lunaprints theinvalid_granterror above.
| Stop after the answers (Pi 1.4.0) | Rotations lost |
|---|---|
| SIGTERM at 100–200 ms | 26 of 30 |
| Hard kill at 200 ms (as on Windows) | 10 of 10 |
| SIGTERM within 50 ms, or at 400 ms or later | 0 of 50 |
Either stop, with --offline added | 0 (no token request) |
Pi 1.5.0, rerun on 2026-11-03 with the stop at 200 ms: SIGTERM lost 10 of 10, a hard kill lost 10 of 10, and either stop with --offline lost 0 of 20 (no token request).
On a real install: sign in to openai in Pi, keep a T3 client connected, leave openai unused for over an hour, then use an openai/* model in a Pi thread.
Version
main cefb544af3 (0.1.6); first seen on 9b1d351ba0 (0.1.5)
Environment
macOS 26.6 and 26.7 (two Macs), Windows 11 and Windows 10, Node 26, Pi 1.4.0 and 1.5.0
Evidence
| 1 | OAuth refresh failed for openai: OpenAI OAuth token request failed (400): {"error": "invalid_grant"} |
| 2 | |
| 3 | # One Windows host, 2026-10-30 (UTC): signed in 14:13:06; the access token expired 15:10:06. |
| 4 | # Probes were hard-killed 308 ms and 323 ms after Pi was ready (15:13:26, 15:18:27). |
| 5 | # auth.json was not written again, and the grant was dead. |
| 6 | # A second Mac: 6 failed runs in 4 episodes (10-16 to 10-30), each the first openai use after an |
| 7 | # idle gap. 27 h of continuous use never failed. |
| 8 | # anthropic: 18 logins lost to "Refresh token not found or invalid" since 08-25, all after T3's |
| 9 | # Pi provider began running on the host; none in the 63 idle expiries before it. |
Related issues
- #14356 was the same single-use rotation failure for managed Codex auth, fixed by #21786 and #21787. Those changes do not touch the Pi provider.
- #30024 (PR #30025) restores SIGTERM before Pi's stdin closes. That does not cover this: Pi's SIGTERM shutdown does not wait for the catalog refresh either, and Windows stops Pi with
taskkill /F.
Fix applied or workaround
- Fix: a PR follows. It passes
--offlineonly to the processes T3 stops right after they answer (status and workspace discovery, text generation, forks), and threads keep the user's launch arguments. Our own build has carried it on four hosts since 2026-10-31. On one Windows host without the workaround below, an expiredopenailogin then sat untouched through 20 h of probes and refreshed normally on its next use. - Workaround without the fix: add
--offlineunder Settings → Providers → Pi → Launch arguments, then sign in again. This also takes the catalog refresh away from threads, and Pi exportsPI_OFFLINE=1into agents' shell commands.
Filed by
Claude Opus 5.5 via Pi in T3 Code, for @astarktc
cc @StiensWout, since you've done much of the recent Pi provider work.