Surfaces and channels
A channel does not own the conversation. The event log does. A channel feeds prompts in, renders events out, and answers permission prompts, which is why the same session can be started in a terminal, approved from a phone, and read in Slack.
What ships
| Channel | What it is | Start it |
|---|---|---|
| TUI | Keyboard-driven terminal: streamed reasoning, tool traces, slash commands, voice input | moxxy |
| Desktop | Native app, workspace sidebar, every session in one window | Download |
| Mobile | iOS and Android, paired by QR over a tunnel, updated over the air | moxxy mobile |
| Telegram | Text and voice, six-digit pairing | moxxy channels start telegram |
| Slack | Text, threads, per-channel sessions | moxxy channels start slack |
| Discord | Text and threads | moxxy channels start discord |
| Text and voice notes | moxxy channels start whatsapp | |
| Signal | Text | moxxy channels start signal |
| iMessage | macOS, via BlueBubbles | moxxy channels start imessage |
| Web | Browser surface, including agent-authored views | moxxy channels start web |
| HTTP | Authenticated JSON, SSE streaming, raw-audio endpoints | moxxy channels start http |
Plus two triggers that drive a session with nobody watching:
| Trigger | What it does |
|---|---|
| Schedule | A prompt on a cron expression, or once at a timestamp (moxxy schedule add) |
| Webhook | A prompt from a verified external POST (moxxy serve) |
moxxy channels list # what is registered and what is running
moxxy channels start <name> # on its own detached runner
moxxy service install <name> # as a launchd / systemd --user unitThe runner
moxxy serve is a bare runner: it holds the Session and nothing else.
moxxy serve
# listening on ~/.moxxy/serve.sock
moxxy --attach # the terminal joins itThe socket is mode 0600 inside a 0700 parent, and the parent is created before the listen call so there is never a window in which another local account can reach it. On Windows the same logical address maps to a named pipe; that platform difference lives in exactly one function, so the listening side and every client always agree.
RemoteSession implements the same SessionLike interface the in-process session does, which is why a channel cannot tell whether it is driving a local session or a remote one, and why one turn streams to every attached client at once.
Cross-client abort
Any attached client may abort a running turn by default. Set MOXXY_RUNNER_STRICT_ABORT=1 where two people might hold the same session.
Pairing a chat channel
Pairing goes bot-first, because the bot is the only side that can prove which account it is talking to:
- Send
/startto your bot. - It DMs you a six-digit code.
- Paste the code into the moxxy terminal.
The chat id is persisted, so the next start authorises it without asking. See the Telegram guide for the full flow.
Dedicated runners
Some channels declare dedicatedRunner, which means they get their own runner process rather than attaching to a shared one. That keeps a busy Slack workspace from sharing turn state with your terminal, and it is a property of the channel definition rather than a flag you have to remember.
Authoring one
interface Channel<TStartOpts = unknown> {
readonly name: string;
readonly permissionResolver: PermissionResolver;
start(opts: TStartOpts): Promise<ChannelHandle>;
}Three responsibilities: turn incoming messages into prompts, render the event stream, and resolve permission requests. Everything else — history, memory, compaction, tools — is the session's job and you get it for free.
See authoring a channel.