MINARA

Matrix

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.

What you get

  • 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.
  • Text only. Image / file events are dropped.
  • 65000-char text limit (Matrix's own ceiling).

Setup

1. Create or claim a Matrix account

Any homeserver works: matrix.org, your own Synapse, Dendrite, etc. Sign in once via a Matrix client (Element, Cinny, etc.).

2. Generate a long-lived access token

The easy path is to copy from Element:

  1. Element desktop / web: Settings → Help & About → Access Token (it's hidden under "Advanced", expand)
  2. Or curl:
    curl -X POST -H 'Content-Type: application/json' \
      -d '{"type":"m.login.password","user":"@bot:example.org","password":"..."}' \
      https://matrix.example.org/_matrix/client/v3/login
    Returns {access_token, user_id, ...}.

Save:

  • MATRIX_HOMESERVER (the URL, e.g. https://matrix.org)
  • MATRIX_ACCESS_TOKEN (the bearer)
  • MATRIX_USER_ID (e.g. @bot:matrix.org)

3. Join the target room

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.

4. Configure Minara

minara auth messaging add
# pick `matrix` from the list; paste homeserver URL, access token,
# user id, and default room id.

Or set env vars:

MATRIX_HOMESERVER=https://matrix.org
MATRIX_ACCESS_TOKEN=syt_...
MATRIX_USER_ID=@bot:matrix.org
MATRIX_DEFAULT_ROOM_ID=!abc:matrix.org

5. Enable inbound (optional)

MESSAGING_MATRIX_INBOUND=1

When set, the agent boots the long-poll daemon. Without this flag, Matrix is push-only.

6. Test

minara auth messaging test matrix

How /sync works (long-poll daemon)

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:

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

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

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

Limits & caveats

  • 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).

Troubleshooting

"Daemon won't start"

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

Reference

On this page