Architecture
This page explains how lazybox is put together and why. For the canonical, always-current detail, read CLAUDE.md and DESIGN.md in the repository.
Client / daemon split
Section titled “Client / daemon split”lazybox is organized as a client/daemon split. The daemon (server) owns all state and IO — the PTYs behind embedded terminals, provider polling, and the store. The TUI is a thin renderer on top.
By default both halves run in the same process, connected by a tokio mpsc
channel pair, so there is no serialization cost and nothing extra to launch. The
same code can run out of process: the daemon exposes a Unix socket speaking
length-prefixed bincode, and the TUI connects over it. Because it’s a Unix
socket, SSH local forwarding (ssh -L) carries it across machines — that’s how
remote over SSH works.
The socket starts with an explicit protocol/build handshake. A stale client or daemon is rejected with a restart message before application frames flow, rather than failing later as a decode error. Frames, command queues, event forwarders, and connection admission are bounded; malformed trailing bytes and oversized payloads are rejected. There is no hand-bumped protocol version: compatibility is negotiated with a build-derived wire fingerprint (a hash over the wire-defining sources), so client and daemon connect only when built from identical wire code — anything else is told to restart/upgrade together.
The crates
Section titled “The crates”lazybox is built from 16 crates (including two vendored libghostty crates), split across shared libraries, providers, the daemon, and the client binary. The core libraries are deliberately layered: core and auth depend on no internal crate, and store may depend on core only. That keeps the foundation acyclic and each concern independently testable.
- core — the source-agnostic domain types (Task, Session, Activity, time helpers).
- auth — the credential provider trait and chain.
- store — the persistence trait and its SQLite backend.
Providers (GitHub, Linear, Slack) depend only on core and auth. The daemon side wires everything together; the client renders it.
The event bus
Section titled “The event bus”The reactive behaviour described in the mental model is built on an in-process broadcast bus inside the daemon. Providers produce events as they poll upstream; subscribers consume them — the TUI to render, the JSON API gateway to forward. This producer/subscriber decoupling is why adding a new source (or a new consumer) doesn’t require touching the others.
The credential chain
Section titled “The credential chain”Authentication is a chain of credential providers tried in order, so the common
case needs zero configuration. For GitHub the chain tries the GH_TOKEN and
GITHUB_TOKEN environment variables first, then falls back to running
gh auth token. The chain is trait-based and extensible (env, command,
and static providers exist today).
The embedded terminal
Section titled “The embedded terminal”Each workspace’s terminal is a real PTY, read on a dedicated thread, parsed by a vendored libghostty-vt parser, and rendered as a widget — the same component both the daemon (which owns the PTY) and the TUI (which replays it) use. The daemon keeps a per-terminal ring buffer so that when a client reconnects, the recent screen contents replay instantly instead of starting blank.
Replay uses sequence numbers and gap resynchronization, while all terminal input and scroll mutations pass through one ordered owner. The client therefore reaches the same screen and scrollback state after a reconnect as a fresh attach. Process exit is a first-class lifecycle event carrying the exit code: clean agent exits can close automatically, while crashes and failed starts keep their final screen visible without deleting the workspace.
Where to go next
Section titled “Where to go next”- The mental model for the concepts these mechanisms serve.
- CLAUDE.md and DESIGN.md for the deep, current architecture notes.