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
+52 -5
View File
@@ -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
+7 -1
View File
@@ -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.