Skip to content

Commit 05369db

Browse files
committed
Tighten workspace-disabled matching
1 parent 8165b93 commit 05369db

5 files changed

Lines changed: 35 additions & 11 deletions

File tree

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,7 @@ accountAttemptLoop: while (attempted.size < Math.max(1, accountCount)) {
19951995
if (hasRemainingAccounts) {
19961996
continue accountAttemptLoop;
19971997
}
1998+
return errorResponse;
19981999
} else {
19992000
const currentWorkspace = accountManager.getCurrentWorkspace(account);
20002001
const workspaceName = currentWorkspace?.name ?? currentWorkspace?.id ?? "unknown";

lib/accounts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,8 +899,8 @@ export class AccountManager {
899899
const currentIdx = account.currentWorkspaceIndex ?? 0;
900900
const totalWorkspaces = account.workspaces.length;
901901

902-
// Find next enabled workspace
903-
for (let i = 1; i <= totalWorkspaces; i++) {
902+
// Search successor workspaces only; the current slot was just evaluated.
903+
for (let i = 1; i < totalWorkspaces; i++) {
904904
const nextIdx = (currentIdx + i) % totalWorkspaces;
905905
const workspace = account.workspaces[nextIdx];
906906
if (workspace && workspace.enabled !== false) {

lib/request/fetch-helpers.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,14 @@ export function isWorkspaceDisabledError(
277277
const normalizedCode = typeof code === "string" ? code.trim().toLowerCase() : "";
278278
const haystack = `${normalizedCode} ${bodyText}`.toLowerCase();
279279

280-
const disabledPatterns = [
281-
/workspace.*(?:disabled|expired|deactivated|terminated)/i,
282-
/account.*(?:disabled|expired|deactivated|terminated)/i,
283-
/(?:workspace|account).*no longer.*(?:active|available|valid)/i,
284-
/(?:workspace|account).*has been.*(?:disabled|expired|closed)/i,
285-
/workspace.*(?:access|subscription).*expired/i,
286-
/organization.*(?:disabled|expired|inactive)/i,
287-
/team.*(?:disabled|expired|inactive)/i,
288-
];
280+
const disabledPatterns = [
281+
/workspace.*(?:disabled|expired|deactivated|terminated)/i,
282+
/account\s+(?:has\s+been|is)\s+(?:disabled|expired|deactivated|terminated|closed)/i,
283+
/(?:workspace|org(?:anization)?).*no longer.*(?:active|available|valid)/i,
284+
/(?:workspace|org(?:anization)?).*has been.*(?:disabled|expired|closed)/i,
285+
/workspace.*(?:access|subscription).*expired/i,
286+
/org(?:anization)?.*(?:disabled|expired|inactive)/i,
287+
];
289288

290289
for (const pattern of disabledPatterns) {
291290
if (pattern.test(haystack)) {

test/fetch-helpers.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,9 @@ describe('isWorkspaceDisabledError', () => {
376376
expect(isWorkspaceDisabledError(403, 'payment_required', '')).toBe(false);
377377
expect(isWorkspaceDisabledError(403, '', 'Payment required to continue')).toBe(false);
378378
expect(isWorkspaceDisabledError(403, '', 'Billing failed for your plan')).toBe(false);
379+
expect(isWorkspaceDisabledError(403, '', 'Your billing account has expired')).toBe(false);
380+
expect(isWorkspaceDisabledError(403, '', 'service account terminated')).toBe(false);
381+
expect(isWorkspaceDisabledError(403, '', 'team plan inactive')).toBe(false);
379382
});
380383
});
381384

test/index-retry.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const accountManagerState = vi.hoisted(() => ({
1111
setAccountEnabledCalls: [] as Array<{ index: number; enabled: boolean }>,
1212
}));
1313

14+
const recoveryState = vi.hoisted(() => ({
15+
forceRecoverable: false,
16+
isRecoverableErrorCalls: 0,
17+
}));
18+
1419
function createMockAccount(
1520
overrides: Partial<Record<string, unknown>> = {},
1621
): Record<string, unknown> {
@@ -272,6 +277,19 @@ vi.mock("../lib/storage.js", () => ({
272277
importAccounts: async () => ({ imported: 0, total: 0 }),
273278
}));
274279

280+
vi.mock("../lib/recovery.js", () => ({
281+
createSessionRecoveryHook: () => null,
282+
isRecoverableError: () => {
283+
recoveryState.isRecoverableErrorCalls += 1;
284+
return recoveryState.forceRecoverable;
285+
},
286+
detectErrorType: () => "tool_use_failed",
287+
getRecoveryToastContent: () => ({
288+
title: "Recoverable error",
289+
message: "retry",
290+
}),
291+
}));
292+
275293
vi.mock("../lib/auto-update-checker.js", () => ({
276294
checkAndNotify: async () => {},
277295
checkForUpdates: async () => ({ hasUpdate: false, currentVersion: "4.5.0", latestVersion: null, updateCommand: "" }),
@@ -293,6 +311,8 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
293311

294312
beforeEach(() => {
295313
resetAccountManagerState();
314+
recoveryState.forceRecoverable = false;
315+
recoveryState.isRecoverableErrorCalls = 0;
296316
vi.resetModules();
297317

298318
for (const key of envKeys) originalEnv[key] = process.env[key];
@@ -532,6 +552,7 @@ describe("OpenAIAuthPlugin rate-limit retry", () => {
532552
expect(accountManagerState.disableCurrentWorkspaceCalls).toBe(0);
533553
expect(accountManagerState.rotateToNextWorkspaceCalls).toBe(0);
534554
expect(accountManagerState.setAccountEnabledCalls).toEqual([]);
555+
expect(recoveryState.isRecoverableErrorCalls).toBe(0);
535556
});
536557

537558
it("retries with a fallback account after a workspace-less account gets a workspace-disabled response", async () => {

0 commit comments

Comments
 (0)