# NATS subjects and streams

> Every NATS subject and JetStream stream RosieOS publishes or subscribes to, with payload schemas, the processes on each side, and which subjects carry command authority.

URL: https://advancedmetalresearch.com/docs/reference/nats-subjects
Section: RosieOS docs / Reference
Last updated: 2026-10-10

RosieOS uses NATS for two different jobs:

- **Observation.** `rt-natspublisher` copies `rt-control` status and telemetry onto NATS for displays and archives. These subjects carry no command authority. Nothing that reads them can move the robot.
- **Motion-server commands.** The Cartesian motion server and the dense trajectory daemon take their leader lease and their commands from a per-cell robot command subject.

`rt-control` itself never reads NATS. Its authority is the lease on its own socket. See [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority).

> [!WARNING] The command subjects have no authentication. The motion servers' leader lease is cooperative fencing between well-behaved controllers on a trusted network; anyone who can publish on the subject can send a `stop`, or try to take the lease. Keep the cell's NATS server on a private network. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

> [!TIP] **Machine-readable.** The subject map, streams and command tables below as [JSON](https://advancedmetalresearch.com/docs/data/nats-subjects.json).

## Subject map

`<host>`, `<peer>` and `<cell>` are deployment names, not fixed strings. `<host>` is passed through a token filter: every run of characters outside `A-Z a-z 0-9 - _` becomes one `-`.

| Subject | Publisher | Subscriber | Payload | Authority |
|---|---|---|---|---|
| `robot/v4/rtcore.<host>.status` | `rt-natspublisher` | Displays, archives | JSON `robot.v4.rtcore.status.v1` | None |
| `robot/v4/rtcore.<host>.telemetry.batch` | `rt-natspublisher` | Displays, archives | JSON `robot.v4.rtcore.telemetry.batch.v1` | None |
| `rosie/rt-core.<host>.telemetry.v1` | `rt-natspublisher` | Archives | Binary `TelemetryBatch` | None |
| `robot/v4/robot.<cell>.command` | Controllers | Cartesian motion server, dense trajectory daemon | JSON `robot.v4.robot-command.v1` | Leader lease commands and motion commands |
| NATS reply subject of each command | Motion servers | The sender | JSON `robot.v4.command-reply.v1` | None |
| `robot/v4/motion-server.<peer>.status` | Cartesian motion server | Controllers, displays | JSON `robot_v4_motion_server_cartesian_live_status_v1` | None |
| `robot/v4/motion-server.<peer>.plan` | Cartesian motion server (self-test plan paths) | — | JSON | None |
| `rt_core.status_subject`, default `<command subject>.status` | Dense trajectory daemon | Controllers | JSON executor status | None |
| `--status-publish-subject` | Dense trajectory daemon | Controllers | JSON ingest and executor status | None |

The exact command and status subjects come from command-line flags or from a deployment manifest (`robot_command_subject` and each node's `cold_path_subjects`). The Cartesian motion server only accepts subjects that name its concrete peer: `robot/v4/motion-server.<peer>.…` or `robot/v4/robot.<peer>.…`. The dev stack uses `robot/v4/robot.dev-cell.command` and `robot/v4/robot.dev-cell.status` for the dense daemon.

## Observation: rt-natspublisher

`rt-natspublisher` reads the public `rt-control` socket, never the core's private IPC, and holds no grant.

```bash
rt-natspublisher --socket /run/rosie-rt-core/public/control.sock \
  --nats-url nats://127.0.0.1:4222 --host cell-a --cadence 500ms
```

| Flag | Environment | Default | Description |
|---|---|---|---|
| `--socket PATH` | — | `/run/rosie-rt-core/public/control.sock` | The public `rt-control` socket |
| `--nats-url URL` | `ROSIE_RT_NATS_URL` | none | `nats://host:port`. Credentials, TLS and paths in the URL are refused. |
| `--host NAME` | `ROSIE_RT_NATS_HOST` | none | The deployment's identity in the subject. Must be concrete: not empty, not `unknown`, no `@ / ? # = , \ " ' * >` or whitespace. |
| `--cadence D` | `ROSIE_RT_NATS_CADENCE` | `500ms` | Status and telemetry-batch interval, whole milliseconds, 1 ms to 5 s |
| `--telemetry-cadence D` | `ROSIE_RT_NATS_TELEMETRY_CADENCE` | the status cadence | Binary telemetry polling interval |
| `--telemetry-max-bytes N` | `ROSIE_RT_NATS_TELEMETRY_MAX_BYTES` | `536870912` (512 MiB) | Byte retention of the binary telemetry stream |
| `--print-env` | — | — | Validate the settings and print them as a systemd environment file |

The installed unit `rosie-rt-natspublisher.service` reads `/etc/rosie-rt-core/natspublisher.env`.

Before publishing, the publisher subscribes to its own status subject for one cadence. If another publisher is already live on that identity, it refuses to start (`another publisher is active on the deployment status subject`).

### Streams

The publisher creates or checks both JetStream streams on connect:

| Stream | Subjects | Retention | Limits |
|---|---|---|---|
| `ROBOT_V4_RTCORE` | `robot/v4/rtcore.>` | limits, file storage, discard old | 250,000 messages, 256 MiB, 10 minutes |
| `ROSIE_RT_CORE_TELEMETRY` | `rosie/rt-core.*.telemetry.v1` | limits, file storage, discard old | no message or age limit; `--telemetry-max-bytes` |

At a 1 kHz cycle, one 100 s telemetry ring window is about 539 MB, so the default 512 MiB cap can evict records before a full window is kept.

### Status payload

```json
{
  "schema": "robot.v4.rtcore.status.v1",
  "stream": "ROBOT_V4_RTCORE",
  "subject": "robot/v4/rtcore.cell-a.status",
  "host": "cell-a",
  "component": "rtcore",
  "published_at": "2026-09-29T12:00:00.000Z",
  "publisher_state": "publishing", "last_error": "",
  "reconnects": 0, "drops": 0, "dropped": 0,
  "status": {
    "name": "robot-v4-rtcore", "state": "running",
    "rtcore_mode": "sim", "simulation_mode": true, "ethercat_live": false,
    "servos_armed_requested": false, "home_valid_axis_mask": 511,
    "axis_enable_mask": 0, "configured_axis_count": 9,
    "safe_min_rad": [ … ], "safe_max_rad": [ … ],
    "rt_core": { "core": { … }, "motion": { … }, "execution": { … }, "axes": [ … ],
                 "configuration_sha256": "…", "machine_sha256": "…" }
  }
}
```

`status` is `null` until the publisher has a valid core sample. `rt_core` holds the public `rt-control` status and the deployment digests; the flat fields beside it keep the shape an earlier consumer expected, and fields that `rt-control` cannot supply are `null`. For the meaning of the `rt_core` fields, see the [rt-control status](https://advancedmetalresearch.com/docs/apis/rt-control-http#status).

The telemetry batch message (`robot.v4.rtcore.telemetry.batch.v1`) carries one sampled record per cadence in `records`, with `sequence`, `source_monotonic_ms` and `interval_ms`. For every cycle, use the binary stream instead.

### Binary telemetry

`rosie/rt-core.<host>.telemetry.v1` carries `rt-control`'s binary `TelemetryBatch` exactly: a 312-byte header and 5,392-byte cycle records, split to fit the server's `max_payload`. The publisher keeps its own cursor and advances it only past acknowledged records. If `rt-control` or the core restarts, it stops with an error rather than join two incarnations. See [Events and telemetry streams](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry).

## Commands: the robot command subject

Both motion servers subscribe to the cell's robot command subject and share its envelope, `robot.v4.robot-command.v1`, with `schema`, `command`, `robot`, `command_id` and `sender_id`. Each ignores the commands the other owns. Their lease fields differ:

| | Cartesian motion server | Dense trajectory daemon |
|---|---|---|
| Lease commands | `leader_acquire`, `leader_renew`, `leader_release`, `leader_cancel` | `leader_acquire`, `leader_renew`, `leader_release` |
| Lease request fields | `controller_boot_id`, `request_id`, `campaign_generation`, `ttl_ms`, `lease_id`, `fence_epoch` | `controller_boot_id`, `ttl_ms`, `lease_id`, `leader_fence_epoch` |
| Motion credential fields | `controller_boot_id`, `leader_lease_id`, `leader_fence_epoch` | the same, plus `motion_intent_seq` |
| Allowed senders | Roles in the manifest's `leader_controller_roles` (default `steamdeck`) | `offline-programming:<instance>` only |
| TTL | 500–2000 ms | `leader.ttl_min_ms`–`leader.ttl_max_ms` (500–2000 ms by default) |
| Commands | `arm`, `disarm`, `home`, `go_home`, `position`, `stop`, `end_run` | `play`, `pause`, `go_home`, `stop` |

`stop` needs no lease on either server. Full field tables are on each server's page: [Cartesian motion server](https://advancedmetalresearch.com/docs/apis/cartesian-motion-server#nats-commands) and [Dense trajectory daemon](https://advancedmetalresearch.com/docs/apis/dense-trajectory-daemon#nats-control-plane).

## Related pages

- [Architecture](https://advancedmetalresearch.com/docs/concepts/architecture)
- [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning)
- [Ports, sockets and environment](https://advancedmetalresearch.com/docs/reference/ports-and-environment)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `rt-core/tools/natspublisher/main.go:44-101`
- `rt-core/tools/natspublisher/publisher/publisher.go:26,44,60-110,140-160,219-224,316-385`
- `rt-core/tools/natspublisher/publisher/nats.go:21-49,188-203`
- `rt-core/tools/natspublisher/publisher/telemetry.go:16-183`
- `rt-core/tools/natspublisher/publisher/payload.go:43-175`
- `rt-core/host/rosie-rt-natspublisher.service:22-24`
- `motion-server/v1/src/robot_v4_cartesian_cli.hpp:25-126,190-208`
- `motion-server/v1/src/robot_v4_cartesian_nats_protocol.hpp:1008-1073,1152,1370-1406`
- `motion-server/v1/src/robot_v4_cartesian_leader_authority.hpp:495-530`
- `motion-server/v1/src/robot_v4_cartesian_daemon.cpp:1166-1199,3508-3545`
- `motion-server/joint-trajectory/v1/src/joint_trajectory_daemon.cpp:171-248`
- `motion-server/joint-trajectory/v1/src/command_dispatch.hpp:25-207`
- `motion-server/joint-trajectory/v1/src/leader_authority.hpp:24-89`
- `dev-stack.sh:282`
