From e19edb12978834ae05b439b16bef0919785d63d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 05:09:35 +0000 Subject: [PATCH 1/5] test(server): reproduce #33701 managed Codex catalog token never renews The managed provider launches Codex with refresh_interval_ms=0, so a running app-server reuses its launch token for model catalog requests after it expires. Assert a positive interval within the broker's 60s renewal margin. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/CodexManagedRuntime.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/CodexManagedRuntime.test.ts b/apps/server/src/provider/CodexManagedRuntime.test.ts index 2c697f8fa8531..ceb05650903d2 100644 --- a/apps/server/src/provider/CodexManagedRuntime.test.ts +++ b/apps/server/src/provider/CodexManagedRuntime.test.ts @@ -147,7 +147,11 @@ it.live.each( assert.include(provider, 'base_url="https://api.openai.com/v1"'); assert.include(provider, 'model_catalog_url="https://api.openai.com/v1/models"'); assert.notInclude(provider, "env_key"); - assert.include(provider, "refresh_interval_ms=0"); + const refreshIntervalMs = Number(/refresh_interval_ms=(\d+)/u.exec(provider)?.[1]); + assert.isTrue( + refreshIntervalMs > 0 && refreshIntervalMs <= 60_000, + `refresh_interval_ms=${refreshIntervalMs} must be in (0, 60000]: 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's 60s renewal margin`, + ); // regression: #16132 Codex fetches each token through this helper, so a running // session picks up renewals instead of keeping the token it launched with. if (source === "managed" && account === "primary") { From 833003bbdefcc2e0011311beebdca517f7ca1c60 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 05:12:10 +0000 Subject: [PATCH 2/5] fix(server): managed Codex renews the token its model catalog uses T3 launched managed Codex with refresh_interval_ms=0 for the token helper. Codex treats 0 as "never refresh proactively": it keeps the first token and asks the helper again only after a 401 on the Responses path. The model catalog request has no such retry, so once the launch token expired every catalog refetch in that app-server failed with 401. New sessions then fell back to generic model metadata. Codex now re-runs the helper every 60 seconds. The broker already renews a saved token once it has 60 seconds or less left, so a token Codex holds for at most 60 seconds is never used past its expiry. Both sides now read the same TOKEN_RENEWAL_MARGIN_MS constant so the two values cannot drift. Fixes pingdotgg/t3code#33701 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/CodexChatGptAuth.ts | 3 ++- apps/server/src/provider/CodexManagedRuntime.test.ts | 5 +++-- apps/server/src/provider/CodexManagedRuntime.ts | 4 ++-- apps/server/src/provider/CodexManagedTokenProtocol.ts | 2 ++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/CodexChatGptAuth.ts b/apps/server/src/provider/CodexChatGptAuth.ts index ad06cdc001ae6..40d1c09de7eda 100644 --- a/apps/server/src/provider/CodexChatGptAuth.ts +++ b/apps/server/src/provider/CodexChatGptAuth.ts @@ -31,6 +31,7 @@ import type { ProviderAuthFlowContext } from "@t3tools/provider-core/server/prov import { HttpClient, HttpClientRequest } from "effect/http"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; import { withChatGptSessionLock } from "./CodexChatGptSessionLock.ts"; +import { TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; @@ -823,7 +824,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* "Token sharing is disabled. Sign in with ChatGPT and enable token sharing.", ); const now = yield* Clock.currentTimeMillis; - if (record.expiresAt - now > 60_000) return record; + if (record.expiresAt - now > TOKEN_RENEWAL_MARGIN_MS) return record; if (record.earliestRefreshAt !== null && record.earliestRefreshAt > now) { if (record.expiresAt > now) return record; return yield* failure( diff --git a/apps/server/src/provider/CodexManagedRuntime.test.ts b/apps/server/src/provider/CodexManagedRuntime.test.ts index ceb05650903d2..dbc487b637fb5 100644 --- a/apps/server/src/provider/CodexManagedRuntime.test.ts +++ b/apps/server/src/provider/CodexManagedRuntime.test.ts @@ -21,6 +21,7 @@ import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as CodexInstallation from "./CodexInstallation.ts"; import { makeCodexManagedRuntime } from "./CodexManagedRuntime.ts"; +import { TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; import { codexAppServerArgs } from "./codexLaunchArgs.ts"; @@ -149,8 +150,8 @@ it.live.each( assert.notInclude(provider, "env_key"); const refreshIntervalMs = Number(/refresh_interval_ms=(\d+)/u.exec(provider)?.[1]); assert.isTrue( - refreshIntervalMs > 0 && refreshIntervalMs <= 60_000, - `refresh_interval_ms=${refreshIntervalMs} must be in (0, 60000]: 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's 60s renewal margin`, + refreshIntervalMs > 0 && refreshIntervalMs <= TOKEN_RENEWAL_MARGIN_MS, + `refresh_interval_ms=${refreshIntervalMs} must be in (0, ${TOKEN_RENEWAL_MARGIN_MS}]: 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's renewal margin`, ); // regression: #16132 Codex fetches each token through this helper, so a running // session picks up renewals instead of keeping the token it launched with. diff --git a/apps/server/src/provider/CodexManagedRuntime.ts b/apps/server/src/provider/CodexManagedRuntime.ts index 3704c919c2791..a48c64bf2a38f 100644 --- a/apps/server/src/provider/CodexManagedRuntime.ts +++ b/apps/server/src/provider/CodexManagedRuntime.ts @@ -10,7 +10,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import { resolveSelfInvocation, selfInvocationArgs } from "@t3tools/shared/nodeRuntime"; import * as CodexInstallation from "./CodexInstallation.ts"; import { makeCodexChatGptAuth } from "./CodexChatGptAuth.ts"; -import { COMMAND_TIMEOUT_MS } from "./CodexManagedTokenProtocol.ts"; +import { COMMAND_TIMEOUT_MS, TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; import { materializeCodexShadowHome } from "./Drivers/CodexHomeLayout.ts"; export interface CodexEffectiveRuntime { @@ -23,7 +23,7 @@ const managedCodexLaunchArgs = (command: string, args: ReadonlyArray) => [ 'model_provider="openai_token_sharing"', // Codex persists this provider ID in rollouts and needs it unchanged to resume. - `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=0}}`, + `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=${TOKEN_RENEWAL_MARGIN_MS}}}`, "features.api_key_model_discovery=true", "features.multi_agent=false", "features.multi_agent_v2=false", diff --git a/apps/server/src/provider/CodexManagedTokenProtocol.ts b/apps/server/src/provider/CodexManagedTokenProtocol.ts index d5f9e610d2188..b177872dde45c 100644 --- a/apps/server/src/provider/CodexManagedTokenProtocol.ts +++ b/apps/server/src/provider/CodexManagedTokenProtocol.ts @@ -18,6 +18,8 @@ export const RESPONSE_BYTES = 64 * 1024; export const SOCKET_IDLE_MS = 5_000; // Existing lock retry (40s), OAuth (20s), plus startup/store headroom (20s). export const COMMAND_TIMEOUT_MS = 80_000; +// Codex caches helper tokens this long, so the broker must renew at least this early. +export const TOKEN_RENEWAL_MARGIN_MS = 60_000; class ProtocolError extends Schema.TaggedError()("ManagedTokenProtocolError", {}) { override get message(): string { return "The managed token frame was invalid or incomplete."; From 514212fadf94c39dcaafc1d3a999ba48ac4cac87 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 05:33:22 +0000 Subject: [PATCH 3/5] fix(server): renew managed tokens before Codex's cached copy can expire Codex reuses a helper token for up to its refresh interval, so the broker must renew earlier than that. Equal values left no room for helper latency; the broker now renews 30 s before the interval would run out. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/CodexManagedRuntime.test.ts | 4 ++-- apps/server/src/provider/CodexManagedRuntime.ts | 4 ++-- apps/server/src/provider/CodexManagedTokenProtocol.ts | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/server/src/provider/CodexManagedRuntime.test.ts b/apps/server/src/provider/CodexManagedRuntime.test.ts index dbc487b637fb5..775385fa11e31 100644 --- a/apps/server/src/provider/CodexManagedRuntime.test.ts +++ b/apps/server/src/provider/CodexManagedRuntime.test.ts @@ -150,8 +150,8 @@ it.live.each( assert.notInclude(provider, "env_key"); const refreshIntervalMs = Number(/refresh_interval_ms=(\d+)/u.exec(provider)?.[1]); assert.isTrue( - refreshIntervalMs > 0 && refreshIntervalMs <= TOKEN_RENEWAL_MARGIN_MS, - `refresh_interval_ms=${refreshIntervalMs} must be in (0, ${TOKEN_RENEWAL_MARGIN_MS}]: 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's renewal margin`, + refreshIntervalMs > 0 && refreshIntervalMs < TOKEN_RENEWAL_MARGIN_MS, + `refresh_interval_ms=${refreshIntervalMs} must be in (0, ${TOKEN_RENEWAL_MARGIN_MS}): 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's renewal margin`, ); // regression: #16132 Codex fetches each token through this helper, so a running // session picks up renewals instead of keeping the token it launched with. diff --git a/apps/server/src/provider/CodexManagedRuntime.ts b/apps/server/src/provider/CodexManagedRuntime.ts index a48c64bf2a38f..d04605e2b47ba 100644 --- a/apps/server/src/provider/CodexManagedRuntime.ts +++ b/apps/server/src/provider/CodexManagedRuntime.ts @@ -10,7 +10,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import { resolveSelfInvocation, selfInvocationArgs } from "@t3tools/shared/nodeRuntime"; import * as CodexInstallation from "./CodexInstallation.ts"; import { makeCodexChatGptAuth } from "./CodexChatGptAuth.ts"; -import { COMMAND_TIMEOUT_MS, TOKEN_RENEWAL_MARGIN_MS } from "./CodexManagedTokenProtocol.ts"; +import { COMMAND_TIMEOUT_MS, TOKEN_REFRESH_INTERVAL_MS } from "./CodexManagedTokenProtocol.ts"; import { materializeCodexShadowHome } from "./Drivers/CodexHomeLayout.ts"; export interface CodexEffectiveRuntime { @@ -23,7 +23,7 @@ const managedCodexLaunchArgs = (command: string, args: ReadonlyArray) => [ 'model_provider="openai_token_sharing"', // Codex persists this provider ID in rollouts and needs it unchanged to resume. - `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=${TOKEN_RENEWAL_MARGIN_MS}}}`, + `model_providers.openai_token_sharing={name="OpenAI Token Sharing",base_url="https://api.openai.com/v1",model_catalog_url="https://api.openai.com/v1/models",wire_api="responses",requires_openai_auth=false,supports_websockets=false,auth={command=${JSON.stringify(command)},args=${JSON.stringify(args)},timeout_ms=${COMMAND_TIMEOUT_MS},refresh_interval_ms=${TOKEN_REFRESH_INTERVAL_MS}}}`, "features.api_key_model_discovery=true", "features.multi_agent=false", "features.multi_agent_v2=false", diff --git a/apps/server/src/provider/CodexManagedTokenProtocol.ts b/apps/server/src/provider/CodexManagedTokenProtocol.ts index b177872dde45c..cacee29243f1d 100644 --- a/apps/server/src/provider/CodexManagedTokenProtocol.ts +++ b/apps/server/src/provider/CodexManagedTokenProtocol.ts @@ -18,8 +18,9 @@ export const RESPONSE_BYTES = 64 * 1024; export const SOCKET_IDLE_MS = 5_000; // Existing lock retry (40s), OAuth (20s), plus startup/store headroom (20s). export const COMMAND_TIMEOUT_MS = 80_000; -// Codex caches helper tokens this long, so the broker must renew at least this early. -export const TOKEN_RENEWAL_MARGIN_MS = 60_000; +// Codex reuses a helper token this long; the broker renews earlier, leaving room for helper latency. +export const TOKEN_REFRESH_INTERVAL_MS = 60_000; +export const TOKEN_RENEWAL_MARGIN_MS = TOKEN_REFRESH_INTERVAL_MS + 30_000; class ProtocolError extends Schema.TaggedError()("ManagedTokenProtocolError", {}) { override get message(): string { return "The managed token frame was invalid or incomplete."; From 9912c30504ad45def24b1b9962cec921f27d70c9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 05:43:03 +0000 Subject: [PATCH 4/5] test(server): renew a managed token with 75 s left Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/CodexChatGptAuth.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/CodexChatGptAuth.test.ts b/apps/server/src/provider/CodexChatGptAuth.test.ts index 4e84553dca26a..0f75e857bdb0f 100644 --- a/apps/server/src/provider/CodexChatGptAuth.test.ts +++ b/apps/server/src/provider/CodexChatGptAuth.test.ts @@ -736,7 +736,8 @@ it.effect( yield* h.phase("succeeded"); assert.strictEqual(Option.getOrThrow(yield* h.auth.read).subject, "user-test"); assert.strictEqual(h.exchanges[0]?.get("client_id"), "oaiapp_test"); - yield* h.seedExpired; + // 75 s left on the one-hour token: inside the renewal margin Codex's 60 s token cache needs. + yield* TestClock.adjust("3525 seconds"); const records = yield* Effect.all([h.auth.access, h.auth.access], { concurrency: "unbounded", }); From a56aa471ea87b289f297031f300c7658f02982b9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 19:23:52 +0000 Subject: [PATCH 5/5] test(server): trim managed Codex refresh interval assertion AI-assisted; verified with the tests and tools listed above. --- apps/server/src/provider/CodexManagedRuntime.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/server/src/provider/CodexManagedRuntime.test.ts b/apps/server/src/provider/CodexManagedRuntime.test.ts index 775385fa11e31..0df166b6d6b05 100644 --- a/apps/server/src/provider/CodexManagedRuntime.test.ts +++ b/apps/server/src/provider/CodexManagedRuntime.test.ts @@ -149,10 +149,8 @@ it.live.each( assert.include(provider, 'model_catalog_url="https://api.openai.com/v1/models"'); assert.notInclude(provider, "env_key"); const refreshIntervalMs = Number(/refresh_interval_ms=(\d+)/u.exec(provider)?.[1]); - assert.isTrue( - refreshIntervalMs > 0 && refreshIntervalMs < TOKEN_RENEWAL_MARGIN_MS, - `refresh_interval_ms=${refreshIntervalMs} must be in (0, ${TOKEN_RENEWAL_MARGIN_MS}): 0 makes Codex reuse the launch token forever for model catalog requests, and a longer interval outlives the broker's renewal margin`, - ); + // regression: #33701 0 makes Codex reuse the launch token forever for catalog requests. + assert.isTrue(refreshIntervalMs > 0 && refreshIntervalMs < TOKEN_RENEWAL_MARGIN_MS); // regression: #16132 Codex fetches each token through this helper, so a running // session picks up renewals instead of keeping the token it launched with. if (source === "managed" && account === "primary") {