Skip to content

Surface Codex RateLimitSnapshot (usage windows) over ACP, not just in /status text #227

Open

Description

@julianmesa-gitkraken

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.ts delivers RateLimitSnapshot from the Codex App Server.
  • CodexEventHandler.handleRateLimitsUpdated stores the full snapshot (primary / secondary windows with usedPercent, window_minutes, resets_at, plus credits and plan_type) into sessionState.rateLimits (a RateLimitsMap).
  • CodexCommands.formatRateLimitLines / formatSingleRateLimit render that snapshot as markdown in the /status output.
  • The App Server exposes account/rateLimits/read and account/usage/read for 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:

  • handleRateLimitsUpdated stores the snapshot and then return null — the account/rateLimits/updated notification is swallowed and nothing is emitted to the client.
  • The usage_update session notification carries only used / size (context window), not rate limits.
  • _meta.quota (CodexAcpServer.buildQuotaMeta) carries only token_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 /status shows 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:

  1. Push on update: when account/rateLimits/updated arrives, instead of swallowing it (handleRateLimitsUpdated currently returns null), emit a session notification carrying the snapshot under _meta (e.g. _meta.codex/rateLimits or _meta.rateLimits), so clients can keep a status indicator current independent of an active turn.
  2. Per-turn: include the latest rateLimits snapshot in the existing usage_update notification's _meta, alongside the context-window used / size it 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

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

  1. tahaburak commented on Sep 26, 2026

    @tahaburak

    Still the case in 1.13.1 and on main (bf37821): src/CodexEventHandler.ts:605-607 merges account/rateLimits/updated into sessionState.rateLimits and returns null.

    Two details that may help whoever picks this up:

    • The app-server notification is sparse and keyed by limitId (handleRateLimitsUpdated merges it with mergeRateLimitSnapshot; createRateLimitsMap handles several buckets from rateLimitsByLimitId). Forwarding the merged entry per limitId, rather than the raw notification, would save every client from repeating that merge.
    • /status is the only route today, and its reset times are local HH:MM (plus on Mon D when not today) with no time zone, so a client parsing it has to assume the agent host's zone. An epoch resetsAt in _meta avoids that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions