Surface Codex RateLimitSnapshot (usage windows) over ACP, not just in /status text #227
Description
Summary
codex-acp already ingests Codex's RateLimitSnapshot (the ChatGPT subscription usage windows that the CLI shows in /status), but the structured snapshot never crosses the ACP boundary. It is only surfaced as human-readable markdown inside the /status slash command output. ACP clients that wrap codex-acp (Zed, Kepler, JetBrains plugins, etc.) therefore cannot render a live "47% used; resets in 2h 14m" usage badge without scraping /status text, and there is no warning state before a turn fails with the usage limit.
This is the new-repo continuation of zed-industries/codex-acp#305 (closed, with a note to re-file here), and the Codex twin of agentclientprotocol/claude-agent-acp#625, which requests the same indicator for Anthropic subscriptions.
What the adapter already has (half of #305 is done)
The data already reaches the wrapper and is stored per-session:
src/app-server/v2/AccountRateLimitsUpdatedNotification.tsdeliversRateLimitSnapshotfrom the Codex App Server.CodexEventHandler.handleRateLimitsUpdatedstores the full snapshot (primary/secondarywindows withusedPercent,window_minutes,resets_at, pluscreditsandplan_type) intosessionState.rateLimits(aRateLimitsMap).CodexCommands.formatRateLimitLines/formatSingleRateLimitrender that snapshot as markdown in the/statusoutput.- The App Server exposes
account/rateLimits/readandaccount/usage/readfor on-demand reads.
So ingestion, storage, and a text view all exist.
What is still missing (the actual ask of #305)
The structured snapshot does not cross ACP as machine-readable data:
handleRateLimitsUpdatedstores the snapshot and thenreturn null— theaccount/rateLimits/updatednotification is swallowed and nothing is emitted to the client.- The
usage_updatesession notification carries onlyused/size(context window), not rate limits. _meta.quota(CodexAcpServer.buildQuotaMeta) carries onlytoken_count+model_usage, not rate limits.
Net effect: a client can only get the windowed allowance by invoking /status and parsing markdown. There is no programmatic, push-based path for a status indicator.
Why exposing it matters
- Clients cannot render a usage badge equivalent to what
codex /statusshows in the CLI without scraping localized markdown. - There is no warning state before a user hits the cap mid-turn.
- ChatGPT Plus / Pro / Business users who pay specifically for the windowed allowance get a worse experience under ACP than under the raw CLI.
- Cost-tracking is not the right metric for subscription users; the windowed allowance is, and it is the only signal that is currently text-only.
Proposed shape
Open to whatever fits the maintainers' constraints. Two non-exclusive routes, both reusing the snapshot already kept in sessionState.rateLimits:
- Push on update: when
account/rateLimits/updatedarrives, instead of swallowing it (handleRateLimitsUpdatedcurrently returnsnull), emit a session notification carrying the snapshot under_meta(e.g._meta.codex/rateLimitsor_meta.rateLimits), so clients can keep a status indicator current independent of an active turn. - Per-turn: include the latest
rateLimitssnapshot in the existingusage_updatenotification's_meta, alongside the context-windowused/sizeit already carries.
UsageUpdate is @experimental in the ACP schema, so _meta is a reasonable place to land this without waiting on typed schema additions; it can be promoted to a typed ACP extension later.
The wire payload would mirror the snapshot already stored, e.g.:
{
"primary": { "usedPercent": 47.0, "windowMinutes": 300, "resetsAt": "2026-05-25T18:14:00Z" },
"secondary": { "usedPercent": 12.5, "windowMinutes": 10080, "resetsAt": "2026-05-29T09:00:00Z" },
"credits": { "unlimited": false, "balance": null },
"planType": "Plus"
}planType is already present on the snapshot (RateLimitSnapshot.plan_type), so labelling Plus / Pro / Business is essentially free; clients can otherwise infer the tier from windowMinutes.
Related
- Surface Codex RateLimitSnapshot (primary/secondary usage windows) over ACP zed-industries/codex-acp#305 (closed) — original request on the old adapter
- Show Claude usage limits in the Agent Panel claude-agent-acp#625 (open) — same feature for the Claude wrapper
Happy to draft a PR if there is alignment on the shape and on whether this goes through _meta or a typed ACP extension.
Activity
tahaburak commentedon Sep 26, 2026
Still the case in 1.13.1 and on
main(bf37821):src/CodexEventHandler.ts:605-607mergesaccount/rateLimits/updatedintosessionState.rateLimitsand returnsnull.Two details that may help whoever picks this up:
limitId(handleRateLimitsUpdatedmerges it withmergeRateLimitSnapshot;createRateLimitsMaphandles several buckets fromrateLimitsByLimitId). Forwarding the merged entry perlimitId, rather than the raw notification, would save every client from repeating that merge./statusis the only route today, and its reset times are localHH:MM(pluson Mon Dwhen not today) with no time zone, so a client parsing it has to assume the agent host's zone. An epochresetsAtin_metaavoids that.