@fleetless/contracts 4.0.0 → 5.0.0-next.1

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 (35) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/artifacts/constants.json +4 -14
  3. package/artifacts/openapi.json +46 -13
  4. package/artifacts/routes.json +2 -2
  5. package/artifacts/schema/bridge-hello.schema.json +1 -0
  6. package/artifacts/schema/bridge-job-status.schema.json +114 -0
  7. package/artifacts/schema/bridge-job-update.schema.json +21 -0
  8. package/artifacts/schema/busy-details.schema.json +12 -2
  9. package/artifacts/schema/cloud-job-query.schema.json +29 -0
  10. package/artifacts/schema/command-result.schema.json +12 -2
  11. package/artifacts/schema/invoke-or-service-response.schema.json +12 -2
  12. package/artifacts/schema/invoke-response.schema.json +12 -2
  13. package/artifacts/schema/job-event.schema.json +12 -2
  14. package/artifacts/schema/job-response.schema.json +12 -2
  15. package/artifacts/schema/job-run-list-response.schema.json +4 -3
  16. package/artifacts/schema/job-run-query.schema.json +2 -1
  17. package/artifacts/schema/job-run.schema.json +4 -3
  18. package/artifacts/schema/job-state.schema.json +1 -0
  19. package/artifacts/schema/job.schema.json +12 -2
  20. package/artifacts/schema/robot-jobs-response.schema.json +12 -2
  21. package/artifacts/schema-outgoing/bridge-hello.schema.json +1 -0
  22. package/artifacts/schema-outgoing/bridge-job-status.schema.json +117 -0
  23. package/artifacts/schema-outgoing/bridge-job-update.schema.json +21 -0
  24. package/dist/errors.d.ts +1 -1
  25. package/dist/errors.js +38 -7
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +2 -2
  28. package/dist/jobs.d.ts +55 -6
  29. package/dist/jobs.js +43 -14
  30. package/dist/protocol.d.ts +161 -41
  31. package/dist/protocol.js +139 -55
  32. package/dist/realtime.d.ts +5 -0
  33. package/dist/rest.d.ts +20 -0
  34. package/dist/routes.js +6 -3
  35. package/package.json +1 -1
package/dist/jobs.d.ts CHANGED
@@ -10,14 +10,20 @@ import { z } from 'zod';
10
10
  * 1. **State is observed by slug, not by id.** The id is informative; a client
11
11
  * watches `robot × slug` and sees whatever job is running there, which is
12
12
  * also why every observer of a slug sees the same job.
13
- * 2. **`lost` is a real outcome and must be said out loud.** Job state
14
- * lives only in the bridge's memory; if it restarts mid-job, the results
15
- * are gone. The cloud then marks the job `lost` — never leaves it reading
16
- * "running" because nobody contradicted it. A system that reports a
17
- * machine is still working when it does not know is worse than one that
18
- * admits it lost track.
13
+ * 2. **What the cloud does not know it calls `unknown`, and `lost` is final.**
14
+ * A job whose robot went quiet — offline past `JOB_OFFLINE_GRACE_MS`, or
15
+ * connected but silent past `JOB_HEARTBEAT_TIMEOUT_MS` — is `unknown`: not
16
+ * terminal, the slug stays occupied, and only a statement of the bridge
17
+ * resolves it (it is running, it ended, or the bridge does not know it and
18
+ * nothing else runs on its action). `lost` is what that last statement
19
+ * produces, and nothing ever leaves it. Neither is left reading "running"
20
+ * because nobody contradicted it: a system that reports a machine is still
21
+ * working when it does not know is worse than one that says so — and one
22
+ * that declares work lost on a guess is wrong the moment the robot comes
23
+ * back and says it finished.
19
24
  */
