# Safety audit log and reject codes

> How the safety audit records state transitions, faults, rejects, and control events, how entries are attributed to a session, and what each reject code means.

The safety audit is the attributable record of every safety-relevant
transition and every rejected command across the robotics fleet. The
Admin → Audit log screen reads it and renders it as a flat, reverse-chronological
table plus a reference card for the reject codes.

This page describes the record that *this* screen shows: the four-kind
taxonomy, the severity tinting, session attribution, and the reject code
table. It is a different record from the fleet console's administrative
audit (actor / action / target robot / verdict), which tracks who changed
what in the console. Do not read one expecting the other.

## What gets written to the safety audit

The screen loads the whole visible record with a single call:

```ts
const EVENTS = useApi(() => api.audit(), []);
```

`api.audit()` returns a list of audit entries. The dependency array is
empty, so the screen fetches once on mount: what you see is a snapshot
taken when you opened the screen, not a live tail. Reload the screen to
pick up entries written since.

Each entry carries the fields the table renders:

| Field      | Rendered as                            | Notes                                                                 |
| ---------- | -------------------------------------- | --------------------------------------------------------------------- |
| `t`        | **When** — `relTime(e.t)` + `" ago"`   | Shown as an age, not an absolute timestamp.                           |
| `kind`     | **Kind** — a colour-coded chip         | One of `state`, `fault`, `reject`, `control`.                          |
| `session`  | **Session** — monospace, or `—`        | Optional. `—` means the entry is not attributed to a session.         |
| `label`    | **Event** — monospace, severity-tinted | The specific event name. For `reject` entries this is the reject code. |
| `severity` | (tints `label`)                        | `bad`, `warn`, or anything else → default ink.                        |
| `detail`   | **Detail** — caption, or `—`           | Optional free-text context for the entry.                             |

`kind` and `severity` are independent axes. `kind` says what class of
thing happened; `severity` says how loudly the console should say it.

## The four kinds: state, fault, reject, control

The header of the screen lists the four kinds, and the **Kind** column
renders each entry's kind as a chip whose text and border take the kind's
colour:

| Kind      | Chip colour       | What the entry records                                                                                     |
| --------- | ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `state`   | accent (`--acc`)  | A safety-relevant state transition. The ordinary, expected traffic in the log.                              |
| `fault`   | warning (`--warn`)| A fault raised by the robot or by the stack.                                                                |
| `reject`  | muted (`--mut`)   | A command the safety layer refused to execute. `label` is the reject code; see [Reject code reference](#reject-code-reference). |
| `control` | alert (`--bad`)   | A control event — rendered in the same colour the table uses for `severity: 'bad'`, the strongest tint on the screen. |

Note that `reject` is deliberately the *quietest* chip. A reject means the
safety layer did its job and stopped something: it is a normal, healthy
outcome, and a wall of muted reject rows is not an incident by itself.
`control` is the loudest chip, because a control event changes who or what
is driving.

## Severity and how an entry is attributed to a session

Severity is applied to the **Event** column only:

- `severity === 'bad'` → the label is drawn in `--bad`.
- `severity === 'warn'` → the label is drawn in `--warn`.
- anything else (including absent) → default ink.

So a row can be a `state` entry with a `bad` label, or a `fault` entry with
an untinted one. Scan the Kind column to classify, scan the Event column's
colour to triage.

Attribution works through the `session` field:

- **Entry has a session id.** The entry belongs to that robotics session.
  The id is rendered monospace so it can be copied and matched against the
  session elsewhere in the console.
- **Entry shows `—`.** The entry has no session attached. Fleet- or
  device-level events that did not happen inside a session land here, and
  they cannot be traced to a session from this screen.

There is no separate "actor" column on this screen. The unit of
attribution here is the session, not the human.

## Reject code reference

Below the table, the screen renders a reference grid built from
`REJECT_HINT` in the robotics domain module. `REJECT_HINT` maps each reject
code to a one-line hint. The grid renders **every key except `NONE`**, with
the code in a fixed-width monospace column and the hint wrapping beside it.

Two consequences worth knowing:

- **`NONE` is a sentinel, not a rejection.** It is the value the domain
  model uses for "no reject", which is why the reference grid filters it
  out. If you see `NONE` in an API payload, nothing was refused.
- **The grid is the authoritative list.** The set of reject codes comes
  from the domain table that the console ships, so the screen always shows
  the current set and the current hint text. Read the hint next to the code
  rather than relying on a copy of the list elsewhere.

The codes are `SCREAMING_SNAKE_CASE` and name the condition that blocked
the command, not the command that was blocked. Match a `reject` row's
**Event** value against the code column in the grid to get its hint.

### PROTECTIVE_STOP_LATCHED

`PROTECTIVE_STOP_LATCHED` is the code you meet first, and it is worth
reading carefully because of the word *latched*. A latched protective stop
does not clear itself when the condition that triggered it goes away — it
stays asserted until something explicitly clears it. Every command that
arrives while the latch is held is refused, and each refusal writes its own
`reject` entry with this code.

That produces a characteristic shape in the log: one `fault` or `state`
entry marking the protective stop, then a run of muted
`PROTECTIVE_STOP_LATCHED` rejects for the same session as the controller
keeps retrying. The rejects are a symptom. The entry above them is the
incident.

Clearing the latch is not something this screen does. The audit log is a
read-only record of what happened; it has no clear, acknowledge, or reset
action.

## Reconstructing an incident from the log

The table is flat and fleet-wide, so reconstruct an incident by narrowing
along the two axes the record gives you:

1. **Anchor on the session.** Copy the session id out of the **Session**
   column and collect every row that carries it. Rows showing `—` are not
   part of that session's story.
2. **Read down to the first loud row.** Entries are shown by age, so walk
   from the newest reject back to the earliest `fault`, `control`, or
   severity-tinted `state` entry for that session. That entry, not the
   rejects that follow it, is the cause.
3. **Use the Detail column.** `detail` carries the per-entry context. A
   row with `—` in Detail has none; the `label` is all the record holds.
4. **Translate every reject.** Look each `reject` label up in the reject
   code grid. The hint tells you what condition the safety layer was
   enforcing when it refused the command.
5. **Re-open the screen to extend the window.** Because the fetch happens
   on mount, an incident still in progress will keep producing entries that
   your open table does not have.

## Immutability and read-only access

The screen labels the log `attributable · immutable`, and it is built to be
exactly that. Its only interaction with the API is the single `api.audit()`
read; it exposes no create, edit, annotate, or delete affordance for an
entry, and there is no way from this screen to change an entry's kind,
severity, session attribution, or detail text. The record is append-only as
far as the console is concerned.

That is the whole point of keeping this log separately from the
administrative audit: "every transition is logged and attributable" is a
claim ClutchCall makes about the robotics safety layer, and this is the
record that has to be able to back it up after the fact. Treat entries as
evidence, and export or copy what you need rather than expecting to curate
it in place.

This screen does not configure retention. Nothing on it sets, displays, or
bounds how long entries are kept, so do not infer a retention window from
the oldest row you happen to see — the list you get is whatever
`api.audit()` returned for your org at load time.

## Related

- [Telemetry](/platform/telemetry) — the metrics, trace, and CDR streams that sit alongside this record
- [Authentication](/concepts/authentication) — the API key scope that gates control-plane reads
