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
73 changes: 68 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6673,6 +6673,24 @@ describe("CodexAdapterV2 post-settle continuation", () => {
notification: false,
expectedClass: "usage_limit",
},
{
name: "rounded-reset",
code: "usageLimitExceeded",
notification: false,
expectedClass: "usage_limit",
},
{
name: "late-rounded-reset",
code: "usageLimitExceeded",
notification: false,
expectedClass: "usage_limit",
},
{
name: "ambiguous-late-reset",
code: "usageLimitExceeded",
notification: false,
expectedClass: "usage_limit",
},
{
name: "late-reset",
code: "usageLimitExceeded",
Expand Down Expand Up @@ -6707,7 +6725,11 @@ describe("CodexAdapterV2 post-settle continuation", () => {
params: {
rateLimits: {
limitId: "codex",
primary: { usedPercent: 100, resetsAt: 2000100000, windowDurationMins: 300 },
primary: {
usedPercent: scenario.name.includes("rounded") ? 99 : 100,
resetsAt: 2000100000,
windowDurationMins: 300,
},
},
},
},
Expand All @@ -6716,9 +6738,37 @@ describe("CodexAdapterV2 post-settle continuation", () => {
scenario: `codex-limit-${scenario.name}`,
entries: [
...codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "Continue." }),
...(scenario.name === "known-reset" || scenario.name === "deferred-reset"
...(scenario.name === "known-reset" ||
scenario.name === "deferred-reset" ||
scenario.name === "rounded-reset"
? [snapshot]
: []),
// Both windows read 99%, so the stop cannot tell which one ran out.
...(scenario.name === "ambiguous-late-reset"
? [
{
...snapshot,
frame: {
method: "account/rateLimits/updated",
params: {
rateLimits: {
limitId: "codex",
primary: {
usedPercent: 99,
resetsAt: 2000100000,
windowDurationMins: 300,
},
secondary: {
usedPercent: 99,
resetsAt: 2000200000,
windowDurationMins: 10080,
},
},
},
},
},
]
: []),
...(scenario.name === "deferred-reset"
? [
{
Expand Down Expand Up @@ -6798,7 +6848,11 @@ describe("CodexAdapterV2 post-settle continuation", () => {
},
},
},
...(scenario.name === "late-reset" ? [snapshot] : []),
...(scenario.name === "late-reset" ||
scenario.name === "late-rounded-reset" ||
scenario.name === "ambiguous-late-reset"
? [snapshot]
: []),
...(scenario.name === "deferred-reset"
? [
{
Expand Down Expand Up @@ -6858,11 +6912,20 @@ describe("CodexAdapterV2 post-settle continuation", () => {
if (terminal?.status !== "failed") return;
assert.equal(terminal.failure.class, scenario.expectedClass);
assert.equal(terminal.threadDisposition, "reusable");
if (scenario.name === "known-reset" || scenario.name === "deferred-reset")
if (
scenario.name === "known-reset" ||
scenario.name === "deferred-reset" ||
scenario.name === "rounded-reset"
)
assert.equal(terminal.failure.resetAt, resetAt);
if (scenario.name === "matching-details")
assert.equal(terminal.failure.message, "Detailed provider allowance explanation.");
if (scenario.name === "late-reset") {
if (scenario.name === "ambiguous-late-reset") assert.isNull(terminal.failure.resetAt);
if (
scenario.name === "late-reset" ||
scenario.name === "late-rounded-reset" ||
scenario.name === "ambiguous-late-reset"
) {
yield* Deferred.await(resetReceipt);
const item = harness.events.find(
(event) =>
Expand Down
15 changes: 12 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4312,7 +4312,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
yield* Ref.update(rateLimitSnapshot, (previous) =>
mergeCodexRateLimits(previous, payload.rateLimits),
);
const resetAt = codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot));
const resetAt = codexUsageLimitResetAt(
yield* Ref.get(rateLimitSnapshot),
DateTime.formatIso(yield* DateTime.now),
);
for (const item of (yield* Ref.get(limitedTurnItems)).values()) {
if (resetAt === null || item.failure.resetAt != null) continue;
const updated = {
Expand Down Expand Up @@ -5554,7 +5557,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
failure.class === "usage_limit"
? {
...failure,
resetAt: codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot)),
resetAt: codexUsageLimitResetAt(
yield* Ref.get(rateLimitSnapshot),
DateTime.formatIso(yield* DateTime.now),
),
}
: failure,
...(input.providerRetry === undefined
Expand Down Expand Up @@ -5591,7 +5597,10 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
...event.failure,
resetAt:
event.failure.resetAt ??
codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot)),
codexUsageLimitResetAt(
yield* Ref.get(rateLimitSnapshot),
DateTime.formatIso(yield* DateTime.now),
),
},
}
: event;
Expand Down
91 changes: 83 additions & 8 deletions apps/server/src/provider/codexUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
codexRateLimitsToUpdate,
codexResetCreditsToContract,
codexUsageLimitMessage,
codexUsageLimitResetAt,
mergeCodexRateLimits,
} from "./codexUsageLimits.ts";

Expand Down Expand Up @@ -298,19 +299,93 @@ describe("mergeCodexRateLimits", () => {
});
});

import { codexUsageLimitResetAt } from "./codexUsageLimits.ts";

describe("codexUsageLimitResetAt", () => {
const at = "2026-10-17T21:52:06.961Z";
const resetAt = "2026-10-18T01:42:09.000Z";

it("keeps the reset when the last whole-percent update before refusal reads 99%", () => {
const snapshot = mergeCodexRateLimits(
{
limitId: "codex",
primary: { usedPercent: 99, resetsAt: 1792287729, windowDurationMins: 300 },
secondary: { usedPercent: 16, resetsAt: 1792874529, windowDurationMins: 10080 },
},
{ limitId: "premium", primary: null, secondary: null },
);
expect(codexUsageLimitResetAt(snapshot, at)).toBe(resetAt);
});

it("does not guess when more than one window reads 99%", () => {
expect(
codexUsageLimitResetAt(
{
primary: { usedPercent: 99, resetsAt: 1792287729 },
secondary: { usedPercent: 99, resetsAt: 1792874529 },
},
at,
),
).toBeNull();
});

it("prefers an explicitly exhausted window over a rounded near-full window", () => {
expect(
codexUsageLimitResetAt(
{
primary: { usedPercent: 100, resetsAt: 1792287729 },
secondary: { usedPercent: 99, resetsAt: 1792874529 },
},
at,
),
).toBe(resetAt);
});

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 },
},
at,
),
).toBe("2033-05-19T07:20:00.000Z");
expect(codexUsageLimitResetAt({ primary: { usedPercent: 100 } })).toBeNull();
expect(codexUsageLimitResetAt({ primary: { usedPercent: 100 } }, at)).toBeNull();
expect(codexUsageLimitResetAt({ primary: { usedPercent: 99 } }, at)).toBeNull();
expect(
codexUsageLimitResetAt({ primary: { usedPercent: 50, resetsAt: 2000000000 } }),
codexUsageLimitResetAt(
{ primary: { usedPercent: 100, resetsAt: 1792287729 }, secondary: { usedPercent: 100 } },
at,
),
).toBeNull();
expect(
codexUsageLimitResetAt(
{ primary: { usedPercent: 100, resetsAt: 1792287729 }, secondary: { usedPercent: 99 } },
at,
),
).toBe(resetAt);
expect(
codexUsageLimitResetAt({ primary: { usedPercent: 98, resetsAt: 2000000000 } }, at),
).toBeNull();
expect(codexUsageLimitResetAt(undefined, at)).toBeNull();
});

it("ignores renewed windows and invalid observation times", () => {
for (const usedPercent of [99, 100]) {
expect(
codexUsageLimitResetAt({ primary: { usedPercent, resetsAt: 1792287729 } }, resetAt),
).toBeNull();
expect(
codexUsageLimitResetAt({ primary: { usedPercent, resetsAt: 1792287729 } }, "invalid"),
).toBeNull();
}
expect(
codexUsageLimitResetAt(
{
primary: { usedPercent: 100, resetsAt: 1792270000 },
secondary: { usedPercent: 99, resetsAt: 1792874529 },
},
at,
),
).toBe("2026-10-24T20:42:09.000Z");
});
});
19 changes: 16 additions & 3 deletions apps/server/src/provider/codexUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,25 @@ export function codexUsageLimitMessage(
return `Codex usage limit reached.${reset}${codexUsageLimitNextStep(snapshot?.rateLimitReachedType)}`;
}

/** All exhausted windows must reset before a continuation can run. */
/**
* Called after a confirmed usage-limit failure. All exhausted windows must reset
* before a continuation can run. Codex reports whole percentages, so a refusal
* can arrive while the window that ran out still reads 99%; that is only clear
* when no window reads 100% and exactly one reads 99%.
*/
export function codexUsageLimitResetAt(
snapshot: CodexRateLimitSnapshot | undefined,
atIso: string,
): string | null {
if (!snapshot) return null;
const windows = codexRateLimitsToWindows(snapshot).filter((window) => window.usedPercent >= 100);
const atMs = Date.parse(atIso);
if (!snapshot || !Number.isFinite(atMs)) return null;
// A window whose reset has passed has renewed since it was observed.
const activeWindows = codexRateLimitsToWindows(snapshot).filter(
(window) => !window.resetsAt || Date.parse(window.resetsAt) > atMs,
);
const exhausted = activeWindows.filter((window) => window.usedPercent >= 100);
const nearFull = activeWindows.filter((window) => window.usedPercent >= 99);
const windows = exhausted.length > 0 ? exhausted : nearFull.length === 1 ? nearFull : [];
if (windows.length === 0 || windows.some((window) => !window.resetsAt)) return null;
return windows.reduce<string | null>(
(latest, window) =>
Expand Down
Loading