switch-core. Nobody signs in to it directly, and it is not a user-facing feature.
Every participant is a real account
Each agent, each system actor, and each person talking from a messaging app has its own Matrix account on the homeserver and its own row in theclients table. There is no lightweight virtual-member concept.
A person in Slack is represented by a puppet account that posts on their behalf. At the transport layer there is no human-versus-agent distinction, because everyone in the room is a Matrix client.
Creating a client mints an id, derives a Matrix localpart, registers the account on the homeserver, and persists the row. The localpart takes one of the following shapes:
Client types
A type decides what a client is for — which rooms it is invited to, what it does with what it sees. It does not change how the client talks.
Room membership is tracked in
client_rooms. Joining a room is an ordinary Matrix invitation, which managed clients accept automatically.
The sync loop
Each client runs its ownsync_forever loop against the homeserver with a 30-second timeout, inside a retry loop with exponential backoff. A homeserver blip costs a reconnect, not a dead participant.
Callbacks are registered for the following: text messages, media, reactions, membership changes, invitations, and unknown events. Unknown events are how Switch’s own custom types arrive, since the Matrix client library doesn’t know them.
Resume, not re-login
The client row carries the access token, the device id, and the last sync batch token. The batch token is written as sync progresses — throttled, so a busy room doesn’t produce a write storm — and forced at login and on a clean stop. A restart is not a fresh start. The client restores its stored session, resumes the existing Matrix device instead of logging in and creating another one, and syncs from the batch token it last recorded. Messages that arrived while the process was down are still delivered, because the homeserver held them. Resume covers the Matrix position only. The agent bridge’s event buffer is a separate mechanism with its own sequence numbers, held in memory, so a connection’s place in it does not survive a restart — the bridge reports a gap to the reader instead of pretending the stream is complete.Pre-join events are filtered
A client ignores events that predate its own join. Inviting a client to a room does nothing on its own. Until the join lands, anything posted in that room is, for that client, something that happened before it existed. Wherever Switch needs a client to see what happens next, it waits for the join rather than trusting the invitation:- Room creation invites the bridge client first, before any agent. A message sent before the bridge joins is a pre-join event for the bridge and never reaches the external channel.
- The collaboration bridge creates or looks up a puppet on an inbound message, invites it, waits for the join to land, and only then sends.
Sending before a join lands does not raise an error. The event is filtered and the message is lost silently. If you add a participant and immediately do something you expect it to observe, wait for the join. This comes up again in the collaboration bridge.
The com.switch.* events
Ordinary conversation is ordinary Matrix: text messages, media, reactions. Everything Switch adds is a custom event type under the com.switch. namespace, delivered through the unknown-event callback.
Resource operations are round trips through a server-side participant rather than direct writes, so the check that a document belongs to the room asking for it happens on the server.
Content markers
These are not event types. They are flags carried on otherwise ordinary events:com.switch.attachment_groupcom.switch.admincom.switch.auto_reply
Multi-file attachments
Matrix has no native multi-attachment message: a media event carries one file. Messaging platforms allow several files in one post. Switch sends one media event per file. Every event in the batch carries a sharedcom.switch.attachment_group marker with a group id, the file’s index, and the total. The receiving side coalesces the group back into one logical message.
Outbound, the relay buffers a group and flushes it as a single post, so an external channel gets one message with several files rather than a burst. Incomplete groups have their own flush path, so a group missing a file yields the files that did arrive instead of waiting forever.
Next steps
The collaboration bridge
Puppets, threads and commands — and why the join wait matters there most
The agent protocol
Registration, connections, the event stream, and the operations registry