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:
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: 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: 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.
  • Telemetry — the metrics, trace, and CDR streams that sit alongside this record
  • Authentication — the API key scope that gates control-plane reads