@@ -117,6 +117,7 @@ import {
117117 createTimestampedBackupPath ,
118118 loadFlaggedAccounts ,
119119 saveFlaggedAccounts ,
120+ withFlaggedAccountStorageTransaction ,
120121 clearFlaggedAccounts ,
121122 StorageError ,
122123 formatStorageErrorHint ,
@@ -128,6 +129,7 @@ import {
128129 extractRequestUrl ,
129130 handleErrorResponse ,
130131 handleSuccessResponse ,
132+ isDeactivatedWorkspaceError ,
131133 getUnsupportedCodexModelInfo ,
132134 resolveUnsupportedCodexFallbackModel ,
133135 refreshAndUpdateToken ,
@@ -183,6 +185,49 @@ import {
183185 getRecoveryToastContent ,
184186} from "./lib/recovery.js" ;
185187
188+ function getWorkspaceIdentityKey ( account : {
189+ organizationId ?: string ;
190+ accountId ?: string ;
191+ refreshToken : string ;
192+ } ) : string {
193+ const organizationId = account . organizationId ?. trim ( ) ;
194+ const accountId = account . accountId ?. trim ( ) ;
195+ const refreshToken = account . refreshToken . trim ( ) ;
196+ if ( organizationId ) {
197+ return accountId
198+ ? `organizationId:${ organizationId } |accountId:${ accountId } `
199+ : `organizationId:${ organizationId } ` ;
200+ }
201+ if ( accountId ) return `accountId:${ accountId } ` ;
202+ return `refreshToken:${ refreshToken } ` ;
203+ }
204+
205+ function matchesWorkspaceIdentity (
206+ account : {
207+ organizationId ?: string ;
208+ accountId ?: string ;
209+ refreshToken : string ;
210+ } ,
211+ identityKey : string ,
212+ ) : boolean {
213+ return getWorkspaceIdentityKey ( account ) === identityKey ;
214+ }
215+
216+ function upsertFlaggedAccountRecord (
217+ accounts : FlaggedAccountMetadataV1 [ ] ,
218+ record : FlaggedAccountMetadataV1 ,
219+ ) : void {
220+ const identityKey = getWorkspaceIdentityKey ( record ) ;
221+ const existingIndex = accounts . findIndex ( ( flagged ) =>
222+ matchesWorkspaceIdentity ( flagged , identityKey ) ,
223+ ) ;
224+ if ( existingIndex >= 0 ) {
225+ accounts [ existingIndex ] = record ;
226+ return ;
227+ }
228+ accounts . push ( record ) ;
229+ }
230+
186231/**
187232 * OpenAI Codex OAuth authentication plugin for opencode
188233 *
@@ -2358,6 +2403,59 @@ while (attempted.size < Math.max(1, accountCount)) {
23582403 threadId : threadIdCandidate ,
23592404 } ) ;
23602405
2406+ const workspaceDeactivated = isDeactivatedWorkspaceError ( errorBody , response . status ) ;
2407+ if ( workspaceDeactivated ) {
2408+ const accountLabel = formatAccountLabel ( account , account . index ) ;
2409+ accountManager . refundToken ( account , modelFamily , model ) ;
2410+ accountManager . recordFailure ( account , modelFamily , model ) ;
2411+ account . lastSwitchReason = "rotation" ;
2412+ runtimeMetrics . failedRequests ++ ;
2413+ runtimeMetrics . accountRotations ++ ;
2414+ runtimeMetrics . lastError = `Deactivated workspace on ${ accountLabel } ` ;
2415+ runtimeMetrics . lastErrorCategory = "workspace-deactivated" ;
2416+
2417+ try {
2418+ const flaggedRecord : FlaggedAccountMetadataV1 = {
2419+ ...account ,
2420+ flaggedAt : Date . now ( ) ,
2421+ flaggedReason : "workspace-deactivated" ,
2422+ lastError : "deactivated_workspace" ,
2423+ } ;
2424+ await withFlaggedAccountStorageTransaction ( async ( current , persist ) => {
2425+ const nextStorage : typeof current = {
2426+ ...current ,
2427+ accounts : current . accounts . map ( ( flagged ) => ( { ...flagged } ) ) ,
2428+ } ;
2429+ upsertFlaggedAccountRecord ( nextStorage . accounts , flaggedRecord ) ;
2430+ await persist ( nextStorage ) ;
2431+ } ) ;
2432+ } catch ( flagError ) {
2433+ logWarn (
2434+ `Failed to persist deactivated workspace flag for ${ accountLabel } : ${ flagError instanceof Error ? flagError . message : String ( flagError ) } ` ,
2435+ ) ;
2436+ }
2437+
2438+ if ( accountManager . removeAccount ( account ) ) {
2439+ accountManager . saveToDiskDebounced ( ) ;
2440+ attempted . clear ( ) ;
2441+ accountCount = accountManager . getAccountCount ( ) ;
2442+ await showToast (
2443+ `Workspace deactivated. Removed ${ accountLabel } from rotation and switching accounts.` ,
2444+ "warning" ,
2445+ { duration : toastDurationMs } ,
2446+ ) ;
2447+ break ;
2448+ }
2449+
2450+ accountManager . markAccountCoolingDown (
2451+ account ,
2452+ ACCOUNT_LIMITS . AUTH_FAILURE_COOLDOWN_MS ,
2453+ "auth-failure" ,
2454+ ) ;
2455+ accountManager . saveToDiskDebounced ( ) ;
2456+ break ;
2457+ }
2458+
23612459 const unsupportedModelInfo = getUnsupportedCodexModelInfo ( errorBody ) ;
23622460 const hasRemainingAccounts = attempted . size < Math . max ( 1 , accountCount ) ;
23632461
@@ -2964,12 +3062,18 @@ while (attempted.size < Math.max(1, accountCount)) {
29643062 ( typeof ( errorBody as { error ?: { message ?: unknown } } ) ?. error ?. message === "string"
29653063 ? ( errorBody as { error ?: { message ?: string } } ) . error ?. message
29663064 : bodyText ) || `HTTP ${ response . status } ` ;
3065+ if ( isDeactivatedWorkspaceError ( errorBody , response . status ) ) {
3066+ throw new Error ( "deactivated_workspace" ) ;
3067+ }
29673068 throw new Error ( message ) ;
29683069 }
29693070
29703071 lastError = new Error ( "Codex response did not include quota headers" ) ;
29713072 } catch ( error ) {
29723073 lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
3074+ if ( lastError . message === "deactivated_workspace" ) {
3075+ throw lastError ;
3076+ }
29733077 }
29743078 }
29753079
@@ -2993,9 +3097,9 @@ while (attempted.size < Math.max(1, accountCount)) {
29933097 return ;
29943098 }
29953099
2996- const flaggedStorage = await loadFlaggedAccounts ( ) ;
29973100 let storageChanged = false ;
29983101 let flaggedChanged = false ;
3102+ const flaggedUpdates = new Map < string , FlaggedAccountMetadataV1 > ( ) ;
29993103 const removeFromActive = new Set < string > ( ) ;
30003104 const total = workingStorage . accounts . length ;
30013105 let ok = 0 ;
@@ -3101,21 +3205,17 @@ while (attempted.size < Math.max(1, accountCount)) {
31013205 refreshResult . message ?? refreshResult . reason ?? "refresh failed" ;
31023206 console . log ( `[${ i + 1 } /${ total } ] ${ label } : ERROR (${ message } )` ) ;
31033207 if ( deepProbe && isFlaggableFailure ( refreshResult ) ) {
3104- const existingIndex = flaggedStorage . accounts . findIndex (
3105- ( flagged ) => flagged . refreshToken === account . refreshToken ,
3106- ) ;
31073208 const flaggedRecord : FlaggedAccountMetadataV1 = {
31083209 ...account ,
31093210 flaggedAt : Date . now ( ) ,
31103211 flaggedReason : "token-invalid" ,
31113212 lastError : message ,
31123213 } ;
3113- if ( existingIndex >= 0 ) {
3114- flaggedStorage . accounts [ existingIndex ] = flaggedRecord ;
3115- } else {
3116- flaggedStorage . accounts . push ( flaggedRecord ) ;
3117- }
3118- removeFromActive . add ( account . refreshToken ) ;
3214+ flaggedUpdates . set (
3215+ getWorkspaceIdentityKey ( flaggedRecord ) ,
3216+ flaggedRecord ,
3217+ ) ;
3218+ removeFromActive . add ( getWorkspaceIdentityKey ( account ) ) ;
31193219 flaggedChanged = true ;
31203220 }
31213221 continue ;
@@ -3197,6 +3297,20 @@ while (attempted.size < Math.max(1, accountCount)) {
31973297 } catch ( error ) {
31983298 errors += 1 ;
31993299 const message = error instanceof Error ? error . message : String ( error ) ;
3300+ if ( message === "deactivated_workspace" ) {
3301+ const flaggedRecord : FlaggedAccountMetadataV1 = {
3302+ ...account ,
3303+ flaggedAt : Date . now ( ) ,
3304+ flaggedReason : "workspace-deactivated" ,
3305+ lastError : message ,
3306+ } ;
3307+ flaggedUpdates . set (
3308+ getWorkspaceIdentityKey ( flaggedRecord ) ,
3309+ flaggedRecord ,
3310+ ) ;
3311+ removeFromActive . add ( getWorkspaceIdentityKey ( account ) ) ;
3312+ flaggedChanged = true ;
3313+ }
32003314 console . log (
32013315 `[${ i + 1 } /${ total } ] ${ label } : ERROR (${ message . slice ( 0 , 160 ) } )` ,
32023316 ) ;
@@ -3210,7 +3324,7 @@ while (attempted.size < Math.max(1, accountCount)) {
32103324
32113325 if ( removeFromActive . size > 0 ) {
32123326 workingStorage . accounts = workingStorage . accounts . filter (
3213- ( account ) => ! removeFromActive . has ( account . refreshToken ) ,
3327+ ( account ) => ! removeFromActive . has ( getWorkspaceIdentityKey ( account ) ) ,
32143328 ) ;
32153329 clampActiveIndices ( workingStorage ) ;
32163330 storageChanged = true ;
@@ -3221,14 +3335,23 @@ while (attempted.size < Math.max(1, accountCount)) {
32213335 invalidateAccountManagerCache ( ) ;
32223336 }
32233337 if ( flaggedChanged ) {
3224- await saveFlaggedAccounts ( flaggedStorage ) ;
3338+ await withFlaggedAccountStorageTransaction ( async ( current , persist ) => {
3339+ const nextStorage : typeof current = {
3340+ ...current ,
3341+ accounts : current . accounts . map ( ( flagged ) => ( { ...flagged } ) ) ,
3342+ } ;
3343+ for ( const flaggedRecord of flaggedUpdates . values ( ) ) {
3344+ upsertFlaggedAccountRecord ( nextStorage . accounts , flaggedRecord ) ;
3345+ }
3346+ await persist ( nextStorage ) ;
3347+ } ) ;
32253348 }
32263349
32273350 console . log ( "" ) ;
32283351 console . log ( `Results: ${ ok } ok, ${ errors } error, ${ disabled } disabled` ) ;
32293352 if ( removeFromActive . size > 0 ) {
32303353 console . log (
3231- `Moved ${ removeFromActive . size } account(s) to flagged pool (invalid refresh token) .` ,
3354+ `Moved ${ removeFromActive . size } account(s) to flagged pool.` ,
32323355 ) ;
32333356 }
32343357 console . log ( "" ) ;
@@ -3249,6 +3372,13 @@ while (attempted.size < Math.max(1, accountCount)) {
32493372 const flagged = flaggedStorage . accounts [ i ] ;
32503373 if ( ! flagged ) continue ;
32513374 const label = flagged . email ?? flagged . accountLabel ?? `Flagged ${ i + 1 } ` ;
3375+ if ( flagged . flaggedReason === "workspace-deactivated" ) {
3376+ console . log (
3377+ `[${ i + 1 } /${ flaggedStorage . accounts . length } ] ${ label } : STILL FLAGGED (workspace deactivated)` ,
3378+ ) ;
3379+ remaining . push ( flagged ) ;
3380+ continue ;
3381+ }
32523382 try {
32533383 const cached = await lookupCodexCliTokensByEmail ( flagged . email ) ;
32543384 const now = Date . now ( ) ;
@@ -3425,7 +3555,11 @@ while (attempted.size < Math.max(1, accountCount)) {
34253555 await saveFlaggedAccounts ( {
34263556 version : 1 ,
34273557 accounts : flaggedStorage . accounts . filter (
3428- ( flagged ) => flagged . refreshToken !== target . refreshToken ,
3558+ ( flagged ) =>
3559+ ! matchesWorkspaceIdentity (
3560+ flagged ,
3561+ getWorkspaceIdentityKey ( target ) ,
3562+ ) ,
34293563 ) ,
34303564 } ) ;
34313565 invalidateAccountManagerCache ( ) ;
0 commit comments