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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions