From ee85ba9a4765b46aa4559e9793ab9ea614a6e097 Mon Sep 17 00:00:00 2001 From: Ortwin Van der Stappen Date: Sat, 22 Aug 2026 16:56:46 +0100 Subject: [PATCH] feat: forward account rate limits to clients as session metadata `account/rateLimits/updated` was already handled -- the snapshot is stored in `sessionState.rateLimits` and rendered by `/status` -- but the handler returned null, so nothing reached the client and the numbers were only visible if the user typed a command. Forward them as session metadata too, via the existing createCodexSessionInfoUpdate helper, so clients see the snapshot under `_meta.codex.rateLimits`. This makes usage indicators free to build. A client that wants to show how much of the plan is left otherwise has to spawn its own `codex app-server` and poll `account/rateLimits/read`: a second Codex process and a duplicate request per refresh, for data the adapter already receives, parses and keeps. /status keeps working, since the state is still recorded. --- src/CodexEventHandler.ts | 4 +- .../data/account-rate-limits-updated.json | 34 ++++++++++++ .../CodexACPAgent/rate-limit-events.test.ts | 55 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/CodexACPAgent/data/account-rate-limits-updated.json create mode 100644 src/__tests__/CodexACPAgent/rate-limit-events.test.ts diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index 7360b6f9..43b4381b 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -470,7 +470,9 @@ export class CodexEventHandler { return this.createMcpToolProgressEvent(notification.params); case "account/rateLimits/updated": this.handleRateLimitsUpdated(notification.params); - return null; + return this.createCodexSessionInfoUpdate({ + rateLimits: notification.params.rateLimits, + }); case "configWarning": return await this.createConfigWarningEvent(notification.params); case "warning": diff --git a/src/__tests__/CodexACPAgent/data/account-rate-limits-updated.json b/src/__tests__/CodexACPAgent/data/account-rate-limits-updated.json new file mode 100644 index 00000000..905509fb --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/account-rate-limits-updated.json @@ -0,0 +1,34 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "session_info_update", + "_meta": { + "codex": { + "rateLimits": { + "limitId": "codex", + "limitName": null, + "primary": { + "usedPercent": 42, + "windowDurationMins": 300, + "resetsAt": 1710000000 + }, + "secondary": { + "usedPercent": 7, + "windowDurationMins": 10080, + "resetsAt": 1710600000 + }, + "credits": null, + "individualLimit": null, + "spendControlReached": false, + "planType": "plus", + "rateLimitReachedType": null + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/rate-limit-events.test.ts b/src/__tests__/CodexACPAgent/rate-limit-events.test.ts new file mode 100644 index 00000000..46909957 --- /dev/null +++ b/src/__tests__/CodexACPAgent/rate-limit-events.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { SessionState } from "../../CodexAcpServer"; +import type { ServerNotification } from "../../app-server"; +import { AgentMode } from "../../AgentMode"; +import { + createCodexMockTestFixture, + createTestSessionState, + setupPromptAndSendNotifications, + type CodexMockTestFixture, +} from "../acp-test-utils"; + +describe("CodexEventHandler - account rate limit events", () => { + let mockFixture: CodexMockTestFixture; + const sessionId = "test-session-id"; + + beforeEach(() => { + mockFixture = createCodexMockTestFixture(); + vi.clearAllMocks(); + }); + + const rateLimitsUpdatedNotification: ServerNotification = { + method: "account/rateLimits/updated", + params: { + rateLimits: { + limitId: "codex", + limitName: null, + primary: { usedPercent: 42, windowDurationMins: 300, resetsAt: 1710000000 }, + secondary: { usedPercent: 7, windowDurationMins: 10080, resetsAt: 1710600000 }, + credits: null, + individualLimit: null, + spendControlReached: false, + planType: "plus", + rateLimitReachedType: null, + }, + }, + }; + + it("should send account rate limits as session metadata", async () => { + await setupPromptAndSendNotifications(mockFixture, sessionId, createSessionState(), [ + rateLimitsUpdatedNotification, + ]); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot( + "data/account-rate-limits-updated.json" + ); + }); + + function createSessionState(): SessionState { + return createTestSessionState({ + sessionId, + currentModelId: "model-id[effort]", + agentMode: AgentMode.DEFAULT_AGENT_MODE, + }); + } +});