[Bug]: ChatGPT connections end after an hour because every refresh returns invalid_grant#14356

opened24d agobyJjustrach

This issue is AI-generated. An AI coding agent wrote it from our own reproduction against OpenAI's token endpoint. We have not run T3 Code itself; the T3 Code behavior below is read from its source.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Connect a ChatGPT account with managed authentication (#14290).
  2. Keep the connection past the access token's one-hour lifetime. CodexChatGptAuth renews it within 60 seconds of expiry, once earliest_refresh_at has passed (CodexChatGptAuth.ts L729-L748).
  3. OpenAI refuses the renewal.

Without T3 Code: take a token set from any client of this flow and, inside the renewal window, send the same request:

sh
1curl -sS https://auth.openai.com/api/accounts/oauth/token \
2 --data-urlencode grant_type=refresh_token \
3 --data-urlencode client_id="$ISSUED_CLIENT_ID" \
4 --data-urlencode refresh_token="$REFRESH_TOKEN" \
5 --data-urlencode resource=https://api.openai.com/v1

Expected behavior

HTTP 200 with a new access token and a replacement refresh token, so the connection stays up.

Actual behavior

OpenAI answers 400 {"error": "invalid_grant"} with no error_description. T3 Code treats invalid_grant on a refresh as a dead token (L391-L406): it removes the connection and shows "Your ChatGPT connection expired or was disconnected. Sign in again." So every connection should end about an hour after sign-in.

We sent this request eight times across three sign-ins: before and after earliest_refresh_at, 60 seconds before expiry (T3 Code's timing), after expiry, and with ext_agent_host_id or scope added or resource dropped (those three reused a token that had just been refused). Every one was refused, including never-used tokens inside the renewal window. pi and nolune send the same four fields.

Impact

Major degradation or frequent failure

Version or commit

main @ 0fcd5f9

Environment

Not environment-specific: the refusal comes from OpenAI's token endpoint. Reproduced with curl and Python on macOS.

Logs or stack traces

shell
1HTTP/1.1 400 Bad Request
2
3{"error": "invalid_grant"}

Workaround

Sign in again when the connection ends. T3 Code's request matches the documented one, so there may be nothing to change here unless OpenAI says a refresh needs more.

Full report, with every attempt, a reproduction script, and the refresh code of T3 Code, pi and nolune: https://github.com/justrach/codegraff/blob/feat/chat-gpt-new/docs/upstream/openai-sign-in-refresh-invalid-grant.md

Tracked for graff in https://github.com/justrach/codegraff/issues/1422.

Triage

Thanks for the write-up. The refresh request you quoted matches what T3 sends, and it also matches OpenAI's Refreshing tokens section: a form-encoded grant_type=refresh_token, the issued oaiapp_ client id (not dynamic_agent_client), the saved refresh token, and resource=https://api.openai.com/v1, with scope left out, posted to the discovered token endpoint.

Clearing the session on invalid_grant is the recovery OpenAI documents for an unusable refresh token (Refresh errors): drop the dead token set and run OAuth again with the saved client id. T3 does that and keeps the registration, so reconnecting doesn't create a new client. A transient failure such as 503 doesn't clear the credentials, and the refresh-recovery tests in CodexChatGptAuth.test.ts cover that path.

Two corrections to the T3-specific claims:

  • This was reproduced with graff and curl, not with T3. The claim that every T3 connection dies after an hour is inferred from the request shape.
  • "Your ChatGPT connection expired or was disconnected. Sign in again." isn't what this path shows. exchange builds that error, but access then remaps every refresh failure except invalid_client to "Could not renew the ChatGPT connection. Retry, or sign in again." The credentials are still removed before that remap.

ext_agent_host_id is required on the authorize request but isn't part of the documented refresh body, so leaving it off isn't a T3 bug. Attempts 5–7 also reused a token OpenAI had already refused, so they don't show whether a first refresh with extra fields would succeed.

There are no other reports of this from a T3 session. Managed ChatGPT auth landed in merged PR #14290 about ten hours before this issue was filed. If OpenAI is rejecting a spec-matching refresh from every client, the fix is on their side. T3 shouldn't keep retrying invalid_grant or keep a rejected refresh token around.

To treat this as a T3 bug, we need a capture from an actual T3 session, not another client. Sign in with ChatGPT in T3 and leave that session alone until after earliest_refresh_at and within a minute of the access token expiring. Then capture the token endpoint's status, error code, and x-request-id, with the refresh token and client id redacted. A graff x-request-id is useful to OpenAI, but it doesn't show that T3 hit the same refusal.

Synced from GitHub

Disclosure: written by an AI coding assistant for the reporter, and reviewed before posting.

Update: this reproduces with OpenAI's own reference implementation, so it isn't specific to T3 Code or to how any client builds the request.

Using openai/sign-in-with-chatgpt-devkit packages/local, unmodified:

  • A new registration, without ext_agent_host_id (the devkit's default).
  • The devkit's own refresh, sent in its own window: past earliest_refresh_at, 39 s before expiry, on a never-used refresh token.
  • The result was 400 invalid_grant ("ChatGPT did not accept this authorization grant").

A second client renewed its own fresh sign-in a few minutes later, inside its window, and got the same answer: "The refresh token has been invalidated. A fresh sign-in is required."

That matches the triage above: the request is the documented one, so the fix has to come from OpenAI. Reported upstream with the full reproduction, code links for the devkit, T3 Code, pi and Codegraff, and what was ruled out: https://github.com/openai/sign-in-with-chatgpt-devkit/issues/5

Synced from GitHub

I'm also experiencing a similar one-hour expiry symptom in T3 Code itself on macOS, using managed ChatGPT sign-in with T3 Code Nightly 0.0.46-nightly.20261004.2644.

The ChatGPT connection stops working after about one hour. I'm currently seeing a 429 error. Restarting T3 Code temporarily fixes it and restores access, so I have to restart T3 Code roughly every hour to keep working.

For clarity, earlier diagnostic checks also confirmed 401 token_expired errors from the running managed Codex process. The current 429 is my reported symptom, not a separately verified diagnostic result. Since restarting restores access, I haven't confirmed that this is the same invalid_grant failure described in the original report.

Synced from GitHub

@stantaneous This happens if your account had a change in security settings, this includes if you recently did a password reset, or enrolled in AAS / Daybreak Blue.

Synced from GitHub

This is particularly frustrating as "Sign in with ChatGPT" seems enforced for the default codex provider regardless of the underlying authentication of the Codex instance on the box

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks for the report. This fault is outside T3 Code, so we cannot fix it here. Please report it to the project that owns that tool. If you find a fault in T3 Code itself, please open a new issue with steps to reproduce it.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Reopening: Julius is keeping this open while he waits for the details he asked for above, and our earlier close went ahead of that.

Closing this as fixed by #21786 and #21787, which ship in the next release. The server now owns ChatGPT token renewal and saves each rotated refresh token, and running Codex sessions fetch fresh tokens through Codex's native auth command, so connections no longer drop after an hour or need a restart. If you still see the connection end after updating, please open a new issue with the error you get.