Skip to content

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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions