createServerTransport() and turn.start() both succeed on a channel without mutableMessages enabled. The error only surfaces deep in the streaming pipeline when the first appendMessage operation is NACKed with error 93002.
Expected: The SDK should detect at attach or turn-start time that mutableMessages is required, and fail fast with a clear error before streaming begins.
Observed: The transport attaches and starts streaming without complaint. The error appears as NACKs on individual append operations.
When streamResponse() is running and the server NACKs with error 93002, the encoder's fire-and-forget appendStream() keeps queuing new operations. Each token from the LLM stream generates an independent server round-trip and NACK, producing a flood of identical errors and PromiseRejectionHandledWarning messages.
Expected: The encoder core needs a circuit breaker. After the first NACK with a non-retryable error (93002), it should abort the stream and stop queuing further appends. appendStream() is fire-and-forget by design, but _flushPending() (which collects failures) only runs on closeStream/abort/close — never on appendStream itself. There's no feedback loop from publish errors back to the stream consumer.
Observed: Every token produces an independent server round-trip and NACK for the entire duration of the stream.
If AI Transport inherently requires mutable messages to function, why does a developer need to explicitly create a channel rule to enable it? The current flow is:
- Developer installs
@ably/ai-transport - Writes code following the docs
- Runs it — gets a flood of cryptic NACK errors
- Has to discover that a channel rule with
mutableMessagesis needed - Goes to the dashboard or CLI to create one
For an AI Transport product, shouldn't channels used by the SDK automatically support the operations the SDK needs? If we know it's an agent doing agentic work via AI Transport, requiring explicit configuration adds friction that doesn't need to exist.
The SDK ships one codec: UIMessageCodec for the Vercel AI SDK. Anyone not using Vercel (LangChain, custom LLM integrations, plain text streaming) must implement the full Codec interface from scratch — createEncoder, createDecoder, createAccumulator, isTerminal, getMessageKey — just to stream text.
This repro script needed a custom codec for a minimal use case (stream text tokens). A built-in PlainTextCodec or similar would significantly reduce the barrier for non-Vercel users and make the SDK genuinely framework-agnostic in practice, not just in architecture.
mkdir /tmp/ait-nack-repro && cd /tmp/ait-nack-repro
# Download the script
curl -sL https://gist.githubusercontent.com/mattheworiordan/db9b9a811d89580a4122ff91e76d1956/raw/repro.mjs -o repro.mjs
# Install dependencies
echo '{"type":"module"}' > package.json
npm install ably @ably/ai-transport
# Run — make sure the app does NOT have mutableMessages on the "no-mutable" namespace
ABLY_API_KEY="your-app-id.key-id:key-secret" node repro.mjs
# If you have the Ably CLI:
ABLY_API_KEY=$(ably auth keys current --value-only) node repro.mjs--- Issue 1: Early detection ---
turn.start() succeeded without detecting mutableMessages is missing.
--- Issue 2: Encoder NACK handling ---
Streaming 50 tokens at ~40ms each...
[AblySDK Error] Ably: Protocol.onNack(): serial = 2; count = 1; err = ... code=93002 ...
[AblySDK Error] Ably: Protocol.onNack(): serial = 3; count = 11; err = ... code=93002 ...
... (continues for every token in the stream)
ISSUE 1 CONFIRMED: No early detection of mutableMessages misconfiguration.
ISSUE 2 CONFIRMED: Encoder kept queuing appends after first NACK.
Found while building an interactive AI Transport demo for the Ably CLI. When a developer runs the demo without mutableMessages configured, they should get a clear, immediate error — not a flood of NACKs.