diff --git a/src/codex/account-usability.ts b/src/codex/account-usability.ts index 973ae5cd45c..51c076f6db2 100644 --- a/src/codex/account-usability.ts +++ b/src/codex/account-usability.ts @@ -39,6 +39,15 @@ export interface CodexAccountUsabilityOptions { * account can always become unable to serve. */ retainAccountForUploadedFiles?: boolean; + /** + * Main is live for this request only because the request carries its own main bearer (#5019). + * + * Main may then win this request's selection, but that credential belongs to the request, not + * to the Pool: selecting it must never be written back as the shared active account. Persisting + * it made every later request -- including one whose caller identity is unreadable, or one that + * owns the cooled subscription -- follow a main the operator never chose. + */ + requestOwnedMainCredential?: boolean; } /** diff --git a/src/codex/auth-context.ts b/src/codex/auth-context.ts index c43e2c9088b..06971c714dc 100644 --- a/src/codex/auth-context.ts +++ b/src/codex/auth-context.ts @@ -1162,6 +1162,7 @@ export async function resolveCodexAuthContext( : options.isMainAccountTokenLive, modelEligibleAccountIds, deniedModelAccountIds, + requestOwnedMainCredential: requestScopedMainCredential, // Request-scoped and deliberately absent from `sharedStateSelectionOptions`: one // conversation's attachments say nothing about where unrelated threads should be served. retainAccountForUploadedFiles: options.retainAccountForUploadedFiles === true, diff --git a/src/codex/routing.ts b/src/codex/routing.ts index e43e32b1d3a..e603371af95 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -851,6 +851,12 @@ export function resolveCodexAccountForThreadDetailed( // An entitlement roster constrains only this model request. It must not rewrite // the operator's shared active/pin choice or the task's ordinary-model affinity. const modelScopedSelection = selectionOptions?.modelEligibleAccountIds !== undefined; + // A main that is live only through this request's own bearer serves this request alone; writing + // it back as the shared active account would route later requests through a credential they do + // not carry (see CodexAccountUsabilityOptions.requestOwnedMainCredential). + const sharesActiveSelection = (accountId: string): boolean => !( + accountId === MAIN_CODEX_ACCOUNT_ID && selectionOptions?.requestOwnedMainCredential === true + ); let preserveExistingModelScopedAffinity = false; const sharedSelectionOptions: CodexAccountUsabilityOptions | undefined = modelScopedSelection ? sharedStateSelectionOptions(selectionOptions) ?? {} @@ -1122,7 +1128,7 @@ export function resolveCodexAccountForThreadDetailed( // process-local cursor to whoever is actually serving and releases the pin; the // operator's persisted activeCodexAccountId is left untouched either way, which is // the thing the preference exists to protect. - promoteActiveCodexAccount(config, strategyPick); + if (sharesActiveSelection(strategyPick)) promoteActiveCodexAccount(config, strategyPick); } return { status: "selected", accountId: strategyPick, affinity: affinityAfterRelease(threadId, releaseReason) }; } @@ -1140,7 +1146,7 @@ export function resolveCodexAccountForThreadDetailed( return { status: "none", affinity: affinityOnNoAccount(threadId, releaseReason) }; } if (!isIndependentCodexQuotaScope(quotaScope) && !modelScopedSelection) { - setActiveCodexAccount(config, selected); + if (sharesActiveSelection(selected)) setActiveCodexAccount(config, selected); } active = selected; } @@ -1161,7 +1167,7 @@ export function resolveCodexAccountForThreadDetailed( && preserveSharedSelectionForModelDetour && activeHealthyForSharedSelection; if (!isIndependentCodexQuotaScope(quotaScope) && !modelOnlyMove) { - setActiveCodexAccount(config, fallback); + if (sharesActiveSelection(fallback)) setActiveCodexAccount(config, fallback); } active = fallback; } else if (