@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/protocol.js CHANGED
@@ -4,18 +4,32 @@ import { assetFailure } from './assets.js';
4
4
  import { applyError, slug } from './common.js';
5
5
  import { robotConfigDoc } from './config.js';
6
6
  import { rosGraph, typeDefinition } from './introspection.js';
7
- import { jobState } from './jobs.js';
7
+ import { jobOrigin, jobState } from './jobs.js';
8
8
  import { rosTypeName } from './common.js';
9
9
  /**
10
- * Bridge <-> cloud protocol, version 4.
10
+ * Bridge <-> cloud protocol, version 5.
11
11
  *
12
12
  * The version is exchanged in the hello handshake. Since 2026-09 the cloud
13
13
  * serves a **window** of versions, not one: every entry of
14
14
  * `PROTOCOL_VERSIONS` whose sunset has not passed. A version is deprecated
15
15
  * by the cloud release that supersedes it and sunset `PROTOCOL_SUNSET_DAYS`
16
- * later. Outside the window the cloud refuses with `protocol_mismatch`,
17
- * which names the window and reaches the robot's detail view as
18
- * `last_hello_error`.
16
+ * later. Outside the window the cloud refuses the hello, and the refusal
17
+ * reaches the robot's detail view as `last_hello_error`: `bridge_too_old`,
18
+ * naming the bridge version to install, for a version below the window.
19
+ *
20
+ * **5 (2026-09-29):** a hard cut, not a window — protocols 2, 3 and 4 are
21
+ * unsupported from this release on, with no sunset (André, 2026-09-29: "we
22
+ * are still building up and need not take care"), so `PROTOCOL_VERSIONS`
23
+ * holds one entry. The bridge tracks every goal on a published action by
24
+ * goal id — its own and anyone else's — and reports them through
25
+ * `job_update`, which gains a required `origin` and `goal_id`: a goal it did
26
+ * not send arrives as `origin: 'external'` under a job id the bridge derives
27
+ * itself. `jobState` gains `unknown`, the cloud's non-terminal "I lost sight
28
+ * of it" (`bridge_disconnected`, `bridge_timeout`) that replaces settling
29
+ * `lost` on a guess; `lost` is final. While connected, the cloud asks about
30
+ * specific jobs with `job_query` and the bridge answers `job_status`. Goal
31
+ * state is reported once per `JOB_HEARTBEAT_INTERVAL_MS`, newest only; the
32
+ * end of a Fleetless job goes at once.
19
33
  *
20
34
  * **4 (2026-09-29):** the bridge sends a `job_update` heartbeat at
21
35
  * `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
@@ -42,25 +56,30 @@ import { rosTypeName } from './common.js';
42
56
  * **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
43
57
  * beside `message`.
44
58
  */
45
- export const PROTOCOL_VERSION = 4;
59
+ export const PROTOCOL_VERSION = 5;
46
60
  /** Days between a version's deprecation and its sunset. */
47
61
  export const PROTOCOL_SUNSET_DAYS = 90;
48
62
  /**
49
- * Every protocol version the cloud has served, oldest first. A test keeps
63
+ * Every protocol version the cloud serves, oldest first. A test keeps
50
64
  * exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
51
65
  * requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
52
66
  * the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
53
67
  * date, so the pull request that moves this window is the one that fails
54
68
  * without saying so; and `scripts/verify-version-tag.mjs` requires a dated
55
69
  * heading for the tag being released.
70
+ *
71
+ * **One entry since protocol 5**, which cut 2, 3 and 4 without a sunset
72
+ * rather than deprecating them. A version absent from the table is
73
+ * `unsupported`, which is exactly what a cut means, so the dropped entries
74
+ * are gone rather than kept with a past date. The changelog test's window
75
+ * check has no previous entry to read then; its hard-cut sibling holds the
76
+ * changelog to naming the cut instead.
56
77
  */
