Skip to content

Commit dd51373

Browse files
committed
fix: harden runtime rotation release audit
1 parent 5292fba commit dd51373

12 files changed

Lines changed: 146 additions & 24 deletions

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ Public documentation for the `codex-multi-auth` Codex CLI multi-account OAuth ma
9090
| [reference/storage-paths.md](reference/storage-paths.md) | Canonical and compatibility storage paths |
9191
| [reference/public-api.md](reference/public-api.md) | Public API stability and semver contract |
9292
| [reference/error-contracts.md](reference/error-contracts.md) | CLI, JSON, and helper error semantics |
93-
| [releases/v2.1.7.md](releases/v2.1.7.md) | Current stable release notes |
94-
| [releases/v2.1.6.md](releases/v2.1.6.md) | Prior stable release notes |
93+
| [releases/v2.1.7.md](releases/v2.1.7.md) | Earlier stable release notes |
94+
| [releases/v2.1.6.md](releases/v2.1.6.md) | Earlier stable release notes |
9595
| [releases/v2.1.5.md](releases/v2.1.5.md) | Earlier stable release notes |
9696
| [releases/v2.1.4.md](releases/v2.1.4.md) | Earlier stable release notes |
9797
| [releases/v2.1.3.md](releases/v2.1.3.md) | Earlier stable release notes |

lib/codex-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,6 +3616,7 @@ export async function runCodexMultiAuthCli(rawArgs: string[]): Promise<number> {
36163616
normalizeFailureDetail,
36173617
loadRuntimeObservabilitySnapshot:
36183618
loadPersistedRuntimeObservabilitySnapshot,
3619+
loadQuotaCache,
36193620
});
36203621
}
36213622
if (command === "usage") {

lib/codex-manager/commands/report.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
type StorageHealthSummary,
3535
} from "../../storage.js";
3636
import type { RuntimeObservabilitySnapshot } from "../../runtime/runtime-observability.js";
37+
import type { QuotaCacheData } from "../../quota-cache.js";
3738
import type { TokenFailure, TokenResult } from "../../types.js";
3839
import { sleep } from "../../utils.js";
3940

@@ -108,6 +109,7 @@ export interface ReportCommandDeps {
108109
getCwd?: () => string;
109110
writeFile?: (path: string, contents: string) => Promise<void>;
110111
loadRuntimeObservabilitySnapshot?: () => Promise<RuntimeObservabilitySnapshot | null>;
112+
loadQuotaCache?: () => Promise<QuotaCacheData | null>;
111113
}
112114

113115
function isRetryableWriteError(error: unknown): boolean {
@@ -315,6 +317,7 @@ export async function runReportCommand(
315317
const refreshFailures = new Map<number, TokenFailure>();
316318
const liveQuotaByIndex = new Map<number, CodexQuotaSnapshot>();
317319
const probeErrors: string[] = [];
320+
const quotaCache = (await deps.loadQuotaCache?.().catch(() => null)) ?? null;
318321
let runtimeSnapshot: RuntimeObservabilitySnapshot | null | undefined;
319322
let runtimeSnapshotLoadError: string | null = null;
320323
try {
@@ -462,6 +465,8 @@ export async function runReportCommand(
462465
now,
463466
refreshFailure: refreshFailures.get(index),
464467
liveQuota: liveQuotaByIndex.get(index),
468+
quotaCache,
469+
allAccounts: storage.accounts,
465470
runtimeOverlay: runtimeSnapshot,
466471
})),
467472
)

lib/codex-manager/commands/rotation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ async function runResetRuntime(
128128
return 1;
129129
}
130130

131-
AccountManager.resetVolatileRuntimeState();
132-
recordRuntimeReset("rotation-reset-runtime");
133131
let unbind: AppBindResult | null = null;
134132
let bind: AppBindResult | null = null;
135133
let appBindRestarted = false;
@@ -145,7 +143,7 @@ async function runResetRuntime(
145143
JSON.stringify({
146144
ok: false,
147145
command: "rotation reset-runtime",
148-
resetVolatileRuntimeState: true,
146+
resetVolatileRuntimeState: false,
149147
appBindRestarted,
150148
error: message,
151149
}),
@@ -156,6 +154,8 @@ async function runResetRuntime(
156154
return 1;
157155
}
158156
}
157+
AccountManager.resetVolatileRuntimeState();
158+
recordRuntimeReset("rotation-reset-runtime");
159159
const payload = {
160160
ok: true,
161161
command: "rotation reset-runtime",

lib/codex-manager/repair-commands.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,12 +1926,15 @@ export async function runDoctor(
19261926
const runtimeSnapshot =
19271927
(await loadPersistedRuntimeObservabilitySnapshot().catch(() => null)) ??
19281928
null;
1929+
const quotaCache = (await loadQuotaCache().catch(() => null)) ?? null;
19291930
const forecastResults = evaluateForecastAccounts(
19301931
storageForChecks.accounts.map((account, index) => ({
19311932
index,
19321933
account,
19331934
isCurrent: index === activeIndex,
19341935
now,
1936+
quotaCache,
1937+
allAccounts: storageForChecks.accounts,
19351938
})),
19361939
);
19371940
const runtimeForecastResults = evaluateForecastAccounts(
@@ -1940,6 +1943,8 @@ export async function runDoctor(
19401943
account,
19411944
isCurrent: index === activeIndex,
19421945
now,
1946+
quotaCache,
1947+
allAccounts: storageForChecks.accounts,
19431948
runtimeOverlay: runtimeSnapshot,
19441949
})),
19451950
);

