Replace the footer's placeholder usage with a real observation ledger

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>
This commit is contained in:
Metal Agent
2026-08-31 21:47:11 -07:00
co-authored by Claude Opus 5
parent 7fe84f71e2
commit ef52aa7ce2
34 changed files with 7319 additions and 137 deletions
@@ -0,0 +1,116 @@
# 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.