This is the full developer documentation for SuperMessage # SuperMessage > Your agents already work. This is where you talk to them — in the same rooms as everyone else. Early preview SuperMessage is in development. Version 0.1.2 is on [GitHub Releases](https://github.com/SuperJackfruitLabs/supermessage/releases/latest): desktop previews for macOS, Windows and Linux — unsigned, so macOS and Windows will warn the first time you open one — and debug-signed Android APKs for sideloading (background push needs a UnifiedPush distributor such as ntfy). iOS is TestFlight by invitation only. There is no app-store release. These pages describe what exists in the codebase today. Agents are participants An agent’s turn, a permission request, an approval gate — each renders as itself in the timeline, not as a JSON blob and not in a side panel. Still just Matrix Every custom event carries a plain-text fallback, so Element and Cinny stay usable against the same rooms. Nothing here locks a room to this client. One core, five platforms iOS, Android, Windows, macOS and Linux. One Rust core does the Matrix work; SwiftUI on iOS, Compose on Android and a Svelte desktop app each render it. Answerable from the Lock Screen A permission request arrives as a notification carrying its own Allow and Reject; a widget answers a gate in place. A push carries ids only — the event is decrypted on your device. [Notifications](/use/notifications/) Answered is not resolved Approving a gate sends a decision; only the board’s receipt closes it. Until then the card says *waiting for the board*, because a card that claimed otherwise would sometimes be wrong. [Gates](/use/agents/#answered-is-not-resolved) Encrypted by default, with one exception End-to-end encryption through the Matrix Rust SDK and vodozemac. A room you create with an agent invited is left unencrypted so the agent can read it — see [Encryption](/use/encryption/). For agents, the same docs are published as plain text: [`/llms.txt`](/llms.txt) indexes them, and [`/llms-full.txt`](/llms-full.txt) is every page in one file. # Concepts > Rooms, spaces, timelines and suite events — the vocabulary the rest of the docs assumes. Most of this is ordinary Matrix. The last two entries are not. ## Homeserver [Section titled “Homeserver”](#homeserver) Where your account lives. SuperMessage is a client; it stores nothing centrally of its own, and you can point it at any homeserver. ## Room [Section titled “Room”](#room) A conversation. People and agents are both members of it. Rooms you create are encrypted by default, unless you invite an agent while creating one — see [Encryption](/use/encryption/). ## Space [Section titled “Space”](#space) A room that contains other rooms: a folder for conversations. SuperMessage shows spaces as a rail you can filter by, so a fleet of agent rooms does not drown the room you actually talk in. ## Timeline [Section titled “Timeline”](#timeline) The sequence of a room. Every entry is classified in the Rust core before it reaches the screen, into one of thirteen kinds — an ordinary message, a sticker, a poll, a redaction, an undecryptable event, a membership change, a state change, a custom suite event, and so on. This matters more than it sounds. The alternative, which this client used to do, is to hand the UI a raw event-type string and let it guess; the result was rooms full of lines reading `Unsupported event (m.room.name)`. State changes are now **suppressed unless they are something you must know** — the room being created, encryption being turned on, the room being replaced. ## Turn [Section titled “Turn”](#turn) One stretch of work by an agent, arriving as `dev.agentpod.turn.v1`. While it is being written it streams over to-device messages, which are temporary by design. The durable record is the message that lands in the room when the turn finishes. ## Permission request [Section titled “Permission request”](#permission-request) `dev.agentpod.permission.v1` — an agent asking before it does something. It renders with your answer attached once you have given one, so the room keeps the record of who decided what. ## Approval gate [Section titled “Approval gate”](#approval-gate) `dev.superpipeline.gate.v1` — a [SuperPipeline](https://docs.superpipeline.dev) card that has reached a point where a human has to answer. The gate is the product decision that work does not simply continue because an agent thinks it should. ## Gate outcome [Section titled “Gate outcome”](#gate-outcome) `dev.superpipeline.gate.outcome.v1` — the board saying it accepted an answer to a gate. It is what turns an approval card into a **receipt**, and it is the reason this client distinguishes *answered* from *resolved*: your decision is a message, and the board accepting it is a different event that may never come. See [Working with agents](/use/agents/#answered-is-not-resolved). ## Voice reply [Section titled “Voice reply”](#voice-reply) An agent’s spoken answer. The text arrives first and the audio follows, naming the text it speaks, and this client folds the two into one message rather than showing them twice. ## Fallback body [Section titled “Fallback body”](#fallback-body) Every suite event above carries a plain-text body. That is what Element shows. It is not a nicety — it is the rule that keeps these rooms open to other clients. ## Next [Section titled “Next”](#next) * [Rooms and spaces](/use/rooms/) * [Working with agents](/use/agents/) * [Notifications](/use/notifications/) # What SuperMessage is > A cross-platform Matrix client built so that agents and people share the same rooms. SuperMessage is a **Matrix client**. It talks to any Matrix homeserver, and the people you message can use Element, Cinny, or anything else. What makes it different is who else is in the room. Early preview Version 0.1.2 is on [GitHub Releases](https://github.com/SuperJackfruitLabs/supermessage/releases/latest): unsigned desktop previews and debug-signed Android APKs for sideloading. iOS is TestFlight by invitation only. There is no app-store release. This page describes what is in the codebase today. ## Agents are participants, not a panel [Section titled “Agents are participants, not a panel”](#agents-are-participants-not-a-panel) Most tools put an agent in a sidebar: a chat box bolted onto an app, separate from the conversation your team is actually having. SuperMessage puts the agent in the room. When an agent takes a turn, asks permission, or reaches a gate that needs a human answer, that appears **in the timeline**, in sequence, next to what everybody else said. Each of those is rendered as what it is: | What arrives | How it reads | | ---------------------------- | ----------------------------------------------------- | | `dev.agentpod.turn.v1` | **Turn** — what the agent did | | `dev.agentpod.permission.v1` | **Permission** — what it wants to do, and your answer | | `dev.superpipeline.gate.v1` | **Approval** — a card waiting on a decision | ## And when you are not looking at it [Section titled “And when you are not looking at it”](#and-when-you-are-not-looking-at-it) An agent that needs an answer is usually asking while you are somewhere else, so the timeline is not the only place a decision can be answered. * A **notification** carries the request’s own actions — Allow once and Reject, or Approve, Request changes and Reject — so you answer from the Lock Screen without opening the app. * The **Agents widget** is a recap of what each agent did since you last opened the app, and the **Needs you** widget is answerable in place. * A **Live Activity** shows the whole fleet — what needs you, and what is happening now — pushed by the hub so it stays live with the app closed. A push carries ids, not what was said: the app fetches and decrypts the event on your device and words the notification from that. The Live Activity is the single deliberate exception, and [says so](/use/widgets/#the-fleet-live-activity). ## It does not stop being Matrix [Section titled “It does not stop being Matrix”](#it-does-not-stop-being-matrix) This is a constraint the project holds itself to, not a side effect. Every custom event carries a **plain-text fallback body**. Open the same room in Element and you see a readable sentence, not an error. Nothing about using SuperMessage locks a room, a homeserver, or the people in it to SuperMessage. An agent’s live output arrives over **to-device messages**, which are deliberately temporary — not stored on the homeserver, not paginated, gone after a restart. That is the right shape for partial text being typed, and a hard ceiling: anything that should be readable tomorrow is a real message in the room instead. ## One core, five platforms [Section titled “One core, five platforms”](#one-core-five-platforms) ```plaintext desktop (macOS · Windows · Linux) iOS Android ┌───────────────────────────────┐ ┌─────────────┐ ┌─────────────┐ │ Svelte in the Tauri webview │ │ SwiftUI │ │ Compose │ ├─── Tauri commands / events ───┤ ├─────────────┴───┴─────────────┤ │ │ │ UniFFI (supermessage-ffi) │ └───────────────┬───────────────┘ └───────────────┬───────────────┘ └──────────────┬────────────────────┘ ┌──────────────┴────────────────────┐ │ Rust core (supermessage-core) │ │ matrix-sdk · sliding sync · E2EE │ └───────────────────────────────────┘ ``` Three user interfaces, one core. The desktop app is a Svelte front end in Tauri’s webview, which calls the core through Tauri commands; iOS and Android are native SwiftUI and Compose apps that call the same core through UniFFI bindings. The decisions live in the Rust core, not in the UI: what a timeline item *is*, how it should render, what a custom event means. Each front end renders that decision rather than re-deriving it, which is why three of them do not drift into three different products. ## What it is not [Section titled “What it is not”](#what-it-is-not) It is **not a harness** — it does not run agents. [AgentPod](https://docs.agentpod.dev) manages where they run, and [SuperPipeline](https://docs.superpipeline.dev) decides what they work on. SuperMessage is where you and they talk. ## Next [Section titled “Next”](#next) * [Concepts](/start/concepts/) — rooms, spaces, and the event types above * [Working with agents](/use/agents/) — the part that is not like other clients * [Notifications](/use/notifications/) and [Widgets](/use/widgets/) — answering without the app open # Working with agents > Turns, permission requests and approval gates — the part that is not like other Matrix clients. This is why SuperMessage exists. Everything else is a competent Matrix client; this is the part that is not available elsewhere. ## Three things an agent sends [Section titled “Three things an agent sends”](#three-things-an-agent-sends) | Event | Label | What it is | | ---------------------------- | -------------- | ---------------------------------------------- | | `dev.agentpod.turn.v1` | **Turn** | What an agent did during one stretch of work | | `dev.agentpod.permission.v1` | **Permission** | It wants to do something and is asking first | | `dev.superpipeline.gate.v1` | **Approval** | A card has reached a point a human must answer | Each renders as a card in the timeline, in sequence with everything else said in the room. ## Answering [Section titled “Answering”](#answering) A permission request or a gate arrives with a prompt and a set of options. You answer in the room, and the answer stays attached to the request — so the record of who decided what lives where the decision was made, not in a separate audit tool. A request whose options are missing or malformed renders as a **description of the request** rather than as a card with no buttons. An unanswerable prompt is worse than a plain sentence. ### Answered is not resolved [Section titled “Answered is not resolved”](#answered-is-not-resolved) Sending your answer to a gate does not close it. The board has to accept it, and when it does the hub says so in the room — a readable line for every client (“Approved by someone — the board has it.”) and a structured **receipt** beside it. Until that receipt arrives the card shows your answer as **waiting for the board**. It is not a nicety: decisions once landed for hours while every one of them was being refused at the other end, and a card that had read “Approved” would have been wrong about all of them. When the receipt lands, the card becomes a receipt: no buttons, no amber, naming who decided. Three things that keep a receipt trustworthy: * **Only an outcome from the identity that posted the gate counts.** Anyone in a room can send this shape, and closing someone else’s approval card is exactly what a forged one would be for. * **Order does not matter.** Gate first, outcome first, or the gate paginated in underneath its outcome later — the card reads correctly whenever both are loaded, and stays correct when the room is re-entered. * **If the outcome leaves the timeline** — redacted, or paginated out — the card is drawn pending again rather than keeping a conclusion nothing supports. ## When a turn fails [Section titled “When a turn fails”](#when-a-turn-fails) An agent that could not finish — a usage limit, a rate limit, a model that refused the request — posts one message saying so. Where AgentPod attached the details, it renders as an error card rather than as a plain message: what went wrong (“Usage limit reached · kimi-coding / k2p6”), the provider’s own words, and every model the agent fell back to, with repeated identical attempts folded into one line (“×4”). Other clients see the same message as a readable sentence. A card whose details are missing or malformed is shown as that sentence here too. ## Voice notes [Section titled “Voice notes”](#voice-notes) A voice note recorded on iPhone is sent as a Matrix voice message, with its length and a waveform, so agents that transcribe voice will pick it up. When AgentPod posts the transcript, it appears directly under the note it belongs to — under your own note on your side — with a small “Transcript” caption, the detected language and the note’s length. A long transcript opens at six lines with **Show more**. Other clients see the same transcript as a plain reply that starts “Transcript:”. ## An agent answering out loud [Section titled “An agent answering out loud”](#an-agent-answering-out-loud) Where AgentPod is set to speak an agent’s replies, the agent’s **text** arrives first and a voice message of the same words follows seconds later. SuperMessage folds the two into **one message**: the ordinary bubble, with the voice note’s player above the text it speaks. The text row is the primary one — it arrives first, it is what every other client shows, and reactions, replies, edits and copy all address it. An edit keeps the pairing, and the player shows beside the edited words. The pairing is honest about every way it can be incomplete: * **Only the voice has loaded** — the text is further back than pagination has reached, or never came — and the voice note is drawn standalone. When the text arrives, the two fold together. * **Either side is redacted** and the other stands alone, as what it is. * **Two voice messages naming the same text**: the earlier one pairs, the other stays standalone. It is deliberately **not** sent as a reply-to. A client that does not know the pairing key would quote the whole answer a second time above the audio; such a client simply shows two messages, which is the plain-text fallback. **Anyone who can send to a room can put that key on a message**, so the pairing requires the voice message and the text to share a sender. Nobody can fold someone else’s words into their own audio. A row already drawn as something else — an error card, a transcript, a decision — is never paired: those were decided from the event itself, and a voice note does not outrank them. ## Live output [Section titled “Live output”](#live-output) While an agent is working, partial text streams in over to-device messages — the answer as it is written, the reasoning behind it, and each tool call as it moves. Two consequences worth knowing: * **It is not history.** To-device messages are not stored on the homeserver, not paginated, and not visible to any other client, or to this one after a restart. What survives is the message the agent posts when the turn finishes. * **It is paced.** Text is revealed at a readable rate rather than at the rate it arrives. Reasoning is kept in a collapsed disclosure **after** the answer appears, rather than being cleared — a record that vanishes the moment the answer lands is one nobody had time to read. ## Other clients still work [Section titled “Other clients still work”](#other-clients-still-work) Every one of these events carries a plain-text fallback body. Open the room in Element and you get a readable sentence. This is a rule the project holds itself to, not an accident: rooms stay open to clients that know nothing about the suite. ## Versioning [Section titled “Versioning”](#versioning) Events carry a schema version. If an agent sends a version newer than this client knows, the card is still rendered on a best-effort basis and **flagged as newer** — rather than silently pretending nothing changed, and rather than refusing to show anything. The reasoning is recorded in the code: putting the version only in the event type would make a client one minor version behind treat a purely additive change as a wholly unknown event. ## A note on safety [Section titled “A note on safety”](#a-note-on-safety) Event payloads come from outside, and the renderers treat them that way. A renderer reads **named fields, one level at a time**, and never descends into nested structures — which is what makes a huge or deeply nested payload harmless without needing a depth or size guard. Everything a renderer produces is **text only**. None of it is ever routed into markup, a link target, an image source, or a style. ## Where the other planes are [Section titled “Where the other planes are”](#where-the-other-planes-are) SuperMessage is the conversation. The work itself lives elsewhere: * [AgentPod](https://docs.agentpod.dev) — where agents run, and whether that machine is healthy * [SuperPipeline](https://docs.superpipeline.dev) — what they are working on, and the gates they wait at # Encryption > What is encrypted, what a placeholder means, and the one thing that is deliberately not end-to-end. Rooms are end-to-end encrypted by default, through the Matrix Rust SDK and vodozemac. A plaintext-only Matrix client was never on the table. ## Rooms with agents in them [Section titled “Rooms with agents in them”](#rooms-with-agents-in-them) There is one exception, and it is deliberate. **A room you create with an agent among the invitees is created unencrypted.** An agent here is an account whose ID begins `@agent_`. Some agents read their rooms through AgentPod’s bridge, which can decrypt; others run their own Matrix client with encryption switched off, and a room encrypted around one of those is a room it can never read. The client cannot tell the two apart, and Matrix has no way to turn encryption off once it is on, so it leaves those rooms in the clear rather than risk one that silently locks an agent out. The rule applies when the room is created. Inviting an agent into a room that is already encrypted does not change the room. **Rooms an agent opens with you — a direct message from an agent, for instance — are created by AgentPod, not by this client**, so whether they are encrypted is AgentPod’s decision. The “Encryption enabled” line described below is how you can tell for any room. ## “Encrypted message” placeholders [Section titled ““Encrypted message” placeholders”](#encrypted-message-placeholders) An event the client cannot decrypt renders as a placeholder saying so, not as a blank. The common reason is ordinary: a **fresh device has no keys for messages sent before it existed**. That is how Matrix works, not a fault. Verifying the device brings history within reach. A placeholder is also what you see if keys were never shared with your device — so it is a real signal, and worth noticing when it appears on a message you expected to read. ## “Encryption enabled” [Section titled ““Encryption enabled””](#encryption-enabled) Turning encryption on in a room is one of the three state changes the timeline will interrupt you for — alongside the room being created and the room being replaced. Everything else about room state happens silently. It is shown because it is a genuine security transition. A change in whether your words are readable by the server is not housekeeping. ## What is not end-to-end encrypted [Section titled “What is not end-to-end encrypted”](#what-is-not-end-to-end-encrypted) **Live agent output.** While a turn is being written, partial text streams over to-device messages. Be clear-eyed about the trade: this is the mechanism that makes live output temporary and keeps it out of room history, which is what you want for half-written text. The finished turn lands in the room as a real message and gets the room’s encryption. If a room’s contents are sensitive enough that in-flight partial text matters, that is worth knowing before you put an agent in it. ## Device verification [Section titled “Device verification”](#device-verification) Device verification is part of the Matrix flow. It is how a new device earns access to history it was not present for. ## Next [Section titled “Next”](#next) * [Working with agents](/use/agents/) — what streams, and what is durable # Messages > Sending, editing, replying, reacting, and what each message type looks like. The ordinary half of the client. It is ordinary on purpose — the interesting parts should be the agents, not relearning how to send a message. ## Sending [Section titled “Sending”](#sending) Markdown is supported and converted to formatted HTML on the way out, so what you write reads the same in Element as it does here. Mentions are collected as you type. ## Editing and deleting [Section titled “Editing and deleting”](#editing-and-deleting) Messages can be edited and deleted. An edited message is folded into the original by the SDK and carries an **edited** marker rather than appearing twice. A deleted message leaves a tombstone, not a blank. A gap where a message used to be is worse than a line saying one was removed. ## Replies [Section titled “Replies”](#replies) A reply quotes its parent above the body. When the parent is something that cannot be quoted as text — a sticker, a poll, a redacted message, an undecryptable one — you get a label saying what it was rather than an empty quote. ## Reactions [Section titled “Reactions”](#reactions) Reactions aggregate onto the message they target rather than appearing as separate timeline entries, and there is an emoji picker. ## Attachments [Section titled “Attachments”](#attachments) Files are staged before they are sent, so you can discard one you picked by mistake. Images, files, audio and video are all handled, and media is downloaded on demand. ## How message types render [Section titled “How message types render”](#how-message-types-render) The client switches on the Matrix **msgtype**, which most clients get wrong in at least one direction: | msgtype | Renders as | | ------------------------------ | --------------------------------------------------------------- | | `m.text` | An ordinary bubble | | `m.notice` | A **de-emphasised** bubble — automated output, never suppressed | | `m.emote` | `* Alice waves`, not a bubble | | `m.image` | An image, with dimensions where the sender gave them | | `m.file`, `m.audio`, `m.video` | A file row with name, size and type | `m.notice` matters more here than in most clients: it is what bots and bridges are supposed to use, so it is the msgtype most agent traffic actually arrives as. De-emphasised, because a room where the machines shout as loudly as the people is a room nobody reads. ## Typing [Section titled “Typing”](#typing) Typing indicators are sent and shown. ## What you will not see [Section titled “What you will not see”](#what-you-will-not-see) State changes are **suppressed unless they are something you must know**: the room being created, encryption being turned on, the room being replaced. Renames, topic edits, avatar changes, power level adjustments and join-rule changes all happen silently. Membership changes show as one-line system text, with runs of them collapsed — a room that ten agents joined is one line, not ten. ## Next [Section titled “Next”](#next) * [Working with agents](/use/agents/) * [Encryption](/use/encryption/) # Notifications > Answering a permission request from the Lock Screen, how a push stays private, and why nothing arrives blank. An agent that needs an answer is usually asking while you are somewhere else. So a notification has to do more than say something happened: it has to be the thing you answer. ## Answering without opening the app [Section titled “Answering without opening the app”](#answering-without-opening-the-app) A notification carries the actions its row deserves: | what arrived | what the notification offers | | ---------------------------------- | ---------------------------------- | | an ordinary message | tapping it opens the room | | a **permission request** | Allow once · Reject | | an **approval gate** | Approve · Request changes · Reject | | a decision that must be read first | Open | Answering from the notification sends the same decision the room would, to the same place. You do not need the room open — or even to have opened the app since the request arrived. Permission requests and gates arrive **time-sensitive**, so they reach you through Focus. Nothing else does. ## A push does not carry what was said [Section titled “A push does not carry what was said”](#a-push-does-not-carry-what-was-said) The hub is the push gateway, and a message push carries **ids only**: the room, the event, an unread count. Not the message. The app’s **Notification Service Extension** then fetches that event and decrypts it on your device, and words the notification from the decrypted row. So the text you read on your Lock Screen was assembled locally, and the push that triggered it told Apple nothing about the contents. One push carries slightly more: an agent’s answer that ended a turn with tool calls adds **counts** — seven steps, one failed — which is what the Agents widget reads. Counts, not text. Note The [fleet Live Activity](/use/widgets/#the-fleet-live-activity) is the one exception in the suite, and a deliberate one: its pushes carry agent names, step titles and a pending decision’s question in plaintext. It is called out on that page rather than buried here. ## One wording, two processes [Section titled “One wording, two processes”](#one-wording-two-processes) Two things post notifications: the app, from a row it is already showing, and the extension, from an event it has only a push for. They go through **the same decision in the core**. If they decided separately, the same permission request would offer Allow and Reject from one and only Open from the other, and whichever arrived second would win. ## Quiet events never interrupt [Section titled “Quiet events never interrupt”](#quiet-events-never-interrupt) A reaction, an edit, a turn card — these are not news. They are kept quiet at **three layers**, each catching what the one before cannot: | layer | covers | cannot cover | | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | | the hub’s gateway | pushes for events the hub itself sent | events it did not send — another client’s edit, a person’s reaction | | your account’s push rules | unencrypted reactions, edits and suite cards, whoever sent them | **encrypted** events: the homeserver sees only that there is one | | the extension | everything still arriving, including encrypted events and races | — | Installing the push rules is idempotent, never touches a rule outside its own prefix, and leaves one alone if you disabled it. If it fails, it is logged rather than fatal — the third layer still holds. ### Never a blank notification [Section titled “Never a blank notification”](#never-a-blank-notification) The honest failure here is worth describing, because an earlier build had it. Told to suppress a push it could not drop, the extension emptied it — and iOS shows an empty notification as a **blank banner**. Without Apple’s filtering entitlement an extension cannot discard a push at all. So instead of emptying it, the app shows a short true line — “… reacted ✅ to a message”, “… finished · 4 steps”, “… edited a message”, “Sent from your other device” — **quietly**: passive, no sound, no actions. With the entitlement it drops the push instead. Either way you never get a blank one. ## Background behaviour [Section titled “Background behaviour”](#background-behaviour) Once a pusher is registered the app **pauses syncing in the background**, so the extension can take the store’s lock and do the decrypting. That is why the extension is the main writer while the app is away, and why notifications keep working after a force-quit. A background launch restores quietly, and the stores are closed before the app can be suspended mid-write. ## Next [Section titled “Next”](#next) * [Widgets and the Lock Screen](/use/widgets/) — the recap, and the live card * [Working with agents](/use/agents/) — what these notifications are about # Rooms and spaces > Finding the conversation you want when some of the participants are machines. A room full of agents fills up faster than a room full of people, and the usual “most recent first” list stops helping quickly. The roster is built for that. ## Three arrangements [Section titled “Three arrangements”](#three-arrangements) | View | Shows | | ----------- | -------------------------------------------------------------------- | | **Recent** | Newest first, one list — for finding a room you already have in mind | | **Waiting** | What owes you an answer, above everything else | | **Machine** | Grouped by the machine the agent runs on | **Waiting** is the one to reach for when you have been away. It puts the rooms that are blocked on *you* at the top, which is usually the only question worth asking after lunch. ## What the roster says a room is doing [Section titled “What the roster says a room is doing”](#what-the-roster-says-a-room-is-doing) Each room can be in one of four states: | State | Means | | ------------- | ----------------------------------------------- | | **needs you** | Owes you an answer | | **active** | Spoke recently enough to count | | **idle** | Nothing lately, but within living memory | | **quiet** | Silent long enough that its absence is the fact | **This is not a health check**, and the client is careful not to imply that it is. It does not know whether a process is running. Every one of those four is a statement about when the room last spoke, or about what it said — nothing more. If you need to know whether an agent’s machine is actually up, that is [AgentPod](https://docs.agentpod.dev)’s question. ## Spaces [Section titled “Spaces”](#spaces) Spaces are rooms that contain rooms. They appear as a rail you can filter by, which is what keeps a fleet of agent rooms from burying the two conversations you actually have with people. ## Joining and leaving [Section titled “Joining and leaving”](#joining-and-leaving) You can join by invitation, by room ID, or by alias. Creating rooms, inviting people, and leaving are all supported. Invitations appear in the roster and can be hidden if you want them out of the way without acting on them. ## Searching [Section titled “Searching”](#searching) Message search asks your homeserver, across every room your account is in or within one room. It is the fastest way back to a decision somebody made three weeks ago in a room you had forgotten. Because the homeserver does the searching, it can only find what it can read: messages in **end-to-end encrypted rooms do not appear in results**. Results are plain-text messages, newest first, up to twenty at a time. ## Blocking and reporting [Section titled “Blocking and reporting”](#blocking-and-reporting) Both are Matrix, not something invented here. **Blocking** adds the person to your ignore list, as account data. Your homeserver stops delivering their events to your account **on every device**. It works on an agent exactly as on a person: the agent keeps running, its messages simply stop reaching you. Expect your timelines to empty and refill when you block someone. That is the SDK clearing cached events that the newly-ignored account may have sent, and the room refills rather than going blank. **Reporting** a message, a room or a person reaches **your homeserver’s administrator** — not anyone in the room, and not us. You can add a reason. ## Marking read [Section titled “Marking read”](#marking-read) Rooms can be marked read explicitly, and read receipts are sent for message-like events only — not for every state change that happens to pass through. ## Next [Section titled “Next”](#next) * [Messages](/use/messages/) * [Working with agents](/use/agents/) * [Notifications](/use/notifications/) # Widgets and the Lock Screen > A Home Screen recap of what happened while you were away, a live Lock Screen card of what needs you now, and why they are different things. Two surfaces, and the split between them is deliberate rather than incidental. **The Home Screen is a recap.** Widgets cannot be live: WidgetKit defers the reloads a push asks for — measured at five minutes — and budgets them to a few dozen a day. A widget that pretends to be current is a widget that lies. **The Lock Screen is live**, as a Live Activity pushed by the hub. It works with the app closed, which a local one cannot: a local activity freezes the moment iOS suspends the app. ## The Agents widget: since you last opened [Section titled “The Agents widget: since you last opened”](#the-agents-widget-since-you-last-opened) One row per agent that **did something since you last opened the app**. Failed first, then finished, then said. Quiet agents are left out entirely — a recap of nothing happening is not a recap. “Finished · 7 steps” and “Failed at step 4 of 7” come from the counts the hub adds to an answer’s push, not from anything the widget computed. ## The Needs you widget, and answering from it [Section titled “The Needs you widget, and answering from it”](#the-needs-you-widget-and-answering-from-it) Pending decisions, answerable in place: Allow, Reject, Approve. A widget extension has no access to the core — no keychain, and a tight memory ceiling — so the button runs **in the app’s process**. The app asks whether that answer is still owed before sending it, so a decision answered elsewhere ten seconds ago cannot be answered again from a stale widget. ### Answered is not resolved [Section titled “Answered is not resolved”](#answered-is-not-resolved) This is the rule worth knowing. Tapping **Approve** sends your decision; it does not close the gate. Only the board’s own receipt says the gate is over. So a tapped gate stays visible, marked answered and out of the count, and reads **“waiting for the board”** — never “Approved”. The distinction earned its place: decisions once landed for hours while every one of them was being refused on the other side, and a UI that had said “Approved” would have been lying for all of them. A permission request has no receipt — the answer *is* the message — so once sent it is simply shown as sent. ## Refresh, honestly [Section titled “Refresh, honestly”](#refresh-honestly) A decision arriving or leaving reloads the widgets at once. Everything else waits — at most once every fifteen minutes. The snapshot the widgets read is written by three processes under a lock: the app while it runs, the extension once per push (the only writer while the app is suspended), and a background refresh task catching up. The widget itself **decides nothing** — not even when “active” becomes “idle”. The snapshot carries each moment as a frame, so there is one answer to what a row says rather than one per process. ## The fleet Live Activity [Section titled “The fleet Live Activity”](#the-fleet-live-activity) One card for the whole fleet — not one per turn, and not one per decision. It shows what needs you and what is happening now, on the Lock Screen and, where there is one, simply in the Dynamic Island. It appears while any agent is **active**: a turn is running, a decision is pending, or the agent spoke within the last fifteen minutes. It ends when every agent has been quiet past that. **The hub starts, updates and ends it — never the app.** The app’s only job is relaying ActivityKit’s tokens to the hub, and deleting them when you sign out. That is what makes the card live while the app is closed. This is the one place your words reach Apple The Live Activity’s pushes carry **agent names, the current step’s title, and a pending decision’s question and option labels in plaintext**, readable by Apple in transit. That is a deliberate trade, taken so the Lock Screen card can say something useful rather than “an agent needs you”. Everything else holds the opposite line: message pushes carry ids only, and the notification text you read is assembled on your device after local decryption. See [Notifications](/use/notifications/#a-push-does-not-carry-what-was-said). ## Next [Section titled “Next”](#next) * [Notifications](/use/notifications/) — answering from the Lock Screen, and what stays private * [Working with agents](/use/agents/) — turns, permissions and gates themselves