@@ -116,6 +116,7 @@ import {
116116 parseRateLimitReason ,
117117 lookupCodexCliTokensByEmail ,
118118 isCodexCliSyncEnabled ,
119+ type Workspace ,
119120} from "./lib/accounts.js" ;
120121import {
121122 getStoragePath ,
@@ -376,6 +377,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
376377 accountIdOverride ?: string ;
377378 accountIdSource ?: AccountIdSource ;
378379 accountLabel ?: string ;
380+ workspaces ?: Workspace [ ] ;
379381 } ;
380382
381383 const resolveAccountSelection = (
@@ -398,6 +400,14 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
398400 return tokens ;
399401 }
400402
403+ // Convert candidates to workspaces
404+ const workspaces : Workspace [ ] = candidates . map ( ( c ) => ( {
405+ id : c . accountId ,
406+ name : c . label ,
407+ enabled : true ,
408+ isDefault : c . isDefault ,
409+ } ) ) ;
410+
401411 if ( candidates . length === 1 ) {
402412 const [ candidate ] = candidates ;
403413 if ( candidate ) {
@@ -406,6 +416,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
406416 accountIdOverride : candidate . accountId ,
407417 accountIdSource : candidate . source ,
408418 accountLabel : candidate . label ,
419+ workspaces,
409420 } ;
410421 }
411422 }
@@ -420,6 +431,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
420431 accountIdOverride : choice . accountId ,
421432 accountIdSource : choice . source ?? "token" ,
422433 accountLabel : choice . label ,
434+ workspaces,
423435 } ;
424436 } ;
425437
@@ -588,6 +600,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
588600 expiresAt : result . expires ,
589601 addedAt : now ,
590602 lastUsed : now ,
603+ workspaces : result . workspaces ,
591604 } ) ;
592605 indexByRefreshToken . set ( result . refresh , newIndex ) ;
593606 if ( accountId ) {
@@ -609,6 +622,11 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
609622 const nextAccountIdSource =
610623 accountId ? accountIdSource ?? existing . accountIdSource : existing . accountIdSource ;
611624 const nextAccountLabel = accountLabel ?? existing . accountLabel ;
625+ // Merge workspaces: preserve enabled status from existing, add new ones
626+ const mergedWorkspaces = result . workspaces ?. map ( ( newWs ) => {
627+ const existingWs = existing . workspaces ?. find ( ( w ) => w . id === newWs . id ) ;
628+ return existingWs ? { ...newWs , enabled : existingWs . enabled } : newWs ;
629+ } ) ?? result . workspaces ;
612630 accounts [ existingIndex ] = {
613631 ...existing ,
614632 accountId : nextAccountId ,
@@ -619,6 +637,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
619637 accessToken : result . access ,
620638 expiresAt : result . expires ,
621639 lastUsed : now ,
640+ workspaces : mergedWorkspaces ,
622641 } ;
623642 if ( oldToken !== result . refresh ) {
624643 indexByRefreshToken . delete ( oldToken ) ;
@@ -1554,8 +1573,13 @@ while (attempted.size < Math.max(1, accountCount)) {
15541573
15551574 const hadAccountId = ! ! account . accountId ;
15561575 const tokenAccountId = extractAccountId ( accountAuth . access ) ;
1576+
1577+ // Use workspace ID if workspaces are configured, otherwise use account ID
1578+ const currentWorkspace = accountManager . getCurrentWorkspace ( account ) ;
1579+ const workspaceId = currentWorkspace ?. id ;
1580+
15571581 const accountId = resolveRequestAccountId (
1558- account . accountId ,
1582+ workspaceId ?? account . accountId ,
15591583 account . accountIdSource ,
15601584 tokenAccountId ,
15611585 ) ;
@@ -1955,35 +1979,62 @@ while (attempted.size < Math.max(1, accountCount)) {
19551979 ) ;
19561980 }
19571981
1958- // Handle workspace disabled/expired errors by marking account as disabled and rotating
1982+ // Handle workspace disabled/expired errors by rotating to next workspace within account
19591983 if ( errorResponse . status === 403 && errorBody ) {
19601984 const errorCode = ( errorBody as { error ?: { code ?: string } } ) ?. error ?. code ?? "" ;
19611985 const errorMessage = ( errorBody as { error ?: { message ?: string } } ) ?. error ?. message ?? "" ;
19621986
19631987 if ( isWorkspaceDisabledError ( errorResponse . status , errorCode , errorMessage ) ) {
19641988 runtimeMetrics . failedRequests ++ ;
1965- runtimeMetrics . accountRotations ++ ;
19661989 runtimeMetrics . lastError = `Workspace disabled for account ${ account . index + 1 } ` ;
19671990
1991+ // Get current workspace info for logging
1992+ const currentWorkspace = accountManager . getCurrentWorkspace ( account ) ;
1993+ const workspaceName = currentWorkspace ?. name ?? currentWorkspace ?. id ?? "unknown" ;
1994+
19681995 logWarn (
1969- `Workspace disabled/expired for account ${ account . index + 1 } (${ account . email ?? "unknown" } ). Disabling account and rotating .` ,
1996+ `Workspace disabled/expired for account ${ account . index + 1 } (${ account . email ?? "unknown" } ) - workspace: ${ workspaceName } . Rotating to next workspace .` ,
19701997 { errorCode, errorMessage } ,
19711998 ) ;
19721999
1973- // Mark the account as disabled
1974- accountManager . setAccountEnabled ( account . index , false ) ;
1975- accountManager . saveToDiskDebounced ( ) ;
2000+ // Disable the current workspace
2001+ accountManager . disableCurrentWorkspace ( account ) ;
19762002
1977- // Show a toast notification
1978- await showToast (
1979- `Workspace disabled for account ${ account . index + 1 } . Switching to another account.` ,
1980- "warning" ,
1981- { duration : toastDurationMs } ,
1982- ) ;
2003+ // Try to rotate to next enabled workspace
2004+ const nextWorkspace = accountManager . rotateToNextWorkspace ( account ) ;
19832005
1984- // Forget session affinity and rotate to next account
1985- sessionAffinityStore ?. forgetSession ( sessionAffinityKey ) ;
1986- break ;
2006+ if ( nextWorkspace ) {
2007+ // Found another enabled workspace, retry with it
2008+ accountManager . saveToDiskDebounced ( ) ;
2009+
2010+ const newWorkspaceName = nextWorkspace . name ?? nextWorkspace . id ;
2011+ await showToast (
2012+ `Workspace ${ workspaceName } disabled. Switched to ${ newWorkspaceName } .` ,
2013+ "warning" ,
2014+ { duration : toastDurationMs } ,
2015+ ) ;
2016+
2017+ logInfo ( `Rotated to workspace ${ newWorkspaceName } for account ${ account . index + 1 } ` ) ;
2018+
2019+ // Retry the request with the new workspace (continue the loop)
2020+ continue ;
2021+ } else {
2022+ // No more enabled workspaces, disable the entire account
2023+ logWarn ( `All workspaces disabled for account ${ account . index + 1 } . Disabling account.` ) ;
2024+
2025+ accountManager . setAccountEnabled ( account . index , false ) ;
2026+ accountManager . saveToDiskDebounced ( ) ;
2027+
2028+ await showToast (
2029+ `All workspaces disabled for account ${ account . index + 1 } . Switching to another account.` ,
2030+ "warning" ,
2031+ { duration : toastDurationMs } ,
2032+ ) ;
2033+
2034+ // Forget session affinity and rotate to next account
2035+ sessionAffinityStore ?. forgetSession ( sessionAffinityKey ) ;
2036+ break ;
2037+ }
19872038 }
19882039 }
19892040
0 commit comments