The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
6.6 KiB
Markdown
117 lines
6.6 KiB
Markdown
# 0013: The first usage adapter is the Codex app-server rate-limit probe
|
|
|
|
Status: accepted; wire contract verified against one live account.
|
|
|
|
Decision 0012 built a usage ledger and a footer that refuse to invent numbers,
|
|
but every observation reaching them was a declared fixture. This decision picks
|
|
the first real source and records the rules that choice establishes for the
|
|
adapters after it.
|
|
|
|
## Why Codex first
|
|
|
|
Of the seven captured harnesses, Codex is the only one whose quota surface is
|
|
all four of: prose-documented upstream with a worked example
|
|
(`codex-rs/app-server/README.md`, "7) Rate limits (ChatGPT)"), pinned by a
|
|
checked-in JSON Schema, genuinely provider-originated, and readable without
|
|
Lumbridge touching a credential. `account/rateLimits/read` returns
|
|
`usedPercent`, `windowDurationMins`, and `resetsAt`; the app-server parses
|
|
those out of the backend's `x-codex-*-used-percent` response headers.
|
|
|
|
Lumbridge launches `codex app-server` and lets the harness resolve its own
|
|
authentication from `CODEX_HOME`. That satisfies the product spec's
|
|
"observes only documented status, usage, and protocol surfaces" without any of
|
|
the credential handling AGENTS.md forbids. Upstream is Apache-2.0; only the
|
|
observable wire contract is mirrored, and its source, commit, and license are
|
|
recorded in the parser and the tests.
|
|
|
|
## The provenance test for every future adapter
|
|
|
|
Ask whether the harness **computes** the value or **forwards** it.
|
|
|
|
- Forwards a provider's own number: `ProviderReported`.
|
|
- Computes, counts, or estimates it: `HarnessReported`, or `LocallyMeasured`
|
|
when Lumbridge did the counting.
|
|
|
|
Codex forwards, so its windows are provider-reported. The ACP `usage_update`
|
|
notification, by contrast, is a *context window* gauge, and at least one
|
|
captured harness fills it with an explicit local estimate. It must never be fed
|
|
through the same path: a context window is not a quota, and rendering one as
|
|
"62% used" would imply a subscription limit that does not exist.
|
|
|
|
## Rules this adapter establishes
|
|
|
|
- **Refusal is structural, not procedural.** The app-server can send its client
|
|
`account/chatgptAuthTokens/refresh`, which returns a bare access token. The
|
|
client has no free-form method string: outbound requests come from a
|
|
two-variant enum, and every server-to-client request is answered `-32601`
|
|
regardless of what it asks for. Asking for a credential is unrepresentable
|
|
rather than merely declined, and refusals are counted so the path is
|
|
auditable.
|
|
- **An account with no quota is a fact, not a fault.** Codex answers `-32600`
|
|
for an API-key user. That records an unavailable observation for both
|
|
windows. Any other rejection faults the probe.
|
|
- **Two windows are two profiles.** `primary` and `secondary` get separate,
|
|
account-free profile identifiers. Merging them would mean silently choosing
|
|
one. A profile ID is persisted, so it never carries an account identity.
|
|
- **A sparse push neither clears nor merges.** Upstream documents that nullable
|
|
values absent from a rolling update "do not clear a previously observed
|
|
value". An absent window on a notification therefore records nothing. It is
|
|
also not merged into the last full read: a merged composite would present
|
|
itself as provider-reported while being partly a memory.
|
|
- **Clock disagreement costs the window, not the number.** A window whose
|
|
computed start is after the observation is dropped while the provider's
|
|
percentage is kept. A reading taken at or after `resetsAt` is unusable
|
|
entirely, because 0012 requires an ended window to read as rolled over.
|
|
- **Identical readings are suppressed, with a floor.** The ledger retains a
|
|
bounded history per profile; re-recording an unchanged percentage every poll
|
|
would evict real history and destroy the burn baseline. An unchanged reading
|
|
is re-emitted only after five minutes, which keeps a rate derivable without
|
|
flooding.
|
|
- **Errors cannot carry text.** `HarnessError` is `Copy` and holds only static
|
|
discriminators, an `io::ErrorKind`, and numbers. A spawn failure deliberately
|
|
discards the underlying error so a filesystem path cannot travel into a log,
|
|
and a rejection keeps the JSON-RPC code while discarding the backend message.
|
|
- **The child is contained.** Cleared environment with a literal allowlist,
|
|
pipes rather than a PTY, stderr discarded, and a per-line byte cap. The child
|
|
leads its own process group and the group is what gets killed, because a pipe
|
|
reports end of file only when every write end closes: a launcher that execs
|
|
the real program as a grandchild with inherited stdio — which is how the npm
|
|
distribution of Codex works — would otherwise leave a reader blocked on a pipe
|
|
nothing will ever write to. Shutdown signals, kills the group, and joins only
|
|
the worker. The reader is never joined, so a descendant that escapes the group
|
|
can leak a parked thread but cannot hang the caller, which matters because
|
|
`Drop` calls shutdown.
|
|
|
|
## Verified against a live account
|
|
|
|
One reading was taken with the user's consent on 2026-08-31, against
|
|
`codex-cli 0.145.0` installed as a standalone native binary. It confirmed the
|
|
whole contract: `usedPercent` an integer, `resetsAt` Unix seconds, `secondary`
|
|
null, and no unmodelled fields inside the window object. The result body also
|
|
carried `rateLimitResetCredits`, `rateLimitsByLimitId`, `planType`, `limitId`,
|
|
`limitName`, `individualLimit`, `credits`, and `spendControlReached`, all
|
|
correctly ignored by the parser. Two unsolicited notifications arrived during
|
|
the exchange — `configWarning` and `remoteControl/status/changed` — which
|
|
exercised the ignore path with real traffic. No server-to-client request
|
|
arrived, so the refusal path remains proven only by test.
|
|
|
|
The live account's `windowDurationMins` was **10080**: a weekly window, not the
|
|
fifteen-minute example in the upstream README. That exposed a real defect no
|
|
fixture built from the documented example would have caught —
|
|
`format_duration_ms` rendered it as "151h 30m" — and days were added to the
|
|
formatter as a result. Fixtures agree with documentation; only a real account
|
|
disagrees with your assumptions.
|
|
|
|
## What is still not true
|
|
|
|
The whole test suite runs without the `codex` binary, a `ChatGPT` account, or
|
|
network access, and that property must be preserved: CI must never depend on an
|
|
installed harness. A single live reading verifies the shape of one account on
|
|
one plan; it does not exercise the `secondary` window, the `-32600` no-quota
|
|
path, or a window that rolls mid-read. Those remain covered by synthetic
|
|
transcripts only.
|
|
|
|
Wiring the probe into the running shell means spawning a process against the
|
|
user's own subscription account on every launch. Until that is wired, the shell
|
|
keeps rendering its declared fixture.
|