Pi logins fail with invalid_grant after T3 stops a discovery or text-generation Pi mid token refresh#30182

opened1d agobyAastarktc

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 rpc for 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), or taskkill /PID … /T /F on Windows (terminateWindowsTree).
  • In RPC mode, Pi starts a background model-catalog refresh at launch unless --offline is set, and that refresh renews any expired OAuth credential. Pi's shutdown, whether from SIGTERM or from stdin closing, waits for extension session_shutdown handlers but not for the refresh, and taskkill /F runs 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.json keeps the spent token and the next refresh gets invalid_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 discoverPiCommandsForCwd runs 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.

  1. Put an expired openai OAuth credential in a scratch PI_CODING_AGENT_DIR.
  2. Start T3's probe command, pi --mode rpc --no-session, and send get_state, get_available_models and get_commands.
  3. Stop it the way T3 does, 100–200 ms after it answers.
  4. auth.json now holds the spent refresh token, and pi -p --model openai/gpt-6-luna prints the invalid_grant error above.
Stop after the answers (Pi 1.4.0)Rotations lost
SIGTERM at 100–200 ms26 of 30
Hard kill at 200 ms (as on Windows)10 of 10
SIGTERM within 50 ms, or at 400 ms or later0 of 50
Either stop, with --offline added0 (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

shell
1OAuth 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.
  • #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 --offline only 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 expired openai login then sat untouched through 20 h of probes and refreshed normally on its next use.
  • Workaround without the fix: add --offline under Settings → Providers → Pi → Launch arguments, then sign in again. This also takes the catalog refresh away from threads, and Pi exports PI_OFFLINE=1 into 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.