# Teleoperation Sessions and Operator Dispatch

> The teleop session record, its safety states, and how the ACD matches a robot's distress call to a certified operator.

A teleop session binds one robot to one human operator for the duration of
a remote-control attachment. In FirstByte robotics, the session is the
unit that the Operations wallboard renders and that the admin overview rolls
up: it carries the link quality, the safety state, and the operator who is
holding the robot right now.

Two console screens read this model:

| Screen | What it shows | Reads |
| ------ | ------------- | ----- |
| Admin · overview | Fleet / operator / session rollups, plus a dispatch control that raises a distress call | `api.robots()`, `api.operators()`, `api.liveSessions()` |
| Operations · wallboard | One card per live session, alarm states first, plus the robot-namespace relay panel and the safety-event feed | `api.liveSessions()`, `api.robots()`, `api.audit()` |

## The session record

Each live session exposes the following fields. The wallboard card renders
all of them.

| Field | Meaning |
| ----- | ------- |
| `id` | Session identifier. The wallboard links to `/ops/sessions?s=<id>`. |
| `robot` | Robot id the session is attached to. Joined against the fleet registry. |
| `site` | Site the robot sits in. |
| `operatorName` | The human currently attached. |
| `region` | The operator's region — the other end of the RTT path. |
| `startedAt` | Attach time. The card shows it as an elapsed "up …". |
| `rttMs` | Round-trip time on the control path. |
| `divergenceDeg` | Commanded-versus-observed arm divergence, in degrees. |
| `state` | The safety state (see below). |
| `velocityScale` | The fraction of full commanded velocity currently allowed. |

A session appears on the wallboard once it is attached; the header's
"N attached" count is simply the number of session records returned.

## Safety states

Six states exist. The console treats them as a fixed set — each has its own
colour token and its own one-line meaning, both used consistently by the
wallboard pills and the overview's *Sessions by state* table.

| State | Meaning |
| ----- | ------- |
| `BOOT` | Edge agent starting. Not yet holding a safety posture. |
| `SAFE` | Holding position — not armed. No operator command reaches the actuators. |
| `ARMED` | Live under operator control. |
| `DEGRADED` | Velocity-scaled because the link is poor. The session is still armed, but commanded motion is being reduced. |
| `DIVERGED` | The arm is not tracking the commanded pose. |
| `PROTECTIVE_STOP` | Latched stop, awaiting clear. |

`PROTECTIVE_STOP` and `DIVERGED` are the two states the wallboard treats as
alarms: the card border and background pick up the state colour instead of
the neutral panel styling, and the state hint is coloured rather than muted.

## How the wallboard orders sessions

Cards are sorted by a fixed priority so that the sessions needing a human
look are the ones at the top of the grid:

```
PROTECTIVE_STOP → DIVERGED → DEGRADED → ARMED → SAFE → BOOT
```

The screen header also carries a live count chip for `ARMED`, `DEGRADED`,
`DIVERGED` and `PROTECTIVE_STOP`, coloured per state. States with no
sessions show `0` rather than disappearing.

## Divergence, velocity scale, and RTT

The three numbers on a session card are the ones that decide whether a
session stays armed. The card highlights them at these display thresholds:

| Metric | Highlighted when | Rendered as |
| ------ | ---------------- | ----------- |
| `rttMs` | above 320 ms | warning tone |
| `divergenceDeg` | above 8° | warning tone |
| `divergenceDeg` | above 15° | the `DIVERGED` state colour |
| `velocityScale` | below 1 | warning tone, shown as a percentage |

`velocityScale` is shown as a rounded percentage of full speed, so anything
under 100% means the session is being scaled back — which is exactly the
condition `DEGRADED` describes. Treat these as the console's display
thresholds for drawing attention, not as the control limits enforced on the
edge.

On the dispatch side, the admin overview highlights a *matched RTT* above
250 ms when it reports a successful match, because that is the number the
operator will feel as soon as they accept.

