From ea8feecdf09d2a5b0fcbb71865c7fe33c8c94d72 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Sat, 21 Mar 2026 17:50:16 +0000 Subject: [PATCH 1/9] chore: add Claude Code commit skill Add a /commit skill that generates commit messages following the repository's conventions, presents them for approval, and commits. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/commit/SKILL.md | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .claude/skills/commit/SKILL.md diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md new file mode 100644 index 00000000..4d96b00c --- /dev/null +++ b/.claude/skills/commit/SKILL.md @@ -0,0 +1,99 @@ +--- +name: commit +description: Generate a commit message for the current staged changes and commit +disable-model-invocation: true +allowed-tools: Bash(git diff *), Bash(git status), Bash(git commit *), Bash(git log *), Bash(curl *), AskUserQuestion +--- + +# Commit Staged Changes + +Generate a high-quality commit message for the currently staged changes, present it +for approval, then commit. + +## Step 1: Gather context + +Run these commands to understand the staged changes: + +1. `git diff --cached --stat` to see which files are staged +2. `git diff --cached` to see the full diff +3. `git status` to check overall state (never use -uall flag) + +If there are no staged changes, tell the user and stop. + +## Step 2: Determine the intent + +Determine the **intent** of the change from the diff. If the intent is +not clear from the code alone, use AskUserQuestion to ask the user to +clarify the purpose of the change before writing the message. + +## Step 3: Generate the commit message + +Write a commit message following ALL of the guidance below. Where the +project-specific guidance conflicts with the general guidance, the +project-specific guidance takes precedence. + +### General commit guidance + +!`curl -sf https://raw.githubusercontent.com/ably/engineering/refs/heads/main/best-practices/commits.md` + +### Project-specific guidance (takes precedence) + +This repository contains the Ably specification. Commit messages should +follow the conventions established in the existing history. + +#### Summary line style + +Specification changes use one of two styles depending on the nature of the +change: + +- **Spec content changes** (adding, modifying, or removing spec points): + Use a `spec/:` prefix where `` identifies the + specification being changed (e.g. `ait` for AI Transport, `chat` for + Chat, `objects` for Objects). Follow with an imperative sentence. + Reference spec point IDs when relevant. + Examples: + - `spec/ait: add initial AI Transport features specification` + - `spec/ait: clarify AIT-CT2 send semantics` + - `spec/chat: extract shared tombstonedAt calculation into RTLO6` + - `spec/objects: delete RTO5c1b1c (redundant to RTO5f3)` + +- **Non-spec changes** (build, CI, tooling, formatting): Use a conventional + commit prefix such as `chore:` or `build:`. + Examples: + - `chore: fix typographical errors and improve consistency in features.md` + - `chore: migrate project to Hugo and restructure build pipeline` + - `build(deps-dev): bump braces from 3.0.2 to 3.0.3 in /build` + +#### Body + +- Keep the body concise. Explain **what** changed and **why**, not just + restate the diff. +- If the commit resolves a Jira ticket, add it on its own line in the body + (e.g. `Resolves AIT-466`). Do NOT include the ticket ID in the summary. +- Omit the body entirely if the summary alone is sufficient. +- Further paragraphs come after blank lines. + - Bullet points are okay, too + - Typically a hyphen (-) is used for the bullet, followed by a single + space + +## Step 4: Present the message + +Show the complete commit message to the user in a fenced code block. + +Then ask: **"Do you want to commit with this message, edit it, or cancel?"** + +## Step 5: Act on the response + +- **Accept / looks good / yes**: Run the commit using a heredoc: + ``` + git commit -m "$(cat <<'EOF' + + EOF + )" + ``` +- **Edit**: The user will provide a revised message or describe changes. + Apply their edits and show the updated message for confirmation again + (return to Step 3). +- **Cancel**: Do nothing. + +After a successful commit, run `git log -1` to confirm. From 9a1cbfc8dd54159fdb201e26f1acbbff6b0ef2b6 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Sat, 21 Mar 2026 17:51:11 +0000 Subject: [PATCH 2/9] spec/ait: add initial AI Transport features specification Define the version, general principles, and architectural constraints for the Ably AI Transport SDK. Covers the two-layer architecture, codec parameterization, header discipline, channel model, dependency injection, and error handling conventions. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 specifications/ai-transport-features.md diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md new file mode 100644 index 00000000..71451bc8 --- /dev/null +++ b/specifications/ai-transport-features.md @@ -0,0 +1,29 @@ +--- +title: AI Transport Features +--- + +## Overview + +This document outlines the feature specification for the Ably AI Transport product. + +The Ably AI Transport SDK provides transport and codec abstractions for building AI applications over Ably. It enables realtime streaming of AI model responses between server and client, with support for conversation history, branching, cancellation, and multi-client synchronization. + +The key words "must", "must not", "required", "shall", "shall not", "should", "should not", "recommended", "may", and "optional" (whether lowercased or uppercased) in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). + +## AI Transport Specification Version {#version} + +- `(AIT-V1)` **Specification Version**: This document defines the Ably AI Transport library features specification ('features spec'). + - `(AIT-V1a)` The current version of the AI Transport library feature specification is version `0.1.0`. + +## General Principles {#general} + +- `(AIT-GP1)` The SDK must be split into a generic layer and one or more framework-specific layers. The generic layer must have no dependencies on any AI framework (e.g. Vercel AI SDK). Framework-specific layers implement codecs and provide convenience wrappers. +- `(AIT-GP2)` All generic components must be parameterized by a `Codec` interface. The codec defines how application-level events are encoded to Ably messages and decoded back. +- `(AIT-GP3)` The generic layer must use only `x-ably-*` message headers. Domain-specific headers (e.g. headers specific to the Vercel AI SDK) must only appear in framework-specific layers. +- `(AIT-GP4)` A single shared Ably channel must be used per transport instance. All features (streaming, cancellation, history) operate over this shared channel. +- `(AIT-GP5)` All dependencies must be passed through constructors or factory options. There must be no singletons, service locators, or global state. +- `(AIT-GP6)` When raising an `ErrorInfo`, avoid relying on the generic `40000` and `50000` codes. + - `(AIT-GP6a)` Prefer codes already defined in `ably-common`, for example — use `InvalidArgument` (`40003`) for an invalid argument passed to a function. + - `(AIT-GP6b)` If the error is AI Transport-specific, define a new code in the `104000–104999` range reserved for this SDK. +- `(AIT-GP7)` Error messages must follow the standard format `unable to ; `. + - `(AIT-GP7a)` Error messages must be written assuming that the audience is the customer developer, not an Ably engineer. From 235a565bf62dc23a7bf9da32e1421cf2b2d6cbaa Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Mon, 23 Mar 2026 14:57:55 +0000 Subject: [PATCH 3/9] spec/ait: add codec layer specification Add encoder core (AIT-CD1 through AIT-CD6) and decoder core (AIT-CD7 through AIT-CD10) spec points covering the contract between domain event streams and Ably message primitives. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index 71451bc8..f9fd97c5 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -27,3 +27,46 @@ The key words "must", "must not", "required", "shall", "shall not", "should", "s - `(AIT-GP6b)` If the error is AI Transport-specific, define a new code in the `104000–104999` range reserved for this SDK. - `(AIT-GP7)` Error messages must follow the standard format `unable to ; `. - `(AIT-GP7a)` Error messages must be written assuming that the audience is the customer developer, not an Ably engineer. + +## Codec {#codec} + +The codec layer defines the contract between domain event streams and Ably's native message primitives (publish, append, update, delete). It is split into a generic `Codec` interface and core encoder/decoder machinery. + +### Encoder Core + +- `(AIT-CD1)` The SDK must provide a `createEncoderCore` factory that returns an `EncoderCore`. The encoder core provides Ably primitives (publish, append, close, abort) that domain-specific encoders wire their event types to. +- `(AIT-CD2)` `startStream` must publish an Ably message with `x-ably-stream: "true"`, `x-ably-status: "streaming"`, and `x-ably-stream-id` set to the provided `streamId`. It must store the returned serial in a tracker keyed by `streamId`. + - `(AIT-CD2a)` If the publish does not return a serial, `startStream` must raise an error. +- `(AIT-CD3)` `appendStream` must append a text delta to an active stream's tracked serial. The delta must be accumulated in the tracker for recovery purposes. + - `(AIT-CD3a)` If no tracker exists for the given `streamId`, `appendStream` must raise an error. +- `(AIT-CD4)` `closeStream` must append a message with `x-ably-status: "finished"`. The closing append must repeat all persistent headers from the stream tracker. After enqueuing the closing append, `closeStream` must flush all pending appends and attempt recovery for any failures before returning. + - `(AIT-CD4a)` If no tracker exists for the given `streamId`, `closeStream` must raise an error. +- `(AIT-CD5)` `abortStream` must mark the specified stream as aborted and append a message with `x-ably-status: "aborted"`. After enqueuing the abort append, it must flush all pending appends and attempt recovery before returning. + - `(AIT-CD5a)` `abortAllStreams` must perform the same operation as `abortStream` for every active stream, then flush all pending appends. + - `(AIT-CD5b)` If no tracker exists for the given `streamId`, `abortStream` must raise an error. +- `(AIT-CD6)` Flushing must await all pipelined appends. For any failed append, recovery must be attempted via `updateMessage` with the full accumulated text. If recovery also fails, the flush must raise an error with code `EncoderRecoveryFailed` (`104000`). + +### Decoder Core + +- `(AIT-CD7)` The SDK must provide a `createDecoderCore` factory that accepts domain-specific hooks and returns a `DecoderCore`. The decoder dispatches on Ably message actions (`message.create`, `message.append`, `message.update`, `message.delete`). + - `(AIT-CD7a)` On `message.create`, the decoder must check the `x-ably-stream` header to determine whether the message enters the streaming path or the discrete path. Stream identity must be read from the `x-ably-stream-id` header. +- `(AIT-CD8)` On `message.append`, the decoder must accumulate the delta text in the stream tracker. If `x-ably-status` is `"finished"`, it must mark the stream as closed and emit end events. If `"aborted"`, it must mark the stream as closed without emitting end events. +- `(AIT-CD9)` On `message.update` with no existing tracker (first-contact), the decoder must create a new tracker. If the message data is non-empty, it must emit start, delta, and (if finished) end events. On `message.update` with an existing tracker where data is a prefix extension, it must emit only the new delta portion. +- `(AIT-CD10)` On `message.delete`, the decoder must invoke the `onStreamDelete` callback (if set) and mark the tracker as closed. + +### Discrete Publishing + +- `(AIT-CD11)` The encoder core must provide a `publishDiscrete` operation that publishes a standalone message with `x-ably-stream: "false"` and caller-provided headers merged with defaults. + - `(AIT-CD11a)` `publishDiscreteBatch` must publish multiple discrete messages atomically in a single channel publish. + +### Encoder Lifecycle + +- `(AIT-CD12)` The encoder core must provide a `close` method that flushes all pending appends, clears all stream trackers, and rejects subsequent operations. Close must be idempotent. + +### Lifecycle Tracker + +- `(AIT-CD13)` The decoder must support a lifecycle tracker that synthesizes missing lifecycle events (e.g. `start`, `start-step`) when a client joins a stream mid-turn. Phases must be emitted in configuration order before content events. + +### Encoder Hooks + +- `(AIT-CD14)` The encoder core must invoke an optional `onMessage` hook before each Ably message is published. The hook receives the message before it is sent to the channel writer. If the hook throws, the encoder must catch and log the exception without interrupting the publish. From 24b69a05ab4cdda5f4dbbe7007669b7c4981e857 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Mon, 23 Mar 2026 22:47:47 +0000 Subject: [PATCH 4/9] spec/ait: add server transport specification Define the server transport layer covering factory construction, turn lifecycle (start, addMessages, streamResponse, end), cancel routing with filter headers, and transport close semantics. Spec points AIT-ST1 through AIT-ST11. --- specifications/ai-transport-features.md | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index f9fd97c5..d8d5fe25 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -70,3 +70,46 @@ The codec layer defines the contract between domain event streams and Ably's nat ### Encoder Hooks - `(AIT-CD14)` The encoder core must invoke an optional `onMessage` hook before each Ably message is published. The hook receives the message before it is sent to the channel writer. If the hook throws, the encoder must catch and log the exception without interrupting the publish. + +## Server Transport {#server-transport} + +The server transport manages the server-side turn lifecycle over an Ably channel. It composes a `TurnManager` for lifecycle event publishing and pipes event streams through the encoder. + +### Factory + +- `(AIT-ST1)` The SDK must provide a `createServerTransport` factory that accepts a channel, codec, optional logger, and optional `onError` callback, and returns a `ServerTransport`. +- `(AIT-ST2)` On construction, the transport must subscribe to the cancel message name (`x-ably-cancel`) on the channel so that cancel messages from clients are routed to active turns. + +### Turn Lifecycle + +- `(AIT-ST3)` `newTurn` must synchronously return a `Turn` object. No channel activity must occur until `start()` is called. + - `(AIT-ST3a)` The turn must be registered for cancel routing immediately on creation, so that cancel messages arriving before `start()` still fire the turn's abort signal. +- `(AIT-ST4)` `start()` must publish a turn-start event (`x-ably-turn-start`) with the turn's `x-ably-turn-id` and `x-ably-turn-client-id` headers. It must be idempotent. + - `(AIT-ST4a)` If the turn was cancelled before `start()`, `start()` must raise an error. + - `(AIT-ST4b)` If the turn-start publish fails, the per-turn `onError` callback must be invoked and the error re-thrown. +- `(AIT-ST5)` `addMessages()` must require that `start()` has been called. For each input message, it must create a codec encoder with transport headers (`x-ably-role: "user"`, `x-ably-turn-id`, `x-ably-msg-id`, and optional parent/forkOf headers) and publish the message through the encoder. + - `(AIT-ST5a)` Per-operation options (parent, forkOf, clientId) must override turn-level defaults. + - `(AIT-ST5b)` The transport must track the `x-ably-msg-id` of the last published user message for auto-linking the assistant response parent. +- `(AIT-ST6)` `streamResponse()` must require that `start()` has been called. It must create a codec encoder with transport headers (`x-ably-role: "assistant"`, `x-ably-turn-id`, unique `x-ably-msg-id`, and parent/forkOf headers) and pipe the event stream through the encoder. + - `(AIT-ST6a)` The assistant message's parent must be resolved in order: per-operation override, last published user msg-id, turn-level parent. + - `(AIT-ST6b)` `streamResponse()` must return a `StreamResult` with a `reason` field indicating `"complete"`, `"cancelled"`, or `"error"`. + - `(AIT-ST6b1)` Reason `"complete"`: the source stream was fully consumed and the encoder was closed. + - `(AIT-ST6b2)` Reason `"cancelled"`: the turn's abort signal fired. If an `onAbort` callback was provided in the turn options, it must be invoked with a write function before the stream ends. + - `(AIT-ST6b3)` Reason `"error"`: the source stream threw. The encoder must be closed best-effort; failure to close must not propagate. + - `(AIT-ST6c)` `streamResponse()` must NOT call `end()` — the caller is responsible for calling `end()` after the stream finishes. +- `(AIT-ST7)` `end()` must require that `start()` has been called. It must publish a turn-end event (`x-ably-turn-end`) with the turn's ID, clientId, and reason header. It must be idempotent. + - `(AIT-ST7a)` After `end()`, the turn must be deregistered from cancel routing regardless of whether the publish succeeds. + +### Cancel Routing + +- `(AIT-ST8)` The server transport must route cancel messages from the channel to registered turns by parsing cancel filter headers from the incoming message. + - `(AIT-ST8a)` `x-ably-cancel-turn-id`: cancel a specific turn by ID. + - `(AIT-ST8b)` `x-ably-cancel-own` (value `"true"`): cancel all turns belonging to the sender's `clientId`. + - `(AIT-ST8c)` `x-ably-cancel-client-id`: cancel all turns belonging to a specific `clientId`. + - `(AIT-ST8d)` `x-ably-cancel-all` (value `"true"`): cancel all turns on the channel. +- `(AIT-ST9)` If a per-turn `onCancel` hook is provided, it must be invoked before aborting. If it returns `false`, the turn must not be aborted. + - `(AIT-ST9a)` If the `onCancel` hook throws, the error must be reported via the per-turn or transport-level `onError` callback, and processing must continue for remaining matched turns. + +### Transport Close + +- `(AIT-ST11)` `close()` must unsubscribe from cancel messages, abort all registered turns, and clean up the turn manager. From a5856e131fdd38fa3de62d8a83b4372063e2279d Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 24 Mar 2026 12:10:27 +0000 Subject: [PATCH 5/9] spec/ait: add client transport specification Add AIT-CT1 through AIT-CT18 covering the client transport layer: factory construction, send/regenerate/edit operations, cancel propagation, event subscriptions, message access, history with pagination, close lifecycle, conversation tree with branching and fork semantics, stream routing, echo detection, multi-client sync, active turn tracking, and waitForTurn. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index d8d5fe25..32b607dc 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -113,3 +113,91 @@ The server transport manages the server-side turn lifecycle over an Ably channel ### Transport Close - `(AIT-ST11)` `close()` must unsubscribe from cancel messages, abort all registered turns, and clean up the turn manager. + +## Client Transport {#client-transport} + +The client transport manages the client-side conversation lifecycle over an Ably channel. It composes a ConversationTree for branching message history, a StreamRouter for per-turn event streams, and a codec decoder for processing incoming Ably messages. + +### Factory + +- `(AIT-CT1)` The SDK must provide a `createClientTransport` factory that accepts a channel, codec, and transport options, and returns a `ClientTransport`. +- `(AIT-CT2)` On construction, the transport must subscribe to the channel for incoming messages before the channel attaches (Ably RTL7g) to guarantee no messages are missed. + +### Send + +- `(AIT-CT3)` `send()` must create a new turn, optimistically insert user messages into the conversation tree, and return an `ActiveTurn` handle containing a decoded event stream, the turn ID, and a cancel function. + - `(AIT-CT3a)` The HTTP POST to the server must be fire-and-forget — the returned stream must be available immediately, without waiting for the POST to complete. + - `(AIT-CT3b)` If the HTTP POST fails (network error or non-2xx response), the error must be emitted via `on('error')`, not thrown. The turn's stream must be closed. + - `(AIT-CT3c)` Each user message must be assigned a unique `x-ably-msg-id` and optimistically inserted into the conversation tree before the POST is sent. + - `(AIT-CT3d)` If `parent` is not explicitly provided and `forkOf` is not set, the parent must be auto-computed from the last message in the current thread. +- `(AIT-CT4)` `send()` must throw if the transport is closed. + +### Regenerate + +- `(AIT-CT5)` `regenerate()` must create a new turn that forks the target message with `forkOf` set to the target, `parent` set to the target's parent, and truncated history (everything before the target). No new user messages are sent. + +### Edit + +- `(AIT-CT6)` `edit()` must create a new turn that forks the target message with replacement user messages. `forkOf` must be set to the target, `parent` to the target's parent. + +### Cancel + +- `(AIT-CT7)` `cancel()` must publish a cancel message to the channel with the appropriate filter headers and close matching local streams. + - `(AIT-CT7a)` If no filter is provided, `cancel()` must default to `{ own: true }`. + +### Event Subscription + +- `(AIT-CT8)` The transport must support event subscriptions via `on(event, handler)` returning an unsubscribe function. + - `(AIT-CT8a)` `on('message')` must notify when the message list changes (messages added, updated, or removed). + - `(AIT-CT8b)` `on('turn')` must notify on turn lifecycle events (start and end) with the turn ID, client ID, and end reason. + - `(AIT-CT8c)` `on('error')` must surface non-fatal transport errors as `ErrorInfo`. + - `(AIT-CT8d)` If the transport is closed, `on()` must return a no-op unsubscribe function. + +### Message Access + +- `(AIT-CT9)` `getMessages()` must return the flattened conversation tree along the currently selected branches. +- `(AIT-CT10)` `getTree()` must expose the conversation tree for branch navigation (sibling listing, selection, node lookup). + +### History + +- `(AIT-CT11)` `history()` must load decoded messages from channel history using `untilAttach` for gapless continuity with the live subscription. + - `(AIT-CT11a)` History messages must be inserted into the conversation tree and trigger a message notification. + - `(AIT-CT11b)` The `limit` option must control the number of complete domain messages returned, not the number of Ably wire messages fetched. The implementation must page through Ably history until enough complete messages are assembled. + - `(AIT-CT11c)` The returned `PaginatedMessages` must support `next()` for loading older pages. Older messages are withheld from `getMessages()` until released by `next()`. + +### Close + +- `(AIT-CT12)` `close()` must unsubscribe from the channel, close all active turn streams, clear all event handlers, and prevent further operations. + - `(AIT-CT12a)` An optional `cancel` filter must publish a cancel message before teardown. Cancel publish failure must be swallowed (best-effort). + - `(AIT-CT12b)` `close()` must be idempotent. + +### Conversation Tree + +- `(AIT-CT13)` The SDK must provide a `ConversationTree` that materializes a branching conversation from a flat oplog of messages. + - `(AIT-CT13a)` Messages must be ordered by Ably serial (lexicographic). Messages without a serial (optimistic inserts) must sort after all serial-bearing messages. + - `(AIT-CT13b)` Fork points must create sibling groups. Messages with the same `x-ably-parent` whose `x-ably-fork-of` chains trace to a common root form a sibling group. The default selection must be the latest sibling. + - `(AIT-CT13c)` `select()` must update the active branch at a fork point. `flatten()` must reflect the current selection. + - `(AIT-CT13d)` `upsert()` must promote null serials to server-assigned serials on echo, re-sorting the message in the list. + +### Stream Router + +- `(AIT-CT14)` The client transport must route decoded events to per-turn `ReadableStream`s via a stream router. + - `(AIT-CT14a)` Terminal events (as determined by the codec's `isTerminal` predicate) must close the stream after enqueue. + - `(AIT-CT14b)` `closeStream()` must close the controller and remove the entry, allowing the consumer to read the stream to completion. + +### Echo Detection + +- `(AIT-CT15)` Own messages (matched by `x-ably-msg-id`) must be detected on the channel subscription and handled as updates to optimistic entries, not as new inserts. + +### Observer / Multi-Client Sync + +- `(AIT-CT16)` Events from non-own turns must be accumulated through the codec's `MessageAccumulator` and upserted into the conversation tree, enabling multi-client synchronization. + - `(AIT-CT16a)` Turn lifecycle events (`x-ably-turn-start`, `x-ably-turn-end`) must update active turn tracking for all clients on the channel. + +### Active Turn Tracking + +- `(AIT-CT17)` `getActiveTurnIds()` must return currently active turns grouped by client ID. + +### Wait for Turn + +- `(AIT-CT18)` `waitForTurn()` must return a promise that resolves when all active turns matching the filter have completed. It must resolve immediately if no matching turns are active. If no filter is provided, it must default to `{ own: true }`. From b31322fa9774c15a5b680b521ecf019a945346f3 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 24 Mar 2026 13:29:48 +0000 Subject: [PATCH 6/9] spec/ait: add error code definitions Add common and AI Transport-specific error code sections to the specification. Common codes (BadRequest 40000, InvalidArgument 40003) are listed for completeness. Custom codes in the 104xxx range cover encoder recovery, transport subscription errors, cancel listener errors, turn lifecycle errors, closed transport operations, and transport send failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index 32b607dc..bf4444a9 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -201,3 +201,55 @@ The client transport manages the client-side conversation lifecycle over an Ably ### Wait for Turn - `(AIT-CT18)` `waitForTurn()` must return a promise that resolves when all active turns matching the filter have completed. It must resolve immediately if no matching turns are active. If no filter is provided, it must default to `{ own: true }`. + +## Common Error Codes used by AI Transport {#common-error-codes} + +This section contains error codes that are common across Ably, but the AI Transport SDK makes use of. The status code for the error should align with the error code (i.e. 4xxxx and 5xxxx shall have statuses 400 and 500 respectively). + +The codes listed here shall be defined in any error enums that exist in the client library. + + // The request was invalid. + // To be accompanied by status code 400. + BadRequest = 40000, + + // Invalid argument provided. + // To be accompanied by status code 400. + InvalidArgument = 40003, + +## AI Transport-specific Error Codes {#error-codes} + +This section contains error codes that are specific to AI Transport. If a specific error code is not listed for a given circumstance, the most appropriate general error code shall be used according to the guidelines of `AIT-GP6`. For example `400xx` for client errors or `500xx` for server errors. + +The AI Transport reserved error code range is `104000 - 104999`. + +The codes listed here shall be defined in any error enums that exist in the client library. + + // Encoder recovery failed after flush — one or more updateMessage calls + // could not recover a failed append pipeline. + // To be accompanied by status code 500. + // Spec: AIT-CD6 + EncoderRecoveryFailed = 104000, + + // A transport-level channel subscription callback threw unexpectedly. + // To be accompanied by status code 500. + TransportSubscriptionError = 104001, + + // Cancel listener or onCancel hook threw while processing a cancel message. + // To be accompanied by status code 500. + // Spec: AIT-ST9a + CancelListenerError = 104002, + + // A turn lifecycle event (turn-start or turn-end) failed to publish. + // To be accompanied by status code 500. + // Spec: AIT-ST4b + TurnLifecycleError = 104003, + + // An operation was attempted on a transport that has already been closed. + // To be accompanied by status code 400. + // Spec: AIT-CT4, AIT-CT12 + TransportClosed = 104004, + + // The HTTP POST to the server endpoint failed (network error or non-2xx response). + // To be accompanied by status code 500. + // Spec: AIT-CT3b + TransportSendFailed = 104005, From 2a2bc165703740d2c6beacd56d49ffede27b85a1 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 24 Mar 2026 23:11:31 +0000 Subject: [PATCH 7/9] spec/ait: add AIT-CT3e for multi-message send chaining When multiple messages are sent in a single send() call, each subsequent message must parent off the previous message in the batch rather than the original auto-computed parent. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index bf4444a9..96143b17 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -130,6 +130,7 @@ The client transport manages the client-side conversation lifecycle over an Ably - `(AIT-CT3b)` If the HTTP POST fails (network error or non-2xx response), the error must be emitted via `on('error')`, not thrown. The turn's stream must be closed. - `(AIT-CT3c)` Each user message must be assigned a unique `x-ably-msg-id` and optimistically inserted into the conversation tree before the POST is sent. - `(AIT-CT3d)` If `parent` is not explicitly provided and `forkOf` is not set, the parent must be auto-computed from the last message in the current thread. + - `(AIT-CT3e)` When multiple messages are sent in a single `send()` call, they must be chained — each subsequent message must parent off the previous message in the batch, not the original auto-computed parent. - `(AIT-CT4)` `send()` must throw if the transport is closed. ### Regenerate From 552ab8dfb75efdb86e9f7c30e33670622b0d2382 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 24 Mar 2026 23:27:09 +0000 Subject: [PATCH 8/9] spec/ait: revise AIT-ST5b and AIT-ST6a parent linking semantics Return msg-ids from addMessages() instead of tracking them internally, and simplify streamResponse() parent resolution to per-operation override then turn-level parent. This makes parent linking explicit rather than relying on implicit internal state. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index 96143b17..9c54cbed 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -89,9 +89,9 @@ The server transport manages the server-side turn lifecycle over an Ably channel - `(AIT-ST4b)` If the turn-start publish fails, the per-turn `onError` callback must be invoked and the error re-thrown. - `(AIT-ST5)` `addMessages()` must require that `start()` has been called. For each input message, it must create a codec encoder with transport headers (`x-ably-role: "user"`, `x-ably-turn-id`, `x-ably-msg-id`, and optional parent/forkOf headers) and publish the message through the encoder. - `(AIT-ST5a)` Per-operation options (parent, forkOf, clientId) must override turn-level defaults. - - `(AIT-ST5b)` The transport must track the `x-ably-msg-id` of the last published user message for auto-linking the assistant response parent. + - `(AIT-ST5b)` `addMessages()` must return an `AddMessagesResult` containing the `x-ably-msg-id` of each published message, in order. This allows the caller to pass the last msg-id as the assistant message's parent. - `(AIT-ST6)` `streamResponse()` must require that `start()` has been called. It must create a codec encoder with transport headers (`x-ably-role: "assistant"`, `x-ably-turn-id`, unique `x-ably-msg-id`, and parent/forkOf headers) and pipe the event stream through the encoder. - - `(AIT-ST6a)` The assistant message's parent must be resolved in order: per-operation override, last published user msg-id, turn-level parent. + - `(AIT-ST6a)` The assistant message's parent must be resolved in order: per-operation `parent` override, then turn-level `parent`. - `(AIT-ST6b)` `streamResponse()` must return a `StreamResult` with a `reason` field indicating `"complete"`, `"cancelled"`, or `"error"`. - `(AIT-ST6b1)` Reason `"complete"`: the source stream was fully consumed and the encoder was closed. - `(AIT-ST6b2)` Reason `"cancelled"`: the turn's abort signal fired. If an `onAbort` callback was provided in the turn options, it must be invoked with a write function before the stream ends. From 0d822e7e926eac66b4f15e8cfcce082ad4bcfdee Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 24 Mar 2026 23:44:33 +0000 Subject: [PATCH 9/9] spec/ait: rename echo detection to optimistic reconciliation Replace "echo" terminology with "relay" and "optimistic reconciliation" in AIT-CT13d and AIT-CT15 to better describe the mechanism by which own messages are matched and merged with their optimistic entries. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/ai-transport-features.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifications/ai-transport-features.md b/specifications/ai-transport-features.md index 9c54cbed..8c1a3476 100644 --- a/specifications/ai-transport-features.md +++ b/specifications/ai-transport-features.md @@ -178,7 +178,7 @@ The client transport manages the client-side conversation lifecycle over an Ably - `(AIT-CT13a)` Messages must be ordered by Ably serial (lexicographic). Messages without a serial (optimistic inserts) must sort after all serial-bearing messages. - `(AIT-CT13b)` Fork points must create sibling groups. Messages with the same `x-ably-parent` whose `x-ably-fork-of` chains trace to a common root form a sibling group. The default selection must be the latest sibling. - `(AIT-CT13c)` `select()` must update the active branch at a fork point. `flatten()` must reflect the current selection. - - `(AIT-CT13d)` `upsert()` must promote null serials to server-assigned serials on echo, re-sorting the message in the list. + - `(AIT-CT13d)` `upsert()` must promote null serials to server-assigned serials on relay, re-sorting the message in the list. ### Stream Router @@ -186,9 +186,9 @@ The client transport manages the client-side conversation lifecycle over an Ably - `(AIT-CT14a)` Terminal events (as determined by the codec's `isTerminal` predicate) must close the stream after enqueue. - `(AIT-CT14b)` `closeStream()` must close the controller and remove the entry, allowing the consumer to read the stream to completion. -### Echo Detection +### Optimistic Reconciliation -- `(AIT-CT15)` Own messages (matched by `x-ably-msg-id`) must be detected on the channel subscription and handled as updates to optimistic entries, not as new inserts. +- `(AIT-CT15)` Own messages (matched by `x-ably-msg-id`) must be detected as relayed messages and reconciled with optimistic entries, not inserted as duplicates. ### Observer / Multi-Client Sync