lib/runtime-rotation-proxy.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,8 +1371,7 @@ export async function startRuntimeRotationProxy(
13711371
reason === "rate-limited" ||
13721372
reason.startsWith("cooling-down") ||
13731373
reason === "policy-blocked",
1374-
) &&
1375-
accountManager.getMinWaitTimeForFamily(context.family, context.model) === 0
1374+
)
13761375
) {
13771376
reloadedAfterNoAccount = true;
13781377
const reloadedManager = await recoverStaleRuntimeState();
@@ -1850,9 +1849,7 @@ export async function startRuntimeRotationProxy(
18501849
baseUrl: `http://${host}:${resolvedPort}`,
18511850
close: async () => {
18521851
await closeServer(server, sockets);
1853-
await Promise.all(
1854-
[...knownAccountManagers].map((manager) => manager.flushPendingSave()),
1855-
);
1852+
await activeAccountManager.flushPendingSave();
18561853
},
18571854
getStatus: () => ({ ...status }),
18581855
};

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,15 @@
122122
"dist/",
123123
"assets/",
124124
"config/",
125-
"scripts/",
125+
"scripts/codex.js",
126+
"scripts/codex-app-launcher.js",
127+
"scripts/codex-app-router.js",
128+
"scripts/codex-bin-resolver.js",
129+
"scripts/codex-multi-auth.js",
130+
"scripts/codex-routing.js",
131+
"scripts/install-codex-auth-utils.js",
132+
"scripts/postinstall.js",
133+
"scripts/preuninstall.js",
126134
"vendor/codex-ai-plugin/",
127135
"vendor/codex-ai-sdk/",
128136
"README.md",
@@ -165,8 +173,8 @@
165173
"@codex-ai/plugin": "file:vendor/codex-ai-plugin",
166174
"@openauthjs/openauth": "^0.4.3",
167175
"hono": "4.12.18",
168-
"undici": "^6.24.1",
169-
"zod": "^4.3.6"
176+
"undici": "6.25.0",
177+
"zod": "4.4.3"
170178
},
171179
"overrides": {
172180
"hono": "4.12.18",

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,54 @@ describe("runReportCommand", () => {
141141
);
142142
});
143143

144+
it("uses quota cache when computing non-live forecast readiness", async () => {
145+
const deps = createDeps({
146+
loadAccounts: vi.fn(async () =>
147+
createStorage([
148+
{
149+
email: "quota@example.com",
150+
refreshToken: "refresh-token-1",
151+
accessToken: "access-token-1",
152+
expiresAt: 10_000,
153+
addedAt: 1,
154+
lastUsed: 1,
155+
accountId: "quota-account",
156+
enabled: true,
157+
},
158+
]),
159+
),
160+
loadQuotaCache: vi.fn(async () => ({
161+
byAccountId: {
162+
"quota-account": {
163+
updatedAt: 900,
164+
status: 200,
165+
model: "gpt-5.3-codex",
166+
primary: { usedPercent: 100, resetAtMs: 61_000 },
167+
secondary: {},
168+
},
169+
},
170+
byEmail: {},
171+
})),
172+
});
173+
174+
const result = await runReportCommand(["--json"], deps);
175+
176+
expect(result).toBe(0);
177+
expect(deps.loadQuotaCache).toHaveBeenCalledTimes(1);
178+
const payload = JSON.parse(
179+
String(
180+
(deps.logInfo as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0] ??
181+
"{}",
182+
),
183+
) as {
184+
forecast: { accounts: Array<{ availability: string; reasons: string[] }> };
185+
};
186+
expect(payload.forecast.accounts[0]?.availability).toBe("delayed");
187+
expect(payload.forecast.accounts[0]?.reasons).toContain(
188+
"quota cache exhausted",
189+
);
190+
});
191+
144192
it("includes runtime observability fields in json output when snapshot is available", async () => {
145193
const deps = createDeps({
146194
loadRuntimeObservabilitySnapshot: vi.fn(async () => ({

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ describe("rotation reset-runtime", () => {
235235
}
236236
});
237237

238-
it("returns failure json when app bind restart throws after runtime reset", async () => {
238+
it("returns failure json without clearing runtime diagnostics when app bind restart throws", async () => {
239239
const { deps, infos, errors, bindCodexAppMock, unbindCodexAppMock } =
240240
createDeps();
241241
const resetSpy = vi.spyOn(AccountManager, "resetVolatileRuntimeState");
@@ -245,14 +245,14 @@ describe("rotation reset-runtime", () => {
245245
const exitCode = await runRotationCommand(["reset-runtime", "--json"], deps);
246246

247247
expect(exitCode).toBe(1);
248-
expect(resetSpy).toHaveBeenCalledTimes(1);
248+
expect(resetSpy).not.toHaveBeenCalled();
249249
expect(unbindCodexAppMock).toHaveBeenCalledTimes(1);
250250
expect(bindCodexAppMock).not.toHaveBeenCalled();
251251
expect(errors).toEqual([]);
252252
expect(JSON.parse(infos.at(-1) ?? "{}")).toMatchObject({
253253
ok: false,
254254
command: "rotation reset-runtime",
255-
resetVolatileRuntimeState: true,
255+
resetVolatileRuntimeState: false,
256256
appBindRestarted: false,
257257
error: "unbind busy",
258258
});

0 commit comments

Comments
 (0)