## Requesting a session from the edge agent

When a robot needs a human, the edge agent asks the platform for a session.
The admin overview's **Raise a distress call** control issues the same
`POST /v1/sessions` request the edge agent makes, so the console path and
the integration path are one code path.

The request identifies the robot. The platform derives two things from the
robot record:

- `pop` — the robot's point of presence, used for the RTT ranking.
- `skill` — the first token of the robot's required certification
  (`reqCert`), split on a middot or whitespace. A robot whose `reqCert`
  reads `pick-place · tier-2` matches on `pick-place`.

There are two outcomes:

| Response | Meaning |
| -------- | ------- |
| `201 matched` | An operator was selected and their console is ringing. The response carries the session id, the matched operator, and the matched RTT. |
| `202 queued` | No Ready operator holds the required certification. The call is enqueued and the ACD serves it when one frees. |

The offer that is pushed to the operator's console carries:

| Field | Meaning |
| ----- | ------- |
| `sessionId` | The session the operator will attach to on accept. |
| `robotId`, `robotModel`, `site` | Which robot is calling, and from where. |
| `rttMs` | The RTT the ACD measured for this operator to this robot's PoP. |
| `operatorId` | The operator being rung. |
| `offeredAt` | When the offer was raised. |
| `expiresAt` | `offeredAt` plus 30 seconds. |

## How the ACD picks an operator

The dispatch selector is `claim_lowest_rtt`. It takes the robot's PoP and
required skill, plus a pool of operator presences, and returns the
lowest-RTT operator who is certified for that skill — or nothing, in which
case the call is queued.

The pool is assembled in this order:

1. **Live readiness.** Operators who have broadcast readiness from the
   operator *Ready* screen — in any tab — over the ring channel. A
   live-Ready operator is always preferred.
2. **Roster fallback.** If no operator has broadcast readiness, the
   directory roster is used instead: operators whose presence is `ready` or
   `attached`, mapped into presences whose certification list is the set of
   their certification skills. This exists so a freshly opened console can
   still dispatch.

Each presence carries `operatorId`, `name`, `available`, `region`, the
operator's `certs` as skill names, and `since`.

## Certifications and skill matching

Matching is exact on the skill token: the ACD only considers operators
whose certification list contains the skill derived from the robot's
`reqCert`. Certification is a hard filter, applied before the RTT ranking —
a nearer operator without the certification is never selected over a
farther certified one. If the filter empties the pool, the result is
`202 queued`, and the response names the skill that went unserved.

## Queued calls and offer expiry

A matched call is an *offer*, not an attachment. The offer is pushed over
the ring channel to the named operator's console, which rings. The operator
accepts and lands in the cockpit on that session id.

The offer window is 30 seconds: `expiresAt` is set to 30 seconds after
`offeredAt` at the moment the offer is raised.

A queued call (`202`) holds the robot's skill requirement and waits. The
ACD serves it when a certified operator frees up.

## Protective stop handling

`PROTECTIVE_STOP` is latched — the session stays in it until it is cleared,
and the wallboard says so in the card hint ("latched, awaiting clear").
Because it sorts first in the card priority, a latched session is always at
the top of the grid.

Neither the wallboard nor the admin overview exposes a clear action. Both
screens are read-only with respect to safety state: they report it, colour
it, and route you to the session detail view. Operator-initiated stops also
land in the wallboard's **Safety events** feed, which streams the audit
records for the org.

## When session state and registry status disagree

The session record and the fleet registry are two different sources. The
session says an operator is attached; the registry says whether the edge
agent is heartbeating. The wallboard joins them per robot and, when the
registry reports anything other than `online`, stamps a warning chip on the
card reading `robot <status>`.

That chip means the session record may be stale. Do not read the card's
RTT, divergence or velocity numbers as a live link until the robot
heartbeats again.

## Session state versus the relay data plane

