@fleetless/contracts 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +68 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +1 -1
  7. package/artifacts/routes.json +1 -1
  8. package/dist/alerts.d.ts +19 -24
  9. package/dist/alerts.js +18 -24
  10. package/dist/app-users.d.ts +7 -6
  11. package/dist/app-users.js +6 -6
  12. package/dist/apps.d.ts +21 -25
  13. package/dist/apps.js +40 -51
  14. package/dist/assets.d.ts +70 -132
  15. package/dist/assets.js +130 -223
  16. package/dist/audit.d.ts +11 -11
  17. package/dist/audit.js +25 -51
  18. package/dist/client-auth.d.ts +4 -4
  19. package/dist/client-auth.js +3 -4
  20. package/dist/common.d.ts +27 -35
  21. package/dist/common.js +26 -35
  22. package/dist/config-issues.d.ts +4 -3
  23. package/dist/config-issues.js +7 -6
  24. package/dist/config.d.ts +31 -37
  25. package/dist/config.js +81 -110
  26. package/dist/errors.d.ts +4 -3
  27. package/dist/errors.js +61 -87
  28. package/dist/identity.d.ts +18 -21
  29. package/dist/identity.js +17 -21
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +12 -13
  32. package/dist/introspection.d.ts +7 -6
  33. package/dist/introspection.js +6 -6
  34. package/dist/jobs.d.ts +12 -12
  35. package/dist/jobs.js +20 -25
  36. package/dist/mcp.d.ts +11 -12
  37. package/dist/mcp.js +10 -12
  38. package/dist/oauth.d.ts +13 -18
  39. package/dist/oauth.js +13 -19
  40. package/dist/protocol.d.ts +51 -62
  41. package/dist/protocol.js +107 -139
  42. package/dist/realtime.d.ts +53 -68
  43. package/dist/realtime.js +77 -103
  44. package/dist/rest.d.ts +182 -243
  45. package/dist/rest.js +301 -395
  46. package/dist/routes.d.ts +4 -3
  47. package/dist/routes.js +3 -2
  48. package/package.json +12 -7
@@ -1,11 +1,11 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
- * Client realtime protocol (spec §11.1): WebSocket subscriptions on
4
- * datapoints. W1 scope: subscribe/unsubscribe plus the datapoint event
5
- * stream; command parity arrives in W4.
4
+ * Client realtime protocol: WebSocket subscriptions on datapoints, the
5
+ * datapoint event stream, and full command parity with REST.
6
6
  */
