# Streaming

[RightNow AI](https://www.rightnowai.co/) / [Decision model](https://www.rightnowai.co/docs/decision-model) / [Streaming](https://www.rightnowai.co/docs/decision-model/streaming)

Updated 2026-11-13

A stream sends changing state from your server and returns structured decisions over a WebSocket connection. Append new text or array items, replace state, and read the decisions that cover accepted changes.

For a single decision over HTTP as events, read [Event stream reply](https://www.rightnowai.co/docs/decision-model/decide#event-stream).

## Connect

Streaming is server to server only. Use Node 22 or later or Python 3. Browsers cannot set the Authorization header on the WebSocket upgrade. Keep the key in the server process.

Use the API origin with `WSS` and `/v1/system-zero/stream`. Authenticate on the upgrade with `Authorization: Bearer <key>`. Request only `system-zero.stream.v1` in `Sec-WebSocket-Protocol`.

Use text JSON messages. Send no browser Origin, URL credential, cookie authentication, compression, binary data, query or trailing slash.

## Use an SDK

[Install an SDK](https://www.rightnowai.co/docs/decision-model/quickstart#install-sdk), then read the key from the server environment. Both languages export `SystemZeroStreamClient`.

TypeScript requires upgrade headers. Supply a server constructor through `WebSocket` if native support is missing.

**TypeScript (stream.ts)**

```typescript
import { SystemZeroStreamClient } from '@rightnowai/system-zero';

const stream = new SystemZeroStreamClient();
stream.on('decision', event => console.log(event.reply, event.request_id));
stream.on('error', event => console.error(event.envelope));
try {
  await stream.connect();
  await stream.start({
    model: 'rightnow-decision', state: 'Order received.',
    questions: { ready: { type: 'boolean', instructions: 'Is the order ready?' } },
  }, { decisionMode: 'on_request' });
  const ack = await stream.appendText(' Payment received.');
  await stream.decide();
  const measured = await stream.waitForDecision(ack);
  console.log(measured.decision.reply);
} finally {
  await stream.close();
}
```

**Python (stream.py)**

```python
import asyncio
from system_zero import SystemZeroStreamClient

async def main():
    async with SystemZeroStreamClient() as stream:
        events = stream.events()
        await stream.connect()
        await stream.start({
            "model": "rightnow-decision", "state": "Order received.",
            "questions": {"ready": {"type": "boolean", "instructions": "Is the order ready?"}},
        }, decision_mode="on_request")
        await stream.append_text(" Payment received.")
        await stream.decide()
        async for event in events:
            if event["type"] == "decision":
                print(event["reply"], event["request_id"])
                break
            if event["type"] == "error":
                print(event["envelope"])

asyncio.run(main())
```

Await each mutation for its acknowledgement. Read `decision` for the reply and request ID, and `error` for its envelope. The clients answer heartbeats and pace changes using the advertised limits.

Reconnect restores accepted state with its idempotency keys. It does not resume a durable stream. Read `possibly_decided` when a lost connection leaves an unknown outcome. Reopening does not prove the work was uncounted.

## Message flow

**Illustration of the example exchange.**

```text
Your server       RightNow
|                        |
|<======== hello ========|
|======== start ========>|
|<========= ack =========|
|===== append_text =====>|
|<========= ack =========|
|======= decide ========>|
|<====== scheduled ======|
|<======== ping =========|
|======== pong =========>|
|<====== decision =======|
|<======= goodbye =======|

start  within 10 s of hello
pong  within 10 s of ping
ping  every 30 s
idle  closes after 120 s
life  60 minutes
```

RightNow sends hello. You send start within 10 seconds of hello. RightNow sends ack. You send append\_text. RightNow sends ack. You send decide. RightNow sends scheduled. RightNow sends ping. You send pong within 10 seconds of ping. RightNow sends decision. RightNow sends goodbye. A ping arrives every 30 seconds. An idle stream closes after 120 seconds. A stream lives 60 minutes.

Read `hello` and its limits, then send `start` before its deadline. Questions, routing, scheduling and the cache key stay fixed for the connection. Read whole decisions in a stream with hello, ack, decision, error frames. The stream does not send individual tokens.

Use append\_text for string state, append\_items for whole array elements, or replace for a complete new state. Objects are replaced whole; field-level patches are not supported. append\_attachments changes only the inline attachment list. Each accepted mutation receives ack and advances state\_version.

With on\_change, accepted updates may be combined into a decision. With on\_request, send `decide(state_version)` for the current accepted version and read scheduled. A decision embeds the ordinary HTTP reply. Each decision reports its snapshot's state\_version and covered\_event\_ids. Check state\_version before using the answer because newer state may already have arrived. Use on\_change (the default) when you want an answer for every state change: the decision starts the moment the event is accepted. on\_request starts a decision only when you ask for one, after the acknowledgement of your event, so each decision adds one network round trip from your client. measured 2026-11-10 02:42Z, three sessions from a US west-coast client: 80-150 ms from event accepted to decision started in on\_request mode; the server's own step in between took 2-5 ms.

Answer each JSON ping with pong and the same nonce within 10000 milliseconds of the completed ping write. Native WebSocket controls are separate. A missed pong closes with 1013 and reason slow\_reader.

Idle = 120 seconds with no client application message and no decide in flight; the clock restarts when a decide settles; heartbeats do not count. The client ends normally with WebSocket close code 1000. There is no client goodbye or client ping JSON message.

A server close can send `goodbye` first. Durable stream resume is unsupported. Reconnect and replay the full desired state with the same per-state idempotency key and byte-identical request. Successful keys have a 24-hour answer window and retain an identity tombstone without request or answer content for 30 days after that window; after answer expiry, the same body returns 409 idempotency\_replay\_unavailable and a changed body returns 400 idempotency\_key\_reuse.

On a new connection the body is the same only if start carried your own prompt\_cache\_key. Without one the server uses the new stream\_id, and the key returns 400 idempotency\_key\_reuse. If a decision did not arrive before the connection closed, the same key and body can return the completed answer. Replay availability is not guaranteed. Read [Idempotency](https://www.rightnowai.co/docs/decision-model/idempotency). Without a key, a decision lost in transit cannot be retrieved. Send the state again; that starts new work.

**Illustration of reconnecting by close code.**

```text
[ Socket closed ]
  |
  |-- 1002 1003 1007 1008 1009
  |   never reconnect
  |-- 1001
  |   reconnect at once
  |-- 1012
  |   wait 500 ms
  |-- 1013
  |   wait the hint or back off
  |-- 1011
  |   back off, then reconnect
  '-- 1000
      reconnect when needed

[ Replay full state, same keys ]
[ Read possibly_decided ]
```

Never reconnect after 1002, 1003, 1007, 1008 or 1009. Reconnect at once after 1001. Wait 500 milliseconds after 1012. After 1013 wait the hint, 300 seconds for maintenance or 1 second for a slow reader, else back off. Back off after 1011. After 1000 reconnect when you need the stream. Then replay the full state with the same keys and read possibly\_decided.

### Update an image

Save prepared PNG files as shelf-before.png and shelf-after.png. This Python example prints four client messages. Send them individually over your authenticated server connection after hello.

**image-messages.py**

```python
import base64
import json
from pathlib import Path

messages = [
    {
        "type": "start",
        "event_id": "evt_1",
        "decision_mode": "on_request",
        "request": {
            "model": "rightnow-decision",
            "state": "Check the current shelf image.",
            "attachments": [
                {
                    "name": "shelf",
                    "type": "image",
                    "data": "shelf-before.png"
                }
            ],
            "questions": {
                "stock": {
                    "type": "choice",
                    "instructions": "Are items visible on shelf?",
                    "criteria": {
                        "present": "Items are visible.",
                        "empty": "The shelf is empty.",
                        "unreadable": "The shelf cannot be assessed."
                    }
                }
            }
        }
    },
    {
        "type": "decide",
        "state_version": 1
    },
    {
        "type": "replace",
        "event_id": "evt_2",
        "state": "Check the current shelf image.",
        "attachments": [
            {
                "name": "shelf",
                "type": "image",
                "data": "shelf-after.png"
            }
        ]
    },
    {
        "type": "decide",
        "state_version": 2
    }
]
for message in messages:
    request = message.get("request", message)
    for attachment in request.get("attachments", []):
        path = Path(attachment["data"])
        encoded = base64.b64encode(path.read_bytes()).decode("ascii")
        attachment["data"] = f"data:image/png;base64,{encoded}"
    print(json.dumps(message))
```

Send start, wait for ack at version 1, then send decide. Read that decision before sending replace. Wait for its ack at version 2, then request the second decision. Keep answering heartbeats and honoring the advertised limits.

Replacement supplies the complete image list. Omit it to retain the previous list; use append\_attachments only when you want to keep existing images and add more. All retained files must still fit the ordinary count and shared byte limits.

Each new answered snapshot charges every image it contains, including retained images: USD 0.003 per image, USD 0.0001 per answered question and USD 0.00001 per answered call, with residual text charged separately. Each of these two snapshots includes one image fee if answered.

An acknowledgement is not a billable answer. Media-token cache hits do not remove file fees. There is no durable media cache. This example requests separate snapshots; automatic decisions may combine updates.

## Client messages

### `start`

- **idempotency\_key**: Optional. string. Minimum length: 1.
- **type**: Required. "start"
- **event\_id**: Required. "evt\_1"
- **request**: Required. Start request. No extra fields.
- **decision\_mode**: Required. "on\_change" or "on\_request"
- **min\_interval\_ms**: Optional. integer. Minimum: 0. Maximum: 60000. Default: 0.

### `append_text`

- **idempotency\_key**: Optional. string. Minimum length: 1.
- **type**: Required. "append\_text"
- **event\_id**: Required. string
- **text**: Required. string. Minimum length: 1.

### `append_items`

- **idempotency\_key**: Optional. string. Minimum length: 1.
- **type**: Required. "append\_items"
- **event\_id**: Required. string
- **items**: Required. array of JSON value. Minimum items: 1. Maximum items: 4096.

### `append_attachments`

- **idempotency\_key**: Optional. string. Minimum length: 1.
- **type**: Required. "append\_attachments"
- **event\_id**: Required. string
- **attachments**: Required. Inline attachment array. Minimum items: 1.

### `replace`

- **idempotency\_key**: Optional. string. Minimum length: 1.
- **type**: Required. "replace"
- **event\_id**: Required. string
- **state**: Required. string, object or array
- **attachments**: Optional. Inline attachment array. Maximum items: 16.

### `decide`

- **type**: Required. "decide"
- **state\_version**: Required. integer. Minimum: 1. Maximum: 100000.

### `pong`

- **type**: Required. "pong"
- **nonce**: Required. integer. Minimum: 1. Maximum: 9007199254740991.

## Server messages

### `hello`

- **type**: Required. "hello"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **protocol**: Required. "system-zero.stream.v1"
- **request\_id**: Required. string. Minimum length: 1. Maximum length: 128.
- **limits**: Required. Advertised limits. No extra fields.
- **rate\_limit**: Required. Rate limit snapshot or null

### `ack`

- **type**: Required. "ack"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **event\_id**: Required. string
- **state\_version**: Required. integer. Minimum: 1. Maximum: 100000.
- **state\_bytes**: Required. integer. Minimum: 0.
- **synthesized\_body\_bytes**: Required. integer. Minimum: 0. Maximum: 3665920.
- **scheduled**: Required. boolean

### `scheduled`

- **type**: Required. "scheduled"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **state\_version**: Required. integer. Minimum: 1. Maximum: 100000.
- **in\_flight**: Required. boolean

### `ping`

- **type**: Required. "ping"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **nonce**: Required. integer. Minimum: 1. Maximum: 9007199254740991.

### `decision`

- **type**: Required. "decision"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **state\_version**: Required. integer. Minimum: 1. Maximum: 100000.
- **covered\_event\_ids**: Required. array of string. Maximum items: 4096. Items are unique.
- **reply**: Required. HTTP decision reply. No extra fields.
- **request\_id**: Required. string. Minimum length: 1. Maximum length: 128.
- **http\_status**: Required. 200
- **server\_timing**: Required. string or null
- **latency\_ms**: Required. number or null. Minimum: 0.
- **rate\_limit**: Required. Rate limit snapshot or null
- **queue\_ms**: Required. number. Minimum: 0.
- **decide\_ms**: Required. number. Minimum: 0.

### `error`

- **type**: Required. "error"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **phase**: Required. "protocol" or "decide" or "lifecycle"
- **state\_version**: Required. integer. Minimum: 0. Maximum: 100000.
- **event\_id**: Required. string or null
- **covered\_event\_ids**: Required. array of string. Maximum items: 4096. Items are unique.
- **http\_status**: Required. integer. Minimum: 400. Maximum: 599.
- **envelope**: Required. HTTP error envelope. No extra fields.
- **retry**: Required. Retry fields. No extra fields.
- **rate\_limit**: Required. Rate limit snapshot or null
- **server\_timing**: Required. string or null
- **fatal**: Required. boolean

### `goodbye`

- **type**: Required. "goodbye"
- **stream\_id**: Required. string
- **seq**: Required. integer. Minimum: 1. Maximum: 9007199254740991.
- **reason**: Required. "idle" or "max\_lifetime" or "auth" or "policy" or "shutdown" or "maintenance" or "overload" or "slow\_reader" or "lease\_lost" or "store\_unavailable" or null
- **state\_version**: Required. integer. Minimum: 0. Maximum: 100000.
- **last\_decision\_state\_version**: Required. integer. Minimum: 0. Maximum: 100000.
- **resume**: Required. object. No extra fields.

## Shared fields

State is a JSON object, array or string. Nested numbers, booleans and null remain JSON. Use compact keys and send large integers or exact decimals as numeric strings when precision matters. Supply your own units, coordinate frames and freshness data.

Each decision follows the ordinary decide API's validation, availability, billing and limits. Keep choice options and score levels within the model list's limits. A larger schema bound is not a serving promise. An ack accepts state into connection memory; it grants no input availability and confirms no decision, charge or durable save.

Question names use at most 64 ASCII letters, digits and underscores, starting with a letter or underscore. Attachment names use at most 64 ASCII letters, digits, underscores or hyphens, starting with a letter. Attachment names must be unique.

If start omits prompt\_cache\_key, the server uses stream\_id. For byte-identical replay across connections, send an explicit stable prompt\_cache\_key.

A [request policy](https://www.rightnowai.co/docs/decision-model/request-policy) goes in `start.request`.

Omitting `replace.attachments` retains the current list. An empty list removes every attachment. Text and array appends keep attachments unchanged. Inline data has no separate MIME field. Document files must be inline PDF data. PDF and video (preview) use the same limits as HTTP. Read [Attachments](https://www.rightnowai.co/docs/decision-model/attachments). The stream does not take upload references.

### Start request

- **model**: Required. string. Minimum length: 1. Maximum length: 200.
- **state**: Required. string, object or array
- **questions**: Required. Named questions. Minimum fields: 1. Maximum fields: 1024.
- **attachments**: Optional. Inline attachment array. Maximum items: 16.
- **prompt\_cache\_key**: Optional. string. Minimum length: 1. Maximum length: 256.
- **route**: Optional. "standard" or "committee". Held feature. "committee" is refused with 404 committee\_not\_available and is unbilled
- **mode**: Optional. "standard" or "deliberate". Held feature. "deliberate" is refused with 404 deliberate\_not\_available and is unbilled

### Boolean question

- **type**: Required. "boolean"
- **instructions**: Required. nonempty string, object or array
- **criteria**: Optional. object. No extra fields.

### Choice question

- **type**: Required. "choice"
- **instructions**: Required. nonempty string, object or array
- **criteria**: Required. object with named entries of string or null. Minimum fields: 1. Maximum fields: 255. Maximum key length: 256.

### Score question

- **type**: Required. "score"
- **instructions**: Required. nonempty string, object or array
- **criteria**: Required. array of string. Minimum items: 2. Maximum items: 10. Items: Minimum length: 1. Items must be nonblank strings.

### Inline data

- **name**: Required. string
- **type**: Required. "document" or "image" or "audio" or "video"
- **data**: Required. string. Must begin with data:.

### Advertised limits

- **max\_duration\_ms**: Required. 3600000
- **idle\_timeout\_ms**: Required. 120000
- **start\_timeout\_ms**: Required. 10000
- **heartbeat\_interval\_ms**: Required. 30000
- **native\_heartbeat\_interval\_ms**: Optional. 10000
- **receipt\_timeout\_ms**: Optional. 240000
- **max\_control\_queue\_frames**: Optional. 2
- **pong\_timeout\_ms**: Required. 10000
- **max\_message\_bytes**: Required. 4194304
- **max\_decide\_body\_bytes**: Required. 3665920
- **max\_pending\_events**: Required. 4096
- **max\_events**: Required. 100000
- **max\_events\_per\_second**: Required. 5
- **max\_send\_queue\_bytes**: Required. 4194304
- **max\_send\_queue\_frames**: Required. 64
- **min\_interval\_ms**: Required. 0
- **event\_burst**: Required. 10
- **max\_messages\_per\_second**: Required. 40
- **message\_burst**: Required. 80
- **max\_streams\_per\_process**: Required. 16
- **max\_native\_control\_frames\_per\_second**: Required. 8
- **native\_control\_frame\_burst**: Required. 16
- **max\_native\_control\_bytes\_per\_second**: Required. 512
- **native\_control\_byte\_burst**: Required. 1024

### Rate limit snapshot

- **limit**: Required. integer. Minimum: 0.
- **remaining**: Required. integer. Minimum: 0.
- **reset\_epoch\_seconds**: Required. integer. Minimum: 0.

### HTTP decision reply

- **id**: Required. string
- **model**: Required. string
- **served\_by**: Required. string
- **created**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **context**: Required. object. No extra fields.
- **answers**: Required. object
- **usage**: Required. object. No extra fields.

### HTTP error envelope

- **error**: Required. HTTP error. No extra fields.
- **request\_id**: Required. string

### HTTP error

- **message**: Required. string
- **type**: Required. "invalid\_request\_error" or "authentication\_error" or "permission\_error" or "not\_found\_error" or "rate\_limit\_error" or "overloaded\_error" or "api\_error" or "server\_error"
- **param**: Required. string or null
- **code**: Required. string or null
- **retryable**: Required. boolean
- **details**: Optional. object with named entries of JSON value

### Retry fields

- **should\_retry**: Required. boolean
- **retry\_after\_ms**: Required. number or null. Minimum: 0.
- **retry\_after**: Required. string or null

### Named questions

- **&lt;question name&gt;**: Required. Boolean question, choice question or score question. Minimum fields: 1. Maximum fields: 1024.

### Inline attachment array

- **&lt;item&gt;**: Optional. Inline data. Maximum items: 16.

### goodbye.resume fields

- **supported**: Required. false
- **action**: Required. "reconnect\_and\_replay"
- **retry\_after\_ms**: Required. integer. Minimum: 0.

### Boolean question.criteria fields

- **true**: Optional. string
- **false**: Optional. string

### HTTP decision reply.context fields

- **route**: Required. "direct" or "long"
- **tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.

### HTTP decision reply.usage fields

- **input\_tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **cached\_input\_tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **media\_tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **long\_input\_tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **media\_items**: Required. integer. Minimum: 0. Maximum: 9007199254740991.
- **thought\_tokens**: Required. integer. Minimum: 0. Maximum: 9007199254740991.

## Ordering and sequence

Each accepted mutation receives ack and increments state\_version once.

IDs are precisely evt\_1, evt\_2 and so on; a rejected update leaves the next ID available for a corrected retry.

Text appends concatenate exactly, arrays append complete values, and replace changes the complete string, array or object state.

Use replace to change an object state; attachments can change independently.

An ack means accepted state in gateway memory, not a decision, bill or durable save.

Every accepted mutation, including an attachment append or replacement, creates a new version with its own supplied key; an unchanged version retains its key.

Rejected mutations leave the previous key intact; an unkeyed mutation clears it.

The gateway maintains one decide in flight and one pending intent.

Dense changes coalesce into the latest state.

In on\_request mode, decide must name the current accepted version; its snapshot is chosen when dispatch becomes eligible.

state\_version is its cumulative coverage watermark; covered\_event\_ids names only the mutation batch newly frozen into that dispatch.

An ack precedes any decision that covers it.

Only a successful decision, never an ack or error, resolves an event-to-decision observation.

During shutdown, valid mutations are applied and acknowledged with scheduled: false; they never start another decide on this connection.

Server seq starts at 1 and increases for every server message.

Idempotency keys contain 1 to 255 printable ASCII characters after trimming surrounding ASCII spaces. An unkeyed mutation clears the previous key.

Reconnect creates a new stream with new event numbering.

Replay the full desired state in start, then append only mutations absent from that replay.

Use full-jitter reconnect backoff, honoring any longer server retry delay.

Heartbeats neither bill inference nor extend idle/lifetime.

## Limits

Read the limits advertised in hello. Stream limits do not enlarge ordinary HTTP limits. Idle time excludes heartbeats and time spent waiting for a decision. Account stream capacity is one stream socket across machines, for free, credit-only and paid accounts.

receipt\_timeout\_ms is a legacy wire hint, not a complete receipt deadline. Continue checking peer liveness and receipt inactivity. Both clients use a receipt deadline of 3818000 ms from command send; confirmed liveness can refresh inactivity timers but cannot extend it, and JSON-only EventTarget transports use a 3848000 ms heartbeat fallback without extending the receipt deadline.

- **max\_duration\_ms**: 3600000 milliseconds
- **idle\_timeout\_ms**: 120000 milliseconds
- **start\_timeout\_ms**: 10000 milliseconds
- **heartbeat\_interval\_ms**: 30000 milliseconds
- **native\_heartbeat\_interval\_ms**: 10000 milliseconds
- **receipt\_timeout\_ms**: 240000 milliseconds
- **max\_control\_queue\_frames**: 2 frames
- **pong\_timeout\_ms**: 10000 milliseconds
- **max\_message\_bytes**: 4194304 UTF-8 bytes
- **max\_decide\_body\_bytes**: 3665920 UTF-8 bytes
- **max\_pending\_events**: 4096 events
- **max\_events**: 100000 events
- **max\_events\_per\_second**: 5 events per second
- **max\_send\_queue\_bytes**: 4194304 bytes
- **max\_send\_queue\_frames**: 64 frames
- **min\_interval\_ms**: 0 milliseconds
- **event\_burst**: 10 events
- **max\_messages\_per\_second**: 40 messages per second
- **message\_burst**: 80 messages
- **max\_streams\_per\_process**: 16 streams
- **max\_native\_control\_frames\_per\_second**: 8 frames per second
- **native\_control\_frame\_burst**: 16 frames
- **max\_native\_control\_bytes\_per\_second**: 512 bytes per second
- **native\_control\_byte\_burst**: 1024 bytes
- **maximum\_decision\_interval**: 60000 milliseconds
- **mutation\_burst**: 10 events
- **application\_message\_rate**: 40 messages per second
- **application\_message\_burst**: 80 messages
- **max\_pending\_probes**: 2 probes
- **write\_progress\_timeout**: 5000 milliseconds
- **decision\_settlement\_timeout**: 5000 milliseconds
- **close\_handshake\_timeout**: 5000 milliseconds
- **consecutive\_retryable\_failures**: 3 failures per state version
- **protocol\_refusal\_limit**: 3 refusals per window
- **protocol\_refusal\_window**: 60000 milliseconds
- **account\_decision\_burst**: 5 starts
- **account\_decision\_ceiling**: 60 starts per minute

## Errors and close codes

Read `error.envelope` and `error.retry`. Decision failures keep the ordinary HTTP status and error envelope. A rejected mutation leaves state, version and its next event ID unchanged. A fatal error requires a following close.

**Stream errors**

| Code | Status | Meaning |
| --- | --- | --- |
| `stream_invalid_message` | 400 | The stream message is invalid. |
| `stream_state_type` | 400 | The stream state does not support this mutation. |
| `stream_event_order` | 409 | The stream event does not name the next state version. |
| `stream_version_mismatch` | 409 | The requested stream version is not current. |
| `stream_not_started` | 400 | The stream has not started. |
| `stream_already_started` | 409 | The stream has already started. |
| `stream_upgrade_required` | 426 | This endpoint accepts WebSocket connections only. |
| `stream_limit_exceeded` | 429 | The stream limit has been reached. |
| `invalid_request` | 400 | Invalid attachment list, item or repeated name. |
| `media_unsupported` | 400 | An attachment count exceeds its kind limit. |
| `payload_too_large` | 413 | The complete decision request exceeds its byte limit. |
| `limiter_unavailable` | 503 | Rate-limit state cannot be read. Honor the retry delay. |
| `service_unavailable` | 503 | Opening capacity is full or the server is stopping. Honor the retry delay. |

In on\_change mode, a 429 or 503 other than maintenance, or HTTP 409 idempotency\_conflict, with retry permitted by the HTTP response schedules the latest state again after its Retry-After delay.

The gateway emits an error frame for each failure and stops automatic retries at three consecutive retryable failures for the same version.

The third error has fatal: true and retains the HTTP retry fields.

It is followed by goodbye with reason overload, the Retry-After delay in resume.retry\_after\_ms, and close 1013 so the client reconnects and replays.

The same fatal outcome applies immediately when the next automatic retry would reach or exceed the stream's remaining lifetime.

On\_request never retries itself.

Retaining the key does not guarantee recovery of an original reply after a dropped connection.

Retry-After-Ms takes precedence over Retry-After.

**Stream close codes**

| Code | Condition | Reason | Reconnect |
| --- | --- | --- | --- |
| 1000 | Normal client close or application idle for 120 seconds | idle | lazy |
| 1001 | Maximum lifetime of one hour | max\_lifetime | immediate |
| 1002 | RFC 6455 framing protocol error | null | never |
| 1003 | Binary data frame | null | never |
| 1007 | Invalid UTF-8 or malformed JSON | null | never |
| 1008 | Refused key/account or 404 unknown\_endpoint, start timeout, message/event-rate, native-control frame/byte budget or total-event limit, 3 protocol refusals within 60 seconds (outside shutdown drain), or WebSocket fragment/chunk policy | auth, policy | never |
| 1009 | WebSocket data-frame/message size, queued incoming bytes, or pending-event count bound exceeded | null | never |
| 1011 | Unexpected internal failure | null | backoff |
| 1012 | Server restart | shutdown | after hint. retry after ms: 500 milliseconds |
| 1013 | Maintenance, overload, slow reader (including a missed native or JSON pong), lost/unrenewable lease or unavailable store | maintenance, overload, slow\_reader, lease\_lost, store\_unavailable | after hint or backoff. maintenance retry after ms: 300000 milliseconds. slow reader retry after ms: 1000 milliseconds |

Honor `goodbye.resume.retry_after_ms` and any longer server retry delay. Use reconnect backoff. A goodbye is best effort; a transport close can arrive without it.

The server never sends 1006; clients can observe it after abrupt loss and reconnect with backoff.

## Manual WebSocket

Illustrative JSON messages from a server runtime. Replace `MODEL_ID` and `SERVED_BY` with values from authenticated discovery. Timing, counts and probabilities illustrate the reply shape. In this exchange the server restarts after the decision, sends goodbye, then closes with code 1012. Wait at least 500 milliseconds before reconnecting.

**Server**

```json
{
  "type": "hello",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 1,
  "protocol": "system-zero.stream.v1",
  "request_id": "example-opening",
  "limits": {
    "max_duration_ms": 3600000,
    "idle_timeout_ms": 120000,
    "start_timeout_ms": 10000,
    "heartbeat_interval_ms": 30000,
    "native_heartbeat_interval_ms": 10000,
    "receipt_timeout_ms": 240000,
    "max_control_queue_frames": 2,
    "pong_timeout_ms": 10000,
    "max_message_bytes": 4194304,
    "max_decide_body_bytes": 3665920,
    "max_pending_events": 4096,
    "max_events": 100000,
    "max_events_per_second": 5,
    "max_send_queue_bytes": 4194304,
    "max_send_queue_frames": 64,
    "min_interval_ms": 0,
    "event_burst": 10,
    "max_messages_per_second": 40,
    "message_burst": 80,
    "max_streams_per_process": 16,
    "max_native_control_frames_per_second": 8,
    "native_control_frame_burst": 16,
    "max_native_control_bytes_per_second": 512,
    "native_control_byte_burst": 1024
  },
  "rate_limit": null
}
```

**Client**

```json
{
  "type": "start",
  "event_id": "evt_1",
  "request": {
    "model": "MODEL_ID",
    "state": "Initial log\n",
    "questions": {
      "ready": {
        "type": "boolean",
        "instructions": "Is the job ready?"
      }
    },
    "prompt_cache_key": "example-affinity"
  },
  "decision_mode": "on_request",
  "min_interval_ms": 0,
  "idempotency_key": "example-state-1"
}
```

**Server**

```json
{
  "type": "ack",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 2,
  "event_id": "evt_1",
  "state_version": 1,
  "state_bytes": 15,
  "synthesized_body_bytes": 158,
  "scheduled": false
}
```

**Client**

```json
{
  "type": "append_text",
  "event_id": "evt_2",
  "text": "Job is ready.\n",
  "idempotency_key": "example-state-2"
}
```

**Server**

```json
{
  "type": "ack",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 3,
  "event_id": "evt_2",
  "state_version": 2,
  "state_bytes": 30,
  "synthesized_body_bytes": 173,
  "scheduled": false
}
```

**Client**

```json
{
  "type": "decide",
  "state_version": 2
}
```

**Server**

```json
{
  "type": "scheduled",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 4,
  "state_version": 2,
  "in_flight": false
}
```

**Server**

```json
{
  "type": "ping",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 5,
  "nonce": 1
}
```

**Client**

```json
{
  "type": "pong",
  "nonce": 1
}
```

**Server**

```json
{
  "type": "decision",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 6,
  "state_version": 2,
  "covered_event_ids": [
    "evt_1",
    "evt_2"
  ],
  "reply": {
    "id": "szd_0123456789abcdef0123456789abcdef",
    "model": "MODEL_ID",
    "served_by": "SERVED_BY",
    "created": 1790000000,
    "context": {
      "route": "direct",
      "tokens": 32
    },
    "answers": {
      "ready": {
        "type": "boolean",
        "probability": 0.9,
        "confidence": 0.9,
        "calibration_version": "unpublished",
        "decision": true,
        "alternatives": [
          {
            "decision": false,
            "confidence": 0.1
          }
        ],
        "calibrated": false
      }
    },
    "usage": {
      "input_tokens": 32,
      "cached_input_tokens": 0,
      "media_tokens": 0,
      "long_input_tokens": 0,
      "media_items": 0,
      "thought_tokens": 0
    }
  },
  "request_id": "example-decision",
  "http_status": 200,
  "server_timing": null,
  "latency_ms": null,
  "rate_limit": null,
  "queue_ms": 0,
  "decide_ms": 40000
}
```

**Server**

```json
{
  "type": "goodbye",
  "stream_id": "szws_0123456789abcdef0123456789abcdef",
  "seq": 7,
  "reason": "shutdown",
  "state_version": 2,
  "last_decision_state_version": 2,
  "resume": {
    "supported": false,
    "action": "reconnect_and_replay",
    "retry_after_ms": 500
  }
}
```

## Next

[Webhooks](https://www.rightnowai.co/docs/decision-model/webhooks)
