Carry

How it works

The daemon owns sessions and clients attach to them. For omp, the app also speaks the harness’s own collab protocol, so it does not have to scrape a terminal. Three sources feed one reducer in the app.

Carry architecture The phone app contains one reducer fed by three sources. Source one: terminal frames from a carry daemon over iroh. Source two: collab frames from a running omp over omp's encrypted relay. Source three: structured frames from a daemon-hosted omp RPC session, also over iroh. Phone app Expo / React Native PTY grid viewterminal.grid One reducertranscript, state,prompts, queue,attention iroh client Collab guestAES-GCM, pi-wire Host: carry daemon owns sessions; clients only attach Managed PTYomp, shell, claude… omp --mode rpc-uiomp_rpc::host mapsRPC → collab frames Machine gatewaycollab.list / collab.link, omp.models, omp.sessions Host: a running omp TUI shared with /collab or collab.autoStart collab host omp relay (E2EE) iroh via relay
Solid green: the iroh link to the daemon. Dashed: the app joining a running omp directly through omp’s relay, with no Carry software on the host.

The daemon owns the session

A session is a managed PTY plus metadata, not a forked “phone agent”. You start a real CLI (omp, a shell, claude, codex…); every client attaches to the same sessionId and sees the same output. Input from any attached client is written to the PTY. Disconnecting detaches only; the PTY keeps running, which is the point compared with a raw SSH session.

Each live session also carries a VT screen (so the phone draws a styled grid with cursor, colours and scrollback instead of raw bytes), an attention detector that reads OSC 9/99/777 notifications, the bell and a configured prompt line, a bounded transcript ring for replay, and a viewer set that suppresses a push for a session already on screen. The persisted record is versioned, so a restarted daemon still lists what it had and can resume it.

Clients attach over iroh

The daemon hosts an iroh endpoint (QUIC, hole-punched, with relay fallback). A client pairs on one ALPN with a 6-digit code, then connects on another with the daemon token. Frames are length-prefixed JSON. The daemon dials out, so no inbound networking is required. The code is limited (10-minute lifetime, single use, throttled, discarded after repeated failure); the token is equivalent to an SSH key. A local WebSocket on 127.0.0.1:7740 serves the in-host CLI.

The app speaks omp’s own collab protocol

A running omp TUI can host an end-to-end-encrypted room on its relay. The app joins as a guest with its own implementation (vendored wire types plus AES-GCM) and gets a snapshot, live events, permission requests, prompts and abort. The room key stays in the link. The daemon can also act as a machine gateway for this: it proxies omp collab list and omp collab link so a paired phone sees every shared session and mints a fresh link on tap — without ever seeing session contents.

New: a daemon-hosted omp RPC session

The daemon can host an omp --mode rpc-ui session itself. Its RPC transport negotiates protocol v2 and reassembles chunked frames. A host adapter then maps RPC into the collab frame vocabulary — entries, state, agents, events — and sends it to attached clients as session.structured over the daemon’s normal session channel. Because the vocabulary is the one the app already renders, the same reducer covers all three sources: collab guest, terminal grid, and daemon-hosted RPC. There is no second protocol in the app.

The project’s review notes record this as verified against a real omp: a WebSocket test sees entry, state, agents and event frames from a live session.

What is not built

This page is the whole picture. The design decisions behind it, and the review of comparable projects that produced them, are kept with the source.