7
7
  /**
8
- * The first frame a client sends after the socket opens (W3, spec §3.4).
8
+ * The first frame a client sends after the socket opens.
9
9
  *
10
10
  * A browser cannot set an `Authorization` header on a WebSocket handshake,
11
11
  * and a token in the query string would outlive the request in server,
@@ -52,7 +52,7 @@ export declare const authError: z.ZodObject<{
52
52
  }, z.core.$strip>;
53
53
  export type AuthError = z.infer<typeof authError>;
54
54
  /**
55
- * Command parity (spec §11.1): everything REST can do — invoke an action,
55
+ * Command parity: everything REST can do — invoke an action,
56
56
  * call a service, publish, cancel — also travels over this socket.
57
57
  *
58
58
  * **Every command carries a `request_id` and every reply echoes it.** A
@@ -103,11 +103,10 @@ export type ClientPublish = z.infer<typeof clientPublish>;
103
103
  * a command has reached the bridge, deliberately, so that the next command —
104
104
  * a stop, say — is never held up behind bookkeeping. Work that follows the
105
105
  * send therefore runs unordered: a publish that *acquires* a slug writes an
106
- * audit record before answering, while an immediately following publish by
107
- * the now-current holder has nothing to write and answers at once. Its reply
108
- * overtakes. Measured in W5: exactly one reversal in fifty-six zero-gap
109
- * bursts, which is the signature of that cause — it can happen only once per
110
- * identity and slug — and not of a race.
106
+ * audit record before answering, while an immediately following publish by the
107
+ * now-current holder has nothing to write and answers at once. Its reply
108
+ * overtakes. That reversal happens at most once per identity and slug, which
109
+ * is what distinguishes it from a race.
111
110
  *
112
111
  * Serialising the replies would mean putting that bookkeeping in front of
113
112
  * every following command, including the stop. The ordering that matters is
@@ -164,7 +163,7 @@ export declare const errorFrame: z.ZodObject<{
164
163
  }, z.core.$strip>;
165
164
  export type ErrorFrame = z.infer<typeof errorFrame>;
166
165
  /**
167
- * Subscribe to a slug's stream (spec §11.3: **state is observed by slug**).
166
+ * Subscribe to a slug's stream — **state is observed by slug**.
168
167
  *
169
168
  * Which kinds are subscribable, and why it is not a matter of taste:
170
169
  *
@@ -203,7 +202,7 @@ export declare const clientUnsubscribe: z.ZodObject<{
203
202
  export type ClientUnsubscribe = z.infer<typeof clientUnsubscribe>;
204
203
  /**
205
204
  * Refusal of a subscribe, addressed by the (robot_id, slug) it refers to.
206
- * Codes follow the §11.5 error culture: stable code + human message.
205
+ * Codes follow the same error culture as REST: stable code plus human message.
207
206
  * robot_id/slug are plain strings ECHOING what the client sent — the frame
208
207
  * must be constructible precisely when those values are malformed, so that
209
208
  * a bad robot_id or slug gets a diagnosis instead of a dead socket.
@@ -230,7 +229,7 @@ export declare const datapointEvent: z.ZodObject<{
230
229
  }, z.core.$strip>;
231
230
  export type DatapointEvent = z.infer<typeof datapointEvent>;
232
231
  /**
233
- * A change in the health of something the developer configured (W6a).
232
+ * A change in the health of something the developer configured.
234
233
  *
235
234
  * The push half of `resourceHealthState`; the REST list is the snapshot half,
236
235
  * and neither is useful alone — a page that loads after the change would see
@@ -241,19 +240,17 @@ export type DatapointEvent = z.infer<typeof datapointEvent>;
241
240
  * looking at the thing that broke.
242
241
  */
243
242
  /**
244
- * Why a live camera session ended (W9a).
243
+ /**
244
+ * Why a live camera session ended.
245
245
  *
246
- * **The reason travels WITH the ending, and that is the whole point of this
247
- * enum existing rather than a state somebody reads afterwards.** W6a put a
248
- * `cause` on the wire, the console named the real reason, and the lead
249
- * observed the gate step and closed it — and the review then found it still
250
- * could not tell, for a different reason: `stopped_by_config_change` is
251
- * **sticky**, nothing moves a camera out of it, and `LiveCameraRow` read that
252
- * *current* state at the moment a stream ended. A config change at 10:00 and
253
- * an unrelated release at 10:30 therefore reported the same cause (DEF-070).
246
+ * **The reason travels WITH the ending, rather than being read afterwards from
247
+ * a state.** Some of these states are sticky — nothing moves a camera out of
248
+ * `stopped_by_config_change` — so a client that reads the current state at the
249
+ * moment a stream ends reports a configuration change from hours ago as the
250
+ * cause of an unrelated ending.
254
251
  *
255
- * A state read after the fact answers "what is true now". A viewer needs
256
- * "what happened to my session", and only an event carries that.
252
+ * A state read after the fact answers "what is true now". A viewer needs "what
253
+ * happened to my session", and only an event carries that.
257
254
  */
