From b67a40dd2e7886652b32297679112761046fc73f Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Mon, 12 Oct 2026 22:40:33 +0530 Subject: [PATCH] fix(server): Codex usage-limit stops keep their reset time A refused Codex turn can have a current usage window at 99%. Keep that reset when no window is fully exhausted, and ignore observations whose reset has passed. Pass Effect's clock through the adapter's reset lookups. Retain main's fresh account read for missing snapshots and its session reset precedence; the earlier published-limit fallback is superseded. Fixes #14169 Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Julius Marminge --- .../Adapters/CodexAdapterV2.ts | 23 +++++++--- .../src/provider/codexUsageLimits.test.ts | 42 ++++++++++++++++--- apps/server/src/provider/codexUsageLimits.ts | 12 +++++- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 5cfd93980a887..e3a052e68122f 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1195,11 +1195,12 @@ function withCodexManagedFailure( function failureResetAt( failure: OrchestrationV2ProviderFailure, snapshot: CodexRateLimitSnapshot | undefined, + atMs: number, ) { // Sharing caps have no reset timestamp in the ordinary Codex allowance snapshot. return classifyCodexManagedError(failure.code)?.usageLimit ? null - : codexUsageLimitResetAt(snapshot); + : codexUsageLimitResetAt(snapshot, atMs); } interface ActiveCodexTurnContext { @@ -4691,8 +4692,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( mergeCodexRateLimits(previous, payload.rateLimits), ); const snapshot = yield* Ref.get(rateLimitSnapshot); + const atMs = DateTime.toEpochMillis(yield* DateTime.now); for (const item of (yield* Ref.get(limitedTurnItems)).values()) { - const resetAt = failureResetAt(item.failure, snapshot); + const resetAt = failureResetAt(item.failure, snapshot, atMs); if (resetAt === null || item.failure.resetAt != null) continue; const updated = { ...item, @@ -6114,7 +6116,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( failure.class === "usage_limit" ? { ...failure, - resetAt: failureResetAt(failure, yield* Ref.get(rateLimitSnapshot)), + resetAt: failureResetAt( + failure, + yield* Ref.get(rateLimitSnapshot), + DateTime.toEpochMillis(yield* DateTime.now), + ), } : failure, ...(input.providerRetry === undefined @@ -6154,7 +6160,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ...event.failure, resetAt: event.failure.resetAt ?? - failureResetAt(event.failure, yield* Ref.get(rateLimitSnapshot)), + failureResetAt( + event.failure, + yield* Ref.get(rateLimitSnapshot), + DateTime.toEpochMillis(yield* DateTime.now), + ), }, } : event; @@ -6187,7 +6197,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( event.failure.resetAt == null && // The account read only reports the ordinary allowance, never a sharing cap's reset. !classifyCodexManagedError(event.failure.code)?.usageLimit && - codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot)) === null; + codexUsageLimitResetAt( + yield* Ref.get(rateLimitSnapshot), + DateTime.toEpochMillis(yield* DateTime.now), + ) === null; if (!awaitsReset) yield* publishRootTerminal(context, event); const continued = continuedRuns.get(context.providerTurnId) ?? [context]; if (event.status !== "completed") { diff --git a/apps/server/src/provider/codexUsageLimits.test.ts b/apps/server/src/provider/codexUsageLimits.test.ts index b560aba5d3c28..9b6e64d905707 100644 --- a/apps/server/src/provider/codexUsageLimits.test.ts +++ b/apps/server/src/provider/codexUsageLimits.test.ts @@ -300,16 +300,46 @@ describe("mergeCodexRateLimits", () => { import { codexUsageLimitResetAt } from "./codexUsageLimits.ts"; describe("codexUsageLimitResetAt", () => { + const stoppedAtMs = Date.parse("2033-05-18T03:00:00.000Z"); + it("waits for every exhausted window and never invents an unknown reset", () => { expect( - codexUsageLimitResetAt({ - primary: { usedPercent: 100, resetsAt: 2000000000 }, - secondary: { usedPercent: 100, resetsAt: 2000100000 }, - }), + codexUsageLimitResetAt( + { + primary: { usedPercent: 100, resetsAt: 2000000000 }, + secondary: { usedPercent: 100, resetsAt: 2000100000 }, + }, + stoppedAtMs, + ), ).toBe("2033-05-19T07:20:00.000Z"); - expect(codexUsageLimitResetAt({ primary: { usedPercent: 100 } })).toBeNull(); expect( - codexUsageLimitResetAt({ primary: { usedPercent: 50, resetsAt: 2000000000 } }), + codexUsageLimitResetAt( + { + primary: { usedPercent: 100, resetsAt: 2000000000 }, + secondary: { usedPercent: 99, resetsAt: 2000100000 }, + }, + stoppedAtMs, + ), + ).toBe("2033-05-18T03:33:20.000Z"); + expect( + codexUsageLimitResetAt({ primary: { usedPercent: 99, resetsAt: 2000000000 } }, stoppedAtMs), + ).toBe("2033-05-18T03:33:20.000Z"); + expect(codexUsageLimitResetAt({ primary: { usedPercent: 100 } }, stoppedAtMs)).toBeNull(); + expect( + codexUsageLimitResetAt({ primary: { usedPercent: 50, resetsAt: 2000000000 } }, stoppedAtMs), + ).toBeNull(); + }); + + it("ignores a window that has renewed since it was observed", () => { + const afterSessionResetMs = Date.parse("2033-05-18T04:00:00.000Z"); + expect( + codexUsageLimitResetAt( + { + primary: { usedPercent: 100, resetsAt: 2000000000 }, + secondary: { usedPercent: 40, resetsAt: 2000100000 }, + }, + afterSessionResetMs, + ), ).toBeNull(); }); }); diff --git a/apps/server/src/provider/codexUsageLimits.ts b/apps/server/src/provider/codexUsageLimits.ts index d4f9c621119df..eb235a168d331 100644 --- a/apps/server/src/provider/codexUsageLimits.ts +++ b/apps/server/src/provider/codexUsageLimits.ts @@ -186,12 +186,20 @@ export function mergeCodexRateLimits( }; } -/** All exhausted windows must reset before a continuation can run. */ +/** + * All exhausted windows must reset before a continuation can run. Codex reports whole + * percentages and can refuse a turn at 99%; windows whose reset has passed are stale. + */ export function codexUsageLimitResetAt( snapshot: CodexRateLimitSnapshot | undefined, + atMs: number, ): string | null { if (!snapshot) return null; - const windows = codexRateLimitsToWindows(snapshot).filter((window) => window.usedPercent >= 100); + const current = codexRateLimitsToWindows(snapshot).filter( + (window) => window.resetsAt === undefined || Date.parse(window.resetsAt) > atMs, + ); + const full = current.filter((window) => window.usedPercent >= 100); + const windows = full.length > 0 ? full : current.filter((window) => window.usedPercent >= 99); if (windows.length === 0 || windows.some((window) => !window.resetsAt)) return null; return windows.reduce( (latest, window) =>