A hard fork of bb with a Rust control plane and a fixed relay layer. No upstream tracking: this repository owns its code.
The goal is not "bb but rewritten". bb's instability is concentrated in a small number of structural decisions, and each one is being replaced:
| bb | this fork |
|---|---|
| Single-threaded Node event loop doing synchronous SQLite work | Rust control plane; storage behind an async seam |
| Producers mutate an in-process WebSocket hub directly | Producers only publish to a scope; the relay owns delivery |
| Server and worker bound together, one cgroup, one lifetime | Server and worker are independent processes and deployment units |
| Plugin runtime executing arbitrary JS inside the server | No plugin runtime; providers are first-class |
| UI is whatever the local Electron shell boots | UI is a client of a URL; web, PWA and desktop are the same thing |
Early. The foundation is the relay layer, because every other decision depends on it and it can be validated on its own.
-
loom-relay— scoped, sharded, replayable event log -
loom-relay-hub— rooms, idempotent fan-out, backpressure signal -
loom— one binary carrying both roles;loom-server(control plane: HTTP + WebSocket surface, publish reaches subscribers through the log) andloom-worker(execution plane) are roles of it, still two processes -
loom-domain— projects, threads, hosts and environments as pure types and invariants - Managed projects: create / list / rename / archive / sources over HTTP, with threads and environments naming their project (
docs/projects.md) - Managed Git worktrees: a
git-worktreeenvironment provider provisioned by the worker, with.worktreeinclude, branch/base reporting and host-side teardown (docs/worktrees.md) - Server-only startup and an independently stoppable local worker:
loom serverandloom worker, the two subcommands of the one binary -
loom-provider-protocol— the server↔worker ACP execution contract, replayable run events, and a terminal-state guarantee - The event model aligned with bb's
ThreadEventcontract (35 provider event types) — seedocs/event-model.md - Persist domain entities (the domain registry is in-process and lost on restart)
- The server role hosts the UI from its own origin: the product app in
apps/appis compiled into the binary (crates/server/build.rs), so the one artifact carries its client and nothing about serving it is configured (docs/ui.md) - The ported bb app (
apps/app) is the only UI — same-origin typed/api/v1routes, the public/wsrealtime contract, and a recorded bb source baseline (docs/ui-baseline.md) - Check in the Node execution plane (
apps/host-daemon) against the worker contract (loom workeris the reference implementation of that contract and exercises all of it today) - Automations: domain, durable storage, typed HTTP surface, a cron/timezone scheduler and agent execution through the existing thread/run/ACP path (
docs/automations.md) - One server, many workers: the log is in-process or durable on local disk, and a second server sharing it is out of scope (
--redis-urlis a removed tombstone) - bb's HTTP/WebSocket/worker contract exported to JSON Schema, with a Rust conformance harness (
docs/contract.md) - CI on every push and PR: format, lint, the full test suite, the declared MSRV and contract reproducibility (
docs/ci.md)
crates/
loom/ loom the one binary, selecting its role from the
`server` / `worker` subcommand
domain/ loom-domain projects, threads, hosts, environments, scopes, events, runs
relay/ loom-relay scopes, event ids, retention, dedup, backends
relay-hub/ loom-relay-hub connections, rooms, delivery
server/ loom-server HTTP, WebSocket, protocol, dispatch, fixed readers, UI hosting
(the control-plane role's implementation)
provider-protocol/ loom-provider-protocol the server↔worker provider contract
worker/ loom-worker the execution plane's implementation: enrollment,
dispatch, ACP agents (the other role of `loom`)
contract/ loom-contract bb's exported contract as a conformance target
contracts/bb/ generated JSON Schema from bb's contract packages
tools/contract-export/ the exporter that produces contracts/bb
apps/app/ the product app: the only UI, compiled into
the binary by crates/server/build.rs
ui/packages/* the bb packages the product app builds against
(domain, contract, thread-view, …)
containers/ the container images and the compose example
docs/
acp-adapter.md
api-coverage.md
architecture.md
ci.md
containers.md
contract.md
event-model.md
domain-persistence.md
handoff.md
process-model.md
projects.md
provider-protocol.md
provider-sessions-research.md
provider-strategy.md
releasing.md
ui.md
ui-package-sync.md
remote-access.md
mobile.md
upgrades.md
deployment-verification.md
worktrees.md
apps/app is the product app: bb's application source with loom's transport,
loom's routes and the unsupported surfaces removed, in the pnpm workspace and
built by the same pnpm build as everything else. It is the only UI this
repository serves. The baseline, dependency closure and product-surface
decisions are recorded in docs/ui-baseline.md, and the
package sync policy in docs/ui-package-sync.md.
Adaptations are ordinary commits, reviewed on their diff.
The client is compiled into the server, so the app's bundle is a prerequisite of every cargo command:
pnpm install
pnpm build # ui/packages/*, then the product app → apps/app/dist
pnpm test
pnpm check:bundle # the app's boot and lazy-route budget, after the build
cargo build --release -p loom # the one binary both roles run from
cargo test --workspace
cargo clippy --workspace --all-targets
cargo fmt --allcargo build --release -p loom puts the artifact at target/release/loom;
loom server starts the control plane and loom worker an execution machine —
the role is the subcommand, and there are no role symlinks. loom server --local-worker runs both on one box, with the server supervising the worker
child. For a quick start, cargo run -p loom -- server.
The ported thread-view, client-core, core-ui, shared-ui, and contract
packages live under ui/packages/ and are what the app builds against. Their
source pin and deliberate hard-fork synchronization policy are recorded in
docs/ui-package-sync.md; the full app baseline and
migration boundary are in docs/ui-baseline.md.
pnpm build produces the UI bundle at apps/app/dist, which cargo build
compiles into the binary (crates/server/build.rs); the server never reads a UI
directory at runtime, so there is no path to configure. That makes the build a
prerequisite rather than an optional extra: without apps/app/dist, cargo build stops and names pnpm --filter @bb/app run build as the instruction. The
full contract is docs/ui.md.
CI runs the check forms of these on every push and PR, plus the declared MSRV
and the contract-reproducibility check; docs/ci.md lists the
jobs, the required checks and the measured duration.
No external services are required: the default backend is in-process. --data-dir
keeps the replay window on local disk and persists the domain entity view
(projects, threads, hosts, environments) across restarts. See
docs/domain-persistence.md for how domain state
recovers.
loom is one server with many workers. A log shared between servers was tried
as a Redis Streams backend and has been removed: it needs a durable domain store
with a single writer and a story for which server owns a run, and neither exists
yet. --redis-url and LOOM_REDIS_URL are kept as tombstones that fail at
startup with that reason rather than being ignored.
Run it:
# Server-only: the control plane and nothing else. It starts no worker and
# never exits because one is missing.
cargo run -p loom -- server # listens on 127.0.0.1:38886
# Single box: the same command also starts and supervises one local worker.
cargo run -p loom -- server --local-worker
curl localhost:38886/health
curl localhost:38886/api/v1/hosts/primary
curl -X POST localhost:38886/api/v1/publish \
-H 'content-type: application/json' \
-d '{"scope":{"kind":"thread","id":"thr_1"},"payload":"{\"hello\":\"loom\"}"}'
curl 'localhost:38886/api/v1/replay?scope_kind=thread&scope_id=thr_1'Worker-only, in a second terminal. It dials the server outbound and can stop without touching it:
cargo run -p loom -- worker --server-url http://127.0.0.1:38886 --name laptop
# → loom-worker "laptop" enrolled as host_01M… with http://127.0.0.1:38886The same binary runs both roles: loom server and loom worker are the only
invocation, and the role is the subcommand — nothing inspects the process name.
With no worker at all, GET /api/v1/hosts/primary answers 200 with
{"host":null,"source":"no_host"} rather than an error — a server-only
deployment degrades, it does not break. The full boundary contract is in
docs/process-model.md.
Minimal domain commands — create a thread, message it, register a host. Each
publishes a typed loom-domain event through the relay:
# Register a host; the `host_registered` event goes to host:{id}.
curl -X POST localhost:38886/api/v1/hosts \
-H 'content-type: application/json' -d '{"name":"laptop"}'
# List projects. The server seeds one personal project on first start; it is
# an ordinary project from then on. A thread must name its project. The body is
# a bare array of bb's `projectSchema` — no envelope.
curl localhost:38886/api/v1/projects
# Create a thread; the `thread_created` event goes to the project's scope. The
# body is bb's `threads.create` shape and the response is the thread itself.
curl -X POST localhost:38886/api/v1/threads \
-H 'content-type: application/json' \
-d '{"projectId":"proj_...","origin":"app","input":[],"environment":{"type":"project-default"}}'
# Message a thread; it appends and, from idle, starts a run. Both events go
# to thread:{id}, in order. With a worker connected, a `RunDispatch` is also
# published to `host:{id}`; the provider's events (assistant/reasoning deltas,
# tool items, and the terminal `turn/completed`) come back as
# `thread_run_event`s on the thread scope, each carrying a bb `ThreadEvent`.
curl -X POST localhost:38886/api/v1/threads/thr_.../messages \
-H 'content-type: application/json' -d '{"content":"hello"}'
# In-flight runs, if you want to see the dispatch table.
curl localhost:38886/api/v1/runsConnect a raw relay/worker client on ws://127.0.0.1:38886/internal/ws, send
{"type":"subscribe","scope":{"kind":"thread","id":"thr_1"}}, and the
published frame arrives.
The UI is served from the same origin: open http://127.0.0.1:38886/. It is the
product app, built with pnpm --filter @bb/app run build and compiled into the
binary — there is no UI directory to point at and no UI variable to
set. --ui-proxy is the one override, development only, and reverse-proxies
to a dev server; LOOM_UI_DIR, the variable that served a bundle from disk, is
no longer read. The client derives its server from
its own origin, talks
typed /api/v1 routes and the public /ws protocol, and recovers from a
reconnect by invalidating and reloading — the contract is in
docs/ui.md.
The provider contract — ACP dispatch through the relay, the report path and the
guarantee that a run always ends — is specified in
docs/provider-protocol.md.
Deploying the multi-machine shape (server plus execution machines) is the flags
on an ExecStart line: there is no installer and no environment file, and the
walkthrough is docs/process-model.md § Deploying it,
with one systemd unit per role or a single loom server --local-worker unit.
The same two processes are published as container images — docker run, or a
docker compose all-in-one —
docs/containers.md, which is also where the limits of a
containerised execution worker are written down. Remote access is
docs/remote-access.md (Tailscale Serve in front of a
loopback bind), phones are
docs/mobile.md (installed PWA, no worker), and upgrades are
docs/upgrades.md. The recorded clean-machine run is
docs/deployment-verification.md.
The control plane never touches a connection. It calls:
relay.publish(Scope::Thread(thread_id), frame)?;Everything else — which shard that lands on, how long it is retained, which node replays it after an outage, which sockets on that node receive it, and how a duplicate is suppressed — is the relay layer's problem. That is what makes server restarts, reconnects and multi-machine dispatch tractable instead of being tangled through every route handler.
See docs/architecture.md.
MIT. The forked bb sources keep their original license and notices.