>` |
@@ -89,14 +88,14 @@ function Compare({ messageId }: { messageId: string }) {
leftView.selectSibling(messageId, i)}
+ onSelect={(i) => leftBranch.select(i)}
/>
rightView.selectSibling(messageId, i)}
+ onSelect={(i) => rightBranch.select(i)}
/>
diff --git a/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx b/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx
new file mode 100644
index 0000000000..65f919a8d7
--- /dev/null
+++ b/src/pages/docs/ai-transport/api/react/core/use-messages-with-seed.mdx
@@ -0,0 +1,73 @@
+---
+title: "useMessagesWithSeed"
+meta_description: "Reconcile a persisted conversation seed with the live AI Transport channel from React and render the composed conversation."
+meta_keywords: "AI Transport, React, useMessagesWithSeed, seed, hydration, loadUntil, Ably"
+---
+
+`useMessagesWithSeed` reconciles a persisted conversation seed with the live Ably channel and returns the composed conversation, oldest-first: the seed followed by the live tail that has not yet been seeded. It is the core primitive behind database-backed [hydration](/docs/ai-transport/features/database-hydration), and the Vercel `useMessageSync` builds on it.
+
+Pass the `view` from a resolved session. [`useClientSession()`](/docs/ai-transport/api/react/core/providers#client-session-provider) returns a handle whose `session.view` is a `View`.
+
+
+```javascript
+import { useMessagesWithSeed } from '@ably/ai-transport/react';
+
+const messages = useMessagesWithSeed({
+ view: session?.view,
+ seed,
+ getMessageId: (message) => message.id,
+});
+```
+
+
+This hook is typically used within a [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider), which resolves the session whose `view` you pass in.
+
+The hook takes the newest seed message's id, via `getMessageId`, as the seam and drives [`View.loadUntil`](/docs/ai-transport/api/javascript/core/client-session) to page the channel back until that id reappears. It then composes the seed followed by the live tail, dropping the single overlapping message at the seam. With no seed (an empty array) it surfaces the live channel window unchanged.
+
+## Parameters
+
+`useMessagesWithSeed` accepts a single `UseMessagesWithSeedOptions` object:
+
+
+
+| Parameter | Required | Description | Type |
+| --- | --- | --- | --- |
+| view | optional | The view over the live channel to reconcile against, for example `session.view`, or `undefined` before the session or view resolves. The hook then surfaces the seed as-is. | `View` or Undefined |
+| seed | required | The persisted conversation, oldest-first. Compared by content, so passing a fresh array each render, for example `data ?? []`, is safe. An empty array is a loaded-but-empty conversation and surfaces the live channel window unchanged. While the seed is still loading, set `skip` instead of passing `[]`. | `TMessage[]` |
+| getMessageId | required | Returns a message's stable domain id, the seam key shared between your store and the channel. The transport's internal `codecMessageId` is never persisted. | `(message: TMessage) => string` |
+| skip | optional | Holds the reconciliation while the seed is still loading, for example during an async store fetch. When `true` the hook does not page the channel and returns `[]`. Clear it once the seed has loaded. Defaults to `false`. | Boolean |
+
+
+
+## Returns
+
+`TMessage[]`. The composed conversation, oldest-first: the seed followed by the live tail newer than the seam. While `skip` is `true`, or before a seed loads, the hook returns `[]`.
+
+## Example
+
+A component that fetches its seed from a store, reads the view from the resolved session, and renders the composed conversation:
+
+
+```javascript
+import { useClientSession, useMessagesWithSeed } from '@ably/ai-transport/react';
+
+function Conversation({ seed }) {
+ const { session } = useClientSession();
+ const messages = useMessagesWithSeed({
+ view: session?.view,
+ seed,
+ getMessageId: (message) => message.id,
+ });
+
+ return (
+ <>
+ {messages.map((message) => (
+
+ ))}
+ >
+ );
+}
+```
+
+
+For Vercel AI SDK applications, the pre-typed `useMessagesWithSeed` from `@ably/ai-transport/vercel/react` omits `getMessageId` because it keys on `UIMessage.id`, and `useMessageSync` wraps it for `useChat`.
diff --git a/src/pages/docs/ai-transport/api/react/core/use-view.mdx b/src/pages/docs/ai-transport/api/react/core/use-view.mdx
index 23ba8bf330..8f17b461e1 100644
--- a/src/pages/docs/ai-transport/api/react/core/use-view.mdx
+++ b/src/pages/docs/ai-transport/api/react/core/use-view.mdx
@@ -43,8 +43,8 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| session | optional | A client session whose default view to subscribe to. Defaults to the nearest provider. | `ClientSession` |
-| view | optional | A specific view to subscribe to directly. Takes priority over `session`. | `View` |
-| limit | optional | Maximum number of older Runs to reveal per page. When provided, auto-loads the first page on mount. | Number |
+| view | optional | A specific view to subscribe to directly. Takes priority over `session`. | `ClientView` |
+| limit | optional | Number of older codecMessages to reveal per page (exactly `limit`, fewer only at the end of history). When provided, auto-loads the first page on mount. | Number |
| skip | optional | When `true`, skip all subscriptions and return an empty handle immediately. | Boolean |
@@ -56,18 +56,17 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| Property | Description | Type |
| --- | --- | --- |
| messages | The visible messages along the selected branch, each paired with its `codecMessageId`. Read the domain object from each entry's `message` field. | `CodecMessage[]` |
-| hasOlder | Whether there are older Runs that can be revealed via `loadOlder`. | Boolean |
+| hasOlder | Whether there are older messages that can be revealed via `loadOlder`. | Boolean |
| loading | Whether a page load is currently in progress. | Boolean |
| loadError | Set when the most recent `loadOlder` call failed. Cleared automatically on the next successful load. | `Ably.ErrorInfo` or Undefined |
-| loadOlder | Reveal older Runs. No-op if already loading. | `() => Promise` |
+| loadOlder | Reveal older messages, resolving to the revealed page (oldest-first). Returns `[]` when nothing was revealed: already loading, no view resolved, history exhausted, or the load failed. | `() => Promise[]>` |
| runOf | Look up the `RunInfo` for the Run that owns the given `codecMessageId`. | |
| run | Direct lookup by Run id. | |
| runs | Snapshot of the visible Runs along the selected branch, in chronological order. Returns `[]` when the view isn't resolved. | `() => RunInfo[]` |
-| branchSelection | Resolve the `BranchSelection` bundle anchored at `codecMessageId`. Always returns a safe object. | |
-| selectSibling | Select a sibling at the branch point anchored at `codecMessageId`. | `(codecMessageId, index) => void` |
-| send | Send one or more `TInput`s on the channel and fire a POST. Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. Takes optional . | `(events, options?) => Promise` |
-| regenerate | Regenerate an assistant message using this view's branch for history. | `(messageId, options?) => Promise` |
-| edit | Edit a user message, forking from this view's branch. | `(messageId, inputs, options?) => Promise` |
+| branchSelection | Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb to navigate it. Always returns a safe handle. | |
+| send | Send an input on the channel and fire a POST. At most one new message per send; the array form carries only wire-only inputs (tool results, approvals). Wrap a domain message via `codec.createUserMessage` (or build the `{ kind: 'user-message', message }` literal) to send a fresh user message. Takes optional . | `(events, options?) => Promise>` |
+| regenerate | Regenerate an assistant message using this view's branch for history. | `(messageId, options?) => Promise>` |
+| edit | Edit a user message, forking from this view's branch. | `(messageId, inputs, options?) => Promise>` |
@@ -83,7 +82,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
-
+
| Property | Description | Type |
| --- | --- | --- |
@@ -91,6 +90,7 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| siblings | The selected sibling and any alternatives in tree-order. Always contains the currently rendered message for known ids. | `TMessage[]` |
| index | Index of the selected sibling within `siblings`. `0` when there is no real branching. | Number |
| selected | Convenience reference to `siblings[index]`. `undefined` only when `siblings` is empty. | `TMessage` or Undefined |
+| select | Select a sibling at this branch point. `index` is clamped to `[0, siblings.length - 1]`; a silent no-op when the anchor is not a branch anchor. | `(index: number) => void` |
@@ -101,15 +101,14 @@ This hook must be used within a [`ClientSessionProvider`](/docs/ai-transport/api
| forkOf | The codec-message-id of the message this send replaces (fork). | String |
| parent | The codec-message-id of the predecessor in the conversation thread. Auto-computed when omitted. | String |
| runId | Reuse an existing `runId` (for example to resume a suspended run). | String |
-| inputEventId | Override the `inputEventId` for this send. Defaults to `crypto.randomUUID()`. | String |
-## Reveal older Runs
+## Reveal older messages
-{`loadOlder(): Promise`}
+{`loadOlder(): Promise[]>`}
-Load older messages into the view by revealing more Runs. When `limit` is set on the hook, the first page auto-loads on mount. Call `loadOlder` again to reveal more. The call is gated so concurrent invocations collapse to one in-flight request.
+Reveal older messages into the view, resolving to the revealed page (oldest-first). When `limit` is set on the hook, the first page auto-loads on mount. Call `loadOlder` again to reveal more. The call is gated so concurrent invocations collapse to one in-flight request; it returns `[]` when nothing was revealed (already loading, no view resolved, history exhausted, or the load failed).
On failure, `loadError` is set. On the next successful load, `loadError` is cleared automatically.
@@ -127,33 +126,39 @@ Direct lookup by Run id. Symmetric with [`runOf`](#run-of) for callers that alre
## Resolve a branch selection
-{`branchSelection(codecMessageId: string): BranchSelection`}
+{`branchSelection(codecMessageId: string): BranchHandle`}
-Resolve the `BranchSelection` bundle anchored at `codecMessageId`. Always returns a safe object: for branch anchors with N siblings, `siblings` carries every sibling Run's view of the anchor slot; for non-anchor messages, `siblings` is `[thisMessage]` and `hasSiblings` is `false`.
+Resolve the `BranchHandle` anchored at `codecMessageId`: the sibling state plus a `select(index)` verb to navigate it. Always returns a safe handle: for branch anchors with N siblings, `siblings` carries every sibling Run's view of the anchor slot; for non-anchor messages, `siblings` is `[thisMessage]` and `hasSiblings` is `false`.
## Select a sibling
-{`selectSibling(codecMessageId: string, index: number): void`}
+Switch the visible sibling through the [`BranchHandle`](#branch-selection) the `branchSelection` call returns, rather than a separate hook method:
-Select a sibling at the branch point anchored at `codecMessageId`. `index` is clamped to `[0, siblings.length - 1]`. Silent no-op when the message is not a branch anchor. Emits `'update'` on the underlying view when the visible output changes.
+
+```javascript
+view.branchSelection(codecMessageId).select(1);
+```
+
+
+`index` is clamped to `[0, siblings.length - 1]`. The call is a silent no-op when the message is not a branch anchor, and emits `'update'` on the underlying view when the visible output changes.
## Send inputs
-{`send(events: TInput | TInput[], options?: SendOptions): Promise`}
+{`send(events: TInput | TInput[], options?: SendOptions): Promise>`}
-Send one or more `TInput`s on the channel and fire a POST. Each `TInput` carries its own routing metadata (`parent`, `target`, `codecMessageId`).
+Send an input on the channel and fire a POST. Each `TInput` carries its own routing metadata (`parent`, `target`, `codecMessageId`).
-To send a fresh user message, build a `UserMessage` input. Use the codec factory (`codec.createUserMessage(message)`) or build the literal directly: `{ kind: 'user-message', message }`. A send containing at least one `UserMessage` mints a fresh Run. A send containing only tool-resolution inputs (`tool-result`, `tool-result-error`, `tool-approval-response`) is a continuation; pair it with `options.runId` to extend a suspended Run.
+To send a fresh user message, build a `UserMessage` input. Use the codec factory (`codec.createUserMessage(message)`) or build the literal directly: `{ kind: 'user-message', message }`. A send introduces at most one new message: exactly one `UserMessage` for a fresh send (which mints a new Run), or none for a continuation. The array form exists only to carry the wire-only inputs that resolve a single assistant turn (`tool-result`, `tool-result-error`, `tool-approval-response`); pair it with `options.runId` to extend a suspended Run. Passing more than one new message rejects with `InvalidArgument`.
## Regenerate an assistant message
-{`regenerate(messageId: string, options?: SendOptions): Promise`}
+{`regenerate(messageId: string, options?: SendOptions): Promise>`}
Regenerate an assistant message. Creates a new Run that targets the message and threads under its parent user message. Both ids are computed automatically from this view's branch.
## Edit a user message
-{`edit(messageId: string, inputs: TInput | TInput[], options?: SendOptions): Promise`}
+{`edit(messageId: string, inputs: TInput | TInput[], options?: SendOptions): Promise>`}
Edit a user message. Creates a new Run that forks the target message with the replacement inputs. `forkOf`, `parent`, and history are computed automatically from this view's branch.
@@ -171,7 +176,6 @@ function Conversation() {
hasOlder,
loadOlder,
branchSelection,
- selectSibling,
send,
regenerate,
} = useView({ limit: 30 });
@@ -187,7 +191,7 @@ function Conversation() {
{branch.hasSiblings && (
selectSibling(codecMessageId, i)}
+ onSelect={(i) => branch.select(i)}
/>
)}
{message.role === 'assistant' && (
diff --git a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
index 4d83219edf..e4465dcd8a 100644
--- a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
+++ b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
@@ -46,7 +46,7 @@ function App() {
| credentials | Fetch credentials mode for the invocation POST. Set to `'include'` for cookie-based cross-origin auth. | `RequestCredentials` |
| fetch | Custom fetch implementation for the invocation POST. Defaults to `globalThis.fetch`. | `typeof globalThis.fetch` |
| chatOptions | Hooks for customising chat request construction (for example `prepareSendMessagesRequest`). Must be stable across renders; wrap in `useMemo` or define outside the component. A new object reference recreates the `ChatTransport`. | |
-| messages | Initial messages to seed the conversation tree with. | `UIMessage[]` |
+| historyPageSize | Wire-message limit fetched per channel-history round trip when paging older history. Defaults to 100. | Number |
| logger | Logger instance for diagnostic output. | `Logger` |
| children | Descendant components that consume the chat transport via [`useChatTransport`](/docs/ai-transport/api/react/vercel/use-chat-transport). | `ReactNode` |
diff --git a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx
index 487f204005..39032e5323 100644
--- a/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx
+++ b/src/pages/docs/ai-transport/api/react/vercel/use-message-sync.mdx
@@ -36,6 +36,7 @@ This hook must be used within a `ChatTransportProvider` (exported from `@ably/ai
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| setMessages | required | The `setMessages` updater function from `useChat()`. Called with an updater that returns the next overlay. | `(updater: (prev: UIMessage[]) => UIMessage[]) => void` |
+| messages | optional | The application's seeded conversation, typically `useChat()`'s `messages` where persisted history was supplied via `useChat({ messages })`. When non-empty, the hook reconciles it with the live channel: it takes the newest entry's `id` as the seam, pages the channel back to it, and composes stored history with the live tail with no duplicate. Omit it, or pass `[]`, for the full live channel history. See [database hydration](/docs/ai-transport/features/database-hydration). | `UIMessage[]` |
| channelName | optional | Channel name of the `ChatTransportProvider` to observe. Omit to use the nearest provider. | String |
| skip | optional | When `true`, skip all subscriptions. | Boolean |
@@ -60,6 +61,15 @@ Use `useMessageSync` whenever you mount a `ChatTransportProvider` and render mes
If you render messages directly from [`useView`](/docs/ai-transport/api/react/core/use-view) instead of from `useChat`, you do not need `useMessageSync`; `useView` already reflects the view state.
+To seed the conversation from your own database, pass the loaded history as `messages`, both to `useChat` and to `useMessageSync`. The hook reconciles the seed with the live channel at the seam, so a reloaded conversation shows its stored history plus the live tail exactly once. See [database hydration](/docs/ai-transport/features/database-hydration).
+
+
+```javascript
+const { messages, setMessages } = useChat({ transport: chatTransport, messages: seed });
+useMessageSync({ messages: seed, setMessages });
+```
+
+
## Example
A complete Vercel-React chat with sync wired in. A second browser tab pointing at the same `channelName` sees every message as soon as it arrives on the channel.
diff --git a/src/pages/docs/ai-transport/concepts/connections.mdx b/src/pages/docs/ai-transport/concepts/connections.mdx
index 7fee460c43..a9c10ef8c4 100644
--- a/src/pages/docs/ai-transport/concepts/connections.mdx
+++ b/src/pages/docs/ai-transport/concepts/connections.mdx
@@ -50,8 +50,8 @@ A connection moves through four stages. Construct it with `createClientSession`
Several `ClientSession` instances can be open against the same session simultaneously. Each one independently:
- Materialises the tree from the channel (the same tree, on the wire).
-- Selects its own branch via the [`view.selectSibling`](/docs/ai-transport/api/javascript/core/client-session#view) method.
-- Holds its own pagination window over older Runs.
+- Selects its own branch via the [`view.branchSelection`](/docs/ai-transport/api/javascript/core/client-session#properties) handle's `select` method.
+- Holds its own pagination window over older messages.
What they share is the channel-backed state. A message one client publishes lands on every other client's tree via the channel subscription. A cancel one client publishes is observed by the agent (and by every other client) through their own subscriptions.
diff --git a/src/pages/docs/ai-transport/concepts/invocations.mdx b/src/pages/docs/ai-transport/concepts/invocations.mdx
index 60367f66de..3019c81989 100644
--- a/src/pages/docs/ai-transport/concepts/invocations.mdx
+++ b/src/pages/docs/ai-transport/concepts/invocations.mdx
@@ -9,7 +9,7 @@ An Invocation is the trigger posted to an agent endpoint to start or continue a

-The client SDK mints `inputEventId` (and the input's `codecMessageId`) when you call `view.send()` / `regenerate()` / `edit()` and stamps them on the channel publish. The HTTP POST is owned by your application code (or by the bundled Vercel [`ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport)): call `activeRun.toInvocation().toJSON()` to get the wire body and POST it to the agent. The agent mints the `runId` and `invocationId` when it creates the Run, and returns them on the HTTP response so the caller can observe them.
+The client SDK mints `inputEventId` (and the input's `codecMessageId`) when you call `view.send()` / `regenerate()` / `edit()` and stamps them on the channel publish. The HTTP POST is owned by your application code (or by the bundled Vercel [`ChatTransport`](/docs/ai-transport/api/javascript/vercel/chat-transport)): call `clientRun.toInvocation().toJSON()` to get the wire body and POST it to the agent. The agent mints the `runId` and `invocationId` when it creates the Run, and returns them on the HTTP response so the caller can observe them.
The same Run can be triggered more than once: a fresh send triggers a fresh Invocation; a tool result follow-up creates another; a retry after a serverless cold start creates another. Each HTTP request produces one agent-minted `invocationId`. The `runId` is fresh on the first invocation and reused on every subsequent continuation.
@@ -17,7 +17,7 @@ The same Run can be triggered more than once: a fresh send triggers a fresh Invo
The client and the agent are separated by an unreliable network and a stateless HTTP boundary. The client publishes input on the channel and your application code posts to the agent endpoint. The agent receives the POST, possibly minutes later, and needs to know exactly which work to do:
-- Which input event triggered the work, so the agent's [`Run.start`](/docs/ai-transport/api/javascript/core/agent-session#run-start) can wait for the exact event on the channel.
+- Which input event triggered the work, so the agent's [`AgentRun.start`](/docs/ai-transport/api/javascript/core/agent-session#run-start) can wait for the exact event on the channel.
- Which channel, since the agent doesn't know the session's channel name except via the trigger.
These two identifiers are the Invocation's payload (`inputEventId`, `sessionName`). They're carried in the HTTP POST body so the agent has them before the channel is observable. Everything else lives on the channel: run identity is resolved from the triggering input event's wire headers (the agent mints `runId` for a fresh run, or reads the existing `runId` off the input event for a continuation). The `invocationId` is minted by the agent per HTTP request.
@@ -28,21 +28,21 @@ An `InvocationData` is the wire shape: the JSON body in the POST.
| Field | What it carries |
| --- | --- |
-| `inputEventId` | The input event on the channel that triggered this invocation. The agent's `Run.start()` waits for the channel message carrying this `event-id` (rewind + live). |
+| `inputEventId` | The input event on the channel that triggered this invocation. The agent's `AgentRun.start()` waits until the channel message carrying this `event-id` has been observed, whether it arrives live or is paged in from channel history. |
| `sessionName` | The session's logical name, used as the Ably channel name. |
-The `Invocation` class is the runtime view of that data. The client side uses `ActiveRun.toInvocation()` to obtain one from a returned `ActiveRun`; the agent side uses `Invocation.fromJSON(data)` to construct one from the parsed POST body, then hands it to `session.createRun(invocation, runtime?)`. `createRun` mints the `invocationId` (one per HTTP request) and, for a fresh run, the `runId`; for a continuation the agent reads the existing `runId` off the triggering input event's wire headers. The application returns `run.runId` and `run.invocationId` on the HTTP response so the caller can observe them.
+The `Invocation` class is the runtime view of that data. The client side uses `ClientRun.toInvocation()` to obtain one from a returned `ClientRun`; the agent side uses `Invocation.fromJSON(data)` to construct one from the parsed POST body, then hands it to `session.createRun(invocation, runtime?)`. `createRun` mints the `invocationId` (one per HTTP request) and, for a fresh run, the `runId`; for a continuation the agent reads the existing `runId` off the triggering input event's wire headers. The application returns `run.runId` and `run.invocationId` on the HTTP response so the caller can observe them.
-End-to-end: the client publishes the input event on the channel and gets back an `ActiveRun`. The client knows `inputCodecMessageId` immediately, before the agent has minted the run; the `runId` resolves later, once the agent's `ai-run-start` event lands. The application POSTs `activeRun.toInvocation().toJSON()` to the agent endpoint. The agent rebuilds the Invocation, calls `createRun` (which mints `invocationId` and resolves `runId` either by minting for a fresh run or by reading the input event for a continuation), and `run.start()` waits for the matching input event to arrive on the channel via rewind and live. Output events the agent publishes carry the `run-id`, `invocation-id`, and `input-codec-message-id` headers; the client resolves `ActiveRun.runId` once `ai-run-start` lands. The application returns `run.runId` and `run.invocationId` on the HTTP response so the original caller can observe the agent-minted identifiers directly.
+End-to-end: the client publishes the input event on the channel and gets back a `ClientRun`. The client knows `inputCodecMessageId` immediately, before the agent has minted the run; `clientRun.runId` is populated later, once the agent's `ai-run-start` event lands (await `clientRun.started`). The application POSTs `clientRun.toInvocation().toJSON()` to the agent endpoint. The agent rebuilds the Invocation, calls `createRun` (which mints `invocationId` and resolves `runId` either by minting for a fresh run or by reading the input event for a continuation), and `run.start()` waits until the matching input event has been observed on the channel, whether it arrives live or is paged in from channel history. Output events the agent publishes carry the `run-id`, `invocation-id`, and `input-codec-message-id` headers; the client reads `clientRun.runId` once `ai-run-start` lands. The application returns `run.runId` and `run.invocationId` on the HTTP response so the original caller can observe the agent-minted identifiers directly.
## What the Invocation layer requires
| Property | Why it matters |
| --- | --- |
| Stable identity | The triggering input event must be addressable from both sides. The client stamps `event-id` on the channel publish, and your code posts the same `inputEventId` in the body. If they diverged, the agent's lookup would fail. |
-| Deterministic generation | The client mints `inputEventId` and `codecMessageId` inside `view.send()` (and the other write methods). Defaults to `crypto.randomUUID()`; override via [`SendOptions`](/docs/ai-transport/api/javascript/core/client-session#view) for deterministic tests. The agent mints `runId` and `invocationId` per request; supply `RunRuntime.runId` / `RunRuntime.invocationId` to override in tests. |
+| Deterministic generation | The client mints `inputEventId` and `codecMessageId` inside `view.send()` (and the other write methods), each defaulting to `crypto.randomUUID()`. The agent mints `runId` and `invocationId` per request; supply `RunRuntime.runId` / `RunRuntime.invocationId` to override them in tests. |
| Idempotency | The agent endpoint may receive the same Invocation more than once (network retry, queue redelivery). The agent must treat duplicate Invocations safely, typically by checking whether the input event has already produced a Run on the channel. |
-| Decoupled timing | The POST may arrive before, simultaneously with, or after the input event on the channel. The agent's `Run.start()` waits for the input event with rewind + live lookup, so the order doesn't matter. |
+| Decoupled timing | The POST may arrive before, simultaneously with, or after the input event on the channel. The agent's `AgentRun.start()` waits until the input event has been observed, whether it is paged in from channel history or arrives live, so the order doesn't matter. |
| Continuation support | A tool-result delivery or a regenerate produces a new Invocation; the client stamps the existing `run-id` on the new input event's wire headers when publishing. The agent's `createRun` reads that `run-id` from the input event instead of minting a fresh one, and the SDK treats it as a continuation. |
## Receive an Invocation
@@ -69,7 +69,7 @@ export async function POST(req) {
```