258
255
  export declare const liveSessionEndReason: z.ZodEnum<{
259
256
  unknown: "unknown";
@@ -267,21 +264,18 @@ export declare const liveSessionEndReason: z.ZodEnum<{
267
264
  }>;
268
265
  export type LiveSessionEndReason = z.infer<typeof liveSessionEndReason>;
269
266
  /**
270
- * A live camera session ended, told to the **client that holds it** (W9a).
267
+ /**
268
+ * A live camera session ended, told to the **client that holds it**.
271
269
  *
272
- * This is the channel `DEF-051`, `DEF-052`, `DEF-053` and `DEF-070` each
273
- * described from a different direction across four waves. Until now the only
274
- * vehicle was `camera_state`, which the cloud stores in `publishState` and
275
- * reads in exactly one place — refusing a *later* joiner — so reporting a
276
- * failure would have written to a dead end.
270
+ * Without this frame the only vehicle is `camera_state`, which the cloud stores
271
+ * and reads in exactly one place — refusing a *later* joiner — so a failure
272
+ * reported through it is written to a dead end.
277
273
  *
278
274
  * Unlike `resourceHealthEvent`, which is developer-only and org-scoped, this
279
- * one is addressed to the **holder of the session**: it names `session_id`
280
- * (W6b gave `liveSessionResponse` one precisely so a session could be
281
- * addressed) and is delivered only to the identity that session was minted
282
- * for. A developer watching the same robot learns about the *resource* health;
283
- * the viewer learns about *their own session*. Two questions, two channels,
284
- * on purpose.
275
+ * one is addressed to the **holder of the session**: it names `session_id` and
276
+ * is delivered only to the identity that session was minted for. A developer
277
+ * watching the same robot learns about the *resource* health; the viewer learns
278
+ * about *their own session*. Two questions, two channels, on purpose.
285
279
  */
286
280
  export declare const liveSessionEvent: z.ZodObject<{
287
281
  type: z.ZodLiteral<"live_session">;
@@ -304,16 +298,15 @@ export declare const liveSessionEvent: z.ZodObject<{
304
298
  }, z.core.$strip>;
305
299
  export type LiveSessionEvent = z.infer<typeof liveSessionEvent>;
306
300
  /**
307
- * A resource's health entry was **withdrawn** (W9a, DEF-071).
301
+ * A resource's health entry was **withdrawn**.
308
302
  *
309
- * The store's `invalidate()` deliberately emitted nothing, reasoning that
310
- * "withdrawing a claim nobody can currently stand behind is not new
311
- * information — the next `GET` already reflects it." That holds for a page
312
- * that loads later. **It is false for a page that is already open, because
313
- * there is no next `GET`:** `ensureSnapshot()` runs on `acquire` and nowhere
314
- * else, there is no interval, and the event handler only ever *writes* keys.
315
- * A camera retargeted to a source that never reports — which is the case the
316
- * clearing exists for — leaves an open tab showing the old value indefinitely.
303
+ * Withdrawing a claim nobody can currently stand behind looks like it needs no
304
+ * event: the next `GET` already reflects it. That holds for a page that loads
305
+ * later. **It is false for a page that is already open, because there is no
306
+ * next `GET`** — a client fetches the snapshot once and then only ever writes
307
+ * keys the event stream gives it. A camera retargeted to a source that never
308
+ * reports, which is the case the clearing exists for, would leave an open tab
309
+ * showing the old value indefinitely.
317
310
  *
318
311
  * **A separate event type rather than a nullable `state` on the existing
319
312
  * one**, so a consumer's `switch` has to name it. A nullable field invites
@@ -360,17 +353,14 @@ export declare const resourceHealthEvent: z.ZodObject<{
360
353
  }, z.core.$strip>;
361
354
  export type ResourceHealthEvent = z.infer<typeof resourceHealthEvent>;
362
355
  /**
363
- * One line of the developer console's activity panel (spec
364
- * `2026-08-20-org-event-stream`).
356
+ * One line of the developer console's activity panel.
365
357
  *
366
358
  * **This is an activity log for humans, not a complete feed.** It is throttled
367
- * and sampled. `orgEventDropped` still reports a drop on the wire, but since
368
- * FL-001 **no Fleetless surface renders it** — the console's gap banner was
369
- * removed on request, and nothing replaced it. A reader of this stream
370
- * therefore cannot tell a complete window from a sampled one, and this
371
- * comment says so rather than implying a notice that exists only in the
372
- * protocol. Anything that needs completeness reads the audit log or the
373
- * job-run history, both durable, both 90 days.
359
+ * and sampled. `orgEventDropped` reports a drop on the wire, but no Fleetless
360
+ * surface renders it, so a reader of this stream cannot tell a complete window
361
+ * from a sampled one unless their own client shows the drop. Anything that
362
+ * needs completeness reads the audit log or the job-run history, both durable,
363
+ * both 90 days.
374
364
  */
375
365
  export declare const ORG_EVENT_SAMPLE_INTERVAL_MS = 1000;
376
366
  /** The backstop above the per-slug cap: a fleet larger than the panel could serve anyway. */
@@ -382,21 +372,16 @@ export declare const ORG_EVENT_BUFFER_IDLE_MS = 3600000;
382
372
  /** A log line, not a payload: a datapoint value is `unknown` and a LaserScan is megabytes. */
383
373
  export declare const ORG_EVENT_DETAIL_MAX_BYTES = 4096;
384
374
  /**
385
- * `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`, spec
386
- * `2026-08-28-alerts-and-datapoint-modal-design` D2). A firing event carries
387
- * the alert's own `severity`; a resolved event is always `info` — resolving
388
- * is good news regardless of how bad the firing was.
375
+ * `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`). A firing
376
+ * event carries the alert's own `severity`; a resolved event is always `info` —
377
+ * resolving is good news regardless of how bad the firing was.
389
378
  *
390
- * `'datapoint'` — since FL-001, **no producer emits this kind**: the
391
- * datapoint producer was made a deliberate no-op (Task 5/6, this stream's own
392
- * per-slug sampling made it redundant with what the datapoint history route
393
- * already serves). The member stays in the enum rather than being removed,
394
- * because a reader may still hold a pre-deploy frame of this kind sitting in
395
- * a buffer (a reconnect replay, a client that hasn't refreshed) and must be
396
- * able to parse it rather than fail closed on an old, valid value. Same shape
397
- * as the correction on `ORG_EVENT_SAMPLE_INTERVAL_MS`'s comment just above:
398
- * name what the wire no longer does instead of leaving a value the cloud can
399
- * never send undocumented.
379
+ * `'datapoint'` — **no producer emits this kind**: per-slug sampling on this
380
+ * stream made it redundant with what the datapoint history route already
381
+ * serves. The member stays in the enum rather than being removed, because a
382
+ * reader may still hold an older frame of this kind in a buffer (a reconnect
383
+ * replay, a client that has not refreshed) and must be able to parse it rather
384
+ * than fail closed on an old, valid value.
400
385
  */
401
386
  export declare const orgEventKind: z.ZodEnum<{
402
387
  datapoint: "datapoint";
package/dist/realtime.js CHANGED
@@ -6,12 +6,11 @@ import { slug } from './common.js';
6
6
  import { clientIdentity } from './client-auth.js';
7
7
  import { job } from './jobs.js';
8
8
  /**
9
- * Client realtime protocol (spec §11.1): WebSocket subscriptions on
10
- * datapoints. W1 scope: subscribe/unsubscribe plus the datapoint event
11
- * stream; command parity arrives in W4.
9
+ * Client realtime protocol: WebSocket subscriptions on datapoints, the
10
+ * datapoint event stream, and full command parity with REST.
12
11
  */
13
12
  /**
14
- * The first frame a client sends after the socket opens (W3, spec §3.4).
13
+ * The first frame a client sends after the socket opens.
15
14
  *
16
15
  * A browser cannot set an `Authorization` header on a WebSocket handshake,
17
16
  * and a token in the query string would outlive the request in server,
@@ -43,7 +42,7 @@ export const authError = z.object({
43
42
  message: z.string().min(1),
44
43
  });
45
44
  /**
46
- * Command parity (spec §11.1): everything REST can do — invoke an action,
45
+ * Command parity: everything REST can do — invoke an action,
47
46
  * call a service, publish, cancel — also travels over this socket.
48
47
  *
49
48
  * **Every command carries a `request_id` and every reply echoes it.** A
@@ -58,20 +57,16 @@ export const clientInvoke = z.object({
58
57
  request_id: z.string().min(1).max(64),
59
58
  robot_id: z.uuid(),
60
59
  slug,
61
- /** Parameters by field path, validated against the config's rules (§4.4). */
60
+ /** Parameters by field path, validated against the configuration's rules. */
62
61
  params: z.record(z.string(), z.unknown()),
63
62
  /**
64
- * How long this one call is worth waiting for (W6b) — the same field,
65
- * meaning and cap as `invokeRequest.patience_ms`; absent means
66
- * `DEFAULT_PATIENCE_MS`.
63
+ * How long this one call is worth waiting for — the same field, meaning and
64
+ * cap as `invokeRequest.patience_ms`; absent means `DEFAULT_PATIENCE_MS`.
67
65
  *
68
- * It is here because **§11.1 parity is a rule, not a preference**: what REST
69
- * can do travels over this socket. The first version of this delta gave
70
- * `patience_ms` to the REST body only — and the SDK invokes exclusively over
71
- * the realtime channel, so the field would have been unreachable for every
72
- * SDK caller while appearing in the documentation. W6a shipped four SDK
73
- * methods no SDK caller could invoke; this is the same defect caught before
74
- * it shipped, by the SDK owner rather than by a reviewer.
66
+ * It is here because **parity is a rule, not a preference**: what REST can do
67
+ * travels over this socket. A field given to the REST body alone would be
68
+ * unreachable to every caller that invokes over the realtime channel, while
69
+ * still appearing in the documentation.
75
70
  */
76
71
  patience_ms: z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional(),
77
72
  });
@@ -79,17 +74,17 @@ export const clientCancel = z.object({
79
74
  type: z.literal('cancel'),
80
75
  request_id: z.string().min(1).max(64),
81
76
  robot_id: z.uuid(),
82
- /** Which slug — required, and the only address a cancel had until W6b. */
77
+ /** Which slug — required, and the coarse address of a cancel. */
83
78
  slug,
84
79
  /**
85
- * Which job on that slug (W6b), or `null` for *whatever is running there*.
80
+ * Which job on that slug, or `null` for *whatever is running there*.
86
81
  *
87
82
  * The two are different requests and both are legitimate. An operator
88
83
  * hitting a stop button means the second: stop the machine, whatever it is
89
84
  * doing. A client cancelling the job it started means the first — and until
90
- * this field existed it could not say so, so a cancel that arrived just
91
- * after its own job ended stopped the next caller's job instead. Same slug,
92
- * same wire frame, entirely different machine behaviour, and nothing in the
85
+ * this field a client could not say so, and a cancel arriving just after its
86
+ * own job ended would stop the next caller's job instead. Same slug, same
87
+ * wire frame, entirely different machine behaviour, and nothing in the
93
88
  * protocol able to tell them apart.
94
89
  *
95
90
  * A named id that is not running answers `not_found` rather than falling
@@ -121,11 +116,10 @@ export const clientPublish = z.object({
121
116
  * a command has reached the bridge, deliberately, so that the next command —
122
117
  * a stop, say — is never held up behind bookkeeping. Work that follows the
123
118
  * send therefore runs unordered: a publish that *acquires* a slug writes an
124
- * audit record before answering, while an immediately following publish by
125
- * the now-current holder has nothing to write and answers at once. Its reply
126
- * overtakes. Measured in W5: exactly one reversal in fifty-six zero-gap
127
- * bursts, which is the signature of that cause — it can happen only once per
128
- * identity and slug — and not of a race.
119
+ * audit record before answering, while an immediately following publish by the
120
+ * now-current holder has nothing to write and answers at once. Its reply
121
+ * overtakes. That reversal happens at most once per identity and slug, which
122
+ * is what distinguishes it from a race.
129
123
  *
130
124
  * Serialising the replies would mean putting that bookkeeping in front of
131
125
  * every following command, including the stop. The ordering that matters is
@@ -141,9 +135,9 @@ export const commandResult = z.object({
141
135
  * - `ok:true` on an invoke or a call: the job that was just created.
142
136
  * - `ok:true` on a cancel: the job the cancel was sent to.
143
137
  * - `ok:false, code:'busy'`: **the job that is already running** — the
144
- * caller has none. This is the §11.3 "inkl. Information, was läuft", and
145
- * it is the whole reason a busy refusal is useful: the caller learns
146
- * whether to wait or to give up (see `busyDetails`).
138
+ * caller has none. Naming what is already running is the whole reason a
139
+ * busy refusal is useful: the caller learns whether to wait or to give up
140
+ * (see `busyDetails`).
147
141
  * - any other refusal: `null`.
148
142
  */
149
143
  job: job.nullable(),
@@ -165,12 +159,11 @@ export const commandResult = z.object({
165
159
  * The same payload the REST envelope carries in `apiError.details` — for
166
160
  * `parameter_invalid`, a `parameterInvalidDetails`.
167
161
  *
168
- * Added because it was missing, and its absence quietly broke §11.1: this
169
- * socket is supposed to do *everything* REST can do, but a
170
- * `parameter_invalid` arriving here had nowhere to put its violations, so
171
- * the same refusal was actionable over HTTP and opaque over the socket.
172
- * A client cannot bind an error to the input that caused it from a code
173
- * alone — which is the entire point of the flat parameter shape.
162
+ * It is here because this socket does *everything* REST can do, and without
163
+ * it a `parameter_invalid` arriving here would have nowhere to put its
164
+ * violations — the same refusal actionable over HTTP and opaque over the
165
+ * socket. A client cannot bind an error to the input that caused it from a
166
+ * code alone, which is the entire point of the flat parameter shape.
174
167
  */
175
168
  details: z.unknown().optional(),
176
169
  });
@@ -187,7 +180,7 @@ export const errorFrame = z.object({
187
180
  message: z.string().min(1),
188
181
  });
189
182
  /**
190
- * Subscribe to a slug's stream (spec §11.3: **state is observed by slug**).
183
+ * Subscribe to a slug's stream — **state is observed by slug**.
191
184
  *
192
185
  * Which kinds are subscribable, and why it is not a matter of taste:
193
186
  *
@@ -209,7 +202,7 @@ export const clientSubscribe = z.object({
209
202
  robot_id: z.uuid(),
210
203
  slug,
211
204
  /**
212
- * What the subscriber expects, and how it wants it (W5).
205
+ * What the subscriber expects, and how it wants it.
213
206
  *
214
207
  * `kind` lets the server answer **`wrong_kind`** instead of accepting a
215
208
  * subscribe the client will then filter to silence — and silence is
@@ -217,8 +210,6 @@ export const clientSubscribe = z.object({
217
210
  * Optional, so an older client that omits it keeps today's behaviour.
218
211
  *
219
212
  * `options` is where a camera says what it wants; a datapoint needs none.
220
- * It exists now rather than later because adding a field to a frame three
221
- * repos parse is cheap once and expensive twice.
222
213
  */
223
214
  /**
224
215
  * `publisher` is here even though a publisher is not subscribable: a client
@@ -238,7 +229,7 @@ export const clientUnsubscribe = z.object({
238
229
  });
239
230
  /**
240
231
  * Refusal of a subscribe, addressed by the (robot_id, slug) it refers to.
241
- * Codes follow the §11.5 error culture: stable code + human message.
232
+ * Codes follow the same error culture as REST: stable code plus human message.
242
233
  * robot_id/slug are plain strings ECHOING what the client sent — the frame
243
234
  * must be constructible precisely when those values are malformed, so that
244
235
  * a bad robot_id or slug gets a diagnosis instead of a dead socket.
@@ -263,7 +254,7 @@ export const datapointEvent = z.object({
263
254
  timestamp_ms: z.number().int().nonnegative(),
264
255
  });
265
256
  /**
266
- * A change in the health of something the developer configured (W6a).
257
+ * A change in the health of something the developer configured.
267
258
  *
268
259
  * The push half of `resourceHealthState`; the REST list is the snapshot half,
269
260
  * and neither is useful alone — a page that loads after the change would see
@@ -274,19 +265,17 @@ export const datapointEvent = z.object({
274
265
  * looking at the thing that broke.
275
266
  */
276
267
  /**
277
- * Why a live camera session ended (W9a).
268
+ /**
269
+ * Why a live camera session ended.
278
270
  *
279
- * **The reason travels WITH the ending, and that is the whole point of this
280
- * enum existing rather than a state somebody reads afterwards.** W6a put a
281
- * `cause` on the wire, the console named the real reason, and the lead
282
- * observed the gate step and closed it — and the review then found it still
283
- * could not tell, for a different reason: `stopped_by_config_change` is
284
- * **sticky**, nothing moves a camera out of it, and `LiveCameraRow` read that
285
- * *current* state at the moment a stream ended. A config change at 10:00 and
286
- * an unrelated release at 10:30 therefore reported the same cause (DEF-070).
271
+ * **The reason travels WITH the ending, rather than being read afterwards from
272
+ * a state.** Some of these states are sticky — nothing moves a camera out of
273
+ * `stopped_by_config_change` — so a client that reads the current state at the
274
+ * moment a stream ends reports a configuration change from hours ago as the
275
+ * cause of an unrelated ending.
287
276
  *
288
- * A state read after the fact answers "what is true now". A viewer needs
289
- * "what happened to my session", and only an event carries that.
277
+ * A state read after the fact answers "what is true now". A viewer needs "what
278
+ * happened to my session", and only an event carries that.
290
279
  */
291
280
  export const liveSessionEndReason = z.enum([
292
281
  /** Another holder of this camera released it — another tab, or another client. */
@@ -312,21 +301,18 @@ export const liveSessionEndReason = z.enum([
312
301
  'unknown',
313
302
  ]);
314
303
  /**
315
- * A live camera session ended, told to the **client that holds it** (W9a).
304
+ /**
305
+ * A live camera session ended, told to the **client that holds it**.
316
306
  *
317
- * This is the channel `DEF-051`, `DEF-052`, `DEF-053` and `DEF-070` each
318
- * described from a different direction across four waves. Until now the only
319
- * vehicle was `camera_state`, which the cloud stores in `publishState` and
320
- * reads in exactly one place — refusing a *later* joiner — so reporting a
321
- * failure would have written to a dead end.
307
+ * Without this frame the only vehicle is `camera_state`, which the cloud stores
308
+ * and reads in exactly one place — refusing a *later* joiner — so a failure
309
+ * reported through it is written to a dead end.
322
310
  *
323
311
  * Unlike `resourceHealthEvent`, which is developer-only and org-scoped, this
324
- * one is addressed to the **holder of the session**: it names `session_id`
325
- * (W6b gave `liveSessionResponse` one precisely so a session could be
326
- * addressed) and is delivered only to the identity that session was minted
327
- * for. A developer watching the same robot learns about the *resource* health;
328
- * the viewer learns about *their own session*. Two questions, two channels,
329
- * on purpose.
312
+ * one is addressed to the **holder of the session**: it names `session_id` and
313
+ * is delivered only to the identity that session was minted for. A developer
314
+ * watching the same robot learns about the *resource* health; the viewer learns
315
+ * about *their own session*. Two questions, two channels, on purpose.
330
316
  */
331
317
  export const liveSessionEvent = z.object({
332
318
  type: z.literal('live_session'),
@@ -338,13 +324,10 @@ export const liveSessionEvent = z.object({
338
324
  /**
339
325
  * **Classified text the cloud produced, never text the robot sent.**
340
326
  *
341
- * An earlier draft of this comment said *"the robot's own words when it has
342
- * any"*, which reads as permission to pass `bridgeCameraState.error.message`
343
- * straight through. Nothing sanitises that field, and this codebase has a
344
- * documented incident of a password reaching a developer surface through
345
- * exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
346
- * exists because of it. Nimbus-W9a stopped at the sentence and asked rather
347
- * than taking the permission it appeared to give (2026-08-19).
327
+ * It is **not** the robot's own words. Nothing sanitises
328
+ * `bridgeCameraState.error.message`, and a camera password reaches a
329
+ * developer surface through exactly that route — which is why the cloud maps
330
+ * a robot's diagnosis to fixed strings rather than forwarding it.
348
331
  *
349
332
  * So: `null` unless the cloud itself has something classified to say. If a
350
333
  * developer needs the robot's own diagnosis later, it arrives as a mapped
@@ -356,16 +339,15 @@ export const liveSessionEvent = z.object({
356
339
  ended_at_ms: z.number().int().nonnegative(),
357
340
  });
358
341
  /**
359
- * A resource's health entry was **withdrawn** (W9a, DEF-071).
342
+ * A resource's health entry was **withdrawn**.
360
343
  *
361
- * The store's `invalidate()` deliberately emitted nothing, reasoning that
362
- * "withdrawing a claim nobody can currently stand behind is not new
363
- * information — the next `GET` already reflects it." That holds for a page
364
- * that loads later. **It is false for a page that is already open, because
365
- * there is no next `GET`:** `ensureSnapshot()` runs on `acquire` and nowhere
366
- * else, there is no interval, and the event handler only ever *writes* keys.
367
- * A camera retargeted to a source that never reports — which is the case the
368
- * clearing exists for — leaves an open tab showing the old value indefinitely.
344
+ * Withdrawing a claim nobody can currently stand behind looks like it needs no
345
+ * event: the next `GET` already reflects it. That holds for a page that loads
346
+ * later. **It is false for a page that is already open, because there is no
347
+ * next `GET`** — a client fetches the snapshot once and then only ever writes
348
+ * keys the event stream gives it. A camera retargeted to a source that never
349
+ * reports, which is the case the clearing exists for, would leave an open tab
350
+ * showing the old value indefinitely.
369
351
  *
370
352
  * **A separate event type rather than a nullable `state` on the existing
371
353
  * one**, so a consumer's `switch` has to name it. A nullable field invites
@@ -392,17 +374,14 @@ export const resourceHealthEvent = z.object({
392
374
  changed_at_ms: z.number().int().nonnegative(),
393
375
  });
394
376
  /**
395
- * One line of the developer console's activity panel (spec
396
- * `2026-08-20-org-event-stream`).
377
+ * One line of the developer console's activity panel.
397
378
  *
398
379
  * **This is an activity log for humans, not a complete feed.** It is throttled
399
- * and sampled. `orgEventDropped` still reports a drop on the wire, but since
400
- * FL-001 **no Fleetless surface renders it** — the console's gap banner was
401
- * removed on request, and nothing replaced it. A reader of this stream
402
- * therefore cannot tell a complete window from a sampled one, and this
403
- * comment says so rather than implying a notice that exists only in the
404
- * protocol. Anything that needs completeness reads the audit log or the
405
- * job-run history, both durable, both 90 days.
380
+ * and sampled. `orgEventDropped` reports a drop on the wire, but no Fleetless
381
+ * surface renders it, so a reader of this stream cannot tell a complete window
382
+ * from a sampled one unless their own client shows the drop. Anything that
383
+ * needs completeness reads the audit log or the job-run history, both durable,
384
+ * both 90 days.
406
385
  */
407
386
  export const ORG_EVENT_SAMPLE_INTERVAL_MS = 1_000;
408
387
  /** The backstop above the per-slug cap: a fleet larger than the panel could serve anyway. */
@@ -414,21 +393,16 @@ export const ORG_EVENT_BUFFER_IDLE_MS = 3_600_000;
414
393
  /** A log line, not a payload: a datapoint value is `unknown` and a LaserScan is megabytes. */
415
394
  export const ORG_EVENT_DETAIL_MAX_BYTES = 4_096;
416
395
  /**
417
- * `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`, spec
418
- * `2026-08-28-alerts-and-datapoint-modal-design` D2). A firing event carries
419
- * the alert's own `severity`; a resolved event is always `info` — resolving
420
- * is good news regardless of how bad the firing was.
396
+ * `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`). A firing
397
+ * event carries the alert's own `severity`; a resolved event is always `info` —
398
+ * resolving is good news regardless of how bad the firing was.
421
399
  *
422
- * `'datapoint'` — since FL-001, **no producer emits this kind**: the
423
- * datapoint producer was made a deliberate no-op (Task 5/6, this stream's own
424
- * per-slug sampling made it redundant with what the datapoint history route
425
- * already serves). The member stays in the enum rather than being removed,
426
- * because a reader may still hold a pre-deploy frame of this kind sitting in
427
- * a buffer (a reconnect replay, a client that hasn't refreshed) and must be
428
- * able to parse it rather than fail closed on an old, valid value. Same shape
429
- * as the correction on `ORG_EVENT_SAMPLE_INTERVAL_MS`'s comment just above:
430
- * name what the wire no longer does instead of leaving a value the cloud can
431
- * never send undocumented.
400
+ * `'datapoint'` — **no producer emits this kind**: per-slug sampling on this
401
+ * stream made it redundant with what the datapoint history route already
402
+ * serves. The member stays in the enum rather than being removed, because a
403
+ * reader may still hold an older frame of this kind in a buffer (a reconnect
404
+ * replay, a client that has not refreshed) and must be able to parse it rather
405
+ * than fail closed on an old, valid value.
432
406
  */
433
407
  export const orgEventKind = z.enum(['datapoint', 'health', 'job', 'bridge', 'audit', 'alert']);
434
408
  /**