From 4e10969aded0cd3d99e30d8deb9d6d109d380037 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 02:15:08 +0800 Subject: [PATCH] fix(codex): ignore empty rate-limit buckets --- CHANGELOG.md | 1 + .../codex/src/app-server/rate-limits.test.ts | 51 +++++++++++++++++++ extensions/codex/src/command-formatters.ts | 16 +++++- extensions/codex/src/commands.test.ts | 20 ++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1923de30765..daf552c7663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - LINE: stop cron recovery from inferring lowercased LINE recipients from canonical session keys, so long-running task replies do not silently retry undeliverable push targets. Fixes #81628. (#81704) Thanks @edenfunf. - TTS: preserve channel-derived voice-note delivery for `/tts audio` replies even when the provider output is not natively voice-compatible. (#82174) Thanks @xuruiray. - Codex app-server: preserve inbound sender metadata and source-channel provenance on mirrored user prompts, including failure snapshots, so channel history keeps the original sender identity. (#82184) Thanks @zknicker. +- Codex account/status: treat metadata-only rate-limit buckets as returned but empty so `/codex status` and `/codex account` report `none returned` instead of counting phantom limits. - Codex/Lossless: keep Codex explicit compaction on native app-server threads while allowing Lossless through the context-engine slot; `openclaw doctor --fix` now migrates legacy `compaction.provider: "lossless-claw"` config to `plugins.slots.contextEngine`. - Cron/doctor: report scheduled jobs with explicit `payload.model` overrides, including provider namespace counts and default-model mismatches, so stale cron model pins are visible during auth or billing investigations. Fixes #82151. Thanks @mgonto. - Codex app-server: keep the short turn-completion idle watchdog armed after the last non-assistant current-turn item completes, so a quiet Codex app-server releases the OpenClaw session lane before the outer attempt timeout. Fixes #82171. (#82172) Thanks @funmerlin. diff --git a/extensions/codex/src/app-server/rate-limits.test.ts b/extensions/codex/src/app-server/rate-limits.test.ts index 9c1a0665977..65efa05e8d2 100644 --- a/extensions/codex/src/app-server/rate-limits.test.ts +++ b/extensions/codex/src/app-server/rate-limits.test.ts @@ -148,4 +148,55 @@ describe("summarizeCodexRateLimits", () => { blockingReason: "Codex usage limit is reached", }); }); + + it("ignores metadata-only Codex buckets", () => { + expect( + summarizeCodexRateLimits({ + rateLimitsByLimitId: { + codex: { + limitId: "codex", + limitName: "Codex", + primary: null, + secondary: null, + credits: null, + planType: "plus", + rateLimitReachedType: null, + }, + }, + }), + ).toBeUndefined(); + }); + + it("keeps displayable buckets when sibling buckets are empty", () => { + const nowMs = 1_700_000_000_000; + const nowSeconds = nowMs / 1000; + + expect( + summarizeCodexRateLimits( + { + rateLimitsByLimitId: { + codex: { + limitId: "codex", + limitName: "Codex", + primary: { usedPercent: 26, windowDurationMins: 300, resetsAt: nowSeconds + 3600 }, + secondary: null, + credits: null, + planType: "plus", + rateLimitReachedType: null, + }, + "gpt-5.3-codex-spark": { + limitId: "gpt-5.3-codex-spark", + limitName: "GPT 5.3 Codex Spark", + primary: null, + secondary: null, + credits: null, + planType: "plus", + rateLimitReachedType: null, + }, + }, + }, + nowMs, + ), + ).toBe("Codex: primary 74% left ⏱1h"); + }); }); diff --git a/extensions/codex/src/command-formatters.ts b/extensions/codex/src/command-formatters.ts index eb3847594b1..8874001257d 100644 --- a/extensions/codex/src/command-formatters.ts +++ b/extensions/codex/src/command-formatters.ts @@ -372,7 +372,8 @@ function formatCodexRateLimitDetails(value: JsonValue | undefined): string { function summarizeRateLimits(value: JsonValue | undefined): string { const entries = extractArray(value); if (entries.length > 0) { - return `${entries.length}`; + const count = entries.filter(isMeaningfulRateLimitSnapshot).length; + return count > 0 ? `${count}` : "none returned"; } if (!isJsonObject(value)) { return "none returned"; @@ -388,7 +389,18 @@ function summarizeRateLimits(value: JsonValue | undefined): string { } function isMeaningfulRateLimitSnapshot(value: JsonValue | undefined): boolean { - return isJsonObject(value) && Object.values(value).some((entry) => entry != null); + if (!isJsonObject(value)) { + return false; + } + const reachedType = + readString(value, "rateLimitReachedType") ?? readString(value, "rate_limit_reached_type"); + if (reachedType) { + return true; + } + return ["primary", "secondary"].some((key) => { + const window = value[key]; + return isJsonObject(window) && Object.values(window).some((entry) => entry != null); + }); } function extractArray(value: JsonValue | undefined): JsonValue[] { diff --git a/extensions/codex/src/commands.test.ts b/extensions/codex/src/commands.test.ts index 75493a06be3..2301edc134f 100644 --- a/extensions/codex/src/commands.test.ts +++ b/extensions/codex/src/commands.test.ts @@ -647,6 +647,17 @@ describe("codex command", () => { const limits = { ok: true as const, value: { + rateLimits: [ + { + limitId: "codex", + limitName: "Codex", + primary: null, + secondary: null, + credits: null, + planType: "plus", + rateLimitReachedType: null, + }, + ], rateLimitsByLimitId: { premium: { limitId: "premium", @@ -657,6 +668,15 @@ describe("codex command", () => { planType: "pro", rateLimitReachedType: null, }, + codex: { + limitId: "codex", + limitName: "Codex", + primary: null, + secondary: null, + credits: null, + planType: "plus", + rateLimitReachedType: null, + }, }, }, };