Skip to content

Gateways

A gateway is an adapter between a chat platform and the daemon. Every connection is named:

[gateways.<platform>.<name>]

[gateways.telegram.nouva] is the connection whose id is telegram:nouva. That naming is what lets one daemon run a bot identity per agent, rather than one bot that switches personality.

The four platforms

TelegramBot API, long polling. Inline approval buttons
DiscordGateway WebSocket. Needs the Message Content intent
WhatsAppMulti-device, through a separate GPL-isolated bridge process
Terminal chatIn-process. nouride chat, no token needed

Nothing is defined by default, so a fresh checkout brings up the dashboard and nothing else, rather than retry-looping against a platform it has no token for.

Ownership and routing

[gateways.telegram.nouva]
enabled = true
token = "telegram-nouva" # the NAME of a secret
agent = "nouva" # this bot is Nouva's

agent makes the connection that agent’s own bot: everything it receives goes to them unless somebody else is addressed by name. Without it, routing falls back to channel bindings in the agent’s own config.toml.

Routing order, highest first:

  1. A mention — this bot’s platform tag, or an agent’s id or display name in the text
  2. A reply to a message an agent wrote
  3. The gateway owneragent = "..."
  4. A channel binding in an agent’s pack
  5. The default agent

A saved connection is not a running connection

Adapters are built at boot. Every gateway change — from the dashboard or the config file — needs a restart, and the dashboard says so and offers the button.

Terminal window
nouride gateway # connection status
nouride gateway restart <id>
nouride gateway add --platform telegram --name nouva --agent nouva --token-stdin
nouride gateway rm <id>

Renaming is the exception that must be done with the daemon stopped, because it rewrites both the config and the database the daemon has open:

Terminal window
nouride gateway rename telegram:old telegram:new --apply

Several bots in one channel

This is the arrangement a multi-agent daemon is for, and it is where all the interesting behaviour lives. Everything below was found on a live Discord with six agents in one channel.

The one fact the rest follows from:

A platform delivers every channel message to every bot in the channel. Six bots means the daemon receives six copies of one message, on six connections, and routes each independently.

Anything that fires per connection fires six times. Anything that does not name an agent is true on all six at once.

Addressing becomes required

A rule that names no agent — “the owner of this connection answers”, “fall back to the default agent” — is true six times over. Both of those shipped once, and both produced six replies to a one-word message.

So when more than one enabled connection shares a platform and the chat is not a DM, both fallbacks switch off and a message has to be addressed:

SignalResolves to
This bot’s own platform tagthat connection’s agent
An agent id or display name in the text (nox:, @nox)that agent
A reply to a message an agent wrotethat agent
A channel binding in a packthat agent
Somebody else’s platform tagnobody — this is the guard that stops the storm
Nothingnobody

A DM keeps both fallbacks: a private message to Nero’s bot is unambiguously Nero’s, however many other bots exist. A platform with one enabled connection keeps them too — that is most installs, and withholding them there would make a single bot stop answering ordinary messages.

One turn per message

Where the signal is the same on every connection — a name in the text, a reply — all six reach the same conclusion, and the agent would answer six times. A claim is taken after routing and before any side effect, keyed by platform : chat : messageId : agentId.

Keyed by agent as well as message on purpose: two connections that reach different conclusions are not in conflict. One message tagging two bots resolves to two agents, and both should answer.

One identity, four outputs

An agent says four kinds of thing — its final answer, its live progress line, an approval prompt and the typing indicator — and each of them goes out through the agent’s own connection, not the one the message arrived on.

Before that was true, the visible result was one agent’s approval request posted under another’s name and avatar, followed by the wrong bot “typing…” for the whole time, and then @WrongBot /approve looking for a request that was never theirs.

Nothing in config says which channels a bot was invited to, so a send can still fail; the answer path falls back to the inbound connection rather than going silent. A log line reading an agent's own connection could not deliver here means one thing: invite that bot to the channel.

Mentions are the platform’s syntax, not ours

The prompt hands each agent the exact token to type for every other agent, taken from that agent’s own connection. Discord gives <@id>, Telegram gives @username. The engine never formats a mention itself.

An agent with no connection of its own falls back to @<agentId>, which the text matcher understands.

A bot is never offered a pairing code

The access gate runs before the bot check and before routing. A code is something a human reads and forwards; a bot does neither, and on a platform that fans out every message that code becomes a new unknown sender for every other connection. One tag once produced twelve messages.

Bots pass the gate and are judged by “is this one of ours” instead. And a pairing code is posted to a DM only — in a shared channel the gate fires once per connection, so a stranger saying anything at all met a wall of codes none of which they could act on.

Alerts

The daemon reports on itself through a gateway you nominate — a connection that gave up reconnecting, a WhatsApp session that needs relinking:

[alerts]
gateway = "telegram:nouva"
chat_id = "123456789"

Without it, a dead gateway is only a line in the log. Boot warns when chat gateways are enabled and this is empty.