"Attached" is a control-plane fact. Whether teleop frames are actually
moving is a data-plane fact, and the wallboard shows it separately: a relay
panel for the `robot` namespace, scoped to the current org.

These two come apart exactly when it matters. A held session that has
stopped delivering frames looks identical to a healthy one on the session
cards alone — the relay panel is what distinguishes them.

## Link health floors

The wallboard's **Link health** table lists the propagation floor for each
operator-to-robot path in use, alongside how many live sessions currently
ride it:

| Path | Floor | Note |
| ---- | ----- | ---- |
| BLR → US-East (Virginia) | 275 ms | QUIC buys jitter and recovery, not propagation |
| BLR → US-West (Oregon) | 238 ms | — |
| Manchester → UK (London) | 177 ms | Shortest glass reach |

The floor is the distance, not the stack. No transport change moves it,
which is why the ACD ranks on RTT and why operator region is part of the
session record.

## What the admin overview counts

The four headline metrics on the admin overview are rollups over the same
live data the wallboard renders:

| Metric | Definition |
| ------ | ---------- |
| Live sessions | Number of session records — sessions attached now. |
| Robots online | Robots with registry status `online`, over the total fleet. |
| Operators available | Operators whose presence is `ready` or `attached`. |
| E-stops · 24h | Operator-initiated stops in the trailing day. |

The *Sessions by state* table below them counts each state from that same
live roster, so its rows always sum to the "Live sessions" headline.

## Read the glass-to-glass hop split

The cockpit shows how long a camera frame takes to go from the robot
camera to the operator screen. It also shows where that time goes.

| Hop | Measured from | Clock |
| --- | ------------- | ----- |
| uplink | Camera capture to the first relay | Two machines |
| relay | Relay entry to relay exit, for each relay | Two machines |
| downlink | The last relay to the browser | Two machines |
| decode | Frame complete to image ready | One machine |
| draw | Image ready to drawn on the canvas | One machine |

The uplink, relay and downlink figures are a difference between the
clocks of two different machines. If the clocks do not agree, the
difference includes that error. The cockpit marks these hops, and the
stored rows keep a `skewed` flag for the same reason.

The decode and draw figures come from one clock. They are exact.

> **WARNING:**
> Do not plan a network from a marked hop alone. A marked hop gives you a
> fact plus an unknown offset. To compare two links, compare the same hop
> on both, or use the decode and draw figures, which carry no offset.

## When the hop split is not available

The relay writes its entry and exit times into each frame. If the relay
does not write them, the cockpit cannot divide the network time, and it
shows one `link` segment for the whole network.

In the stored rows this shows as `relay_count = 0`. Then:

- `network_ms` holds the whole network time. It is always correct.
- `uplink_ms`, `relay_ms` and `downlink_ms` are `0`. They are **not**
  measurements. Do not read them as "the uplink is instant".
- `decode_ms` and `draw_ms` are still correct. The browser measures them.

A value of `relay_count` of 1 or more means the split is real.

## Where the numbers are kept

The operator browser is the only place that can see the whole chain. A
relay knows when it received and sent a frame, but decode and draw
happen after the last relay sends the frame, so no server can measure
them.

The cockpit collects one row each second and sends the rows in groups.
The rows go to `robot_g2g_sample`, and a one-minute summary goes to
`robot_g2g_1m_q`.

The rows keep percentiles, not averages. One late frame in a good second
moves an average a long way, and that makes a good link look bad. The
p50 tells you what the link usually does. The p95 tells you what the
worst frames do, which is what an operator feels.

> **NOTE:**
> Telemetry never delays the session. If the platform does not accept the
> rows, the cockpit keeps a small number of them and tries again with the
> next group. It does not retry on a timer, and it drops the oldest rows
> first. A session continues at full quality when its telemetry fails.

## Related

- [Modalities overview](/modalities/overview)
- [Authentication](/concepts/authentication) — relay tokens and namespace scope
- [Telemetry](/platform/telemetry) — metrics, traces, and record streams