20
25
  export declare const jobState: z.ZodEnum<{
26
+ unknown: "unknown";
21
27
  failed: "failed";
22
28
  running: "running";
23
29
  succeeded: "succeeded";
@@ -25,17 +31,42 @@ export declare const jobState: z.ZodEnum<{
25
31
  lost: "lost";
26
32
  }>;
27
33
  export type JobState = z.infer<typeof jobState>;
34
+ /**
35
+ * Who started a job.
36
+ *
37
+ * `fleetless` for every job the cloud minted from an invocation. `external`
38
+ * for a goal the bridge found active on a published action without having
39
+ * sent it — started by anyone else on the robot's ROS graph, or the bridge's
40
+ * own goal after its mapping was lost. An external job has the same shape,
41
+ * states, live stream and cancel as any other, but no parameters (ROS 2
42
+ * publishes a goal's request nowhere), no starter, and it lives in memory
43
+ * only: it is never written to `job_runs` and never counts towards quotas.
44
+ */
45
+ export declare const jobOrigin: z.ZodEnum<{
46
+ fleetless: "fleetless";
47
+ external: "external";
48
+ }>;
49
+ export type JobOrigin = z.infer<typeof jobOrigin>;
50
+ /**
51
+ * One job, as the cloud tells every client about it — a Fleetless job or an
52
+ * external goal alike, told apart only by `origin`.
53
+ */
28
54
  export declare const job: z.ZodObject<{
29
55
  id: z.ZodUUID;
30
56
  robot_id: z.ZodUUID;
31
57
  slug: z.ZodString;
32
58
  state: z.ZodEnum<{
59
+ unknown: "unknown";
33
60
  failed: "failed";
34
61
  running: "running";
35
62
  succeeded: "succeeded";
36
63
  cancelled: "cancelled";
37
64
  lost: "lost";
38
65
  }>;
66
+ origin: z.ZodEnum<{
67
+ fleetless: "fleetless";
68
+ external: "external";
69
+ }>;
39
70
  started_at: z.ZodISODateTime;
40
71
  updated_at: z.ZodISODateTime;
41
72
  seq: z.ZodNumber;
@@ -65,12 +96,17 @@ export declare const jobEvent: z.ZodObject<{
65
96
  robot_id: z.ZodUUID;
66
97
  slug: z.ZodString;
67
98
  state: z.ZodEnum<{
99
+ unknown: "unknown";
68
100
  failed: "failed";
69
101
  running: "running";
70
102
  succeeded: "succeeded";
71
103
  cancelled: "cancelled";
72
104
  lost: "lost";
73
105
  }>;
106
+ origin: z.ZodEnum<{
107
+ fleetless: "fleetless";
108
+ external: "external";
109
+ }>;
74
110
  started_at: z.ZodISODateTime;
75
111
  updated_at: z.ZodISODateTime;
76
112
  seq: z.ZodNumber;
@@ -89,6 +125,11 @@ export type JobEvent = z.infer<typeof jobEvent>;
89
125
  /**
90
126
  * What a busy refusal tells the caller: what is already running. A refusal that
91
127
  * only says "busy" forces the caller to guess whether to wait or to give up.
128
+ *
129
+ * `running` is whatever occupies the slug — a `running` job, an `unknown` one
130
+ * the robot has not accounted for yet, or an `external` goal someone else
131
+ * started — and its `state` and `origin` say which, so a caller can tell
132
+ * "wait for it" from "cancel what someone else started".
92
133
  */
93
134
  export declare const busyDetails: z.ZodObject<{
94
135
  running: z.ZodObject<{
@@ -96,12 +137,17 @@ export declare const busyDetails: z.ZodObject<{
96
137
  robot_id: z.ZodUUID;
97
138
  slug: z.ZodString;
98
139
  state: z.ZodEnum<{
140
+ unknown: "unknown";
99
141
  failed: "failed";
100
142
  running: "running";
101
143
  succeeded: "succeeded";
102
144
  cancelled: "cancelled";
103
145
  lost: "lost";
104
146
  }>;
147
+ origin: z.ZodEnum<{
148
+ fleetless: "fleetless";
149
+ external: "external";
150
+ }>;
105
151
  started_at: z.ZodISODateTime;
106
152
  updated_at: z.ZodISODateTime;
107
153
  seq: z.ZodNumber;
@@ -203,6 +249,7 @@ export declare const jobRun: z.ZodObject<{
203
249
  service: "service";
204
250
  }>;
205
251
  state: z.ZodEnum<{
252
+ unknown: "unknown";
206
253
  failed: "failed";
207
254
  running: "running";
208
255
  succeeded: "succeeded";
@@ -239,6 +286,7 @@ export declare const jobRunQuery: z.ZodObject<{
239
286
  robot_id: z.ZodOptional<z.ZodUUID>;
240
287
  slug: z.ZodOptional<z.ZodString>;
241
288
  state: z.ZodOptional<z.ZodEnum<{
289
+ unknown: "unknown";
242
290
  failed: "failed";
243
291
  running: "running";
244
292
  succeeded: "succeeded";
@@ -263,6 +311,7 @@ export declare const jobRunListResponse: z.ZodObject<{
263
311
  service: "service";
264
312
  }>;
265
313
  state: z.ZodEnum<{
314
+ unknown: "unknown";
266
315
  failed: "failed";
267
316
  running: "running";
268
317
  succeeded: "succeeded";
package/dist/jobs.js CHANGED
@@ -11,14 +11,35 @@ import { slug, wireSeqCursor, wireTimestampMs } from './common.js';
11
11
  * 1. **State is observed by slug, not by id.** The id is informative; a client
12
12
  * watches `robot × slug` and sees whatever job is running there, which is
13
13
  * also why every observer of a slug sees the same job.
14
- * 2. **`lost` is a real outcome and must be said out loud.** Job state
15
- * lives only in the bridge's memory; if it restarts mid-job, the results
16
- * are gone. The cloud then marks the job `lost` — never leaves it reading
17
- * "running" because nobody contradicted it. A system that reports a
18
- * machine is still working when it does not know is worse than one that
19
- * admits it lost track.
14
+ * 2. **What the cloud does not know it calls `unknown`, and `lost` is final.**
15
+ * A job whose robot went quiet — offline past `JOB_OFFLINE_GRACE_MS`, or
16
+ * connected but silent past `JOB_HEARTBEAT_TIMEOUT_MS` — is `unknown`: not
17
+ * terminal, the slug stays occupied, and only a statement of the bridge
18
+ * resolves it (it is running, it ended, or the bridge does not know it and
19
+ * nothing else runs on its action). `lost` is what that last statement
20
+ * produces, and nothing ever leaves it. Neither is left reading "running"
21
+ * because nobody contradicted it: a system that reports a machine is still
22
+ * working when it does not know is worse than one that says so — and one
23
+ * that declares work lost on a guess is wrong the moment the robot comes
24
+ * back and says it finished.
25
+ */
26
+ export const jobState = z.enum(['running', 'unknown', 'succeeded', 'failed', 'cancelled', 'lost']);
27
+ /**
28
+ * Who started a job.
29
+ *
30
+ * `fleetless` for every job the cloud minted from an invocation. `external`
31
+ * for a goal the bridge found active on a published action without having
32
+ * sent it — started by anyone else on the robot's ROS graph, or the bridge's
33
+ * own goal after its mapping was lost. An external job has the same shape,
34
+ * states, live stream and cancel as any other, but no parameters (ROS 2
35
+ * publishes a goal's request nowhere), no starter, and it lives in memory
36
+ * only: it is never written to `job_runs` and never counts towards quotas.
37
+ */
38
+ export const jobOrigin = z.enum(['fleetless', 'external']);
39
+ /**
40
+ * One job, as the cloud tells every client about it — a Fleetless job or an
41
+ * external goal alike, told apart only by `origin`.
20
42
  */
21
- export const jobState = z.enum(['running', 'succeeded', 'failed', 'cancelled', 'lost']);
22
43
  export const job = z.object({
23
44
  id: z.uuid().meta({
24
45
  description: 'The job\'s id, minted by the cloud when the invocation is accepted. Informative — state is observed by slug; a cancel names this id to stop one specific job rather than whatever is running.',
@@ -28,7 +49,10 @@ export const job = z.object({
28
49
  description: 'The action or service this job is running, as the published configuration exposes it. One slug carries one job at a time, so every observer of that slug sees the same one.',
29
50
  }),
30
51
  state: jobState.meta({
31
- description: 'Where the job stands: `running`, `succeeded`, `failed`, `cancelled` or `lost`. `lost` is a real outcome — the bridge restarted mid-job and the result is gone — stated rather than left reading `running` by default.',
52
+ description: 'Where the job stands: `running`, `unknown`, `succeeded`, `failed`, `cancelled` or `lost`. `unknown` is not an outcome — the robot went offline or silent and the cloud does not know yet; the slug stays occupied and the bridge\'s next statement resolves it, `error` naming why the cloud lost sight of it. `lost` is final: the bridge stated it does not know the job and nothing else runs on its action, or the action server vanished mid-goal.',
53
+ }),
54
+ origin: jobOrigin.meta({
55
+ description: "Who started this job. `fleetless` for everything minted by the cloud; `external` for a goal the bridge found active on a published action without having sent it — no parameters, no starter, never written to `job_runs`.",
32
56
  }),
33
57
  started_at: z.iso.datetime().meta({
34
58
  description: 'When the cloud minted this job, as an ISO 8601 timestamp. For a job adopted from a reconnecting bridge, this is **adoption time**, not the real start — the cloud never minted it.',
@@ -47,7 +71,7 @@ export const job = z.object({
47
71
  * exists for the same reason on the audit log.
48
72
  *
49
73
  * **Scoped honestly: per cloud process, per run.** Job state lives in memory
50
- * — that is why `lost` exists at all — so this counter restarts when
74
+ * — that is why `unknown` and `lost` exist at all — so this counter restarts when
51
75
  * the cloud does, alongside the jobs it orders. Sound, because it only ever
52
76
  * orders jobs that coexist in one registry — and stated, because a reader
53
77
  * who assumed `auditEvent.seq`'s durable semantics would be wrong.
@@ -85,7 +109,7 @@ export const job = z.object({
85
109
  })
86
110
  .nullable()
87
111
  .meta({
88
- description: 'Why the job failed: a human `message`, a `code` where one exists, and `details` for the codes that carry a documented payload. `null` unless `state` is `failed`.',
112
+ description: 'Why the job failed, or why the cloud does not know how it stands: a human `message`, a `code` where one exists, and `details` for the codes that carry a documented payload. Set on `failed` and `lost`, and on `unknown` — where `code` is `bridge_disconnected` or `bridge_timeout`, the cloud\'s own reason for not knowing, cleared when the bridge reports the job running again.',
89
113
  }),
90
114
  });
91
115
  /**
@@ -111,6 +135,11 @@ export const jobEvent = z.object({
111
135
  /**
112
136
  * What a busy refusal tells the caller: what is already running. A refusal that
113
137
  * only says "busy" forces the caller to guess whether to wait or to give up.
138
+ *
139
+ * `running` is whatever occupies the slug — a `running` job, an `unknown` one
140
+ * the robot has not accounted for yet, or an `external` goal someone else
141
+ * started — and its `state` and `origin` say which, so a caller can tell
142
+ * "wait for it" from "cancel what someone else started".
114
143
  */
115
144
  export const busyDetails = z.object({
116
145
  running: job,
@@ -210,16 +239,16 @@ export const jobRun = z.object({
210
239
  description: 'Whether the slug was an `action` or a `service`.',
211
240
  }),
212
241
  state: jobState.meta({
213
- description: 'How the run ended, or `running` while it is still going. `lost` means the bridge restarted mid-run and the outcome is unknowable rather than unknown.',
242
+ description: 'How the run ended, or `running` while it is still going. `unknown` while the robot has not accounted for it — offline or silent — and updated once the bridge says how it stands. `lost` is final: the bridge did not know the run and nothing else ran on its action, so the outcome is unknowable rather than unknown.',
214
243
  }),
215
244
  started_at: z.iso.datetime().meta({
216
245
  description: 'When the run started, as an ISO 8601 timestamp. Runs are listed and filtered by this instant.',
217
246
  }),
218
247
  ended_at: z.iso.datetime().nullable().meta({
219
- description: 'When the run finished, as an ISO 8601 timestamp. `null` while it is still `running` — a run has an end only once it has one.',
248
+ description: 'When the run finished, as an ISO 8601 timestamp. `null` while it is still `running` or `unknown` — a run has an end only once it has one.',
220
249
  }),
221
250
  duration_ms: z.number().int().nonnegative().nullable().meta({
222
- description: 'How long the run took, in milliseconds. `null` while it is still `running`, never `0` standing in for "nothing so far".',
251
+ description: 'How long the run took, in milliseconds. `null` while it is still `running` or `unknown`, never `0` standing in for "nothing so far".',
223
252
  }),
224
253
  result: z.unknown().nullable().meta({
225
254
  description: 'What the action or service returned once it succeeded, shaped by ROS itself. `null` otherwise.',
@@ -278,7 +307,7 @@ export const jobRunQuery = z
278
307
  description: 'Only runs of this action or service.',
279
308
  }),
280
309
  state: jobState.optional().meta({
281
- description: 'Only runs in this state — `running`, `succeeded`, `failed`, `cancelled` or `lost`.',
310
+ description: 'Only runs in this state — `running`, `unknown`, `succeeded`, `failed`, `cancelled` or `lost`.',
282
311
  }),
283
312
  kind: jobRunKind.optional().meta({
284
313
  description: 'Only `action` runs, or only `service` runs.',
@@ -1,15 +1,29 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  /**
4
- * Bridge <-> cloud protocol, version 4.
4
+ * Bridge <-> cloud protocol, version 5.
5
5
  *
6
6
  * The version is exchanged in the hello handshake. Since 2026-09 the cloud
7
7
  * serves a **window** of versions, not one: every entry of
8
8
  * `PROTOCOL_VERSIONS` whose sunset has not passed. A version is deprecated
9
9
  * by the cloud release that supersedes it and sunset `PROTOCOL_SUNSET_DAYS`
10
- * later. Outside the window the cloud refuses with `protocol_mismatch`,
11
- * which names the window and reaches the robot's detail view as
12
- * `last_hello_error`.
10
+ * later. Outside the window the cloud refuses the hello, and the refusal
11
+ * reaches the robot's detail view as `last_hello_error`: `bridge_too_old`,
12
+ * naming the bridge version to install, for a version below the window.
13
+ *
14
+ * **5 (2026-09-29):** a hard cut, not a window — protocols 2, 3 and 4 are
15
+ * unsupported from this release on, with no sunset (André, 2026-09-29: "we
16
+ * are still building up and need not take care"), so `PROTOCOL_VERSIONS`
17
+ * holds one entry. The bridge tracks every goal on a published action by
18
+ * goal id — its own and anyone else's — and reports them through
19
+ * `job_update`, which gains a required `origin` and `goal_id`: a goal it did
20
+ * not send arrives as `origin: 'external'` under a job id the bridge derives
21
+ * itself. `jobState` gains `unknown`, the cloud's non-terminal "I lost sight
22
+ * of it" (`bridge_disconnected`, `bridge_timeout`) that replaces settling
23
+ * `lost` on a guess; `lost` is final. While connected, the cloud asks about
24
+ * specific jobs with `job_query` and the bridge answers `job_status`. Goal
25
+ * state is reported once per `JOB_HEARTBEAT_INTERVAL_MS`, newest only; the
26
+ * end of a Fleetless job goes at once.
13
27
  *
14
28
  * **4 (2026-09-29):** the bridge sends a `job_update` heartbeat at
15
29
  * `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
@@ -36,7 +50,7 @@ import { z } from 'zod';
36
50
  * **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
37
51
  * beside `message`.
38
52
  */
39
- export declare const PROTOCOL_VERSION = 4;
53
+ export declare const PROTOCOL_VERSION = 5;
40
54
  /** Days between a version's deprecation and its sunset. */
41
55
  export declare const PROTOCOL_SUNSET_DAYS = 90;
42
56
  export interface ProtocolVersionEntry {
@@ -47,17 +61,24 @@ export interface ProtocolVersionEntry {
47
61
  deprecated_at: string | null;
48
62
  }
49
63
  /**
50
- * Every protocol version the cloud has served, oldest first. A test keeps
64
+ * Every protocol version the cloud serves, oldest first. A test keeps
51
65
  * exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
52
66
  * requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
53
67
  * the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
54
68
  * date, so the pull request that moves this window is the one that fails
55
69
  * without saying so; and `scripts/verify-version-tag.mjs` requires a dated
56
70
  * heading for the tag being released.
71
+ *
72
+ * **One entry since protocol 5**, which cut 2, 3 and 4 without a sunset
73
+ * rather than deprecating them. A version absent from the table is
74
+ * `unsupported`, which is exactly what a cut means, so the dropped entries
75
+ * are gone rather than kept with a past date. The changelog test's window
76
+ * check has no previous entry to read then; its hard-cut sibling holds the
77
+ * changelog to naming the cut instead.
57
78
  */
58
79
  export declare const PROTOCOL_VERSIONS: readonly ProtocolVersionEntry[];
59
80
  /** The newest bridge package. The cloud mails organisations still below it. */
60
- export declare const LATEST_BRIDGE_VERSION = "5.0.0";
81
+ export declare const LATEST_BRIDGE_VERSION = "6.0.0";
61
82
  export interface ProtocolStatus {
62
83
  status: 'current' | 'deprecated' | 'unsupported';
63
84
  /** ISO date, or null for a current or unknown version. */
@@ -144,37 +165,43 @@ export declare const MAX_PATIENCE_MS = 120000;
144
165
  */
145
166
  export declare const MIN_PATIENCE_MS = 1000;
146
167
  /**
147
- * How often a protocol-4 bridge sends a `job_update` heartbeat for every
148
- * running job — the last known state, whether or not the action itself said
149
- * anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
168
+ * How often the bridge reports goal state: one `job_update` per active goal
169
+ * on a published action — its own and external ones — carrying the newest
170
+ * state and feedback, whether or not the action said anything new. Whatever
171
+ * happened in between is dropped, so an action that sends feedback at 100 Hz
172
+ * costs one frame a second; the end of a Fleetless job is the exception and
173
+ * goes at once. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
150
174
  * can be a small multiple of it and still absorb a missed beat or two, rare
151
175
  * enough that it costs nothing next to the datapoint traffic a busy robot
152
176
  * already sends.
153
177
  */
154
178
  export declare const JOB_HEARTBEAT_INTERVAL_MS = 1000;
155
179
  /**
156
- * How long a protocol-4 job may go without a `job_update` — heartbeat or
157
- * real progress, either counts — before the cloud settles it `lost` with
180
+ * How long a running job may go without a `job_update` — heartbeat or real
181
+ * progress, either counts — before the cloud marks it `unknown` with
158
182
  * `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
159
183
  * slack for an ordinary scheduling jitter, small next to `patience_ms`
160
- * because it no longer has to cover the acceptance gap too. `patience_ms`
161
- * bounds only the time from `invoke` to the *first* update on a protocol-4
162
- * job; every rearm after that uses this constant instead. A protocol-3
163
- * bridge sends no heartbeat, so this constant does not apply to it —
164
- * `patience_ms` keeps bounding the whole running job there, exactly as
165
- * before.
184
+ * because it does not have to cover the acceptance gap too. `patience_ms`
185
+ * bounds only the time from `invoke` to the *first* update; every rearm after
186
+ * that uses this constant instead.
187
+ *
188
+ * `unknown`, not `lost`: silence is the cloud's guess, not the bridge's
189
+ * statement. The cloud then asks with `job_query`, and asks again after the
190
+ * same interval for as long as no `job_status` answers and the bridge stays
191
+ * connected.
166
192
  */
167
193
  export declare const JOB_HEARTBEAT_TIMEOUT_MS = 5000;
168
194
  /**
169
195
  * How long a running job survives its robot going offline before the cloud
170
- * gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
171
- * long enough that an ordinary Wi-Fi dead zone — the case this constant
172
- * exists for — never costs a job, since a robot with no safety layer of its
173
- * own (§ Fleetless is not a safety layer) keeps driving through one and the
174
- * result the cloud is waiting for is often still coming. A robot connected
175
- * the whole time never reaches this bound at all: while online, silence is
176
- * `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
177
- * (protocol 3), never this one's.
196
+ * marks it `unknown` with `bridge_disconnected`. Five minutes: long enough
197
+ * that an ordinary Wi-Fi dead zone — the case this constant exists for —
198
+ * never touches a job, since a robot with no safety layer of its own
199
+ * (§ Fleetless is not a safety layer) keeps driving through one and the
200
+ * result the cloud is waiting for is often still coming. Past it the job is
201
+ * still not given up: `unknown` keeps the slug occupied until the
202
+ * reconnecting bridge says how the job stands. A robot connected the whole
203
+ * time never reaches this bound at all: while online, silence is
204
+ * `JOB_HEARTBEAT_TIMEOUT_MS`'s question, never this one's.
178
205
  */
179
206
  export declare const JOB_OFFLINE_GRACE_MS = 300000;
180
207
  /** Re-exported so consumers keep importing wire names from one place. */
@@ -193,11 +220,14 @@ export { slug } from './common.js';
193
220
  * `state` is the bridge's own current answer, not a history. A bridge that
194
221
  * has a terminal result still in hand reports it here and the cloud writes it
195
222
  * down, instead of publishing `lost` over a job that in fact succeeded.
223
+ * Never `unknown`: that is the cloud's word for not having heard, and a
224
+ * bridge listing a job has, by definition, something to say about it.
196
225
  */
197
226
  export declare const activeJob: z.ZodObject<{
198
227
  job_id: z.ZodUUID;
199
228
  slug: z.ZodString;
200
229
  state: z.ZodEnum<{
230
+ unknown: "unknown";
201
231
  failed: "failed";
202
232
  running: "running";
203
233
  succeeded: "succeeded";
@@ -216,6 +246,7 @@ export declare const bridgeHello: z.ZodObject<{
216
246
  job_id: z.ZodUUID;
217
247
  slug: z.ZodString;
218
248
  state: z.ZodEnum<{
249
+ unknown: "unknown";
219
250
  failed: "failed";
220
251
  running: "running";
221
252
  succeeded: "succeeded";
@@ -615,18 +646,38 @@ export type CloudPublish = z.infer<typeof cloudPublish>;
615
646
  /**
616
647
  * Progress on a job, bridge → cloud. `timestamp_ms` is capture time, so a
617
648
  * burst delivered late after a reconnect is visibly late.
649
+ *
650
+ * Since protocol 5 this frame reports **every goal active on a published
651
+ * action**, not only the ones the bridge sent. A goal the bridge cannot
652
+ * attribute to a job of its own arrives with `origin: 'external'` and a
653
+ * `job_id` the bridge derives from robot, slug and goal id, the same id on
654
+ * every report of that goal — the cloud mints an external job the first time
655
+ * it sees one and never recomputes the id itself. `goal_id` is the ROS 2
656
+ * goal id, so a cancel can name the goal; `null` for a service job, which
657
+ * has no goal.
658
+ *
659
+ * Sent once per `JOB_HEARTBEAT_INTERVAL_MS` per active goal, newest state
660
+ * only. The end of a Fleetless job is sent at once; an external goal's end at
661
+ * the next tick, and an external goal that started and ended between two
662
+ * ticks is never reported at all.
618
663
  */
619
664
  export declare const bridgeJobUpdate: z.ZodObject<{
620
665
  type: z.ZodLiteral<"job_update">;
621
666
  job_id: z.ZodUUID;
622
667
  slug: z.ZodString;
623
668
  state: z.ZodEnum<{
669
+ unknown: "unknown";
624
670
  failed: "failed";
625
671
  running: "running";
626
672
  succeeded: "succeeded";
627
673
  cancelled: "cancelled";
628
674
  lost: "lost";
629
675
  }>;
676
+ origin: z.ZodEnum<{
677
+ fleetless: "fleetless";
678
+ external: "external";
679
+ }>;
680
+ goal_id: z.ZodNullable<z.ZodString>;
630
681
  feedback: z.ZodNullable<z.ZodUnknown>;
631
682
  progress: z.ZodNullable<z.ZodNumber>;
632
683
  result: z.ZodNullable<z.ZodUnknown>;
@@ -639,23 +690,20 @@ export declare const bridgeJobUpdate: z.ZodObject<{
639
690
  }, z.core.$strip>;
640
691
  export type BridgeJobUpdate = z.infer<typeof bridgeJobUpdate>;
641
692
  /**
642
- * Jobs the bridge can no longer account for **while connected** — a
643
- * tracker dropped, an action server that vanished mid-goal, anything where
644
- * the honest answer is "I lost this" rather than a state.
693
+ * The bridge's own, definite statement **while connected** that it no longer
694
+ * knows these jobs — an action server that vanished mid-goal, a goal the
695
+ * server no longer knows — where the honest answer is "I lost this" rather
696
+ * than a state. The cloud settles each named job `lost`, final.
645
697
  *
646
- * The restart case is not this frame's job: a restarted bridge has nothing
647
- * left to enumerate, so it is `hello.active_job_ids` that closes that gap.
648
- * Both paths end in the same place — the cloud publishes `lost` rather than
649
- * leaving a job reading "running" because nobody contradicted it.
698
+ * Only the bridge says `lost` now. The cloud's own guesses — offline past
699
+ * `JOB_OFFLINE_GRACE_MS`, silent past `JOB_HEARTBEAT_TIMEOUT_MS` — make a
700
+ * job `unknown` instead, and a restart is answered by `hello.active_jobs`.
650
701
  *
651
- * **`error` (since protocol 4) is optional and, when present, applies to
652
- * every job named in `job_ids`.** A vanished action server is discovered
653
- * once, by the bridge's own liveness check on that one goal, so a frame
654
- * naming several jobs at once — plausible if several goals shared the same
655
- * server — always shares the same cause. Absent means today's behaviour:
656
- * the cloud settles the job `lost` with no specific code, the same as a
657
- * protocol-3 bridge's frame, which carries no `error` at all and still
658
- * parses under this schema unchanged.
702
+ * **`error` is optional and, when present, applies to every job named in
703
+ * `job_ids`.** A vanished action server is discovered once, by the bridge's
704
+ * own liveness check on that one action, so a frame naming several jobs at
705
+ * once always shares the same cause. Absent, the cloud settles the job
706
+ * `lost` with no specific code.
659
707
  */
660
708
  export declare const bridgeJobLost: z.ZodObject<{
661
709
  type: z.ZodLiteral<"job_lost">;
@@ -666,6 +714,78 @@ export declare const bridgeJobLost: z.ZodObject<{
666
714
  }, z.core.$strip>>;
667
715
  }, z.core.$strip>;
668
716
  export type BridgeJobLost = z.infer<typeof bridgeJobLost>;
717
+ /**
718
+ * The cloud asks the bridge how specific jobs stand, while connected;
719
+ * `request_id` correlates the `job_status` answer.
720
+ *
721
+ * Sent for a job that went `unknown` with `bridge_timeout`: the bridge is
722
+ * connected but the cloud has not heard about the job, so it asks instead of
723
+ * guessing. Unanswered within `JOB_HEARTBEAT_TIMEOUT_MS`, the job stays
724
+ * `unknown` and the cloud asks again after the same interval.
725
+ */
726
+ export declare const cloudJobQuery: z.ZodObject<{
727
+ type: z.ZodLiteral<"job_query">;
728
+ request_id: z.ZodString;
729
+ job_ids: z.ZodArray<z.ZodUUID>;
730
+ }, z.core.$strip>;
731
+ export type CloudJobQuery = z.infer<typeof cloudJobQuery>;
732
+ /** One job the bridge recognises, in a `job_status` answer. Same fields as `job_update`'s. */
733
+ export declare const bridgeJobStatusEntry: z.ZodObject<{
734
+ job_id: z.ZodUUID;
735
+ state: z.ZodEnum<{
736
+ unknown: "unknown";
737
+ failed: "failed";
738
+ running: "running";
739
+ succeeded: "succeeded";
740
+ cancelled: "cancelled";
741
+ lost: "lost";
742
+ }>;
743
+ feedback: z.ZodNullable<z.ZodUnknown>;
744
+ progress: z.ZodNullable<z.ZodNumber>;
745
+ result: z.ZodNullable<z.ZodUnknown>;
746
+ error: z.ZodNullable<z.ZodObject<{
747
+ code: z.ZodString;
748
+ message: z.ZodString;
749
+ details: z.ZodOptional<z.ZodUnknown>;
750
+ }, z.core.$strip>>;
751
+ }, z.core.$strip>;
752
+ export type BridgeJobStatusEntry = z.infer<typeof bridgeJobStatusEntry>;
753
+ /**
754
+ * The bridge's answer to a `job_query`. Every queried id lands in exactly
755
+ * one of the two lists.
756
+ *
757
+ * `unknown_job_ids` is an answer, not a failure — the same stance
758
+ * `type_definitions.unresolved` takes. It names the queried jobs the bridge
759
+ * does not recognise at all; the cloud settles one `lost` with
760
+ * `job_unknown_to_bridge` once no goal the bridge cannot attribute is active
761
+ * on its action (one of those may be that very job), and at once for a
762
+ * service job.
763
+ */
764
+ export declare const bridgeJobStatus: z.ZodObject<{
765
+ type: z.ZodLiteral<"job_status">;
766
+ request_id: z.ZodString;
767
+ jobs: z.ZodArray<z.ZodObject<{
768
+ job_id: z.ZodUUID;
769
+ state: z.ZodEnum<{
770
+ unknown: "unknown";
771
+ failed: "failed";
772
+ running: "running";
773
+ succeeded: "succeeded";
774
+ cancelled: "cancelled";
775
+ lost: "lost";
776
+ }>;
777
+ feedback: z.ZodNullable<z.ZodUnknown>;
778
+ progress: z.ZodNullable<z.ZodNumber>;
779
+ result: z.ZodNullable<z.ZodUnknown>;
780
+ error: z.ZodNullable<z.ZodObject<{
781
+ code: z.ZodString;
782
+ message: z.ZodString;
783
+ details: z.ZodOptional<z.ZodUnknown>;
784
+ }, z.core.$strip>>;
785
+ }, z.core.$strip>>;
786
+ unknown_job_ids: z.ZodArray<z.ZodUUID>;
787
+ }, z.core.$strip>;
788
+ export type BridgeJobStatus = z.infer<typeof bridgeJobStatus>;
669
789
  /** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
670
790
  export declare const cloudIntrospectRequest: z.ZodObject<{
671
791
  type: z.ZodLiteral<"introspect_request">;