Lets an agent watch a conversation they are not a participant in — for example, a supervisor browsing "All Conversations" and opening one that's assigned to someone else. The SDK has no automatic focus-tracking; your app calls conversation/join-as-reader and conversation/leave itself in response to your own UI's focus/unfocus signal (a route change, a list-item click, a tab switch — whatever "the agent is now looking at this conversation" means in your app).
Clients: examples use only the public MessagingClient API — client.dispatch({ action, value }). dispatch() returns a Promise; always await it or handle rejection.
When to use it
Use conversation/join-as-reader when the agent brings into focus an open conversation they are not already a participant in (not the ASSIGNED_AGENT, not a MANAGER). Skip it if the agent already has a role on the conversation — joining as a reader on top of an existing role is unnecessary and the demo app explicitly guards against it.
Call conversation/leave (with role: 'READER') when that conversation loses focus — the agent navigates away, closes the tab, or selects a different conversation. A reader that never leaves keeps accumulating as a stale participant, so pair every join with a corresponding leave.
⚠️ Note: message content for every conversation on the account already streams to your callback via the account-wide
message/receivestream, participant or not. Joining as reader does not unlock message delivery — it adds the agent as aREADERparticipant, which is what lets you act on the conversation (see "Why" below) and shows up to others as a participant-list entry (conversations/update).
How to use it
The actions
interface ConversationJoinAsReaderAction {
action: 'conversation/join-as-reader';
value: { conversationId: string; dialogId: string };
}
// resolves void
interface ConversationLeaveAction {
action: 'conversation/leave';
value: {
conversationId: string;
dialogId?: string;
role: 'MANAGER' | 'READER';
shouldJoinAsReader?: boolean;
};
}
// resolves void
Join always adds the agent with role: 'READER'. Leave is shared across a few related behaviors — pass role: 'READER' to drop the reader participation you added with join-as-reader.
Both actions are basic — no client-side permission gate; session.permissions.actions['conversation/join-as-reader'] and ['conversation/leave'] are always true. Authorization is enforced entirely server-side by UMS, so a rejection from the server surfaces as a UmsError, not a PermissionDeniedError. Handle that in your catch.
The flow
async function onConversationFocusChange(newId: string | null, oldId: string | null) {
// Unfocus: leave the previously-focused conversation as reader
if (oldId) {
const oldConv = conversations.get(oldId);
const isReader = oldConv?.mainDialog.participants.some(
(p) => p.id === myAgentId && p.role === 'READER',
);
if (oldConv?.stage === 'OPEN' && isReader) {
await leaveAsReader(oldId, oldConv.mainDialog.id);
}
}
// Focus: join the newly-focused conversation as reader
if (newId) {
const conv = conversations.get(newId);
const isParticipant = conv?.mainDialog.participants.some(
(p) => p.id === myAgentId,
);
if (conv?.stage === 'OPEN' && !isParticipant) {
await joinAsReader(newId, conv.mainDialog.id);
}
}
}
async function joinAsReader(conversationId: string, dialogId: string) {
try {
await client.dispatch({
action: 'conversation/join-as-reader',
value: { conversationId, dialogId },
});
} catch (err) {
console.error('Failed to join as reader', err);
}
}
async function leaveAsReader(conversationId: string, dialogId: string) {
try {
await client.dispatch({
action: 'conversation/leave',
value: { conversationId, dialogId, role: 'READER' },
});
} catch (err) {
console.error('Failed to leave as reader', err);
}
}
Wire onConversationFocusChange to whatever your app uses to track the currently-open conversation (a router watcher, a selected-item state change, etc.) — the SDK itself has no focusConversation() API and doesn't call join/leave for you.
Why
-
To act on the conversation. Some actions (like
conversation/takeover, gated separately by its own feature + privilege check) require the agent to already be a participant. Joining as reader first establishes that participation so a subsequent takeover — or other participant-scoped action — can succeed. -
To get live, conversation-scoped updates. Joining as
READERadds the agent to the participant list, soconversations/updateevents for this conversation (participant changes, state changes, etc.) reflect the agent's presence and other participants can see they're being observed.
READER is intentionally the lightest-weight role — contrast with conversation/join, which adds the agent as a full MANAGER for active supervision, and conversation/takeover, which replaces the ASSIGNED_AGENT outright. Join-as-reader never touches conversation ownership.
The full set of supported dispatch actions can be found in the API Reference.