Troubleshooting

Common AI Transport problems and how to fix them. Each entry follows the same shape: the symptom you see, what to check to confirm the cause, and the fix to apply.

Entries are ordered roughly by how often they cause support tickets, most common first.

Channel rule not configured for AI Transport

Symptom: Users see an empty assistant message, or a message containing only the first token. The server errors on subsequent publishes. The initial message.create succeeds, but the message.append operations that AI Transport uses to stream tokens fail with error code 93002 (Can only update/delete/append messages on channels with mutableMessages enabled) because no channel rule permitting appends matches the channel. This is the single most common AI Transport failure.

Confirm:

  • Check the server logs for error 93002 after the first token. The message reads Can only update/delete/append messages on channels with mutableMessages enabled.
  • Open the Ably dashboard for your app's settings.
  • Find the channels your conversations live on (for example channel names like conversations:abc, covered by the match expression conversations:*).
  • Check whether Message annotations, updates, deletes, and appends is enabled on a channel rule matching those channels.

Fix:

  • Enable the Message annotations, updates, deletes, and appends rule on a match expression covering those channels, using the dashboard, Control API, or CLI.
  • Note that enabling this rule causes matching messages to be persisted regardless of whether persistence is enabled separately.

Capability or token scope mismatch

Symptom: The token authenticates, but specific operations fail. There are two common shapes:

  • Channel pattern mismatch: channel attachment fails, or a publish is rejected. The capability covers conversations:* but your app uses chat:abc.
  • Missing operation: the connection works but a specific operation does not. Clients cannot send the user's message (missing publish). A replay after a reload or a discontinuity fails, and only live messages arrive (missing history). Agent presence never updates (missing presence).

Confirm:

  • Decode the JWT returned by your auth endpoint and inspect the x-ably-capability claim. It is a JSON-encoded map of channel patterns to permitted operations.
  • Compare each operation your application performs against the capability for the relevant channel name.

Fix:

  • Make the channel pattern in the capability cover the channel names your application uses. Patterns are case-sensitive; wildcards like conversations:* only cover channels with that exact prefix.
  • Grant every operation the application needs: subscribe and publish for messages, history for loading past conversation, presence for agent presence. Check the full list of capability operations against the AI Transport capability shape.

History disappears

Symptom: Past conversation messages are not available when the user expects them. Two scenarios trigger the same root cause:

  • The user opens the app the next day, tries to scroll back, and older messages are gone.
  • The user is offline longer than the retention period. On reconnect the client subscribes again with a replay, but the replay resolves with found: false and the missed messages do not arrive.

In both cases the cause is the same: no channel rule matching the channel is configured to persist messages long enough for the use case.

Confirm:

  • Check your package's retention period. The appends rule persists messages for that period, and older messages are no longer on the channel.
  • Confirm whether your application is meant to read history from Ably alone, or to hydrate from an external store.

Fix:

  • Check that your retention period covers your expected scroll-back window.
  • For conversations that need to be retained for longer than the channel allows, persist completed responses to your own store with their serials, and replay the channel from the stored serial when the client loads.

A second subscription misses messages

Symptom: A component that subscribes with history after another subscription is already running shows the replayed history and the live messages, but some messages from in between are missing.

Confirm:

  • Check whether the transport's channel was already attached when the subscription started. The replay reads history from the point where the channel attached, so only the first subscription with history on a transport has no gap.

Fix:

  • Subscribe with history once per transport, and share that subscription's deliveries between the components that need them.
  • To replay again, for example after a discontinuity, unsubscribe and subscribe again from the serial of the last message you received, as reconnection and recovery describes.

Duplicate or missing text after a reload

Symptom: After a reload, a response appears twice, or the start of a response is missing.

Confirm:

  • Check which serial the page replays from. A response appears twice when your store holds it and the replay starts from a serial before it. Text is missing when the replay starts from a serial after messages your store does not hold.
  • Check that the serial your store saves is the one pipe() resolved with for that response, and that it is saved in the same write as the response.

Fix:

  • Store the serial pipe() or send() returns together with the messages it belongs to, and replay with fromSerial from that serial.
  • Merge messages by id, so a message that both your store and the replay hold appears once.

Message too large to publish

Symptom: A publish fails with error code 40009 (maximum message length exceeded). Inside pipe() this surfaces on the agent as PipeFailed (104008), with the Ably error as its cause. Tool outputs or model responses that contain large payloads do not reach the channel.

