diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts
index dd82007afa..bf8438cb4c 100644
--- a/src/data/nav/aitransport.ts
+++ b/src/data/nav/aitransport.ts
@@ -31,6 +31,27 @@ export default {
},
],
},
+ {
+ name: 'Sessions & Identity',
+ pages: [
+ {
+ name: 'Overview',
+ link: '/docs/ai-transport/features/sessions-identity',
+ },
+ {
+ name: 'Identifying users and agents',
+ link: '/docs/ai-transport/features/sessions-identity/identifying-users-and-agents',
+ },
+ {
+ name: 'Online status',
+ link: '/docs/ai-transport/features/sessions-identity/online-status',
+ },
+ {
+ name: 'Resuming sessions',
+ link: '/docs/ai-transport/features/sessions-identity/resuming-sessions',
+ },
+ ],
+ },
],
api: [],
} satisfies NavProduct;
diff --git a/src/images/content/diagrams/ai-transport-before-and-after.png b/src/images/content/diagrams/ai-transport-before-and-after.png
new file mode 100644
index 0000000000..d29ae4a6f1
Binary files /dev/null and b/src/images/content/diagrams/ai-transport-before-and-after.png differ
diff --git a/src/pages/docs/ai-transport/features/sessions-identity/identifying-users-and-agents.mdx b/src/pages/docs/ai-transport/features/sessions-identity/identifying-users-and-agents.mdx
new file mode 100644
index 0000000000..4614747840
--- /dev/null
+++ b/src/pages/docs/ai-transport/features/sessions-identity/identifying-users-and-agents.mdx
@@ -0,0 +1,419 @@
+---
+title: "Identifying users and agents"
+meta_description: "Establish trusted identity and roles in decoupled AI sessions"
+meta_keywords: "user authentication, agent identity, JWT authentication, token authentication, verified identity, capabilities, authorization, user claims, RBAC, role-based access control, API key authentication, message attribution"
+---
+
+Secure AI applications require agents to trust who sent each message and understand what that sender is authorized to do. Ably's identity system uses token-based authentication to provide cryptographically-verified identities with custom attributes that you can access throughout your applications.
+
+## Why identity matters
+
+In decoupled architectures, identity serves several critical purposes:
+
+- Prevent spoofing: Without verified identity, malicious users could impersonate others by claiming to be someone else. Ably supports cryptographically binding each client's identity to their credentials, making spoofing impossible.
+- Message attribution: Agents need to know whether messages come from users or other agents. This is essential for conversation flows in which agent responses should be securely distinguished from user prompts.
+- Personalized behavior: Different users may have different privileges or attributes. A premium user might get access to more capable models, while a free user gets basic functionality. Ably allows your trusted authentication server to embed this information in the client's credentials, allowing this information to be securely passed to agents.
+- Authorization decisions: Some operations should only be performed for specific users. For example, human-in-the-loop (HITL) tool calls that access sensitive data might require admin privileges. Ably allows agents to verify the privilege level and role of the user resolving the tool call.
+
+## Authenticating users
+
+Use [token authentication](/docs/auth/token) to authenticate users securely. Your authentication server generates a token that is signed with the secret part of your Ably API key. Clients use this token to connect to Ably, and the token signature ensures it cannot be tampered with.
+
+The following examples use [JWT authentication](/docs/auth/token#jwt) for its simplicity and standard tooling support. For other approaches, see [token authentication](/docs/auth/token).
+
+Create a server endpoint that generates signed JWTs after verifying user authentication:
+
+
+```javascript
+// Server code
+import express from "express";
+import jwt from "jsonwebtoken";
+
+const app = express();
+
+// Mock authentication middleware.
+// This should be replaced with your actual authentication logic.
+function authenticateUser(req, res, next) {
+ // Assign a mock user ID for demonstration
+ req.session = { userId: "user123" };
+ next();
+}
+
+// Return the claims payload to embed in the signed JWT.
+function getJWTClaims(userId) {
+ // Returns an empty payload, so the token
+ // inherits the capabilities of the signing key.
+ return {};
+}
+
+// Define an auth endpoint used by the client to obtain a signed JWT
+// which it can use to authenticate with the Ably service.
+app.get("/api/auth/token", authenticateUser, (req, res) => {
+ const [keyName, keySecret] = "{{API_KEY}}".split(":");
+
+ // Sign a JWT using the secret part of the Ably API key.
+ const token = jwt.sign(getJWTClaims(req.session.userId), keySecret, {
+ algorithm: "HS256",
+ keyid: keyName,
+ expiresIn: "1h",
+ });
+
+ res.type("application/jwt").send(token);
+});
+
+app.listen(3001);
+```
+
+
+
+
+The JWT is signed with the secret part of your Ably API key using [HMAC-SHA-256](https://datatracker.ietf.org/doc/html/rfc4868). This example does not embed any claims in the JWT payload, so by default the token inherits the capabilities of the Ably API key used to sign the token.
+
+Configure your client to obtain a signed JWT from your server endpoint using an [`authCallback`](/docs/auth/token#auth-callback). The client obtains a signed JWT from the callback and uses it to authenticate requests to Ably. The client automatically makes a request for a new token before it expires.
+
+
+
+
+```javascript
+// Client code
+import * as Ably from "ably";
+
+const ably = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch("/api/auth/token");
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ }
+});
+
+ably.connection.on("connected", () => {
+ console.log("Connected to Ably");
+});
+```
+
+
+## Authenticating agents
+
+Agents typically run on servers in trusted environments where API keys can be securely stored. Use [API key authentication](/docs/auth#basic-authentication) to authenticate agents directly with Ably.
+
+
+```javascript
+// Agent code
+import * as Ably from "ably";
+
+const ably = new Ably.Realtime({
+ key: "{{API_KEY}}"
+});
+
+ably.connection.on("connected", () => {
+ console.log("Connected to Ably");
+});
+```
+
+
+
+
+
+
+## Specifying capabilities
+
+Use [capabilities](/docs/auth/capabilities) to specify which operations clients can perform on which channels. This applies to both users and agents, allowing you to enforce fine-grained permissions.
+
+### User capabilities
+
+Add the [`x-ably-capability`](/docs/api/realtime-sdk/authentication#ably-jwt) claim to your JWT to specify the allowed capabilities of a client. This allows you to enforce fine-grained permissions, such as restricting some users to only subscribe to messages while allowing others to publish.
+
+Update your `getJWTClaims` function to specify the allowed capabilities for the authenticated user:
+
+
+```javascript
+// Server code
+
+// Return the claims payload to embed in the signed JWT.
+// Includes the `x-ably-capabilities` claim, which controls
+// which operations the user can perform on which channels.
+function getJWTClaims(userId) {
+ const orgId = "acme"; // Mock organization ID for demonstration
+ const capabilities = {
+ // The user can publish and subscribe to channels within the organization,
+ // that is, any channel matching `org:acme:*`.
+ [`org:${orgId}:*`]: ["publish", "subscribe"],
+ // The user can only subscribe to the `announcements` channel.
+ announcements: ["subscribe"],
+ };
+ return {
+ "x-ably-capability": JSON.stringify(capabilities),
+ };
+}
+```
+
+
+When a client authenticates with this token, Ably enforces these capabilities server-side. Any attempt to perform unauthorized operations will be rejected. For example, a client with the capabilities above can publish to channels prefixed with `org:acme:`, but an attempt to publish to a channel prefixed with `org:foobar:` will fail with error code [`40160`](/docs/platform/errors/codes#40160):
+
+
+```javascript
+// Client code
+const acmeChannel = ably.channels.get("org:acme:{{RANDOM_CHANNEL_NAME}}");
+await acmeChannel.publish("prompt", "What is the weather like today?"); // succeeds
+
+const foobarChannel = ably.channels.get("org:foobar:{{RANDOM_CHANNEL_NAME}}");
+await foobarChannel.publish("prompt", "What is the weather like today?"); // fails
+
+const announcementsChannel = ably.channels.get("announcements");
+await announcementsChannel.publish("prompt", "What is the weather like today?"); // fails
+await announcementsChannel.subscribe((msg) => console.log(msg)); // succeeds
+```
+
+
+
+
+### Agent capabilities
+
+When using API key authentication, provision API keys through the [Ably dashboard](https://ably.com/dashboard) or [Control API](/docs/account/control-api) with only the capabilities required by the agent.
+
+The following example uses the Control API to create an API key with specific capabilities for a weather agent:
+
+
+
+
+```shell
+curl --location --request POST 'https://control.ably.net/v1/apps/{{APP_ID}}/keys' \
+--header 'Content-Type: application/json' \
+--header 'Authorization: Bearer ${ACCESS_TOKEN}' \
+--data-raw '{
+ "name": "weather-agent-key",
+ "capability": {
+ "org:acme:weather:*": ["publish", "subscribe"]
+ }
+}'
+```
+
+
+This creates an API key that can only publish and subscribe on channels matching `org:acme:weather:*`. The agent can then use this key to authenticate:
+
+
+```javascript
+// Agent code
+const weatherChannel = ably.channels.get("org:acme:weather:{{RANDOM_CHANNEL_NAME}}");
+await weatherChannel.subscribe((msg) => console.log(msg)); // succeeds
+await weatherChannel.publish("update", "It's raining in London"); // succeeds
+
+const otherChannel = ably.channels.get("org:acme:other:{{RANDOM_CHANNEL_NAME}}");
+await otherChannel.subscribe((msg) => console.log(msg)); // fails
+await otherChannel.publish("update", "It's raining in London"); // fails
+```
+
+
+
+
+## Establishing verified identity
+
+Use the [`clientId`](/docs/messages#properties) to identify the user or agent that published a message. The method for setting `clientId` depends on your authentication approach:
+
+- When using [basic authentication](/docs/auth/identified-clients#basic), specify the `clientId` directly in the client options when instantiating the client instance.
+- When using [token authentication](/docs/auth/identified-clients#token), specify an explicit `clientId` when issuing the token.
+
+### User identity
+
+Users typically authenticate using [token authentication](/docs/auth/identified-clients#token). Add the [`x-ably-clientId`](/docs/api/realtime-sdk/authentication#ably-jwt) claim to your JWT to establish a verified identity for each user client. This identity appears as the [`clientId`](/docs/messages#properties) in all messages the user publishes, and subscribers can trust this identity because only your server can issue JWTs with specific `clientId` values.
+
+As with all clients, the method for setting `clientId` depends on your [authentication approach](#identity).
+
+Update your `getJWTClaims` function to specify a `clientId` for the user:
+
+
+```javascript
+// Return the claims payload to embed in the signed JWT.
+function getJWTClaims(userId) {
+ // Returns a payload with the `x-ably-clientId` claim, which ensures
+ // that the user's ID appears as the `clientId` on all messages
+ // published by the client using this token.
+ return { "x-ably-clientId": userId };
+}
+```
+
+
+When a client authenticates using this token, Ably's servers automatically attach the `clientId` specified in the token to every message the user publishes:
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Publish a message - the clientId is automatically attached
+await channel.publish("prompt", "What is the weather like today?");
+```
+
+
+Agents can then access this verified identity to identify the sender:
+
+
+```javascript
+// Agent code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Subscribe to messages from clients
+await channel.subscribe("prompt", (message) => {
+ // Access the verified clientId from the message
+ const userId = message.clientId;
+ const prompt = message.data;
+
+ console.log(`Received message from user: ${userId}`);
+ console.log(`Prompt:`, prompt);
+});
+```
+
+
+The `clientId` in the message can be trusted, so agents can use this identity to make decisions about what actions the user can take. For example, agents can check user permissions before executing tool calls, route messages to appropriate AI models based on subscription tiers, or maintain per-user conversation history and context.
+
+### Agent identity
+
+Agent code typically runs in a trusted environment, so you can use [basic authentication](/docs/auth/identified-clients#basic) and directly specify the `clientId` when instantiating the agent client. This identity appears as the [`clientId`](/docs/messages#properties) in all messages the agent publishes, allowing subscribers to identify the agent which published a message.
+
+
+```javascript
+// Agent code
+import * as Ably from "ably";
+
+const ably = new Ably.Realtime({
+ key: "{{API_KEY}}",
+ // Specify an identity for this agent
+ clientId: "weather-agent"
+});
+```
+
+
+When subscribers receive messages, they can use the `clientId` to determine which agent published the message:
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+await channel.subscribe((message) => {
+ if (message.clientId === "weather-agent") {
+ console.log("Weather agent response:", message.data);
+ }
+});
+```
+
+
+
+
+## Adding roles and attributes
+
+Embed custom roles and attributes in messages to enable role-based access control (RBAC) and convey additional context about users and agents. This enables agents to make authorization decisions without additional database lookups.
+
+### User claims
+
+Use [authenticated claims for users](/docs/auth/capabilities#custom-restrictions-on-channels-) to embed custom claims in JWTs that represent user roles or attributes.
+
+Add claims with names matching the `ably.channel.*` pattern to your JWT to specify user claims for specific channels. Claims can be scoped to individual channels or to [namespaces](/docs/channels#namespaces) of channels. The most specific user claim matching the channel is automatically included under `extras.userClaim` in all messages the client publishes.
+
+Update your `getJWTClaims` function to specify some user claims:
+
+
+```javascript
+// Return the claims payload to embed in the signed JWT.
+function getJWTClaims(userId) {
+ // Returns a payload with `ably.channel.*` claims, which ensures that
+ // the most specific claim appears as the `message.extras.userClaim`
+ // on all messages published by the client using this token.
+ return {
+ // The user is an editor on all acme channels.
+ "ably.channel.org:acme:*": "editor",
+ // The user is a guest on all other channels.
+ "ably.channel.*": "guest",
+ };
+}
+```
+
+
+When a client authenticates with a JWT containing `ably.channel.*` claims, Ably automatically includes the most specific matching claim value in the `message.extras.userClaim` field on messages published by the client:
+
+
+```javascript
+// Agent code
+const channel = ably.channels.get("org:acme:{{RANDOM_CHANNEL_NAME}}");
+
+// Subscribe to user prompts
+await channel.subscribe("prompt", async (message) => {
+ // Access the user's role from the user claim in message extras
+ const role = message.extras?.userClaim;
+
+ console.log(`Message from user with role: ${role}`);
+});
+```
+
+
+The `message.extras.userClaim` in the message can be trusted, so agents can rely on this information to make decisions about what actions the user can take. For example, an agent could allow users with an "editor" role to execute tool calls that modify documents, while restricting users with a "guest" role to read-only operations.
+
+### Agent metadata
+
+Use [`message.extras.headers`](/docs/api/realtime-sdk/types#extras) to include custom metadata in agent messages, such as agent roles or attributes.
+
+Agents can directly specify metadata in `message.extras.headers`. Since agents run as trusted code in server environments, this metadata can be trusted by subscribers. This is useful for communicating agent characteristics, such as which model the agent uses, the agent's role in a multi-agent system, or version information.
+
+
+
+
+```javascript
+// Agent code
+import * as Ably from "ably";
+
+const ably = new Ably.Realtime({
+ key: "{{API_KEY}}",
+ clientId: "weather-agent"
+});
+
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+await channel.publish({
+ name: "update",
+ data: "It's raining in London",
+ extras: {
+ headers: {
+ model: "gpt-4"
+ }
+ }
+});
+```
+
+
+Clients and other agents can access this metadata when messages are received:
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+await channel.subscribe((message) => {
+ if (message.clientId === "weather-agent") {
+ const model = message.extras?.headers?.model;
+ console.log(`Response from weather agent using ${model}:`, message.data);
+ }
+});
+```
+
diff --git a/src/pages/docs/ai-transport/features/sessions-identity/index.mdx b/src/pages/docs/ai-transport/features/sessions-identity/index.mdx
new file mode 100644
index 0000000000..e1b9be5a05
--- /dev/null
+++ b/src/pages/docs/ai-transport/features/sessions-identity/index.mdx
@@ -0,0 +1,63 @@
+---
+title: "Sessions & identity overview"
+meta_description: "Manage session lifecycle and identity in decoupled AI architectures"
+meta_keywords: "AI sessions, session management, channel-oriented sessions, connection-oriented sessions, session persistence, session lifecycle, identity management, decoupled architecture, session resumption, multi-device, multi-user"
+---
+
+Ably AI Transport provides robust session management and identity capabilities designed for modern AI applications. Sessions persist beyond individual connections, enabling agents and clients to connect independently through shared channels. Built-in token-based authentication provides verified user identity and fine-grained authorization for channel operations.
+
+## What is a session?
+
+A session is an interaction between a user (or multiple users) and an AI agent where messages and data are exchanged, building up shared context over time. In AI Transport, sessions are designed to persist beyond the boundaries of individual connections, enabling modern AI experiences where users expect to:
+
+- Resume conversations across devices: Start a conversation on mobile and seamlessly continue on desktop with full context preserved
+- Return to long-running work: Close the browser while agents continue processing in the background, delivering results when you return
+- Recover from interruptions: Experience connection drops, browser refreshes, or network instability without losing conversation progress
+- Collaborate in shared sessions: Multiple users can participate in the same conversation simultaneously and remain in sync
+
+These capabilities represent a fundamental shift from traditional request/response AI experiences to continuous, resumable interactions that remain accessible across all user devices and locations. Sessions have a lifecycle: they begin when a user starts interacting with an agent, remain active while the interaction continues, and can persist even when users disconnect - enabling truly asynchronous AI workflows.
+
+Managing this lifecycle in AI Transport's decoupled architecture involves detecting when users are present, deciding when to stop or continue agent work, and handling scenarios where users disconnect and return.
+
+## Connection-oriented vs channel-oriented sessions
+
+In traditional connection-oriented architectures, sessions are bound to the lifecycle of a WebSocket or SSE connection:
+
+1. Client opens connection to agent server to establish a session
+2. Agent streams response over the connection
+3. When the connection closes, the session ends
+
+This tight coupling means network interruptions terminate sessions, agents cannot continue work after disconnections, and supporting multiple devices or users introduces significant complexity.
+
+AI Transport uses a channel-oriented model where sessions persist independently of individual connections. Clients and agents communicate through [Channels](/docs/channels):
+
+1. Client sends a single request to agent server to establish a session
+2. Server responds with a unique ID for the session, which is used to identify the channel
+3. All further communication happens over the channel
+
+In this model, sessions are associated with the channel, enabling seamless reconnection, background agent work, and multi-device access without additional complexity.
+
+
+
+
+The channel-oriented model provides key benefits for modern AI applications: sessions maintain continuity in the face of disconnections, users can refresh or navigate back to the ongoing session, multiple users or devices can participate in the same session, and agents can continue long-running or asynchronous workloads even when clients disconnect.
+
+The following table compares how each architecture addresses the engineering challenges of delivering these capabilities:
+
+| Challenge | Connection-oriented sessions | Channel-oriented sessions |
+|-----------|------------------------------|---------------------------|
+| Routing | Agents must track which instance holds each session. Reconnecting clients need routing logic to find the correct agent instance across your infrastructure. | Agents and clients only need the channel name. Ably handles message delivery to all subscribers without agents tracking sessions or implementing routing logic. |
+| Message resume | Agents must buffer sent messages and implement replay logic. When clients reconnect, agents must determine what was missed and retransmit without duplicates or gaps, distinctly for each connection. | When clients reconnect, they automatically receive messages published while disconnected. The channel maintains history without agents implementing buffering or replay logic, eliminating the need for server-side session state. |
+| Abandonment detection | Agents must implement logic to distinguish between brief network interruptions and users who have actually left, so they can decide whether to continue work or clean up resources. | Built-in presence tracking signals when users enter and leave channels, providing clear lifecycle events to agents without custom detection logic. |
+| Multi-user and multi-device | Agents must manage multiple concurrent connections from the same user across devices, or from multiple users in collaborative sessions. This requires tracking connections, synchronizing state, and ensuring all participants receive consistent updates. | Multiple users and devices can connect to the same channel. The channel handles message delivery to all participants, simplifying agent logic for multi-user and multi-device scenarios. |
+
+## Identity in channel-oriented sessions
+
+In connection-oriented architectures, the agent server handles authentication directly when establishing the connection. When the connection is opened, the server verifies credentials and associates the authenticated user identity with that specific connection.
+
+In channel-oriented sessions, agents don't manage connections or handle authentication directly. Instead, your server authenticates users and issues tokens that control their access to channels. Ably enforces these authorization rules and provides verified identity information to agents, giving you powerful capabilities for managing who can participate in sessions and what they can do:
+
+- Verified identity: Agents automatically receive the authenticated identity of message senders, with cryptographic guarantees that identities cannot be spoofed
+- Fine-grained authorization: Control precisely what operations each user can perform on specific channels through fine-grained capabilities
+- Rich user attributes: Pass authenticated user data to agents for personalized behavior without building custom token systems
+- Role-based participation: Distinguish between different types of participants, such as users and agents, to customize behaviour based on their role
diff --git a/src/pages/docs/ai-transport/features/sessions-identity/online-status.mdx b/src/pages/docs/ai-transport/features/sessions-identity/online-status.mdx
new file mode 100644
index 0000000000..7a852d482f
--- /dev/null
+++ b/src/pages/docs/ai-transport/features/sessions-identity/online-status.mdx
@@ -0,0 +1,280 @@
+---
+title: "Online status"
+meta_description: "Use Ably Presence to show which users and agents are currently connected to an AI session"
+meta_keywords: "presence, online status, multi-device, multi-user, session abandonment, async workflows"
+---
+
+Modern AI applications require agents to know when users are online, when they've fully disconnected, and how to handle users connected across multiple devices. Ably's [Presence](/docs/presence-occupancy/presence) feature provides realtime online status with automatic lifecycle management, allowing agents to decide when to continue processing, when to wait for user input, and when to clean up resources. Presence detects which users and agents are currently connected to a session, distinguishes between a single device disconnecting and a user going completely offline, and enables responsive online/offline indicators.
+
+## Why online status matters
+
+In channel-oriented sessions, online status serves several critical purposes:
+
+- Session abandonment detection: Agents need to know when users have fully disconnected to decide whether to continue processing, pause work, or clean up resources. Presence provides reliable signals when all of a user's devices have left the session.
+- Multi-device coordination: A single user can connect from multiple devices simultaneously. Presence tracks each connection separately while maintaining stable identity across devices, allowing you to distinguish between "one device left" and "user completely offline".
+- Agent availability signaling: Clients need to know when agents are online and ready to process requests. Agents can enter presence to advertise availability and leave when they complete work or shut down.
+- Collaborative session awareness: In sessions with multiple users, participants can see who else is currently present. This enables realtime collaboration features and helps users understand the current session context.
+
+## Going online
+
+Use the [`enter()`](/docs/presence-occupancy/presence#enter) method to signal that a user or agent is online. When a client enters presence, they are added to the presence set and identified by their `clientId`. You can optionally include data when entering presence to communicate additional context.
+
+
+
+You have flexibility in when to enter presence. For example, an agent might choose to appear as online only while processing a specific task, or remain present for the duration of the entire session. Users typically enter presence when they connect to a session and remain present until they disconnect.
+
+
+
+For example, a user client can enter presence when joining a session:
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Enter presence with metadata about the user's device
+await channel.presence.enter({
+ device: "mobile",
+ platform: "ios"
+});
+```
+
+
+Similarly, an agent can enter presence to signal that it's online:
+
+
+```javascript
+// Agent code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Enter presence with metadata about the agent
+await channel.presence.enter({
+ model: "gpt-4"
+});
+```
+
+
+### Going online from multiple devices
+
+A single user can be present on a channel from multiple devices simultaneously. Ably tracks each connection separately using a unique [`connectionId`](/docs/connect#connection-ids), while maintaining the same [`clientId`](/docs/auth/identified-clients#assign) across all connections.
+
+When a user connects from multiple devices, each device enters presence independently. All connections share the same `clientId` but have different `connectionId` values.
+
+For example, when the user connects from their desktop browser:
+
+
+```javascript
+// Client code (device 1: desktop browser)
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+await channel.presence.enter({ device: "desktop" });
+```
+
+
+And then connects from their mobile app while still connected on desktop:
+
+
+```javascript
+// Client code (device 2: mobile app)
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+await channel.presence.enter({ device: "mobile" });
+```
+
+
+Both devices are now members of the presence set with the same `clientId` but different `connectionId` values. When you query the presence set, you'll see two separate entries:
+
+
+```javascript
+// Query presence to see both devices
+const members = await channel.presence.get();
+for (const { clientId, connectionId, data } of members) {
+ console.log(clientId, connectionId, data);
+}
+// Example output:
+// user-123 hd67s4!abcdef-0 { device: "desktop" }
+// user-123 hd67s4!ghijkl-1 { device: "mobile" }
+```
+
+
+When either device leaves or disconnects, the other device remains in the presence set.
+
+## Going offline
+
+Clients can go offline in two ways: explicitly by calling the leave method, or automatically when Ably detects a disconnection.
+
+### Explicitly going offline
+
+Use the [`leave()`](/docs/presence-occupancy/presence#leave) method when a user or agent wants to mark themselves as offline. This immediately notifies presence subscribers on the channel and removes the entry from the presence set, even if they remain connected to Ably.
+
+
+
+For example, a user client can explicitly leave presence:
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Leave presence when the user marks themselves offline
+await channel.presence.leave();
+```
+
+
+Similarly, an agent can leave presence when it completes its work or shuts down:
+
+
+```javascript
+// Agent code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Leave presence when the agent shuts down
+await channel.presence.leave();
+```
+
+
+Optionally include data when leaving presence to communicate the reason for going offline. This data is delivered to presence subscribers listening to `leave` events and is also available in [presence history](/docs/presence-occupancy/presence#history):
+
+
+```javascript
+// Leave with a reason
+await channel.presence.leave({
+ reason: "session-completed",
+ timestamp: Date.now()
+});
+```
+
+
+Subscribers receive the `leave` data in the presence message:
+
+
+```javascript
+// Subscribe to leave events to see why members left
+await channel.presence.subscribe("leave", (presenceMessage) => {
+ console.log(`${presenceMessage.clientId} left`);
+ if (presenceMessage.data) {
+ console.log(`Reason: ${presenceMessage.data.reason}`);
+ }
+});
+```
+
+
+### Going offline after disconnection
+
+When a client loses connection unexpectedly, Ably detects the lost connection and automatically leaves the client from the presence set.
+
+By default, clients remain present for 15 seconds after an abrupt disconnection. This prevents excessive enter/leave events during brief network interruptions. If the client reconnects within this window, they remain in the presence set without triggering leave and reenter events.
+
+Use the `transportParams` [client option](/docs/api/realtime-sdk#client-options) to configure disconnection detection and presence lifecycle behaviour. After an abrupt disconnection, the `heartbeatInterval` transport parameter controls how quickly Ably detects the dead connection, while the `remainPresentFor` option controls how long the member is kept in presence before Ably emits the leave event.
+
+
+
+For example, if implementing resumable agents using techniques such as durable execution, configure a longer `remainPresentFor` period to allow time for the new agent instance to come online and resume processing before the previous instance appears as offline. This provides a seamless handoff:
+
+
+```javascript
+// Agent code
+const ably = new Ably.Realtime({
+ key: "{{API_KEY}}",
+ clientId: "weather-agent",
+ // Allow 30 seconds for agent resume and reconnection
+ transportParams: {
+ remainPresentFor: 30000
+ }
+});
+```
+
+
+## Viewing who is online
+
+Participants in a session can query the current presence set or subscribe to presence events to see who else is online and react to changes in realtime. Users might want to see which agents are processing work, while agents might want to detect when specific users are offline to pause or cancel work.
+
+
+
+### Retrieving current presence members
+
+Use [`presence.get()`](/docs/api/realtime-sdk/presence#get) to retrieve the current list of users and agents in the session. Each presence member is uniquely identified by the combination of their `clientId` and `connectionId`. This is useful for showing who is currently available or checking if a specific participant is online before taking action.
+
+
+```javascript
+// Get all currently present members
+const members = await channel.presence.get();
+
+// Display each member - the same user will appear once per distinct connection
+members.forEach((member) => {
+ console.log(`${member.clientId} (connection: ${member.connectionId})`);
+});
+```
+
+
+### Subscribing to presence changes
+
+Use [`presence.subscribe()`](/docs/api/realtime-sdk/presence#subscribe) to receive realtime notifications when users or agents enter or leave the session. This enables building responsive UIs that show online users, or implementing agent logic that reacts to user connectivity changes.
+
+
+```javascript
+// Client code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Subscribe to changes to the presence set
+await channel.presence.subscribe(async (presenceMessage) => {
+ // Get the current synced presence set after any change
+ const members = await channel.presence.get();
+
+ // Display each member - the same user will appear once per distinct connection
+ members.forEach((member) => {
+ console.log(`${member.clientId} (connection: ${member.connectionId})`);
+ });
+});
+```
+
+
+You can also subscribe to specific presence events:
+
+
+```javascript
+// Subscribe only to enter events
+await channel.presence.subscribe("enter", (presenceMessage) => {
+ console.log(`${presenceMessage.clientId} joined on connection ${presenceMessage.connectionId}`);
+});
+
+// Subscribe only to leave events
+await channel.presence.subscribe("leave", (presenceMessage) => {
+ console.log(`${presenceMessage.clientId} left on connection ${presenceMessage.connectionId}`);
+});
+```
+
+
+### Detecting when a user is offline on all devices
+
+Agents can monitor presence changes to detect when a specific user has gone completely offline across all devices. This is useful for deciding whether to pause expensive operations, cancel ongoing work, deprioritize tasks, or schedule work for later.
+
+
+```javascript
+// Agent code
+const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+await channel.presence.subscribe(async (presenceMessage) => {
+ // Get the current synced presence set
+ const members = await channel.presence.get();
+
+ // Check if all clients are offline
+ if (members.length === 0) {
+ console.log(`All clients are offline`);
+ }
+
+ // Check if a specific client is offline
+ if (!members.map(m => m.clientId).includes(targetUserId)) {
+ console.log(`${targetUserId} is now offline on all devices`);
+ }
+});
+```
+
diff --git a/src/pages/docs/ai-transport/features/sessions-identity/resuming-sessions.mdx b/src/pages/docs/ai-transport/features/sessions-identity/resuming-sessions.mdx
new file mode 100644
index 0000000000..558bd36d1c
--- /dev/null
+++ b/src/pages/docs/ai-transport/features/sessions-identity/resuming-sessions.mdx
@@ -0,0 +1,140 @@
+---
+title: Resuming sessions
+description: How clients and agents reconnect to ongoing AI Transport sessions after network interruptions or service restarts
+meta_keywords: "session resumption, reconnection, hydration, presence sync, conversation history, channel history, untilAttach, durable execution, agent restart, message recovery, failover"
+---
+
+AI Transport uses a channel-oriented model where sessions persist independently of individual connections. Both users and agents can disconnect and rejoin without ending the session. When users or agents rejoin, they need to resume the session from where they left off.
+
+An agent or user might resume an existing session when:
+
+- A user goes offline or navigates away before returning, expecting to see the latest conversation state
+- An agent goes offline and comes back online when the user returns
+- An agent resumes after a failover or service restart
+
+## Hydrating presence
+
+When you attach to a channel, Ably automatically syncs the complete current presence set to your client. You can then query the presence set or subscribe to presence events without any additional hydration steps. This works the same way for both users and agents.
+
+For details on obtaining the synced presence set, see [Viewing who is online](/docs/ai-transport/sessions-and-identity/online-status#viewing-presence).
+
+## User resumes a session
+
+Users resume by reattaching to the same session channel and hydrating the conversation transcript, in-progress model output, or other session state.
+
+### Hydrating conversation history
+
+The hydration strategy you choose depends on your application model and your chosen approach to token streaming. Clients typically hydrate conversation state using one of these patterns:
+
+- Hydrate entirely from the channel: Use [rewind](/docs/channels/options/rewind) or [history](/docs/storage-history/history) to obtain previous messages on the channel.
+- Hydrate in-progress responses from the channel: Load completed messages from your database and catch up on any in-progress responses from the channel.
+
+For detailed examples of hydrating the token stream, see the token streaming documentation:
+- [Message-per-response hydration](/docs/ai-transport/features/token-streaming/message-per-response#hydration)
+- [Message-per-token hydration](/docs/ai-transport/features/token-streaming/message-per-token#hydration)
+
+## Agent resumes a session
+
+When an agent restarts, it needs to resume from where it left off. This involves two distinct concerns:
+
+1. **Recovering the agent's execution state**: The current step in the workflow, local variables, function call results, pending operations, and any other state needed to continue execution. This state is internal to the agent and typically not visible to users.
+
+2. **Catching up on session activity**: Any user messages, events, or other activity that occurred while the agent was offline.
+
+These are separate problems requiring different solutions. Agent execution state is handled by your application and you choose how to persist and restore the internal state your agent needs to resume.
+
+
+
+Ably provides access to channel message history, enabling agents to retrieve any messages sent while they were offline. When your agent comes back online, it reattaches to the same channel and catches up on messages it missed. This channel-oriented model provides several key benefits:
+
+- Guaranteed message delivery: Clients can continue publishing messages even while the agent faults and relocates since the channel exists independently of the agent
+- Reliable catch-up: The agent can retrieve any messages published during the interim when it comes back online
+- Ordered delivery: Messages are delivered in the order they were published, ensuring agents process events in the correct sequence
+- Channel-based addressing: The agent only needs the channel name to reconnect, no need to track individual client connections or manage connection state
+
+### Catching up on messages using history
+
+When an agent resumes, it needs to retrieve messages published while it was offline. Use [channel history](/docs/storage-history/history) with the [`untilAttach` option](/docs/storage-history/history#continuous-history) to catch up on historical messages while preserving continuity with live message delivery.
+
+
+
+#### Persisted session state
+
+Your agent should persist the following state to enable resumption:
+
+- Channel name: The channel the agent was processing
+- Last processed timestamp: The timestamp of the last message successfully processed by the agent
+
+This state allows the agent to reconnect to the correct channel and retrieve only the messages it missed.
+
+#### Catching up with continuity
+
+The recommended pattern uses `untilAttach` to paginate backwards through history while maintaining continuity with live message delivery. This ensures no messages are lost between history retrieval and subscription.
+
+
+
+
+```javascript
+// Agent code
+import * as Ably from 'ably';
+
+const ably = new Ably.Realtime({
+ key: process.env.ABLY_API_KEY,
+ clientId: 'agent:assistant'
+});
+
+// Load persisted session state
+const channelName = await loadChannelName();
+const lastProcessedTimestamp = await loadLastProcessedTimestamp();
+
+// Use a channel in a namespace with persistence enabled
+// to access more than 2 minutes of message history
+const channel = ably.channels.get(channelName);
+
+// Subscribe to live messages (implicitly attaches the channel)
+await channel.subscribe('prompt', (message) => {
+ // Process the live message
+ processMessage(message);
+
+ // Persist the timestamp after successful processing
+ saveLastProcessedTimestamp(message.timestamp);
+});
+
+// Fetch history up until the point of attachment, starting from last checkpoint
+let page = await channel.history({
+ untilAttach: true,
+ start: lastProcessedTimestamp,
+ direction: 'forwards'
+});
+
+// Paginate through all missed messages
+while (page) {
+ for (const message of page.items) {
+ // Process the historical message
+ await processMessage(message);
+
+ // Persist the timestamp after successful processing
+ await saveLastProcessedTimestamp(message.timestamp);
+ }
+
+ // Move to next page if available
+ page = page.hasNext() ? await page.next() : null;
+}
+```
+
+
+
+
+This pattern provides guaranteed continuity between historical and live message processing by ensuring that:
+
+1. The subscription starts receiving live messages immediately when you subscribe
+2. History retrieval stops exactly at the point the channel attached
+3. No messages are lost between the end of history and the start of live delivery