57
78
  export const PROTOCOL_VERSIONS = [
58
- { version: 2, bridge_from: '3.0.0', deprecated_at: '2026-09-22' },
59
- { version: 3, bridge_from: '4.0.0', deprecated_at: '2026-09-29' },
60
- { version: 4, bridge_from: '5.0.0', deprecated_at: null },
79
+ { version: 5, bridge_from: '6.0.0', deprecated_at: null },
61
80
  ];
62
81
  /** The newest bridge package. The cloud mails organisations still below it. */
63
- export const LATEST_BRIDGE_VERSION = '5.0.0';
82
+ export const LATEST_BRIDGE_VERSION = '6.0.0';
64
83
  const DAY_MS = 24 * 60 * 60 * 1000;
65
84
  function isoDate(date) {
66
85
  return date.toISOString().slice(0, 10);
@@ -163,37 +182,43 @@ export const MAX_PATIENCE_MS = 120_000;
163
182
  */
164
183
  export const MIN_PATIENCE_MS = 1_000;
165
184
  /**
166
- * How often a protocol-4 bridge sends a `job_update` heartbeat for every
167
- * running job — the last known state, whether or not the action itself said
168
- * anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
185
+ * How often the bridge reports goal state: one `job_update` per active goal
186
+ * on a published action — its own and external ones — carrying the newest
187
+ * state and feedback, whether or not the action said anything new. Whatever
188
+ * happened in between is dropped, so an action that sends feedback at 100 Hz
189
+ * costs one frame a second; the end of a Fleetless job is the exception and
190
+ * goes at once. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
169
191
  * can be a small multiple of it and still absorb a missed beat or two, rare
170
192
  * enough that it costs nothing next to the datapoint traffic a busy robot
171
193
  * already sends.
172
194
  */
173
195
  export const JOB_HEARTBEAT_INTERVAL_MS = 1_000;
174
196
  /**
175
- * How long a protocol-4 job may go without a `job_update` — heartbeat or
176
- * real progress, either counts — before the cloud settles it `lost` with
197
+ * How long a running job may go without a `job_update` — heartbeat or real
198
+ * progress, either counts — before the cloud marks it `unknown` with
177
199
  * `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
178
200
  * slack for an ordinary scheduling jitter, small next to `patience_ms`
179
- * because it no longer has to cover the acceptance gap too. `patience_ms`
180
- * bounds only the time from `invoke` to the *first* update on a protocol-4
181
- * job; every rearm after that uses this constant instead. A protocol-3
182
- * bridge sends no heartbeat, so this constant does not apply to it —
183
- * `patience_ms` keeps bounding the whole running job there, exactly as
184
- * before.
201
+ * because it does not have to cover the acceptance gap too. `patience_ms`
202
+ * bounds only the time from `invoke` to the *first* update; every rearm after
203
+ * that uses this constant instead.
204
+ *
205
+ * `unknown`, not `lost`: silence is the cloud's guess, not the bridge's
206
+ * statement. The cloud then asks with `job_query`, and asks again after the
207
+ * same interval for as long as no `job_status` answers and the bridge stays
208
+ * connected.
185
209
  */
186
210
  export const JOB_HEARTBEAT_TIMEOUT_MS = 5_000;
187
211
  /**
188
212
  * How long a running job survives its robot going offline before the cloud
189
- * gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
190
- * long enough that an ordinary Wi-Fi dead zone — the case this constant
191
- * exists for — never costs a job, since a robot with no safety layer of its
192
- * own (§ Fleetless is not a safety layer) keeps driving through one and the
193
- * result the cloud is waiting for is often still coming. A robot connected
194
- * the whole time never reaches this bound at all: while online, silence is
195
- * `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
196
- * (protocol 3), never this one's.
213
+ * marks it `unknown` with `bridge_disconnected`. Five minutes: long enough
214
+ * that an ordinary Wi-Fi dead zone — the case this constant exists for —
215
+ * never touches a job, since a robot with no safety layer of its own
216
+ * (§ Fleetless is not a safety layer) keeps driving through one and the
217
+ * result the cloud is waiting for is often still coming. Past it the job is
218
+ * still not given up: `unknown` keeps the slug occupied until the
219
+ * reconnecting bridge says how the job stands. A robot connected the whole
220
+ * time never reaches this bound at all: while online, silence is
221
+ * `JOB_HEARTBEAT_TIMEOUT_MS`'s question, never this one's.
197
222
  */
198
223
  export const JOB_OFFLINE_GRACE_MS = 300_000;
199
224
  /** Re-exported so consumers keep importing wire names from one place. */
@@ -212,6 +237,8 @@ export { slug } from './common.js';
212
237
  * `state` is the bridge's own current answer, not a history. A bridge that
213
238
  * has a terminal result still in hand reports it here and the cloud writes it
214
239
  * down, instead of publishing `lost` over a job that in fact succeeded.
240
+ * Never `unknown`: that is the cloud's word for not having heard, and a
241
+ * bridge listing a job has, by definition, something to say about it.
215
242
  */
216
243
  export const activeJob = z.object({
217
244
  job_id: z.uuid(),
@@ -228,16 +255,14 @@ export const bridgeHello = z.object({
228
255
  * Every job this bridge still knows about, right now.
229
256
  *
230
257
  * A reconnect and a restart look **identical** on the wire — same token,
231
- * same version, same frame — but must end differently: after a dropped
232
- * connection the running jobs are still running, after a restart their
233
- * results are gone forever. Enumerating what the bridge still has settles
234
- * it without either side guessing: the cloud marks every job it believed
235
- * running that is *not* named here as `lost`.
236
- *
237
- * Deliberately needs no persistence at the bridge: a live process lists its
238
- * live jobs, a process that just started lists none — exactly the truth
239
- * the cloud needs. A breadcrumb file would only add a window in which the
240
- * crash beat the write.
258
+ * same version, same frame — so the bridge enumerates what it still has:
259
+ * its live jobs, and after a restart every job whose goal it recognised
260
+ * again from its persisted job-to-goal mapping. A job the cloud holds
261
+ * `running` or `unknown` that is *not* named here is "not known to the
262
+ * bridge"; it becomes `lost` (`job_unknown_to_bridge`) only once the
263
+ * bridge's goal reports show its action free of goals it cannot
264
+ * attribute — one of those may be that very job — and at once for a
265
+ * service job, which has no goals to look at.
241
266
  *
242
267
  * Defaulted, so a bridge that sends no such field still parses; no jobs
243
268
  * and no report both mean the same thing to the cloud: nothing to keep
@@ -466,12 +491,30 @@ export const cloudPublish = z.object({
466
491
  /**
467
492
  * Progress on a job, bridge → cloud. `timestamp_ms` is capture time, so a
468
493
  * burst delivered late after a reconnect is visibly late.
494
+ *
495
+ * Since protocol 5 this frame reports **every goal active on a published
496
+ * action**, not only the ones the bridge sent. A goal the bridge cannot
497
+ * attribute to a job of its own arrives with `origin: 'external'` and a
498
+ * `job_id` the bridge derives from robot, slug and goal id, the same id on
499
+ * every report of that goal — the cloud mints an external job the first time
500
+ * it sees one and never recomputes the id itself. `goal_id` is the ROS 2
501
+ * goal id, so a cancel can name the goal; `null` for a service job, which
502
+ * has no goal.
503
+ *
504
+ * Sent once per `JOB_HEARTBEAT_INTERVAL_MS` per active goal, newest state
505
+ * only. The end of a Fleetless job is sent at once; an external goal's end at
506
+ * the next tick, and an external goal that started and ended between two
507
+ * ticks is never reported at all.
469
508
  */
470
509
  export const bridgeJobUpdate = z.object({
471
510
  type: z.literal('job_update'),
472
511
  job_id: z.uuid(),
473
512
  slug,
513
+ /** Never `unknown`: that is the cloud's word for not having heard. */
474
514
  state: jobState,
515
+ origin: jobOrigin,
516
+ /** The ROS 2 goal id; `null` for a service job, which has no goal. */
517
+ goal_id: z.string().min(1).nullable(),
475
518
  feedback: z.unknown().nullable(),
476
519
  progress: z.number().min(0).max(1).nullable(),
477
520
  result: z.unknown().nullable(),
@@ -482,29 +525,70 @@ export const bridgeJobUpdate = z.object({
482
525
  timestamp_ms: z.number().int().nonnegative(),
483
526
  });
484
527
  /**
485
- * Jobs the bridge can no longer account for **while connected** — a
486
- * tracker dropped, an action server that vanished mid-goal, anything where
487
- * the honest answer is "I lost this" rather than a state.
528
+ * The bridge's own, definite statement **while connected** that it no longer
529
+ * knows these jobs — an action server that vanished mid-goal, a goal the
530
+ * server no longer knows — where the honest answer is "I lost this" rather
531
+ * than a state. The cloud settles each named job `lost`, final.
488
532
  *
489
- * The restart case is not this frame's job: a restarted bridge has nothing
490
- * left to enumerate, so it is `hello.active_job_ids` that closes that gap.
491
- * Both paths end in the same place — the cloud publishes `lost` rather than
492
- * leaving a job reading "running" because nobody contradicted it.
533
+ * Only the bridge says `lost` now. The cloud's own guesses — offline past
534
+ * `JOB_OFFLINE_GRACE_MS`, silent past `JOB_HEARTBEAT_TIMEOUT_MS` — make a
535
+ * job `unknown` instead, and a restart is answered by `hello.active_jobs`.
493
536
  *
494
- * **`error` (since protocol 4) is optional and, when present, applies to
495
- * every job named in `job_ids`.** A vanished action server is discovered
496
- * once, by the bridge's own liveness check on that one goal, so a frame
497
- * naming several jobs at once — plausible if several goals shared the same
498
- * server — always shares the same cause. Absent means today's behaviour:
499
- * the cloud settles the job `lost` with no specific code, the same as a
500
- * protocol-3 bridge's frame, which carries no `error` at all and still
501
- * parses under this schema unchanged.
537
+ * **`error` is optional and, when present, applies to every job named in
538
+ * `job_ids`.** A vanished action server is discovered once, by the bridge's
539
+ * own liveness check on that one action, so a frame naming several jobs at
540
+ * once always shares the same cause. Absent, the cloud settles the job
541
+ * `lost` with no specific code.
502
542
  */
503
543
  export const bridgeJobLost = z.object({
504
544
  type: z.literal('job_lost'),
505
545
  job_ids: z.array(z.uuid()),
506
546
  error: z.object({ code: z.string().min(1), message: z.string().min(1) }).optional(),
507
547
  });
548
+ /**
549
+ * The cloud asks the bridge how specific jobs stand, while connected;
550
+ * `request_id` correlates the `job_status` answer.
551
+ *
552
+ * Sent for a job that went `unknown` with `bridge_timeout`: the bridge is
553
+ * connected but the cloud has not heard about the job, so it asks instead of
554
+ * guessing. Unanswered within `JOB_HEARTBEAT_TIMEOUT_MS`, the job stays
555
+ * `unknown` and the cloud asks again after the same interval.
556
+ */
557
+ export const cloudJobQuery = z.object({
558
+ type: z.literal('job_query'),
559
+ request_id: z.string().min(1).max(64),
560
+ job_ids: z.array(z.uuid()).min(1),
561
+ });
562
+ /** One job the bridge recognises, in a `job_status` answer. Same fields as `job_update`'s. */
563
+ export const bridgeJobStatusEntry = z.object({
564
+ job_id: z.uuid(),
565
+ /** Never `unknown` — the bridge only ever states a definite fact about a job it recognises. */
566
+ state: jobState,
567
+ feedback: z.unknown().nullable(),
568
+ progress: z.number().min(0).max(1).nullable(),
569
+ result: z.unknown().nullable(),
570
+ /** Same shape as `job.error`, `details` included — see `jobs.ts`. */
571
+ error: z
572
+ .object({ code: z.string().min(1), message: z.string().min(1), details: z.unknown().optional() })
573
+ .nullable(),
574
+ });
575
+ /**
576
+ * The bridge's answer to a `job_query`. Every queried id lands in exactly
577
+ * one of the two lists.
578
+ *
579
+ * `unknown_job_ids` is an answer, not a failure — the same stance
580
+ * `type_definitions.unresolved` takes. It names the queried jobs the bridge
581
+ * does not recognise at all; the cloud settles one `lost` with
582
+ * `job_unknown_to_bridge` once no goal the bridge cannot attribute is active
583
+ * on its action (one of those may be that very job), and at once for a
584
+ * service job.
585
+ */
586
+ export const bridgeJobStatus = z.object({
587
+ type: z.literal('job_status'),
588
+ request_id: z.string().min(1).max(64),
589
+ jobs: z.array(bridgeJobStatusEntry),
590
+ unknown_job_ids: z.array(z.uuid()),
591
+ });
508
592
  /** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
509
593
  export const cloudIntrospectRequest = z.object({
510
594
  type: z.literal('introspect_request'),
@@ -121,12 +121,17 @@ export declare const commandResult: z.ZodObject<{
121
121
  robot_id: z.ZodUUID;
122
122
  slug: z.ZodString;
123
123
  state: z.ZodEnum<{
124
+ unknown: "unknown";
124
125
  failed: "failed";
125
126
  running: "running";
126
127
  succeeded: "succeeded";
127
128
  cancelled: "cancelled";
128
129
  lost: "lost";
129
130
  }>;
131
+ origin: z.ZodEnum<{
132
+ fleetless: "fleetless";
133
+ external: "external";
134
+ }>;
130
135
  started_at: z.ZodISODateTime;
131
136
  updated_at: z.ZodISODateTime;
132
137
  seq: z.ZodNumber;
package/dist/rest.d.ts CHANGED
@@ -893,12 +893,17 @@ export declare const invokeResponse: z.ZodObject<{
893
893
  robot_id: z.ZodUUID;
894
894
  slug: z.ZodString;
895
895
  state: z.ZodEnum<{
896
+ unknown: "unknown";
896
897
  failed: "failed";
897
898
  running: "running";
898
899
  succeeded: "succeeded";
899
900
  cancelled: "cancelled";
900
901
  lost: "lost";
901
902
  }>;
903
+ origin: z.ZodEnum<{
904
+ fleetless: "fleetless";
905
+ external: "external";
906
+ }>;
902
907
  started_at: z.ZodISODateTime;
903
908
  updated_at: z.ZodISODateTime;
904
909
  seq: z.ZodNumber;
@@ -946,12 +951,17 @@ export declare const invokeOrServiceResponse: z.ZodUnion<readonly [z.ZodObject<{
946
951
  robot_id: z.ZodUUID;
947
952
  slug: z.ZodString;
948
953
  state: z.ZodEnum<{
954
+ unknown: "unknown";
949
955
  failed: "failed";
950
956
  running: "running";
951
957
  succeeded: "succeeded";
952
958
  cancelled: "cancelled";
953
959
  lost: "lost";
954
960
  }>;
961
+ origin: z.ZodEnum<{
962
+ fleetless: "fleetless";
963
+ external: "external";
964
+ }>;
955
965
  started_at: z.ZodISODateTime;
956
966
  updated_at: z.ZodISODateTime;
957
967
  seq: z.ZodNumber;
@@ -995,12 +1005,17 @@ export declare const jobResponse: z.ZodObject<{
995
1005
  robot_id: z.ZodUUID;
996
1006
  slug: z.ZodString;
997
1007
  state: z.ZodEnum<{
1008
+ unknown: "unknown";
998
1009
  failed: "failed";
999
1010
  running: "running";
1000
1011
  succeeded: "succeeded";
1001
1012
  cancelled: "cancelled";
1002
1013
  lost: "lost";
1003
1014
  }>;
1015
+ origin: z.ZodEnum<{
1016
+ fleetless: "fleetless";
1017
+ external: "external";
1018
+ }>;
1004
1019
  started_at: z.ZodISODateTime;
1005
1020
  updated_at: z.ZodISODateTime;
1006
1021
  seq: z.ZodNumber;
@@ -1088,12 +1103,17 @@ export declare const robotJobsResponse: z.ZodObject<{
1088
1103
  robot_id: z.ZodUUID;
1089
1104
  slug: z.ZodString;
1090
1105
  state: z.ZodEnum<{
1106
+ unknown: "unknown";
1091
1107
  failed: "failed";
1092
1108
  running: "running";
1093
1109
  succeeded: "succeeded";
1094
1110
  cancelled: "cancelled";
1095
1111
  lost: "lost";
1096
1112
  }>;
1113
+ origin: z.ZodEnum<{
1114
+ fleetless: "fleetless";
1115
+ external: "external";
1116
+ }>;
1097
1117
  started_at: z.ZodISODateTime;
1098
1118
  updated_at: z.ZodISODateTime;
1099
1119
  seq: z.ZodNumber;
package/dist/routes.js CHANGED
@@ -2086,7 +2086,9 @@ export const ROUTES = [
2086
2086
  '`202` with an `invokeResponse` the moment the job exists; a service answers `200` with a `serviceCallResponse` once the result is in — ' +
2087
2087
  'two shapes, carried by one union (`invokeOrServiceResponse`) and told apart by whether `kind` or a bare `result` arrives. Parameters are checked **before** anything about the world (offline, busy): ' +
2088
2088
  'the same request must get the same verdict whether or not the robot happens to be reachable, or a developer testing against an offline ' +
2089
- 'robot never learns their parameters were wrong. A service the robot reports as failed answers `502` carrying **the job\'s own error ' +
2089
+ 'robot never learns their parameters were wrong. A slug is `409 busy` while it holds a `running` job, an `unknown` one the robot has ' +
2090
+ 'not accounted for yet, or an `external` goal someone else started; the refusal\'s `details.running` names that job, `state` and ' +
2091
+ '`origin` included. A service the robot reports as failed answers `502` carrying **the job\'s own error ' +
2090
2092
  'code**, which is an open set and not one of the codes above.',
2091
2093
  },
2092
2094
  {
@@ -2113,8 +2115,9 @@ export const ROUTES = [
2113
2115
  errors: [...CLIENT_GUARD, 'invalid_uuid', 'not_found', 'validation_error', 'not_cancellable', 'robot_offline'], transport: 'http',
2114
2116
  notes: 'The body is optional: a bodyless `POST` was every caller\'s shape before `job_id` existed, and absent or `job_id: null` both mean ' +
2115
2117
  '"cancel whatever is running". A named `job_id` that is **not** what is running cancels nothing and answers `404` — the caller named an ' +
2116
- 'id and thereby ruled the other one out. A service is `422 not_cancellable`: a service call has no goal to cancel. Nothing running is a ' +
2117
- '`200` with `job: null`.',
2118
+ 'id and thereby ruled the other one out. An `external` job is cancelled the same way, through its goal id. Cancelling an `unknown` job ' +
2119
+ 'also cancels every external goal on its action, since one of them may be that job. A service is `422 not_cancellable`: a service call ' +
2120
+ 'has no goal to cancel. Nothing running is a `200` with `job: null`.',
2118
2121
  },
2119
2122
  {
2120
2123
  method: 'POST', path: '/api/robots/:id/publishers/:slug', section: 'commands',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "4.0.0",
3
+ "version": "5.0.0-next.1",
4
4
  "description": "Fleetless wire contracts: the bridge-cloud protocol, the REST API schemas and the error codes, as zod schemas with generated JSON Schema and OpenAPI artifacts.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dehne Robotik GmbH",