Confirm:

  • Identify the message that failed. Tool results that include binary data, large embeddings, or full document bodies are the usual culprits.
  • Check your Ably package's message size limit. The cap is 64 KiB on Free and Standard, 256 KiB on Pro and Enterprise.

Fix:

  • Stream large tool results across multiple events instead of publishing them in one message.
  • Persist large payloads to an external store and send only a reference (URL or ID) over the channel.

Two devices share a clientId

Symptom: Behaviour that depends on which device published a message misbehaves across two devices for the same user. Both devices' messages carry the same clientId, and presence shows the user appearing and disappearing as both devices update their state.

Confirm:

  • Inspect the JWT each device receives. If both tokens have the same x-ably-clientId, they are indistinguishable to the Ably service.
  • Check whether your token-issuing logic generates a unique identifier per device (typically userId + deviceId), or only uses userId.

Fix:

  • Assign a unique clientId per device for any case where ownership matters. A common pattern is <userId>:<deviceId> so the user remains identifiable while devices remain distinguishable.
  • Check your ownership assumptions against multi-device and fan-out.

Reconnect loop

Symptom: The client connects, drops, reconnects, drops again. The cycle repeats every few seconds. Channel state never stabilises.

Confirm:

  • Inspect the Ably connection state on the client (realtimeClient.connection.state). A reconnect loop cycles connecting → connected → disconnected rapidly.
  • If each disconnected event carries a token error, your auth endpoint is returning tokens that Ably is rejecting on every reconnect.

Fix:

  • Verify the auth endpoint signs tokens with the correct key for the right environment (a dev key against a prod app is a common cause).
  • Lengthen token TTL if it is set very short. The SDK refreshes ahead of expiry; very short lifetimes fight the refresh.
  • If the token is rejected for capability rather than signing, see capability or token scope mismatch.

Agent process crashes mid-stream

Symptom: A streamed response stops part-way through and never finishes. No more text arrives, and no finish event or other end of the response follows.

Confirm:

  • Check server logs for the crashed process around the timestamp of the affected turn. An exception or an OOM kill typically appears there.
  • If you cannot see the crash directly, check infrastructure signals: serverless function timeouts, container OOM kills, and process restarts are common causes.
  • If the process did not crash, check whether pipe() rejected. A pipe that fails rejects with PipeFailed, and the route has to tell clients the response ended, for example with an abort event.

Fix:

  • Add structured error logging around the streaming path so the cause is visible. Common causes are model provider errors, OOM in long tool calls, and serverless function timeouts.
  • Decide on the application response: surface a retry control to the user, or auto-retry from the application layer. AI Transport does not retry the LLM call automatically.

Publish fails in suspended state

Symptom: A publish from the client returns an error. The connection has been disconnected for more than around two minutes and has moved to suspended state.

Confirm:

  • Inspect the connection state at the moment of the failed publish. A suspended connection has lost message continuity, and there is no live connection to the Ably platform. send() rejects with SessionSendFailed (104005), with the Ably failure as its cause.

Fix:

  • Check connection state before publishing user-driven actions, and queue them locally while the connection is not connected.
  • Flush the queue once the connection returns to connected. ably-js does not buffer publishes through a suspended state because continuity has already been lost.

Messages arrive with no event

Symptom: Something publishes to the conversation channel, and your handler receives a delivery with event set to undefined. The message is visible in the Ably dashboard.

Confirm:

  • Inspect the message's extras in the dashboard. A codec built with defineCodec reads the event type from extras.ai.type. A message without it is not the codec's, so the codec produces no event for it.
  • Check the transport's error event. A message the codec recognises but fails to decode is reported there as SessionMessageProcessingFailed, and still delivered with no event.
  • Check what published it. A plain channel.publish() from application code, or a Pub/Sub SDK client sharing the channel, produces messages in this shape.

Fix:

  • Publish conversation content through the transport's send() or pipe(), so the codec encodes it.
  • Keep application-level traffic off the conversation channel, or ignore deliveries with no event in your handler. Every message on the channel is delivered, so your handler can still read another publisher's message from delivery.message.

When to contact support

If you have worked through the entries on this page and the symptom does not match, or the fix did not work, capture:

  • The channel name.
  • The date and time of the affected operation.
  • The clientId of the affected server agent or client device.
  • The first error log entry on either side.

Open a support ticket with these. The Ably side of the system is observable to support; the application side needs the IDs to correlate.