Skip to content

Commit b7a7301

Browse files
committed
fix codex usage-limit recovery copy
1 parent 3db1508 commit b7a7301

4 files changed

Lines changed: 120 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
1414
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
1515
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
16+
- Codex harness: make subscription usage-limit errors without reset times explain that OpenClaw cannot determine the reset and point users to wait until Codex is available, use another Codex account, or switch to another configured model/provider. Thanks @amknight.
1617
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
1718
- Telegram: route normal `[telegram][diag]` polling diagnostics through `runtime.log` while keeping non-diag warnings and persistence failures on `runtime.error`, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.
1819

extensions/codex/src/app-server/event-projector.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ describe("CodexAppServerEventProjector", () => {
718718

719719
expect(result.promptError).toContain("You've reached your Codex subscription usage limit.");
720720
expect(result.promptError).toContain("Next reset in");
721-
expect(result.promptError).toContain("Run /codex account");
721+
expect(result.promptError).toContain("Wait until the reset time");
722722
expect(result.promptErrorSource).toBe("prompt");
723723
});
724724

extensions/codex/src/app-server/rate-limits.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ import {
77
} from "./rate-limits.js";
88

99
describe("formatCodexUsageLimitErrorMessage", () => {
10+
it("gives actionable guidance when Codex omits reset details", () => {
11+
const message = formatCodexUsageLimitErrorMessage({
12+
message: "You've reached your usage limit.",
13+
codexErrorInfo: "usageLimitExceeded",
14+
rateLimits: {
15+
rateLimits: {
16+
limitId: "codex",
17+
primary: { usedPercent: 100, windowDurationMins: 10_080, resetsAt: null },
18+
secondary: null,
19+
},
20+
},
21+
nowMs: Date.UTC(2026, 4, 10, 23, 0, 0),
22+
});
23+
24+
expect(message).toContain("You've reached your Codex subscription usage limit.");
25+
expect(message).toContain("Your weekly Codex usage limit is reached.");
26+
expect(message).toContain("OpenClaw could not determine a reset time from Codex.");
27+
expect(message).toContain("Wait until Codex becomes available");
28+
expect(message).toContain("use another Codex account if available");
29+
expect(message).toContain("switch to another configured model/provider");
30+
expect(message).not.toContain("Codex did not return a reset time");
31+
expect(message).not.toContain("/codex account");
32+
});
33+
1034
it("preserves Codex retry hints when structured reset windows are absent", () => {
1135
const message = formatCodexUsageLimitErrorMessage({
1236
message:
@@ -24,6 +48,7 @@ describe("formatCodexUsageLimitErrorMessage", () => {
2448

2549
expect(message).toContain("You've reached your Codex subscription usage limit.");
2650
expect(message).toContain("Codex says to try again at May 11th, 2026 9:00 AM.");
51+
expect(message).toContain("Wait until the retry time");
2752
expect(message).not.toContain("Codex did not return a reset time");
2853
});
2954

@@ -42,10 +67,67 @@ describe("formatCodexUsageLimitErrorMessage", () => {
4267
});
4368

4469
expect(message).toContain("Next reset in 1 hour, ");
70+
expect(message).toContain("Wait until the reset time");
4571
expect(message).toMatch(/\b[A-Z][a-z]{2} \d{1,2}(?:, \d{4})? at \d{1,2}:\d{2} [AP]M\b/u);
4672
expect(message).not.toMatch(/\(\d{4}-\d{2}-\d{2}T/u);
4773
expect(message).not.toContain("Codex did not return a reset time");
4874
});
75+
76+
it("uses the blocking reset when multiple Codex windows are exhausted", () => {
77+
const nowMs = 1_700_000_000_000;
78+
const nowSeconds = nowMs / 1000;
79+
const message = formatCodexUsageLimitErrorMessage({
80+
message: "You've reached your usage limit.",
81+
codexErrorInfo: "usageLimitExceeded",
82+
rateLimits: {
83+
rateLimits: {
84+
limitId: "codex",
85+
primary: { usedPercent: 100, windowDurationMins: 300, resetsAt: nowSeconds + 3600 },
86+
secondary: {
87+
usedPercent: 100,
88+
windowDurationMins: 10_080,
89+
resetsAt: nowSeconds + 24 * 3600,
90+
},
91+
},
92+
},
93+
nowMs,
94+
});
95+
96+
expect(message).toContain("Next reset in 1 day");
97+
expect(message).not.toContain("Next reset in 1 hour");
98+
expect(message).toContain("Wait until the reset time");
99+
});
100+
101+
it("does not use sibling bucket resets when the blocked Codex bucket omits a reset", () => {
102+
const nowMs = 1_700_000_000_000;
103+
const nowSeconds = nowMs / 1000;
104+
const message = formatCodexUsageLimitErrorMessage({
105+
message: "You've reached your usage limit.",
106+
codexErrorInfo: "usageLimitExceeded",
107+
rateLimits: {
108+
rateLimitsByLimitId: {
109+
codex: {
110+
limitId: "codex",
111+
limitName: "Codex",
112+
primary: { usedPercent: 100, windowDurationMins: 300, resetsAt: null },
113+
secondary: null,
114+
},
115+
"gpt-5.3-codex-spark": {
116+
limitId: "gpt-5.3-codex-spark",
117+
limitName: "GPT 5.3 Codex Spark",
118+
primary: { usedPercent: 0, windowDurationMins: 300, resetsAt: nowSeconds + 3600 },
119+
secondary: null,
120+
},
121+
},
122+
},
123+
nowMs,
124+
});
125+
126+
expect(message).toContain("OpenClaw could not determine a reset time from Codex.");
127+
expect(message).toContain("Wait until Codex becomes available");
128+
expect(message).not.toContain("Next reset");
129+
expect(message).not.toContain("1 hour");
130+
});
49131
});
50132

51133
describe("Codex rate limit blocking resets", () => {

extensions/codex/src/app-server/rate-limits.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,31 @@ export function formatCodexUsageLimitErrorMessage(params: {
4141
return undefined;
4242
}
4343
const nowMs = params.nowMs ?? Date.now();
44-
const nextReset = selectNextRateLimitReset(params.rateLimits, nowMs);
44+
const usageSummary = summarizeCodexAccountUsage(params.rateLimits, nowMs);
45+
const blockingReset = selectBlockingRateLimitReset(params.rateLimits, nowMs);
46+
const nextReset =
47+
blockingReset ??
48+
(usageSummary?.blocked ? undefined : selectNextRateLimitReset(params.rateLimits, nowMs));
4549
const parts = ["You've reached your Codex subscription usage limit."];
50+
let recoveryAction = "Wait until Codex becomes available";
4651
if (nextReset) {
4752
parts.push(`Next reset ${formatResetTime(nextReset.resetsAtMs, nowMs)}.`);
53+
recoveryAction = "Wait until the reset time";
4854
} else {
4955
const codexRetryHint = extractCodexRetryHint(message);
5056
if (codexRetryHint) {
5157
parts.push(`Codex says to try again ${codexRetryHint}.`);
58+
recoveryAction = "Wait until the retry time";
5259
} else {
53-
parts.push("Codex did not return a reset time for this limit.");
60+
if (usageSummary?.blockingPeriod && usageSummary.blockingReason) {
61+
parts.push(`Your ${usageSummary.blockingReason}.`);
62+
}
63+
parts.push("OpenClaw could not determine a reset time from Codex.");
5464
}
5565
}
56-
parts.push("Run /codex account for current usage details.");
66+
parts.push(
67+
`${recoveryAction}, use another Codex account if available, or switch to another configured model/provider.`,
68+
);
5769
return parts.join(" ");
5870
}
5971

@@ -126,10 +138,12 @@ export function summarizeCodexAccountUsage(
126138
const blockedSnapshots = snapshots.filter(snapshotHasLimitBlock);
127139
const blockingSnapshot =
128140
blockedSnapshots.find(isCodexLimitSnapshot) ?? blockedSnapshots[0] ?? undefined;
129-
const blockingReset = blockingSnapshot
130-
? selectSnapshotBlockingReset(blockingSnapshot, nowMs)
141+
const blockingWindow = blockingSnapshot
142+
? selectSnapshotBlockingWindow(blockingSnapshot, nowMs)
131143
: undefined;
132-
const blockingPeriod = formatBlockingLimitPeriod(blockingReset?.windowDurationMins);
144+
const blockingReset =
145+
blockingWindow && blockingWindow.resetsAtMs > nowMs ? blockingWindow : undefined;
146+
const blockingPeriod = formatBlockingLimitPeriod(blockingWindow?.windowDurationMins);
133147
const blockedUntilText = blockingReset
134148
? formatAccountResetTime(blockingReset.resetsAtMs, nowMs)
135149
: undefined;
@@ -426,6 +440,22 @@ function selectSnapshotBlockingReset(
426440
return candidates.toSorted(resetSort)[0];
427441
}
428442

443+
function selectSnapshotBlockingWindow(
444+
snapshot: JsonObject,
445+
nowMs: number,
446+
): RateLimitReset | undefined {
447+
const resetWindow = selectSnapshotBlockingReset(snapshot, nowMs);
448+
if (resetWindow) {
449+
return resetWindow;
450+
}
451+
const exhaustedWindows = readWindowEntries(snapshot)
452+
.map((entry) => entry.window)
453+
.filter((window) => window.usedPercent !== undefined && window.usedPercent >= 100);
454+
return exhaustedWindows.toSorted(
455+
(left, right) => (right.windowDurationMins ?? 0) - (left.windowDurationMins ?? 0),
456+
)[0];
457+
}
458+
429459
function readWindowEntries(snapshot: JsonObject): RateLimitWindowEntry[] {
430460
return LIMIT_WINDOW_KEYS.flatMap((key) => {
431461
const window = readRateLimitWindow(snapshot, key);

0 commit comments

Comments
 (0)