Skip to content

Commit 115979c

Browse files
committed
fix-refresh-guardian-commit-failures
1 parent 412b379 commit 115979c

3 files changed

Lines changed: 61 additions & 5 deletions

File tree

lib/accounts.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ export class AccountManager {
779779
account.refreshToken = auth.refresh;
780780
account.access = auth.access;
781781
account.expires = auth.expires;
782-
const tokenAccountId = extractAccountId(auth.access);
782+
const tokenAccountId = extractAccountId(auth.access)?.trim() || undefined;
783783
if (
784784
tokenAccountId &&
785785
(shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId))
@@ -836,6 +836,8 @@ export class AccountManager {
836836
const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
837837
try {
838838
return await withAccountStorageTransaction(async (_current, persist) => {
839+
// Snapshot the live in-memory pool under the storage lock so refresh
840+
// persistence merges against the latest account state.
839841
const nextStorage = structuredClone(
840842
this.buildStorageSnapshot(),
841843
) as AccountStorageV3;

lib/refresh-guardian.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from "./logger.js";
22
import { refreshExpiringAccounts } from "./proactive-refresh.js";
33
import type { AccountManager } from "./accounts.js";
4+
import { CodexAuthError } from "./errors.js";
45
import type { CooldownReason } from "./storage.js";
56
import type { TokenResult } from "./types.js";
67

@@ -148,12 +149,19 @@ export class RefreshGuardian {
148149
sourceIndex: sourceAccount.index,
149150
error: error instanceof Error ? error.message : String(error),
150151
});
151-
const account = manager.getAccountByIdentity(sourceAccount, refreshedAuth);
152+
const account =
153+
manager.getAccountByIdentity(sourceAccount, refreshedAuth) ??
154+
manager.getAccountByIdentity(sourceAccount);
155+
const cooldownReason: CooldownReason =
156+
error instanceof CodexAuthError && !error.retryable
157+
? "auth-failure"
158+
: "network-error";
152159
if (account) {
153-
manager.markAccountCoolingDown(account, this.bufferMs, "network-error");
160+
manager.markAccountCoolingDown(account, this.bufferMs, cooldownReason);
154161
}
155162
this.stats.failed += 1;
156-
this.stats.networkFailed += 1;
163+
if (cooldownReason === "auth-failure") this.stats.authFailed += 1;
164+
else this.stats.networkFailed += 1;
157165
return !!account;
158166
}
159167
this.stats.refreshed += 1;

test/refresh-guardian.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
2-
import type { AccountManager, ManagedAccount, OAuthAuthDetails } from "../lib/accounts.js";
2+
import type { AccountManager, ManagedAccount } from "../lib/accounts.js";
33
import {
44
extractAccountEmail,
55
extractAccountId,
66
sanitizeEmail,
77
} from "../lib/auth/token-utils.js";
8+
import { CodexAuthError } from "../lib/errors.js";
89
import { findMatchingAccountIndex } from "../lib/storage.js";
10+
import type { OAuthAuthDetails } from "../lib/types.js";
911

1012
const refreshExpiringAccountsMock = vi.fn();
1113
const applyRefreshResultMock = vi.fn();
@@ -872,4 +874,48 @@ describe("refresh-guardian", () => {
872874
expect(stats.networkFailed).toBe(1);
873875
expect(stats.rateLimited).toBe(1);
874876
});
877+
878+
it("treats non-retryable commit failures as auth cooldowns", async () => {
879+
const accountA = createManagedAccount(0);
880+
const manager = createManagerMock([accountA]);
881+
const commitRefreshedAuthMock = manager
882+
.commitRefreshedAuth as ReturnType<typeof vi.fn>;
883+
commitRefreshedAuthMock.mockRejectedValueOnce(
884+
new CodexAuthError("refresh persistence failed", { retryable: false }),
885+
);
886+
887+
const { RefreshGuardian } = await import("../lib/refresh-guardian.js");
888+
const guardian = new RefreshGuardian(() => manager, {
889+
bufferMs: 60_000,
890+
intervalMs: 5_000,
891+
});
892+
893+
refreshExpiringAccountsMock.mockResolvedValue(
894+
new Map([
895+
[
896+
0,
897+
{
898+
refreshed: true,
899+
reason: "success",
900+
tokenResult: {
901+
type: "success",
902+
access: "access-0",
903+
refresh: "refresh-0-new",
904+
expires: Date.now() + 3_600_000,
905+
},
906+
},
907+
],
908+
]),
909+
);
910+
911+
await guardian.tick();
912+
913+
expect(
914+
manager.markAccountCoolingDown as ReturnType<typeof vi.fn>,
915+
).toHaveBeenCalledWith(accountA, 60_000, "auth-failure");
916+
const stats = guardian.getStats();
917+
expect(stats.failed).toBe(1);
918+
expect(stats.authFailed).toBe(1);
919+
expect(stats.networkFailed).toBe(0);
920+
});
875921
});

0 commit comments

Comments
 (0)