A message is a durable row in a conversation. It survives reconnects, restarts, and the sender closing their laptop.
Why server-ordered
Every participant, sender included, renders the row the server stored — not
a locally-guessed one. That is why sendMessage() resolves with the stored
message and the sender still receives the fan-out message event: the
first is the durability signal, the second is what everyone renders.
Without that, two clients with skewed clocks disagree about order.
Types
type ChatMessageType = 'text' | 'system' | 'event' | 'attachment';Idempotency
Pass a clientMessageId and a retry cannot double-post:
await chat.sendMessage({
text: 'Hello everyone',
clientMessageId: crypto.randomUUID(),
});The server dedupes on (conversation, sender, clientMessageId). The
response says which happened, so your UI can tell a fresh send from a
replay rather than guessing.
Edits and deletes
Editing rewrites the text and fires messageUpdated. Deleting is a soft
delete: the row stays, the content goes, and messageDeleted fires. History
stays navigable and an audit trail survives.
Threads are flat
A reply to a reply joins the same thread as its parent. There is no nesting, which means no unbounded-depth UI and no conversation nobody can follow.
Limits
Text is capped at 4000 characters and metadata at 4096 bytes by default;
over either returns RAVEN_MESSAGE_TOO_LARGE, which is deliberately
distinct from the attachment limit because you cannot act on "too large"
without knowing which limit you crossed.