diff --git a/README.md b/README.md index c602e199..78abe177 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,7 @@ codex-multi-auth doctor --json ## Release Notes -- Current prerelease: [docs/releases/v2.3.0-beta.0.md](docs/releases/v2.3.0-beta.0.md) — install via `npm i -g codex-multi-auth@beta` +- Current prerelease: [docs/releases/v2.3.0-beta.1.md](docs/releases/v2.3.0-beta.1.md) — install via `npm i -g codex-multi-auth@beta` - Current stable: [docs/releases/v2.2.2.md](docs/releases/v2.2.2.md) — install via `npm i -g codex-multi-auth` - Previous stable: [docs/releases/v2.2.1.md](docs/releases/v2.2.1.md) - Previous stable: [docs/releases/v2.2.0.md](docs/releases/v2.2.0.md) diff --git a/lib/recovery/storage.ts b/lib/recovery/storage.ts index 4e04a3f4..68ff20f7 100644 --- a/lib/recovery/storage.ts +++ b/lib/recovery/storage.ts @@ -672,7 +672,7 @@ export function findEmptyMessageByIndex( if (idx < 0 || idx >= messages.length) continue; const targetMsg = messages[idx]; - if (!targetMsg) continue; + if (!targetMsg || targetMsg.role !== "assistant") continue; if (!messageHasContent(targetMsg.id)) { return targetMsg.id; diff --git a/lib/rotation.ts b/lib/rotation.ts index ca41b075..50aae032 100644 --- a/lib/rotation.ts +++ b/lib/rotation.ts @@ -385,7 +385,7 @@ export interface HybridSelectionConfig { healthWeight: number; /** Weight for token count (default: 5) */ tokenWeight: number; - /** Weight for freshness/last used (default: 0.1) */ + /** Weight for freshness/last used (default: 2) */ freshnessWeight: number; } diff --git a/lib/storage/flagged-storage.ts b/lib/storage/flagged-storage.ts index f5e55c26..ccdb128f 100644 --- a/lib/storage/flagged-storage.ts +++ b/lib/storage/flagged-storage.ts @@ -46,12 +46,14 @@ export function normalizeFlaggedStorage( value === "initial" || value === "rotation" || value === "best" || - value === "restore"; + value === "restore" || + value === "manual"; const isCooldownReason = ( value: unknown, ): value is AccountMetadataV3["cooldownReason"] => value === "auth-failure" || value === "network-error" || + value === "server-error" || value === "rate-limit"; let rateLimitResetTimes: diff --git a/test/flagged-storage.test.ts b/test/flagged-storage.test.ts index 51fa4262..73458cf0 100644 --- a/test/flagged-storage.test.ts +++ b/test/flagged-storage.test.ts @@ -28,4 +28,40 @@ describe("flagged storage helper", () => { expect(result.accounts[0]?.refreshToken).toBe("token-1"); expect(result.accounts[0]?.lastError).toBe("oops"); }); + + it("preserves the 'manual' last switch reason on round-trip", () => { + const result = normalizeFlaggedStorage( + { + version: 1, + accounts: [ + { + refreshToken: "token-1", + flaggedAt: 10, + lastSwitchReason: "manual", + }, + ], + }, + { isRecord, now: () => 99 }, + ); + + expect(result.accounts[0]?.lastSwitchReason).toBe("manual"); + }); + + it("preserves the 'server-error' cooldown reason on round-trip", () => { + const result = normalizeFlaggedStorage( + { + version: 1, + accounts: [ + { + refreshToken: "token-1", + flaggedAt: 10, + cooldownReason: "server-error", + }, + ], + }, + { isRecord, now: () => 99 }, + ); + + expect(result.accounts[0]?.cooldownReason).toBe("server-error"); + }); }); diff --git a/test/fs-retry.test.ts b/test/fs-retry.test.ts new file mode 100644 index 00000000..7c089378 --- /dev/null +++ b/test/fs-retry.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { + FILE_RETRY_CODES, + shouldRetryFileOperation, +} from "../lib/fs-retry.js"; + +function errnoError(code: string): NodeJS.ErrnoException { + const error = new Error(code) as NodeJS.ErrnoException; + error.code = code; + return error; +} + +describe("shouldRetryFileOperation", () => { + it("treats every transient lock code as retryable", () => { + for (const code of ["EBUSY", "EPERM", "EAGAIN", "ENOTEMPTY", "EACCES"]) { + expect(shouldRetryFileOperation(errnoError(code)), code).toBe(true); + expect(FILE_RETRY_CODES.has(code), code).toBe(true); + } + }); + + it("does not retry non-transient errors", () => { + for (const code of ["ENOENT", "EISDIR", "EINVAL", "EROFS"]) { + expect(shouldRetryFileOperation(errnoError(code)), code).toBe(false); + } + }); + + it("returns false for non-errors and errors without a code", () => { + expect(shouldRetryFileOperation(undefined)).toBe(false); + expect(shouldRetryFileOperation(null)).toBe(false); + expect(shouldRetryFileOperation("EACCES")).toBe(false); + expect(shouldRetryFileOperation(new Error("no code"))).toBe(false); + }); +});