Files
lumbridge-code/docs/ARCHITECTURE.md
T
Metal AgentandClaude Opus 5 ef52aa7ce2 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>
2026-08-31 21:47:11 -07:00

293 lines
16 KiB
Markdown

# Architecture
## Recommended shape
Lumbridge should be two cooperating Rust processes:
```text
native desktop UI
| local authenticated IPC
Lumbridge session runtime
|-- local PTYs and process trees
|-- SSH/Tailscale transport to remote Lumbridge runtimes
|-- ACP clients and adapters
|-- repositories and worktrees
|-- usage/event ledger
`-- secret-store handles
```
Separating the UI from the session runtime lets terminals survive renderer
restarts, permits a future headless/SSH mode, and gives persistence one owner.
The first development build may run both in one process, but boundaries and IPC
messages should be real from the beginning.
## Planned crates
- `lumbridge-core`: IDs, commands, events, errors, capability and usage types.
- `lumbridge-runtime`: session ownership, supervision, recovery, IPC server.
- `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,
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,
handshake, reconnect, heartbeat, and remote-runtime discovery.
- `lumbridge-secrets`: Keychain/libsecret adapters and redaction.
- `lumbridge-git`: repositories, worktrees, diffs, status, conflict state.
- `lumbridge-buzz`: credential-free Buzz event/broker preparation and pane-share
safety gates; platform adapters own signing and transport.
- `lumbridge-ui`: native desktop state and rendering.
- `lumbridge`: installable application entry point.
The scaffold currently contains `lumbridge-core`, `lumbridge-storage`,
`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
The runtime owns PTYs, child process groups, resize signals, input ordering, and
raw output. The first `lumbridge-pty` boundary now uses `portable-pty` behind a
single-owner session, a fixed credential-free child environment, validated
sizes, an 8 KiB bounded output queue, resize/input/wait operations, and Unix
process-group cleanup. It deliberately does not emulate a terminal or persist
scrollback. The terminal engine turns output into immutable render snapshots and
bounded deltas for the UI. Scrollback is chunked and persisted separately from
the live screen to prevent large agent transcripts from blocking input.
The first `lumbridge-terminal` implementation wraps `alacritty_terminal` behind
a Lumbridge-owned input and immutable snapshot contract. It parses styled cells,
cursor state, alternate screen, terminal modes, title events, keyboard input,
bracketed paste, resize, and terminal-generated protocol replies. OSC 52
clipboard writes are disabled by default. The renderer never imports the
upstream terminal type, and protocol replies return through the same actor input
queue as human input. See decision 0005.
Each `lumbridge-runtime` actor owns one `PtySession` on a dedicated thread.
Bounded command and event queues serialize input, resize, close, and shutdown
against ordered raw-byte output. A generic pane-indexed registry now owns and
routes multiple actors without knowing whether their panels are attached. The
GPUI slice feeds three independent actor sessions through three terminal engines
while three non-terminal surfaces retain deterministic comparison output. Its
adapter groups adjacent cells into native GPUI paint runs and renders
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
drives terminal rows and columns and resizes both the engine and PTY. The engine
now exposes retained-history offsets and page/top/bottom viewport movement
without writing scroll keys to the PTY. Text selection, mouse reporting, and a
lower-level terminal canvas remain. These actors still run in-process; moving the
same framework-neutral contract behind local authenticated IPC is the next
durability step. See decisions 0004 and 0005.
## Default workspace composition
The default desktop workspace is a responsive horizontal row of self-contained
vertical panels: one panel in compact windows, three at normal desktop widths,
and five on a 3440 px ultrawide. Inside every panel, the top 20% holds that pane's
agent, tool, context, goal, target, and status; the middle 60% holds its terminal
or work surface; and the bottom 20% holds that pane's answer, choices, chat, and
approval boundary. The selected panel alone owns keyboard input. Six surfaces
continue updating so performance comparisons remain meaningful, with a sliding
visible window keeping the selected pane onscreen. See decisions 0008 and 0009.
Panel attachment is independent from session lifetime. Detaching removes a pane
from the layout tree but retains its definition in the workspace's detached
registry and leaves its runtime-owned process alive. Reattaching restores that
identity without a launch. Terminating an attached or detached session is a
separate Execute-capability operation. The GPUI slice now layers an unbounded
panel registry above the six-surface comparison fixture. New Terminal, Browser,
Markdown, and Review panels receive monotonic local IDs, are inserted beside
the selection, and persist their identity, order, selection, and attachment in
a credential-free SQLite snapshot. Each Terminal panel is keyed into the
runtime registry by that durable panel ID. See decisions 0010 and 0011.
We should evaluate, not blindly copy, WezTerm, Zellij, RMUX, tmux, and cmux. The
first spike must compare a reusable terminal crate with a small first-party layer.
Remaining correctness cases include OSC 8 links, Kitty
keyboard/graphics negotiation, Unicode width, IME, mouse modes, shell integration,
and simultaneous automation plus human input.
## Workspace command plane
Human UI, local automation, remote clients, and agents reduce the same typed
workspace commands. Stable request IDs make retries idempotent; validated pane
IDs, split ratios, launch intents, and close dispositions prevent untyped JSON
from becoming the product API. A multi-command setup plan is reduced atomically.
Observe, Configure, and Execute capabilities are checked before mutation, and a
layout-only agent cannot smuggle an executable or arbitrary arguments into a
split request. Current code is an in-process reducer; serialization, durable
events, and authenticated IPC are subsequent layers. See decision 0006.
## UI decision gate
Do not lock the project to a webview or to Zed's application implementation
details. The measured spike compares:
1. GPUI for a Zed-like native model and excellent text-heavy interaction.
2. Floem for an independent native Rust model with existing editor primitives.
The winner must render six busy panes smoothly, keep input latency low, support
IME/accessibility, package on macOS and both Linux targets, and avoid a license
or upstream-stability trap. Current Zed GPUI is the provisional winner because
its AccessKit semantics compile while the pinned Floem revision has no semantic
accessibility integration. This remains behind a narrow adapter until platform
assistive-technology, IME, packaging, licensing, and release measurements pass.
## Local-first and remote session path
SQLite is device-local. It stores workspace metadata, pane layouts, event and
usage history, remote routing profiles, and small snapshots. It never stores SSH
private keys, Tailscale credentials, provider API keys, or subscription tokens.
Secrets remain in the OS credential store or in the user's existing SSH agent.
A remote pane is not a local PTY wrapped around a long-lived `ssh` process. Its
durable owner is a per-user `lumbridge-runtime` on the destination:
```text
MacBook Lumbridge UI/runtime
|
| ssh host lumbridge remote connect --stdio
| or: tailscale ssh host lumbridge remote connect --stdio
v
remote per-user Lumbridge runtime -- Unix socket -- PTYs, agents, worktrees
|
`-- remote SQLite + chunked scrollback on that machine
```
The SSH child carries a versioned framed protocol over stdio. Normal OpenSSH
remains the default because it honors the user's config, agent, host keys,
ProxyJump, and Tailscale addresses. `tailscale ssh` is an explicit transport for
users who want Tailscale's SSH proxy and host-key path. Lumbridge does not
configure a tailnet, weaken ACLs, copy SSH keys, or require a listening TCP port.
The remote runtime assigns a stable session ID before acknowledging a launch.
On network loss the local pane becomes disconnected, the remote PTY continues,
and reconnect resumes from the last acknowledged output sequence. A second
authorized Lumbridge installation can attach to the same remote session after
the remote runtime arbitrates input ownership. Collaborative simultaneous input
is not part of the first release.
## Harness integration
Each harness is described by a versioned manifest: executable discovery, launch
arguments, environment allowlist, resume semantics, status/usage probes, ACP
command when available, and hook installation rules.
ACP is preferred because it provides structured prompts, plans, tool calls,
permissions, content blocks, and session lifecycle. A supervised PTY is always
available because terminal fidelity and arbitrary CLI compatibility are product
requirements. Harness-specific adapters translate both paths into the same event
model without pretending a PTY has capabilities it cannot prove.
Lumbridge Harness is a separate optional orchestrator, not a UI framework. The
DeepSeek Harness snapshot is a useful MIT-licensed reference for plugin seams,
durable session events, approvals, ACP, and agent lifecycle, but its developer-
preview Node/TypeScript/web composition is not embedded into the Rust desktop
process. If we maintain a fork, it lives in its own repository and communicates
through a versioned, capability-scoped protocol. A first-party Rust service may
later replace that implementation without changing the desktop contract.
The Harness can consume redacted workspace projections and emit suggestions or
typed command plans. Suggestions have no authority. Plan execution is routed
through the same command plane as human actions, and trace export to a hosted
model requires explicit scope and destination consent. See decision 0007.
## Usage model
Usage is an append-only observation stream, not a mutable percentage field.
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
source or uncertainty.
## Buzz collaboration
Buzz channel messages, replies, agents, and attachments use the upstream Rust
SDK's signed Nostr semantics. SQLite stores a public identity and opaque
credential-store handle, never the private identity key. Pane images cross the
network only after local capture, redaction preview, explicit destination, and
confirmation. See `BUZZ_INTEGRATION.md` for the contract and test plan.
## Persistence
SQLite in WAL mode stores metadata, commands/events, normalized usage, and small
snapshots. Large scrollback chunks and binary attachments use content-addressed
files. A write-ahead event is committed before an external mutation is reported
as accepted. Startup replays incomplete operations and reconciles live children.
Default data roots are `~/Library/Application Support/ai.karti.lumbridge/` on
macOS and `${XDG_DATA_HOME:-~/.local/share}/lumbridge/` on Linux. Backups and
exports are explicit; Lumbridge does not synchronize the database through a
hidden hosted account.
## Security
- macOS secrets: Keychain; Linux secrets: Secret Service/libsecret, with an
explicit encrypted-file fallback only if the user enables it.
- Never pass keys in process arguments. Prefer inherited file descriptors or a
minimal child environment when a harness requires environment variables.
- IPC is local, authenticated, permission-restricted, and version-negotiated.
- Transcript and crash-report redaction happens before persistence or export.
- Repository trust, harness approval mode, and sandbox mode are visible per pane.
- Remote control and plugins are out of scope until a capability/permission model
exists.
## Platform adapters
Shared contracts cover PTY, process tree, notifications, secret store, paths,
autostart, updater, and packaging. macOS uses `forkpty`/process groups and native
Keychain. Ubuntu and Omarchy use Unix PTYs, cgroups/systemd scopes when available,
and Secret Service. Omarchy is treated as Arch Linux, not as a separate kernel.