Skip to content

feat: push the account's usage windows as _account/rate_limits_update - #524

Open
bcstrawn wants to merge 1 commit into
agentclientprotocol:mainfrom
bcstrawn:feat/rate-limits-extension
Open

bcstrawn wants to merge 1 commit into
agentclientprotocol:mainfrom
bcstrawn:feat/rate-limits-extension

Conversation

@bcstrawn

Copy link
Copy Markdown

What

A push-only ACP extension, the account-level companion of authStatus: the agent pushes the account's usage windows (primary / secondary: usedPercent, windowDurationMins, resetsAt) as _account/rate_limits_update, advertised under agentCapabilities._meta.rateLimits. One payload per limitId, pushed only when that limit's payload changed. Spec: docs/rate-limits-extension.md.

Why

Codex refuses a turn on a spent window with codexErrorInfo: "usageLimitExceeded", and that error carries no reset time. The reset is on the app-server's account/rateLimits/updated, which codex-acp already receives and merges, but only feeds the /status command's text with. A client that wants to park the session and resume it when the window clears (agent-of-empires does this for Claude today, from _claude/rateLimit) has nothing structured to schedule against.

Attaching it as _meta on usage_update (claude-agent-acp's approach) was considered and rejected: a refused first prompt produces no token-usage update, and that is exactly the moment the reset matters.

Design notes

  • Observed once at the app-server connection (CodexAppServerClient.onAccountNotification, run in receive order before the per-session fan-out), and merged against a connection-level baseline per limitId. Per-session state cannot be the baseline: it is reset on every session create, so a younger session would report planType: null for the account the older one knew, and the fan-out's independent queues can reorder copies. The per-session merge stays for /status.
  • limitName and the account metadata carry forward through the existing mergeRateLimitSnapshot; the windows are taken as reported.
  • Baseline and duplicate filter are dropped on logout or an authStatus change to another account.
  • Credits, the individual spend limit and spend controls are billing state and stay out of the payload; the doc states that the workspace / credit rateLimitReachedType variants therefore carry no reset in this payload and clients must not derive one from the rolling windows.
  • No version field, following authStatus; happy to add one if the repo would rather standardise on the goal / diffStats convention.

Codex maps exhausted quota and plans without included usage onto the same usageLimitExceeded; this extension reports the windows and the reached type as codex reports them and does not try to distinguish those cases.

Testing

src/__tests__/CodexACPAgent/rate-limits-events.test.ts (12 cases): capability marker; payload subset; duplicate suppression; sparse merge keeping name and plan; all-carried-forward update producing no push; per-limitId independence; model-specific limits (normalModelSlug); and the connection seam, driven through the mock app-server, including a two-session fan-out producing exactly one push and the logout reset. With the connection subscription removed, four of the twelve fail. Full suite: 711 passed.

AI usage

Drafted with Claude Code; independently reviewed by two model reviewers (Claude, Codex) before opening, whose findings (the connection-level observation, per-limitId semantics, the account-change reset, the seam tests) are in this version.

🤖 Generated with Claude Code

Codex refuses a turn on a spent window with `codexErrorInfo:
"usageLimitExceeded"`, and that error carries no reset time. The reset
is on the app-server's `account/rateLimits/updated`, which the agent
received and merged but never forwarded: it only fed the `/status`
command's text. A client that wants to park the session and resume it
when the window clears had nothing structured to schedule against.

Push the merged snapshot as a connection-scoped notification, the same
contract shape as `authStatus`: advertised under
`agentCapabilities._meta.rateLimits`, one payload per `limitId`
(`normalModelSlug` names the model of a model-specific limit), pushed
only when that limit's payload changed. The sparse notification is
observed once at the app-server connection, before it fans out to the
per-session handlers (`CodexAppServerClient.onAccountNotification`),
and merged against a connection-level baseline per `limitId`. Neither
per-session state nor the fan-out can serve here: session state is
reset on every session create, so a younger session would report
`planType: null` for the account the older one knew, and the copies a
fan-out delivers through independent asynchronous queues can arrive
out of order and let a stale snapshot overwrite a newer one. The
baseline and the duplicate filter are dropped on logout or an
`authStatus` change to another account. `limitName` and the account
metadata carry forward; the windows are taken as reported. Field names
and units are codex's own `RateLimitSnapshot`. Credits and spend
controls are billing state, not usage windows, and stay out of the
payload; the doc says which reached types therefore carry no reset.

The per-session handler keeps merging into `sessionState.rateLimits`
for `/status` and is otherwise unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant