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:
co-authored by
Claude Opus 5
parent
7fe84f71e2
commit
ef52aa7ce2
+52
-5
@@ -28,7 +28,8 @@ messages should be real from the beginning.
|
||||
- `lumbridge-pty`: portable PTY and process-tree adapters.
|
||||
- `lumbridge-terminal`: VT parsing, scrollback, selection, search, render model.
|
||||
- `lumbridge-acp`: ACP client, capability negotiation, transcript normalization.
|
||||
- `lumbridge-harness`: manifests, launch profiles, hooks, PTY fallback adapters.
|
||||
- `lumbridge-harness`: manifests, launch profiles, hooks, PTY fallback adapters,
|
||||
and documented status/usage probes.
|
||||
- `lumbridge-provider`: BYOK providers and provider-neutral usage records.
|
||||
- `lumbridge-storage`: SQLite migrations, event log, snapshots, retention.
|
||||
- `lumbridge-remote`: OpenSSH/Tailscale command construction, framed stdio,
|
||||
@@ -41,10 +42,10 @@ messages should be real from the beginning.
|
||||
- `lumbridge`: installable application entry point.
|
||||
|
||||
The scaffold currently contains `lumbridge-core`, `lumbridge-storage`,
|
||||
`lumbridge-buzz`, `lumbridge-pty`, `lumbridge-runtime`, `lumbridge-terminal`, and
|
||||
the entry point. `lumbridge-core` also contains the first typed workspace
|
||||
command reducer. Larger runtime crates are added after their architecture spikes
|
||||
pass.
|
||||
`lumbridge-buzz`, `lumbridge-pty`, `lumbridge-runtime`, `lumbridge-terminal`,
|
||||
`lumbridge-harness`, and the entry point. `lumbridge-core` also contains the
|
||||
first typed workspace command reducer and the usage ledger. Larger runtime
|
||||
crates are added after their architecture spikes pass.
|
||||
|
||||
## Terminal path
|
||||
|
||||
@@ -200,6 +201,52 @@ Observations include account profile, provider, harness, model, units, time
|
||||
window, reset time, provenance, confidence, and source timestamp. Projections are
|
||||
derived views that can be recomputed as forecasting improves.
|
||||
|
||||
`lumbridge-core` now implements that stream. A bounded per-profile `UsageLedger`
|
||||
accepts observations in time order, and `project` derives the footer view:
|
||||
window fraction, reset, burn rate, and exhaustion. Facts keep their reported
|
||||
provenance; every derived value is labelled estimated. The projection withholds
|
||||
a burn rate from a single sample, withholds a window fraction without a reported
|
||||
ceiling, withholds an exhaustion estimate that falls after the reset, treats an
|
||||
unavailable observation as invalidating older facts, and reports an expired
|
||||
window as rolled over rather than freezing its last percentage. `FooterUsage`
|
||||
renders the five product-spec questions and returns explicit unavailable phrases
|
||||
instead of placeholder numbers. See decision 0012.
|
||||
|
||||
`lumbridge-harness` is the other side of that boundary: it runs processes,
|
||||
reads a clock, and parses untrusted wire text, then hands back observations.
|
||||
Its first adapter probes Codex's documented `account/rateLimits/read` surface
|
||||
over the app-server's JSON-RPC stdio protocol. 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. All protocol decisions live in a pure state machine driven by
|
||||
`&str` lines and an explicit timestamp, with a `replay_transcript` entry point
|
||||
that verifies the whole path against a fixture without a process, a clock, or a
|
||||
network. See decision 0013.
|
||||
|
||||
Its second adapter reads Claude Code's session transcripts, the only local
|
||||
surface that reports what that harness spent. It is a tail follower with a
|
||||
per-file byte offset, so its total is monotonic, and it reports nothing until
|
||||
it has read the existing backlog to the end — a partially-read backlog is
|
||||
indistinguishable from a burst of spend, and the ledger would derive a rate
|
||||
from it. The transcript carries no window or ceiling, so that profile reports
|
||||
consumption against none. The parser models four token counters and nothing
|
||||
else, so the conversations in those files are not representable in a Lumbridge
|
||||
value. See decision 0014.
|
||||
|
||||
Claude Code's subscription windows come from a different surface: the CLI pipes
|
||||
a `rate_limits` object carrying the five-hour and seven-day windows to whatever
|
||||
`statusLine` command the user has configured, on every turn. A small installed
|
||||
bridge writes those fields — and only those — to a local feed the probe tails,
|
||||
so the windows are `ProviderReported` and no credential is ever read. Reading
|
||||
Claude Code's OAuth token would be more capable and is what comparable tools
|
||||
do; Lumbridge does not, because AGENTS.md forbids it. See decision 0015.
|
||||
|
||||
The GPUI shell now runs both probes; a probe contributes its own profiles on
|
||||
top of the declared ones, so the Claude windows appear once they report. Its
|
||||
footer strip
|
||||
shows every declared harness at once, and a harness with no adapter renders
|
||||
the honest gap.
|
||||
|
||||
Subscription balance is provider-specific and sometimes unavailable. BYOK calls
|
||||
usually expose token counts but cost still depends on cached tokens, reasoning,
|
||||
tool calls, and current pricing. Adapters normalize facts without erasing their
|
||||
|
||||
@@ -97,7 +97,13 @@ Terminal, Browser, Markdown, and Review panels beside the selection or
|
||||
reattaches an existing session. Every Terminal panel receives an independent
|
||||
actor-owned VT session; the three seed non-terminal surfaces keep deterministic
|
||||
updates running. Counters
|
||||
separate external PTY batches/lines from total model updates. The GPUI footer
|
||||
separate external PTY batches/lines from total model updates. The GPUI footer's
|
||||
usage zone is no longer a constant string: it renders a `lumbridge-core`
|
||||
projection for the selected panel's account profile, with a provenance chip and
|
||||
explicit unavailable phrases, and its sidebar attention count and runtime rows
|
||||
are derived from live panel and ledger state. The Floem shell still renders the
|
||||
static footer fixtures, so the two candidates are no longer comparable on that
|
||||
strip. The GPUI footer also
|
||||
reports dispatch-to-element-build p50/p95 over a bounded 256-sample window. It
|
||||
is deliberately not called key-to-present or frame-present latency: neither
|
||||
candidate exposes a reliable public cross-platform post-present callback. True
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# 0012: The footer renders a derived projection, never a stored percentage
|
||||
|
||||
Status: accepted. Supplemented by decisions 0013 and 0014, which supply the
|
||||
first two real adapters. Every rule below still holds.
|
||||
|
||||
Decision 0002 established that usage observations carry provenance. This
|
||||
decision fixes how those observations become the five answers the footer owes
|
||||
the user, and what the footer must do when it cannot answer.
|
||||
|
||||
## The contract
|
||||
|
||||
`lumbridge-core` owns an append-only `UsageLedger` of `UsageObservation`s per
|
||||
`AccountProfile`. An observation is a fact: consumed units, an optional ceiling,
|
||||
an optional provider window, a provenance, and the time it was observed. The
|
||||
ledger never edits an observation and refuses one that predates the newest entry
|
||||
for that profile, because an append-only stream cannot move backwards.
|
||||
|
||||
`UsageLedger::project` derives the footer view. Reported facts keep their own
|
||||
provenance. Anything the projection computes is labelled `Estimated` even when
|
||||
every input was provider-reported, because a derived rate is not a reported
|
||||
fact.
|
||||
|
||||
## Rules the projection enforces
|
||||
|
||||
- A burn rate needs at least two observations inside the same window, the same
|
||||
unit, and a minimum elapsed sample. One reading yields no rate at all rather
|
||||
than a rate of zero.
|
||||
- A window fraction requires a reported ceiling. A self-hosted endpoint has no
|
||||
quota, so it reports consumption and explicitly says no ceiling exists.
|
||||
- An exhaustion estimate is withheld when the window resets first. Telling a
|
||||
user they will run out after the quota has already refilled is worse than
|
||||
telling them nothing.
|
||||
- An `Unavailable` observation invalidates older facts for that profile instead
|
||||
of letting a stale reading keep rendering as current.
|
||||
- An expired window is reported as rolled over, not as a frozen percentage from
|
||||
a window that ended.
|
||||
- Staleness is surfaced, not hidden: past the staleness budget the value still
|
||||
renders but its confidence drops to unknown and the footer says so.
|
||||
|
||||
## What the UI may do
|
||||
|
||||
A compact surface may substitute the short provenance label. It may not drop the
|
||||
label, round an estimate into a reported fact, or substitute a placeholder
|
||||
number for a missing one. `FooterUsage` therefore returns explicit phrases —
|
||||
`usage unavailable`, `reset time unavailable`, `burn rate unavailable`,
|
||||
`no usage source` — and the GPUI shell renders unavailable values in the quiet
|
||||
tone with the provenance chip coloured by source rather than by value.
|
||||
|
||||
A pane with no harness attached shows the detached roll-up. It never inherits
|
||||
another pane's account, because attributing one agent's spend to another is the
|
||||
same class of untruth as inventing the number.
|
||||
|
||||
## Status of the numbers
|
||||
|
||||
The GPUI shell's observations are still a declared spike fixture. The path is
|
||||
real: the fixture records through the same ledger and reads back through the
|
||||
same projection, so the honest-gap behaviour above is exercised on every frame
|
||||
rather than asserted only in tests.
|
||||
|
||||
The first real adapter now exists in `lumbridge-harness` (decision 0013) and its
|
||||
integration tests drive a synthetic Codex transcript through this ledger and
|
||||
this footer, proving every rule above survives the adapter path. Pointing the
|
||||
running shell at a live account spawns a process against the user's own
|
||||
subscription, so it stays a separate, user-consented step.
|
||||
|
||||
All arithmetic is integer arithmetic and fractions are carried as permille, so a
|
||||
rendered percentage cannot drift from the stored fact.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,103 @@
|
||||
# 0014: Claude Code usage comes from its transcripts, and reports spend without a ceiling
|
||||
|
||||
Status: accepted for token spend. **Corrected by decision 0015**: this record
|
||||
originally claimed Claude Code exposes no quota window. That was wrong — it
|
||||
does, through the status line — and the claim is struck below rather than
|
||||
quietly edited out, because the reasoning that followed from it shaped the
|
||||
adapter.
|
||||
|
||||
Decision 0013 established the provenance test for a usage adapter: does the
|
||||
harness forward a provider's number, or record its own? Claude Code is the case
|
||||
that makes the second half of that test matter.
|
||||
|
||||
## Correction
|
||||
|
||||
The original text asserted that "Claude Code publishes no quota API and no
|
||||
remaining-balance command" and that a user asking how much they have left
|
||||
"gets the honest answer — Lumbridge can show what was spent, and cannot show
|
||||
what remains, because Claude Code does not say."
|
||||
|
||||
**Both statements are false.** Claude Code 2.1.80 and later pipe a
|
||||
`rate_limits` object with `five_hour` and `seven_day` windows to the configured
|
||||
`statusLine` command on every turn, and the CLI documents that shape itself.
|
||||
There is also an account usage endpoint. The correct claim is narrower and is
|
||||
the only one this record still makes: *the transcript* carries spend and not
|
||||
quota. Decision 0015 covers the window surface.
|
||||
|
||||
## The surface
|
||||
|
||||
What Claude Code writes to disk per session is the API's `usage` object for
|
||||
every assistant turn,
|
||||
appended to a per-session JSONL transcript under
|
||||
`$CLAUDE_CONFIG_DIR/projects` (default `~/.claude/projects`). Each assistant
|
||||
record carries `input_tokens`, `output_tokens`,
|
||||
`cache_creation_input_tokens`, and `cache_read_input_tokens`, alongside the
|
||||
model that produced it.
|
||||
|
||||
Summing those is how this adapter reports spend. **A subscription's window,
|
||||
ceiling, and reset are not in the transcript**, so this profile reports
|
||||
consumption against no ceiling rather than implying a quota — but that is a
|
||||
fact about this file, not about Claude Code, and the quota is read separately
|
||||
under decision 0015.
|
||||
|
||||
The `statusLine` hook does require mutating the user's `settings.json`, which
|
||||
is why the transcript came first: reading a file the harness already writes
|
||||
needs no change to the user's configuration. That was a good reason to build
|
||||
this adapter, and a bad reason to conclude the other surface did not exist.
|
||||
|
||||
## Why harness-reported
|
||||
|
||||
Claude Code forwards the provider's per-response counts faithfully, so a single
|
||||
record's numbers originate with the provider. But a running total is the
|
||||
*harness's* record — an append-only log Lumbridge is reading, not a provider's
|
||||
statement of account. Under 0013's rule the value is therefore
|
||||
`HarnessReported`, and the footer says `harness · approximate`.
|
||||
|
||||
## Content blindness is structural, not procedural
|
||||
|
||||
**These files contain the user's conversations.** The parser models exactly
|
||||
four integers, a record type, and a model name. Every other field — message
|
||||
text, tool inputs, file contents, `service_tier`, `inference_geo`,
|
||||
`server_tool_use` — is discarded by serde during parsing, because there is no
|
||||
field in the wire types that could hold it. A test asserts that no debug
|
||||
rendering of a parsed record can contain message content.
|
||||
|
||||
That is the same discipline as the Codex probe's refusal path: the safe
|
||||
behaviour is made unrepresentable rather than merely avoided.
|
||||
|
||||
## What "tokens" counts
|
||||
|
||||
The reported number is `input + output + cache_creation + cache_read`.
|
||||
|
||||
Cache reads are included because they are billed and do count against a
|
||||
subscription window, even though they are re-reads of context already counted
|
||||
once. Excluding them would understate consumption. Weighting them by price
|
||||
would require a price list this adapter does not have, and would turn a counted
|
||||
fact into an estimate. In practice cache reads dominate the total — on the
|
||||
transcripts this was verified against they were roughly 98% of it — so the
|
||||
derived burn rate is largely a measure of context re-read per turn rather than
|
||||
of new work. The split is preserved in `TokenTally` for a detail view that can
|
||||
say so.
|
||||
|
||||
## Priming before reporting
|
||||
|
||||
The probe is a tail follower: it remembers a byte offset per transcript and
|
||||
reads only what was appended since, so its running total is monotonic by
|
||||
construction. The ledger rejects an observation that moves a profile backwards,
|
||||
so this is a correctness requirement, not an optimisation.
|
||||
|
||||
It also **reports nothing until it has read every existing transcript to its
|
||||
end**. A partially-read backlog is indistinguishable from an enormous burst of
|
||||
spend, and the ledger will derive a burn rate from it: the first run against a
|
||||
20 MB backlog with an 8 MB per-poll budget reported **forty-six billion tokens
|
||||
an hour**, which was catch-up, not usage. The first reading must be a true
|
||||
baseline before any rate derived from it can mean anything. A shrinking file
|
||||
resets its own offset rather than trusting a position into different content.
|
||||
|
||||
## Bounds
|
||||
|
||||
A scan walks at most six directory levels and 4096 files, never follows
|
||||
symlinks out of the tree, reads at most 8 MB per poll, and refuses a single
|
||||
record over 1 MB while still advancing past it. A transcript's last line is
|
||||
routinely a partial record still being written; it is left unconsumed for the
|
||||
next read rather than parsed or skipped.
|
||||
@@ -0,0 +1,97 @@
|
||||
# 0015: Claude Code's subscription windows come from the status line, not the credential
|
||||
|
||||
Status: accepted; bridge installed and verified end to end.
|
||||
|
||||
Decision 0014 concluded that Claude Code cannot report a remaining balance.
|
||||
That was wrong, and the error was one of not looking rather than of reasoning:
|
||||
the transcript genuinely carries no quota, and that fact was generalised into a
|
||||
claim about the harness. Claude Code reports its quota through three other
|
||||
channels.
|
||||
|
||||
## The four surfaces
|
||||
|
||||
| Surface | Carries | Cost | Needs a credential |
|
||||
|---|---|---|---|
|
||||
| **Status line `rate_limits`** | `five_hour`, `seven_day` — `used_percentage` + `resets_at` | none; relayed from headers the CLI already received | no |
|
||||
| Account usage endpoint | the same windows, plus per-model scoped limits | tight budget; 429s under polling | **yes** |
|
||||
| Stream `rate_limit_info` | `status`, `rateLimitType`, `unifiedWindows` | none | no, but requires driving the harness |
|
||||
| `/usage` in a PTY | rendered text | a turn | no, but screen-scraped |
|
||||
|
||||
The status-line shape is documented inside the CLI itself:
|
||||
|
||||
```text
|
||||
"five_hour": { // present only while the API reports it and its resets_at has not passed
|
||||
"used_percentage": number, // Percentage of limit used (0-100)
|
||||
"resets_at": number // Unix epoch seconds when this window resets
|
||||
}
|
||||
```
|
||||
|
||||
`rate_limits_available` is the honest-gap signal, also documented: "False when
|
||||
plan rate limits do not apply (API key, Bedrock, Vertex, or missing profile
|
||||
scope)". An account with no subscription window says so, rather than leaving
|
||||
Lumbridge to infer it from an absence.
|
||||
|
||||
## Why the status line and not the endpoint
|
||||
|
||||
The account usage endpoint is more authoritative — it answers without an active
|
||||
session and exposes per-model scoped limits. It also requires reading Claude
|
||||
Code's OAuth credential, and AGENTS.md says Lumbridge "must not scrape their
|
||||
private credentials." Other tools in this space do read it; that is their call
|
||||
to make, and it is a real capability difference.
|
||||
|
||||
Lumbridge keeps the rule. The status line is pushed *to* a command the user
|
||||
installs, so no credential is ever touched, and the data is free because the
|
||||
CLI is relaying rate-limit headers it already received on its own API calls.
|
||||
The cost is that a window only appears once a session has run, and that
|
||||
installation edits `settings.json`.
|
||||
|
||||
The endpoint is recorded here as a rejected-for-now alternative rather than an
|
||||
unconsidered one. Reopening it means amending AGENTS.md first.
|
||||
|
||||
## Provenance
|
||||
|
||||
`ProviderReported`. Under decision 0013's test the CLI forwards the provider's
|
||||
number rather than computing one — the same standing as Codex's forwarded
|
||||
`x-codex-*` headers. The footer shows `provider · exact`.
|
||||
|
||||
## The bridge
|
||||
|
||||
`scripts/claude-statusline-bridge.sh` is installed as Claude Code's
|
||||
`statusLine` command. It appends the rate-limit fields — and only those — to
|
||||
`$XDG_DATA_HOME/lumbridge/claude-rate-limits.jsonl`, and prints a status line.
|
||||
|
||||
Three properties it must hold:
|
||||
|
||||
- **It cannot fail.** A status-line command that errors or hangs degrades the
|
||||
user's session, so every step is guarded and it always exits 0 having
|
||||
printed something.
|
||||
- **It copies nothing else.** The payload also carries the session's cost,
|
||||
transcript path, working directory, and model. The bridge builds a fresh
|
||||
record from three numeric fields per window rather than filtering the
|
||||
original, so there is no path by which the rest travels.
|
||||
- **It stays bounded.** The feed rotates past 256 KB. Lines are appended rather
|
||||
than overwritten so concurrent sessions do not clobber each other; the probe
|
||||
reads the newest line, because each line is a complete snapshot and an older
|
||||
one is superseded, never summed.
|
||||
|
||||
Installation is a separate, explicit, reversible script — not something a probe
|
||||
does on the user's behalf. It backs up `settings.json`, writes through a
|
||||
temporary file, refuses to replace a `statusLine` someone else configured, and
|
||||
supports `--dry-run` and `--uninstall`.
|
||||
|
||||
## Window handling
|
||||
|
||||
Each window becomes its own account-free profile — `claude-code-five-hour` and
|
||||
`claude-code-seven-day` — because two windows are two facts about two quota
|
||||
periods, and merging them means silently picking one. The pane maps to the
|
||||
five-hour window: it is the limit that actually stops work.
|
||||
|
||||
A reading's window start is derived from `resets_at` minus the documented
|
||||
window length, which is what lets the ledger scope a burn rate to one quota
|
||||
period. Two rules follow decision 0012 unchanged: a window whose `resets_at`
|
||||
has passed is stale, so its reset is dropped and only the percentage survives;
|
||||
and a start the local clock places in the future costs the start, not the
|
||||
reset, which is the half that bounds a forecast.
|
||||
|
||||
A percentage above 100 — possible on a gateway spend limit once breached — is
|
||||
clamped to the whole rather than stored as more than a full window.
|
||||
Reference in New Issue
Block a user