Choose a turn transport
Both transports create the same persisted messages and use the same durable turn machinery. A socket disconnect detaches the client; it does not cancel the turn.
Lifecycle
- Create a chat. Save both
idandagent_id. - Optionally upload files and save their IDs.
- Start a turn over the WebSocket or with
POST /v1/agents/{agent_id}/chats/{chat_id}/turns. - Keep the returned
turn_id. Only one turn may bein_progressfor a chat at a time. - Continue with the same
chat_id, or load persisted history with Get Chat.
There is no lazy chat creation. A WebSocket
send_message without chat_id returns MISSING_CHAT_ID; an unknown or unowned chat returns CHAT_NOT_FOUND.WebSocket turn sequence
Every accepted WebSocket send or edit first returnsturn_accepted at seq: 0. Durable replayable frames then begin at sequence 1.
turn_id, order and deduplicate it by (turn_id, seq), and persist the greatest sequence seen for each turn.
Events
A turn ends with exactly one enveloped
message, error, or interrupted event. The bare interrupted control frame is only an interrupt acknowledgement and is not terminal.
This table describes turn-enveloped events. WebSocket errors sent during socket setup/control, before turn_accepted, and after turn acceptance use three different contexts, and code is not universal across them. See WebSocket error frames.
Streaming text
Eachtext_delta.data.content is a new chunk, not a running total. On terminal message, replace the assembled buffer with message.data.content; it is the authoritative persisted answer.
If a turn is cancelled, the enveloped terminal interrupted.data.content is the authoritative persisted partial answer. If a turn fails, refetch the chat: partial content can be represented by the durable turn snapshot while the error is recorded.
Durable recovery
On a cold load, treatrunning_turn_id, not is_processing, as the authoritative signal that the chat still owns a running turn. is_processing is a liveness hint and can be false while a stale running row is awaiting server resolution.
To recover a disconnected WebSocket turn:
- Call
GET /v1/chats/{chat_id}and replace local history withmessages; renderactive_turn.partial_contentas the in-progress assistant text. - Advance the cursor to
running_turn_last_seq. That watermark covers both the returned messages andactive_turn.partial_content; do not append an older streaming buffer to the snapshot. - While
running_turn_idstill equals the turn, keep the composer locked, reconnect, and sendresumefrom the refreshed watermark. - If exact replay frames arrive, deduplicate by
(turn_id, seq)and continue. Exact replay is a latency optimization, not the correctness boundary. - If an enveloped
resumefallback frame arrives, that subscription will send no more turn frames. Immediately repeat the REST reconciliation and retry after backoff. - When
running_turn_idbecomesnull, the terminal persisted state is inmessagesand the composer can unlock.
resume.data.status values are raw lifecycle values (running, done, error, cancelled). REST turn status uses the public values in_progress, completed, failed, and cancelled. Branch on alive and status; treat reason (gap, degraded, or expired) as diagnostic context.
Legacy chats without an agent binding
New chats always return a non-nullagent_id. Older chats can return agent_id: null from List Chats and Get Chat. Keep your organization’s configured published agent UUID in client configuration, verify it against List Agents, and use that ID in the WebSocket or REST-turn URL. The current display name and is_trellis_default flag are historical metadata; drive the fallback by the configured ID, not by either label.
The server accepts that agent for an unbound legacy row, but does not backfill the row. Apply the fallback every time the chat is loaded.
Shared limits
Limits are keyed by the authenticatedmember_id, not by JWT or socket. Re-authenticating does not reset them, and multiple JWTs for the same member share the same buckets.
HTTP throttles return
429 with Retry-After. A throttled WebSocket send/edit receives an enveloped pre-turn error with code: "RATE_LIMIT" and retry_after_seconds.
Persisted message roles and types
Persisted messages containrole, type, content, timestamp, and metadata.
Tool call/result
content is a JSON-encoded string in REST history. Parse it before dispatching on its structured fields.