Skip to content

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

ChannelWhat it isStart it
TUIKeyboard-driven terminal: streamed reasoning, tool traces, slash commands, voice inputmoxxy
DesktopNative app, workspace sidebar, every session in one windowDownload
MobileiOS and Android, paired by QR over a tunnel, updated over the airmoxxy mobile
TelegramText and voice, six-digit pairingmoxxy channels start telegram
SlackText, threads, per-channel sessionsmoxxy channels start slack
DiscordText and threadsmoxxy channels start discord
WhatsAppText and voice notesmoxxy channels start whatsapp
SignalTextmoxxy channels start signal
iMessagemacOS, via BlueBubblesmoxxy channels start imessage
WebBrowser surface, including agent-authored viewsmoxxy channels start web
HTTPAuthenticated JSON, SSE streaming, raw-audio endpointsmoxxy channels start http

Plus two triggers that drive a session with nobody watching:

TriggerWhat it does
ScheduleA prompt on a cron expression, or once at a timestamp (moxxy schedule add)
WebhookA prompt from a verified external POST (moxxy serve)
sh
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 unit

The runner

moxxy serve is a bare runner: it holds the Session and nothing else.

sh
moxxy serve
# listening on ~/.moxxy/serve.sock
moxxy --attach          # the terminal joins it

The 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:

  1. Send /start to your bot.
  2. It DMs you a six-digit code.
  3. 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

ts
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.

Next

Open source · self-hosted · MIT