read() to mark a conversation as read up to a message, surfacing a read receipt to the sender where the platform supports one. It is fire-and-forget: space.send(...) resolves to undefined.
message.read() and space.read(message) are sugar for the canonical form above.
- WhatsApp Business — a per-message receipt, which also marks every earlier message in the conversation as read.
- iMessage (remote) — chat-level: the target only identifies the chat, and every unread message in it is marked read. Local mode rejects with an .
- Telegram / Slack — silently no-op. Neither surfaces read state for bot conversations, so the signal is vacuously satisfied — the same best-effort contract as
typing.
Inbound read receipts
When a recipient reads a message the agent sent, the same content type arrives as an inbound onapp.messages. iMessage (dedicated and shared lines) reports these today.
message.senderis the reader;message.content.targetis the message you sent. Never the other way round.- The target is a fully-built
Messagewithdirection: "outbound", sotarget.content,target.edit(...), andtarget.unsend()are all available on it. message.timestampis when the reader’s device marked it read — not when your process observed the event. The two diverge when a receipt arrives late via Continuity sync.message.senderis always present on an inbound read receipt. Receipts the platform could not attribute to a reader are dropped rather than surfaced with an unknown sender, so a “who has read this” tally never counts phantom readers.- Direct messages are the reliable case today. iMessage does not report the reader’s identity on a read event — it names the receiving line instead — so in a DM the reader is recovered from the conversation itself. A group conversation carries no such information, so a group receipt is dropped unless the platform names a reader other than your own line. Treat group read receipts as best-effort.
- Where a group does report readers, it emits one message per reader, all sharing the same
content.target.id. Aggregate by that id againstspace.getMembers()to answer “has everyone read it”. - The agent’s own actions are suppressed:
space.read(...)does not echo back as an inbound event. - No direction check is needed. Outbound
readis fire-and-forget and produces noMessage, socontent.type === "read"onapp.messagesis always a receipt.