Architecture
Reasoning channel
The reasoning channel is request/response:- A
ReasoningJobis created by the backend and published to theassistant_channelpubsub topic for the integration. - The assistant plugin subscribes to an SSE stream and receives the job envelope.
- The plugin executes the reasoning (via local LLM, remote API, or subagent).
- The plugin posts the result back to the backend’s reasoning job completion endpoint.
Envelope shape
BYO encryption
When the job is BYO-encrypted, thepayload contains a BYOEnvelope with ciphertext and nonce fields. The plugin decrypts using the XChaCha20-Poly1305 key derived from the X25519 key exchange performed during pairing.
SSE stream
The plugin connects toGET /api/v1/openclaw/integrations/{id}/stream with the relay token as Bearer auth. Events are delivered as Server-Sent Events.
Heartbeat
The plugin posts heartbeat updates to/api/v1/openclaw/integrations/{id}/heartbeat every 30 seconds, carrying host status and capability information.
Capability probe
When the backend sends a reasoning job withpurpose: "capability_probe", the assistant self-describes its available skills and returns a capability profile. This is displayed in the desktop assistant settings.
Chat channel
The chat with a paired assistant is one continuous conversation, keyed bysession_id (the relay session). It is append-only: either side may add a message at any time, like a normal messaging app. There is no per-message turn to open and close, and no one-reply-per-message rule. The same conversation spans the live call and the post-call review, and the post-call summary is just another message in it.
In the direct-relay topology there is no backend. The desktop runs a loopback HTTP listener and advertises a callback URL (tunneled when the assistant runs on a remote host). The assistant posts every message it wants to add to the conversation to this listener.
Endpoint
POST {callback_url}/tasks/{task_id} with the callback token as Bearer auth. The path task_id must be a UUID, but it is only a correlation handle, not a lock. What identifies the conversation is session_id in the body. Echo session_id back exactly as received: the desktop routes on it, and a session prefixed with insights: renders your reply in the Insights tab instead of the chat.
Body
session_id and status are required. For status: "message", send a stable, unique message_id; the desktop dedupes by it, so retries are safe. user_message_id is optional and links a message to the user message it answers, used only to scope the typing indicator.
Statuses
The desktop still accepts the older one-shot statuses (
completed, silent, tool_call) for backward compatibility, but new assistants should use message and typing.
How a conversation flows
Stream the agent’s work as it happens: emittyping with a short label (“Searching the CRM”) while working, and one message per thing the agent says. An agent that produces several outputs (an answer, a follow-up, then a result after a long tool run) sends several message callbacks in order, each with its own message_id. Nothing finalizes the conversation, so there is no timeout that kills a slow or multi-step turn.
User messages arrive as plain text and are appended to the same conversation; feed them straight to the agent’s persistent per-session memory. This includes an approval reply like “/approve” or “continue”: they are ordinary text the agent interprets itself. Do not special-case them, and do not assume one user message yields exactly one reply. Because the agent keeps its own session memory, an approval reaches the paused agent and it resumes, streaming further message callbacks.
Messages for a session_id the desktop no longer knows are dropped with {"ok": true, "ignored": true, "reason": "session_ended"}.
Insights
Live insights for the Insights tab come from the transcript, not the chat. When the desktop forwards a transcript turn for analysis, it setssession_id to insights:<call-session>. Reply to it exactly as you would any other turn (a normal message with reply_text); because you echo session_id back unchanged, the desktop sees the insights: prefix and renders your reply in the Insights tab instead of the conversation. Keep it to one short sentence, and reply none (or stay silent) when nothing is worth surfacing. The coaching framing for these turns (the situation, the active playbook, and the one-sentence contract) is set by the desktop and sent once as a system_prelude on the insights: session when the call starts, so treat that prelude as the standing context for the session and do not prepend a coaching instruction of your own. Insights never appear in the chat, and transcript analysis never pollutes it.
Providers
StageWhisper’s provider registry mapsprovider_kind values (e.g., openclaw, hermes) to descriptors with install commands, pairing instructions, and protocol version requirements. Each provider implements the same envelope contract.
Current providers: OpenClaw, Hermes (coming soon).