Skip to content

Commit 29c8f7d

Browse files
committed
fix: address remaining PR 387 docs and cleanup comments
1 parent 971bcea commit 29c8f7d

5 files changed

Lines changed: 131 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
163163
- active requests use a bounded outbound request budget so one prompt cannot walk the full pool indefinitely
164164
- repeated cross-account 5xx bursts trigger a short cooldown instead of continuing aggressive rotation
165165
- proactive refresh is staggered to reduce background refresh bursts
166-
- `codex auth status` and `codex auth report --json` surface recent runtime request metrics and active cooldown windows
166+
- `codex auth status` surfaces recent runtime request metrics in text output, and `codex auth report --json` exposes the machine-readable cooldown/runtime snapshot
167167

168168
---
169169

@@ -277,6 +277,7 @@ codex auth login
277277
- Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
278278
- Requests fail fast with a pool cooldown message: wait for the cooldown window or inspect `codex auth status`
279279
- Requests fail fast after repeated upstream 5xx errors: inspect `codex auth report --json` for runtime traffic and cooldown details
280+
- Storage cleanup fails with `EBUSY` / `EPERM` (Windows): run `codex auth doctor --fix` to retry, or manually remove `~/.codex/multi-auth/<project-key>/` and re-login
280281
- OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
281282
- Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
282283
- `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account

lib/storage/account-clear.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { promises as fs } from "node:fs";
22

3+
function isRetryableFsError(error: unknown): boolean {
4+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
5+
return code === "EBUSY" || code === "EPERM";
6+
}
7+
8+
async function sleep(ms: number): Promise<void> {
9+
await new Promise((resolve) => setTimeout(resolve, ms));
10+
}
11+
312
export async function clearAccountStorageArtifacts(params: {
413
path: string;
514
resetMarkerPath: string;
@@ -12,7 +21,21 @@ export async function clearAccountStorageArtifacts(params: {
1221
required: boolean,
1322
): Promise<void> => {
1423
try {
15-
await fs.unlink(targetPath);
24+
for (let attempt = 0; attempt < 5; attempt += 1) {
25+
try {
26+
await fs.unlink(targetPath);
27+
return;
28+
} catch (error) {
29+
const code = (error as NodeJS.ErrnoException).code;
30+
if (code === "ENOENT") {
31+
return;
32+
}
33+
if (!isRetryableFsError(error) || attempt >= 4) {
34+
throw error;
35+
}
36+
await sleep(10 * 2 ** attempt);
37+
}
38+
}
1639
} catch (error) {
1740
const code = (error as NodeJS.ErrnoException).code;
1841
if (code === "ENOENT") {

test/account-clear.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { describe, expect, it, vi } from "vitest";
1+
import { promises as fs } from "node:fs";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
23
import { clearAccountStorageArtifacts } from "../lib/storage/account-clear.js";
34

45
describe("account clear helper", () => {
6+
afterEach(() => {
7+
vi.restoreAllMocks();
8+
vi.useRealTimers();
9+
});
10+
511
it("clears primary, wal, and backups after writing marker", async () => {
612
await expect(
713
clearAccountStorageArtifacts({
@@ -13,4 +19,34 @@ describe("account clear helper", () => {
1319
}),
1420
).resolves.toBeUndefined();
1521
});
22+
23+
it.each(["EBUSY", "EPERM"] as const)(
24+
"retries transient %s errors when clearing required artifacts",
25+
async (code) => {
26+
vi.useFakeTimers();
27+
const unlinkSpy = vi.spyOn(fs, "unlink");
28+
let attempts = 0;
29+
unlinkSpy.mockImplementation(async (targetPath) => {
30+
if (String(targetPath).endsWith("tmp-accounts.json") && attempts < 2) {
31+
attempts += 1;
32+
const error = new Error(code) as NodeJS.ErrnoException;
33+
error.code = code;
34+
throw error;
35+
}
36+
return undefined as never;
37+
});
38+
39+
const clearPromise = clearAccountStorageArtifacts({
40+
path: `${process.cwd()}/tmp-accounts.json`,
41+
resetMarkerPath: `${process.cwd()}/tmp-accounts.marker`,
42+
walPath: `${process.cwd()}/tmp-accounts.wal`,
43+
backupPaths: [],
44+
logError: vi.fn(),
45+
});
46+
47+
await vi.runAllTimersAsync();
48+
await expect(clearPromise).resolves.toBeUndefined();
49+
expect(unlinkSpy).toHaveBeenCalled();
50+
},
51+
);
1652
});

test/codex-manager-report-command.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function createDeps(
5858
hasResetMarker: false,
5959
hasWal: false,
6060
})),
61+
loadRuntimeObservabilitySnapshot: vi.fn(async () => null),
6162
normalizeFailureDetail: vi.fn((message) => message ?? "unknown"),
6263
logInfo: vi.fn(),
6364
logError: vi.fn(),
@@ -140,6 +141,67 @@ describe("runReportCommand", () => {
140141
);
141142
});
142143

144+
it("includes runtime observability fields in json output when snapshot is available", async () => {
145+
const deps = createDeps({
146+
loadRuntimeObservabilitySnapshot: vi.fn(async () => ({
147+
version: 1,
148+
updatedAt: 2000,
149+
responsesRequests: 4,
150+
authRefreshRequests: 2,
151+
diagnosticProbeRequests: 1,
152+
currentRequestId: "req_123",
153+
poolExhaustionCooldownUntil: 9000,
154+
serverBurstCooldownUntil: 12000,
155+
runtimeMetrics: {
156+
startedAt: 1000,
157+
totalRequests: 4,
158+
successfulRequests: 3,
159+
failedRequests: 1,
160+
responsesRequests: 4,
161+
authRefreshRequests: 2,
162+
diagnosticProbeRequests: 1,
163+
outboundRequestAttemptBudget: 6,
164+
outboundRequestAttemptsConsumed: 5,
165+
requestAttemptBudgetExhaustions: 0,
166+
poolExhaustionFastFails: 1,
167+
serverBurstFastFails: 0,
168+
rateLimitedResponses: 1,
169+
serverErrors: 0,
170+
networkErrors: 0,
171+
userAborts: 0,
172+
authRefreshFailures: 0,
173+
emptyResponseRetries: 0,
174+
accountRotations: 1,
175+
sameAccountRetries: 0,
176+
streamFailoverAttempts: 0,
177+
streamFailoverCandidatesConsidered: 0,
178+
lastStreamFailoverCandidateCount: 0,
179+
streamFailoverRecoveries: 0,
180+
streamFailoverCrossAccountRecoveries: 0,
181+
cumulativeLatencyMs: 42,
182+
lastRequestAt: 1999,
183+
lastError: null,
184+
},
185+
})),
186+
});
187+
188+
const result = await runReportCommand(["--json"], deps);
189+
190+
expect(result).toBe(0);
191+
const jsonOutput = JSON.parse(
192+
(deps.logInfo as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0] ?? "{}",
193+
) as {
194+
runtime: {
195+
poolExhaustionCooldownUntil: number;
196+
serverBurstCooldownUntil: number;
197+
runtimeMetrics: Record<string, unknown>;
198+
};
199+
};
200+
expect(jsonOutput.runtime.poolExhaustionCooldownUntil).toBe(9000);
201+
expect(jsonOutput.runtime.serverBurstCooldownUntil).toBe(12000);
202+
expect(jsonOutput.runtime.runtimeMetrics).toBeDefined();
203+
});
204+
143205
it("respects live probe account and probe budgets", async () => {
144206
const deps = createDeps({
145207
loadAccounts: vi.fn(async () =>
@@ -285,7 +347,7 @@ describe("runReportCommand", () => {
285347
);
286348
expect(jsonOutput.forecast.accounts[3]?.liveQuota?.planType).toBe("pro");
287349
expect(jsonOutput.forecast.recommendation.selectedReason).toEqual(
288-
expect.any(String),
350+
"Lowest risk ready account (low, score 0).",
289351
);
290352
});
291353

@@ -497,6 +559,7 @@ describe("runReportCommand", () => {
497559

498560
expect(result).toBe(0);
499561
expect(deps.fetchCodexQuotaSnapshot).not.toHaveBeenCalled();
562+
expect(deps.saveAccounts).toHaveBeenCalledTimes(4);
500563
const jsonOutput = JSON.parse(
501564
(deps.logInfo as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0] ?? "{}",
502565
) as {

test/config-explain.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ describe("getPluginConfigExplainReport", () => {
3939
delete process.env.CODEX_MODE;
4040
delete process.env.CODEX_AUTH_FAST_SESSION_STRATEGY;
4141
delete process.env.CODEX_MULTI_AUTH_CONFIG_PATH;
42+
delete process.env.CODEX_AUTH_RATE_LIMIT_DEDUP_WINDOW_MS;
43+
delete process.env.CODEX_AUTH_RATE_LIMIT_STATE_RESET_MS;
44+
delete process.env.CODEX_AUTH_RATE_LIMIT_MAX_BACKOFF_MS;
45+
delete process.env.CODEX_AUTH_RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS;
4246
for (const configPath of tempConfigPaths) {
4347
await removeWithRetry(configPath, { force: true }).catch(() => {});
4448
}

0 commit comments

Comments
 (0)