# Optimistic updates Your users see their messages the instant they hit send. AI Transport inserts the message into the local tree before the server confirms, and reconciles automatically using the `codec-message-id`. User messages appear in the conversation immediately, before the server confirms. The client inserts the message into the tree optimistically. When the server publishes it to the session, the transport reconciles the optimistic message with the published one using the `codec-message-id`. The message keeps its content and its place in the conversation. > Optimistic updates need a durable session, because the SDK reconciles your optimistic message against the conversation tree it holds. ![Diagram showing the same message id on a local-insert path and a wire-publish path, reconciling by id when the channel echo arrives](https://raw.githubusercontent.com/ably/docs/main/src/images/content/diagrams/ait-optimistic-update.png) ## How it works When a user sends a message, the client inserts it into the conversation tree before the POST request reaches the server. The message appears in the UI immediately. Behind the scenes: 1. The client generates a `codec-message-id` and inserts the message into the conversation tree. 2. The client publishes the input directly to the session, carrying that `codec-message-id`. 3. The application POSTs the invocation pointer (`inputEventId`, `sessionName`) to the agent endpoint to wake the agent. The POST does not carry the message body; the agent reads the input event off the session. 4. The client receives the message it published back through its session subscription. 5. The session matches the echo to the optimistic insert by `codec-message-id` and promotes it to a confirmed message. After reconciliation, the optimistic message is replaced by the wire-confirmed version. ## Serial promotion Optimistic messages do not have an Ably serial number; they exist only in the local tree. When the message the client published arrives back through its session subscription, the reconciled message receives a real Ably serial. This is serial promotion. Serial promotion matters for ordering. Messages in the conversation tree are ordered by their Ably serial. Optimistic messages sit at the end of the tree until they are promoted. Once promoted, their position reflects the true order of publication assigned by the Ably channel. ## Send several inputs at once A call to `send` takes an array, and at most one entry in it may introduce a new message. The rest of the entries have to be wire-only references to messages that already exist: a `regenerate` signal, or the tool resolutions (`tool-result`, `tool-result-error`, or `tool-approval-response`) of one assistant turn. An input carrying a `codecMessageId` is a reference; a `user-message` never is. The check runs before any local insertion or publish, so a send carrying two new messages rejects with `InvalidArgument` and leaves the tree untouched: ### Javascript ``` // Client-side. Throws: two user-message inputs introduce two new messages. await view.send([ { kind: 'user-message', message: { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Here is my updated question' }] } }, { kind: 'user-message', message: { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'And some additional context' }] } }, ]); ``` A valid array pairs one new message with references. Resolving two tool calls on the same assistant turn and following them with a prompt looks like this: ### Javascript ``` // Client-side. One new message, two wire-only tool resolutions. await view.send([ { kind: 'tool-result', codecMessageId, payload: { toolCallId: firstCallId, output: firstOutput } }, { kind: 'tool-result', codecMessageId, payload: { toolCallId: secondCallId, output: secondOutput } }, { kind: 'user-message', message: { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Now summarise both.' }] } }, ]); ``` The new message is inserted optimistically and reconciled by its `codec-message-id` when the echo arrives. The wire-only entries are published without an optimistic insert, because the messages they reference are already in the tree. To send two prompts as two turns, call `send` twice. ## Handle errors A publish failure rejects `send()`. It is not also emitted on the session's `error` event, because one error reaches you through one mechanism and the caller is already awaiting the promise. Catch it at the call site: ### Javascript ``` // Client-side. try { const run = await view.send({ kind: 'user-message', message: { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, }); // The SDK publishes; waking the agent is yours. await fetch('/api/chat', { method: 'POST', body: JSON.stringify(run.toInvocation().toJSON()) }); } catch (error) { // `InsufficientCapability` when the token lacks publish on the channel, // `SessionSendFailed` otherwise. showRetry(error.message); } ``` The SDK clears the optimistic node on that failure only when the publish never produced a server-assigned serial. A node that was acked is part of the canonical channel state, so it stays, and every observer already sees it. If the publish succeeds but the agent-wake POST fails, the message stays in the tree, because it is a real published conversation entry. Retry the POST; the agent finds the trigger by paging history. ## Reconciliation details The session uses the `codec-message-id` header (under the `extras.ai.transport` tier) to match published messages to optimistic inserts. When the client publishes an input, the SDK generates a client-side codec-message-id and sets it on the published message. The domain message's own `id` is preserved verbatim from the source stream; correlation does not rely on it. When the client receives the published message through its subscription, the SDK matches on the codec-message-id and promotes the optimistic message to a confirmed one. A published message that does not match any optimistic insert (for example, a message from another client) is added to the tree normally. The reconciliation logic only applies to messages the current client sent. ## Edge cases and unhappy paths - A failed publish rejects `send()` and clears the optimistic node, but only if that node never received a server-assigned serial. A failed agent-wake POST leaves the published message in the tree because it is a real conversation entry, so retry the POST rather than resending the message. - `regenerate()` inserts nothing optimistically. It is a wire-only signal, so its reply run appears only once the agent's run-start arrives. A spinner keyed on an optimistic insert never shows for a regenerate. - The session matches on the client-side `codec-message-id` the SDK generates, so a duplicate `message.id` cannot confuse reconciliation. [Database hydration](https://ably.com/docs/ai-transport/durable-sessions/database-hydration.md) matches on `message.id`, so use `crypto.randomUUID()` to generate IDs. - A published message that arrives before the server returns from POST still reconciles correctly. Reconciliation uses the `codec-message-id` rather than request ordering. - Two clients sending at the same time each insert an optimistic message and publish it to the session. Each client only reconciles its own; the other side appears as a normal observer message. - A user message published with a different `codec-message-id` is added to the tree as a new message, and one published with no `codec-message-id` is dropped. The SDK sets this header automatically; only custom codecs that publish raw messages need to set it themselves. ## FAQ ### Why do I still need a server when messages are optimistic? The optimistic insert is a local UI optimisation. The client publishes the canonical message to the session itself, so every other client sees it. The server runs the LLM and publishes the reply. ### Does optimistic insertion work for assistant messages? Streamed assistant text arrives only from the agent. The one client-side insertion is a [client tool result](https://ably.com/docs/ai-transport/durable-sessions/tool-calling.md#client-executed) under the Vercel codec, which the client publishes as an optimistic reply run that the tree reconciles onto the agent's `ai-run-start`. ### What if the server publishes the message but the response never arrives? The optimistic message reconciles when the publish arrives. If the publish never arrives, through a server failure, your retention window expiring, or a network drop, the optimistic message stays unreconciled. Await `run.started` with a timeout of your own if you want to surface that state to the user. ### Can I roll back an optimistic message? Yes. Remove it from your application state when you decide the send has failed. The transport does not impose a rollback policy. ### Does this work with branching? Yes. The optimistic message carries the `parent` and `forkOf` headers for its [branch](https://ably.com/docs/ai-transport/durable-sessions/branching.md) from the moment the client inserts it. ## Related features - [Token streaming](https://ably.com/docs/ai-transport/streaming/token-streaming.md): how the agent's response streams after the user's message. - [Conversation branching](https://ably.com/docs/ai-transport/durable-sessions/branching.md): edits use optimistic updates for the revised message. - [Multi-device and fan-out](https://ably.com/docs/ai-transport/streaming/multi-device.md): optimistic messages reconcile across devices. ## Related Topics - [Overview](https://ably.com/docs/ai-transport/durable-sessions.md): A drop-in durable session layer for AI applications. AI Transport holds the conversation and the message state your UI renders, including branching, and you render from its React hooks. - [Sessions](https://ably.com/docs/ai-transport/durable-sessions/sessions.md): Understand sessions in AI Transport: persistent, shared conversation state that exists independently of any connection, and the ClientSession and AgentSession objects that attach to it. - [Conversation tree](https://ably.com/docs/ai-transport/durable-sessions/conversation-tree.md): Understand how AI Transport organises messages into a branching conversation tree, and how views give each client its own linear path through it. - [Branching, edit, and regenerate](https://ably.com/docs/ai-transport/durable-sessions/branching.md): Edit user messages, regenerate AI responses, and navigate branches with Ably AI Transport. The full history is preserved in the conversation tree. - [Tool calling](https://ably.com/docs/ai-transport/durable-sessions/tool-calling.md): Stream tool invocations and results through Ably AI Transport. Server-executed and client-executed tools with persistent state. - [Human-in-the-loop](https://ably.com/docs/ai-transport/durable-sessions/human-in-the-loop.md): Add human approval gates to AI agent workflows with Ably AI Transport. Approve tool executions and provide input across devices. - [Database hydration](https://ably.com/docs/ai-transport/durable-sessions/database-hydration.md): Hydrate an AI conversation from your own database with AI Transport and reconcile it with the live Ably channel, with no gaps and no duplicate messages. - [Migrate from Streaming](https://ably.com/docs/ai-transport/durable-sessions/move-up-from-streaming.md): What changes in an application already streaming with Ably AI Transport when it moves the conversation into a durable session, and what stays exactly as it is. ## Documentation Index To discover additional Ably documentation: 1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages. 2. Identify relevant URLs from that index. 3. Fetch target pages as needed. Avoid using assumed or outdated documentation paths.