Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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") {
Expand Down
42 changes: 36 additions & 6 deletions apps/server/src/provider/codexUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
12 changes: 10 additions & 2 deletions apps/server/src/provider/codexUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(
(latest, window) =>
Expand Down
Loading