Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions devlog/_plan/261003_release_native_accounts/000_public_scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Native-account release stabilization

This unit tracks three public proposals for the next integration train. It records
source provenance and review order; it does not claim that the proposals have
landed or passed integration checks.

| Public source | Proposed behavior | Observed source head |
| --- | --- | --- |
| [#6496](https://github.com/lidge-jun/opencodex/pull/6496) | Explain revoked-session quota failures while retaining the last-known plan. | `8b645854fcae802c93c516a10245f9c3b7eabb76` |
| [#6507](https://github.com/lidge-jun/opencodex/pull/6507) | Keep stored native-main credentials in Pool health on translated Claude turns. | `7ffd1a856957c320d139ff262b4adaf4dd3da07b` |
| [#6505](https://github.com/lidge-jun/opencodex/pull/6505) | Try the next combo target when the current ChatGPT plan cannot use a model. | `ce2c1a60fd6934ee84b1c52d4392a6128911d914` |

All three proposals were open against `dev` when inspected on 2026-10-03.
Their author is @vadymhimself. Carries preserve source authorship and coauthor
trailers. Source heads and reviews must be rechecked before adoption.

Review quota diagnostics first, then stored-main ownership and refresh, then
combo refusal behavior. Each coherent change receives its own regression checks,
independent security review where applicable, and applicable CI at the exact PR
head. The integration coordinator owns merging and final integrated verification.

Unpublished security analysis and implementation plans remain in ignored scratch
space under the repository's security-working-notes policy. This public record
contains only scope already disclosed by the linked proposals.
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ only these fields when comparing network modes, rather than the full account lis
| `internal_error` | An internal refresh step failed. |

Only `http_error` includes `httpStatus`. Other statuses do not imply HTTP 0 or an
account entitlement problem.
account entitlement problem. An `http_error` may also include `code` when the provider
named a reason that only a new sign-in fixes (`token_invalidated`, `invalid_refresh_token`,
`invalid_workspace_selected`). A `token_invalidated` response indicates a revoked session;
the row then shows `needsReauth: true` with the last-known plan.

### Which proxy path is used?

Expand Down
32 changes: 1 addition & 31 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"compaction-event-ownership.test.ts": "responses",
"zed-retained-budget.test.ts": "providers",
"jev-decision-scope.test.ts": "server",
"codex-main-token-invalidated.test.ts": "codex-integration",
"codex-shim-path-readiness.test.ts": "codex-integration",
"codex-shim-fnm.test.ts": "codex-integration",
"claude-intercept-cleanup.test.ts": "claude-integration",
Expand Down Expand Up @@ -57,42 +58,11 @@
"openai-chat-tool-call-id-remint.test.ts": "adapters/openai",
"coding-agent-json-lines-scaling.test.ts": "providers",
"usage-snapshot-digest-reuse.test.ts": "usage",
"release-desktop-scripts.test.ts": "ci-workflows",
"installed-gate-drivers.test.ts": "ci-workflows",
"gui-desktop-sidecar-script.test.ts": "gui",
"standalone-build-script.test.ts": "gui",
"standalone-service.test.ts": "service",
"standalone.test.ts": "lib",
"worker-embed.test.ts": "lib",
"worker-embed-coverage.test.ts": "lib",
"server-combo-held-response.test.ts": "server",
"key-attribution.test.ts": "usage",
"jev-stats.test.ts": "usage",
"provider-send-path-import.test.ts": "server",
"socks5-fetch.test.ts": "lib",
"socks5-upload-lifecycle.test.ts": "lib",
"provider-egress.test.ts": "lib",
"provider-egress-outbound.test.ts": "providers",
"provider-egress-fetch.test.ts": "responses",
"provider-egress-management-validation.test.ts": "server",
"start-args.test.ts": "cli",
"start-ownership-publication.test.ts": "cli",
"responses-core-modules.test.ts": "responses",
"responses-devin-401-replay.test.ts": "responses",
"responses-grok-devin-preflight.test.ts": "responses",
"responses-passthrough-transient-policy.test.ts": "responses",
"responses-spend-ledger-wiring.test.ts": "responses",
"responses-send-budget-errors.test.ts": "responses",
"responses-4546-incident-regression.test.ts": "responses",
"chat-responses-control-integration.test.ts": "responses",
"coding-agent-tool-result-images.test.ts": "adapters",
"cold-spawn-warmup.test.ts": "ci-workflows",
"stepfun-provider.test.ts": "providers",
"warmup-registration.test.ts": "ci-workflows",
"hub-usage.test.ts": "server",
"client-hub-usage.test.ts": "clients",
"cli-usage-hub.test.ts": "cli",
"cli-companion.test.ts": "cli",
"deepseek-quota-currency.test.ts": "providers",
"mimo-token-plan-capacity.test.ts": "providers",
"command-code-tool-text-prose-split.test.ts": "providers",
Expand Down
14 changes: 8 additions & 6 deletions src/codex/auth-api/account-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,26 +353,28 @@ export async function listCodexAuthAccountsSnapshot(
? mainResult.hasCredential
: getMainAccountCredentialPresence() ?? false;
const mainMissingCredential = mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential;
const liveQuotaRefresh = mainSnapshotLive && mainResult.quotaRefreshGeneration !== undefined
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
? mainResult.quotaRefresh : undefined;
const mainNeedsReauth = mainMissingCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
const mainHealth = projectCodexAccountHealth({
accountId: MAIN_CODEX_ACCOUNT_ID,
needsReauth: mainNeedsReauth,
});
// The main row carries the same attribution as a pool row. Reaching this point without
// `mainMissingCredential` means the runtime reauth flag is what set `mainNeedsReauth`, so the
// cause is a refresh that did not complete.
// A diagnostic status is not causal evidence: a live bare 401 can be transient while an
// earlier refresh failure still owns the quarantine. Only a current terminal probe attributes it.
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
? "missing_credential"
: mainNeedsReauth
? "refresh_failed"
? mainResult.terminalAuthFailure === true && liveQuotaRefresh?.status === "http_error"
? "unauthorized" : "refresh_failed"
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
const main: CodexAuthAccountDto = {
id: MAIN_CODEX_ACCOUNT_ID,
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
plan: mainInfo.plan,
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
? { quotaRefresh: mainResult.quotaRefresh } : {}),
...(liveQuotaRefresh ? { quotaRefresh: liveQuotaRefresh } : {}),
logLabel: "main",
isMain: true,
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
Expand Down
37 changes: 25 additions & 12 deletions src/codex/auth-api/main-account-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUN
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
import { captureMainAccountIdentityGeneration, clearMainAccountInfoCache, getMainAccountInfoCache, getMainQuotaCredentialGeneration, isMainAccountIdentityGenerationLive, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential, setMainAccountCredentialPresence, setMainAccountInfoCache } from "../main-account-cache";
import type { MainQuotaWriter, MainAccountInfo } from "../main-account-cache";
import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
import { isCodexTerminalAuthCode } from "../quota-refresh-outcome";
import type { CodexQuotaRefreshOutcome, CodexTerminalAuthCode } from "../quota-refresh-outcome";
import { observeMainReserveRevocation } from "../reserve-availability";
import type { AdmissionLease } from "../../lib/admission";
import { nonEmptyPlan } from "./runtime-config";
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
import { MAIN_TERMINAL_AUTH_CODES, readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
import { readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";

/**
* Last reset-credit count this process parsed for the main account, tagged with the
Expand Down Expand Up @@ -79,14 +80,18 @@ function sharedMainQuotaPacingEnabled(config: OcxConfig): boolean {
* itself would make a real 401 permanently transient.
*/
export async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
if (resp.status === 401) {
if (!accessTokenLive) return true;
const code = await readMainAuthErrorCode(resp);
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
}
if (resp.status !== 403) return false;
return (await classifyMainAuthResponse(resp, accessTokenLive)).terminal;
}

/** As `isTerminalMainAuthResponse`, also naming the provider code that made it terminal. */
async function classifyMainAuthResponse(
resp: Response,
accessTokenLive: boolean,
): Promise<{ terminal: boolean; code?: CodexTerminalAuthCode }> {
if (resp.status !== 401 && resp.status !== 403) return { terminal: false };
const code = await readMainAuthErrorCode(resp);
return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
if (isCodexTerminalAuthCode(code)) return { terminal: true, code };
return { terminal: resp.status === 401 && !accessTokenLive };
}

export interface MainResetQuotaProof {
Expand All @@ -103,6 +108,8 @@ export interface MainAccountInfoFetchResult {
quotaRefresh?: CodexQuotaRefreshOutcome;
/** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
quotaRefreshGeneration?: number;
/** This current-credential usage response supplied terminal auth evidence; never public. */
terminalAuthFailure?: true;
/** Whether this attempt safely inspected the physical native-main credential. */
credentialChecked: boolean;
/** Meaningful only when credentialChecked is true. */
Expand Down Expand Up @@ -288,7 +295,8 @@ export async function fetchMainAccountInfoWhileOwned(
const resp = admission.response;
quotaPhase = "publish";
if (!resp.ok) {
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
const terminalAuthFailure = authFailure.terminal;
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
if (retried) return retried;
if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
Expand All @@ -302,10 +310,15 @@ export async function fetchMainAccountInfoWhileOwned(
if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
}
// Keep the last-known plan visible: after a plan change revokes the session, the plan the
// account HAD is what tells the operator why it stopped working.
return {
info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
quotaRefresh: { status: "http_error", httpStatus: resp.status },
info: { ...EMPTY_MAIN_ACCOUNT_INFO, plan: nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan()) },
credentialChecked: true, hasCredential: true,
quotaRefresh: { status: "http_error", httpStatus: resp.status,
...(authFailure.code ? { code: authFailure.code } : {}) },
quotaRefreshGeneration,
...(terminalAuthFailure ? { terminalAuthFailure: true as const } : {}),
};
}
quotaPhase = "body";
Expand Down
6 changes: 2 additions & 4 deletions src/codex/auth-api/pool-quota-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
import { seedLoginRowsForTests } from "./login-state";
import { nonEmptyPlan } from "./runtime-config";
import { CODEX_TERMINAL_AUTH_CODES } from "../quota-refresh-outcome";

export const POOL_CACHE_TTL = 5 * 60_000;
export const POOL_QUOTA_REFRESH_CONCURRENCY = 4;

export const MAIN_TERMINAL_AUTH_CODES = new Set([
"invalid_workspace_selected",
"invalid_refresh_token",
]);
export const MAIN_TERMINAL_AUTH_CODES: ReadonlySet<string> = new Set(CODEX_TERMINAL_AUTH_CODES);

export async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
try {
Expand Down
19 changes: 17 additions & 2 deletions src/codex/quota-refresh-outcome.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
/** Diagnostic only: never use this outcome as quota, entitlement, or admission evidence. */
export type CodexQuotaRefreshOutcome =
| { status: "http_error"; httpStatus: number }
/** `code` is set only for a provider code that requires a new sign-in (fixed vocabulary). */
| { status: "http_error"; httpStatus: number; code?: CodexTerminalAuthCode }
| { status: "ok" | "not_reported" | "timeout" | "network_error" | "invalid_response" | "internal_error" };

/** Provider auth codes that only a new sign-in can recover. */
export const CODEX_TERMINAL_AUTH_CODES = [
"invalid_workspace_selected",
"invalid_refresh_token",
// ChatGPT revokes every session of an account whose plan changes (for example Pro to Free),
// while the access token's `exp` still lies in the future.
"token_invalidated",
] as const;
export type CodexTerminalAuthCode = typeof CODEX_TERMINAL_AUTH_CODES[number];
export function isCodexTerminalAuthCode(code: unknown): code is CodexTerminalAuthCode {
return (CODEX_TERMINAL_AUTH_CODES as readonly unknown[]).includes(code);
}

/** The management response is untrusted at the CLI boundary; copy only the fixed vocabulary. */
export function projectCodexQuotaRefreshOutcome(value: unknown): CodexQuotaRefreshOutcome | undefined {
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
const row = value as Record<string, unknown>;
if (row.status === "http_error") {
return typeof row.httpStatus === "number" && Number.isInteger(row.httpStatus)
&& row.httpStatus >= 100 && row.httpStatus <= 599
? { status: "http_error", httpStatus: row.httpStatus }
? { status: "http_error", httpStatus: row.httpStatus,
...(isCodexTerminalAuthCode(row.code) ? { code: row.code } : {}) }
: undefined;
}
switch (row.status) {
Expand Down
2 changes: 1 addition & 1 deletion structure/dashboard-and-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ A private per-dispatch identity generation fences the diagnostic independently o
quota metadata. Both snapshot and account DTO publication omit externally invalidated
attempts; the generation itself is never serialized or stored in the quota cache.
The CLI reconstructs the object using a fixed vocabulary and bounded numeric HTTP
status, so an unexpected management response cannot add raw upstream material.
status, so an unexpected management response cannot add raw upstream material. An `http_error` may carry `code` only from `CODEX_TERMINAL_AUTH_CODES` (`src/codex/quota-refresh-outcome.ts`; e.g. `token_invalidated` after a plan change revokes the session), and a failed read keeps the main row's last-known plan. Reauth attribution uses current terminal-auth evidence behind the same generation fence; a transient HTTP401 diagnostic does not replace an existing refresh-failure cause.

> Decision record: [ADR-0078](decisions/ADR-0078-usage-accounting.md)

Expand Down
2 changes: 1 addition & 1 deletion tests/codex-integration/codex-auth-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ describe("main quota refresh diagnostics", () => {
expect(reads).toBe(1);
if (invalidation === "none") {
const expected = outcome === "http" ? { status: "http_error", httpStatus: 503 }
: outcome === "terminal_http" ? { status: "http_error", httpStatus: 403 }
: outcome === "terminal_http" ? { status: "http_error", httpStatus: 403, code: "invalid_workspace_selected" }
: { status: outcome === "body" ? "network_error" : "ok" };
expect(result.quotaRefresh).toEqual(expected);
} else {
Expand Down
Loading
Loading