Federated Matrix protocol via the Client-Server API. Inbound runs as a long-poll daemon, no HTTP webhook entry point.
🟡 Outbound-ready, inbound via long-poll daemon, Matrix is
federated, so there is no single webhook URL. Minara runs a /sync
daemon that streams events into the agent. Plaintext rooms only;
end-to-end encrypted rooms are not supported in this PR.
Outbound text via the Client-Server API:
PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}
with Authorization: Bearer <access_token> and {msgtype:"m.text", body}.
Inbound via long-poll daemon. When MESSAGING_MATRIX_INBOUND=1,
Minara opens a GET /sync?since=<batch>&timeout=30000 loop. Events
are filtered to messages from MATRIX_DEFAULT_ROOM_ID only (a
codex-round-1 security fix; the daemon does not emit from every
joined room).
Cursor persisted to disk at <dataDir>/matrix-sync.json, so a
restart resumes from the last next_batch instead of replaying
history.
From the bot's account, join the room you want the agent to monitor.
Note the room id (Element: Room → Settings → Advanced → "Internal
room ID"). Looks like !abc:matrix.org. Save as
MATRIX_DEFAULT_ROOM_ID.
The daemon runs in the background, opening a long-poll request
against the homeserver:
GET /sync?timeout=30000&since=<next_batch>&filter={"room":{"timeline":{"types":["m.room.message"]}}}
The homeserver holds the connection until a new event arrives or the
30-second timeout expires, then returns the events plus a fresh
next_batch cursor.
Three guardrails:
First sync drops history. A fresh install has no cursor, so
/sync would normally return the room's entire timeline. The
daemon discards this initial batch and persists only the
next_batch token. Prevents replaying months-old messages on
first boot.
Room-scope filter. Only events in MATRIX_DEFAULT_ROOM_ID are
forwarded to the agent. Without this, every room the bot is in
could trigger a reply, a privacy / scope risk on shared
surfaces.
Self-loop guard. Events where sender === MATRIX_USER_ID are
filtered, so the bot's own outbound posts (which round-trip
through /sync) do not become inbound triggers.
Reconnect: the daemon uses exponential backoff with jitter (1s → 30s)
on network failures via the shared
daemon-runner.
No end-to-end encryption. The daemon ignores m.room.encrypted
events. Use plaintext rooms only. Adding olm / matrix-rust-sdk for
E2EE is a large future undertaking.
No federation handshake. The bot is a Matrix client; it cannot
receive events for rooms it has not joined.
Bearer in header, not query. Matrix's ?access_token= query
form is deprecated. Minara uses Authorization: Bearer.
Bandwidth. Long-poll holds an open HTTP connection 24/7. Most
homeservers handle this trivially, but firewall / load-balancer
config matters (no idle-connection drops below ~60 seconds).
MESSAGING_MATRIX_INBOUND is unset. Set to 1 and restart.
"Daemon emits no events"
The bot is not joined to MATRIX_DEFAULT_ROOM_ID. Re-check from
Element or via GET /joined_rooms.
The room is encrypted. Plaintext only.
"Daemon replays every old message on startup"
<dataDir>/matrix-sync.json is missing or next_batch is null.
Stop the daemon, delete the file, restart. The first sync after
deletion will drop history correctly.
"401 on outbound (M_UNKNOWN_TOKEN)"
Access token expired or revoked. Generate a fresh one via the
login flow above.