@fleetless/contracts 3.0.0 → 4.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.
- package/CHANGELOG.md +23 -1
- package/CONTRIBUTING.md +47 -30
- package/artifacts/constants.json +10 -2
- package/artifacts/schema/bridge-job-lost.schema.json +17 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +18 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol.d.ts +67 -6
- package/dist/protocol.js +66 -7
- package/dist/realtime.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,29 @@
|
|
|
3
3
|
All notable changes to `@fleetless/contracts` are recorded here. The format
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
5
|
project uses [semantic versioning](https://semver.org/spec/v2.0.0.html) over
|
|
6
|
-
the wire shapes.
|
|
6
|
+
the wire shapes. A pull request that changes what a consumer sees adds its
|
|
7
|
+
entry under `## [Unreleased]`; the release renames that heading to the
|
|
8
|
+
version.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Protocol 4: a job heartbeat, and a vanished action server ends its job.**
|
|
15
|
+
Protocol bumped: bridges from `4.1.0`, and protocol 3 sunsets
|
|
16
|
+
2026-12-27 (protocol 2 sunset 2026-12-21). The bridge now sends a
|
|
17
|
+
`job_update` heartbeat every `JOB_HEARTBEAT_INTERVAL_MS` for every running
|
|
18
|
+
job; `job_lost` gains an optional `error`, so a bridge that finds its
|
|
19
|
+
action server gone can say `action_server_lost` instead of leaving the
|
|
20
|
+
cloud to guess. `JOB_HEARTBEAT_TIMEOUT_MS` bounds a protocol-4 job's
|
|
21
|
+
silence once it has been heard from at all; `patience_ms` now bounds only
|
|
22
|
+
the acceptance gap on such a job (unchanged for protocol 3, which sends no
|
|
23
|
+
heartbeat). `JOB_OFFLINE_GRACE_MS` (five minutes) replaces the informal
|
|
24
|
+
one-minute disconnect grace a job got before. `errors.ts` documents
|
|
25
|
+
`action_server_lost` and every job error code already in use that had
|
|
26
|
+
never been written down: `action_failed`, `goal_rejected`,
|
|
27
|
+
`goal_send_failed`, `result_failed`, `goal_uncontrollable`,
|
|
28
|
+
`bridge_disconnected`, `config_changed`.
|
|
7
29
|
|
|
8
30
|
## [3.0.0] — 2026-09-22
|
|
9
31
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -73,8 +73,9 @@ an issue first — so we can say what else has to move with it.
|
|
|
73
73
|
|
|
74
74
|
**CI runs on GitHub Actions**, in this repository
|
|
75
75
|
(`.github/workflows/verify.yml`) — the suite, on every push and every pull
|
|
76
|
-
request. `release.yml`
|
|
77
|
-
|
|
76
|
+
request. `release.yml` (the **Release** button) calls that same file on the
|
|
77
|
+
commit it publishes, so a release is never checked by a different pipeline
|
|
78
|
+
than a push.
|
|
78
79
|
|
|
79
80
|
**Your pull request is verified, a fork's included** — the same file, the
|
|
80
81
|
same suite. The first run by a first-time contributor waits for a maintainer
|
|
@@ -82,10 +83,11 @@ to press approve on it; that is a button on your run, not a setting anybody
|
|
|
82
83
|
has to change, so checks sitting idle for a while are the queue and not a
|
|
83
84
|
failure. The run reads code and reaches nothing else: it is granted
|
|
84
85
|
`contents: read`, no secret is exposed to it, and publishing lives in a
|
|
85
|
-
workflow only a
|
|
86
|
+
workflow only a maintainer's own **Run workflow** press can trigger.
|
|
86
87
|
|
|
87
|
-
Run `pnpm typecheck && pnpm build && pnpm test &&
|
|
88
|
-
test
|
|
88
|
+
Run `pnpm typecheck && pnpm build && pnpm test && node --test
|
|
89
|
+
'.github/release/*.test.mjs' && pnpm artifacts && pnpm run test:pack`
|
|
90
|
+
yourself first and you've seen everything `verify` will tell you.
|
|
89
91
|
Mind `pnpm artifacts`: `artifacts/` is generated *and* committed, and CI
|
|
90
92
|
fails if regenerating it changes a file — so commit whatever it writes
|
|
91
93
|
together with the schema you changed. That is the single most common reason
|
|
@@ -123,22 +125,31 @@ is refused by a `prepublishOnly` script — the rule has a mechanism rather
|
|
|
123
125
|
than only a sentence. (`publish` is unaffected: it publishes the tarball
|
|
124
126
|
`verify` packed, and npm runs no prepare lifecycle for a tarball argument.)
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
**Release is a button**, not a tag you push. Press **Run workflow** on
|
|
129
|
+
`release` (the Actions tab), on `main`. The version comes from the
|
|
130
|
+
Conventional Commits since the last tag; a release PR turns
|
|
131
|
+
`CHANGELOG.md`'s `## [Unreleased]` into that version, dated, and sets it in
|
|
132
|
+
`package.json` — the same two things `scripts/verify-version-tag.mjs` always
|
|
133
|
+
checked, now written by the release PR instead of by hand. That PR merges
|
|
134
|
+
itself once the required `verify` check passes; the merge commit is tagged,
|
|
135
|
+
`verify.yml` runs again on it and packs the tarball, and `publish` ships
|
|
136
|
+
exactly that tarball.
|
|
137
|
+
|
|
138
|
+
A person still writes the `## [Unreleased]` entries — in the feature's own
|
|
139
|
+
pull request, as the change goes in — because the release only renames that
|
|
140
|
+
heading to a version; it never writes prose. An empty `## [Unreleased]`
|
|
141
|
+
refuses the release outright, before any branch or commit exists.
|
|
142
|
+
|
|
143
|
+
A pull request that moves the protocol window writes one of those entries
|
|
144
|
+
too, and `test/changelog.test.ts` is red until it does: some section has to
|
|
145
|
+
name the newest `bridge_from` together with the date the version before it
|
|
146
|
+
sunsets. It need not be the newest section — a release that leaves the
|
|
147
|
+
protocol alone has nothing true to restate about it.
|
|
148
|
+
|
|
149
|
+
For a pre-release — a branch elsewhere that must pin this change before it
|
|
150
|
+
is final — check `prerelease` among the workflow's inputs: it publishes
|
|
151
|
+
`X.Y.Z-next.N` under the `next` dist-tag, from any branch, with no tag, no
|
|
152
|
+
release PR and no changelog entry.
|
|
142
153
|
|
|
143
154
|
`publish` carries no npm token. It authenticates by **trusted publishing**:
|
|
144
155
|
GitHub mints a short-lived credential for the job, npm checks it against the
|
|
@@ -150,12 +161,18 @@ The job refuses a missing credential by name, rather than failing on an opaque
|
|
|
150
161
|
error from deep inside `npm publish`. There is no repository secret to add and
|
|
151
162
|
no `.npmrc` anywhere.
|
|
152
163
|
|
|
153
|
-
**If the publish job goes red after `npm publish` already ran,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
**If the publish job goes red after `npm publish` already ran, run Release
|
|
165
|
+
again.** It sees that npm already has the version and does not publish
|
|
166
|
+
twice — npm refuses to republish a version, and a second attempt would only
|
|
167
|
+
read like a broken run. The rerun continues from there: it waits for the
|
|
168
|
+
registry to serve it, then finishes. Check `npm view
|
|
169
|
+
@fleetless/contracts@<version>` first if you want to see for yourself before
|
|
170
|
+
pressing anything.
|
|
171
|
+
|
|
172
|
+
Creating or deleting a `v*` tag is restricted — the organisation ruleset
|
|
173
|
+
`release-tags` allows only admins and the release App, which is how the
|
|
174
|
+
workflow tags a release without a maintainer pushing one by hand. An admin
|
|
175
|
+
can still remove a bad tag; ask one if you are not. What removing it does
|
|
176
|
+
*not* undo is a publish: the tag is retractable, the npm version is not —
|
|
177
|
+
and Release computes the next version from the newest tag, so deleting one
|
|
178
|
+
changes what a later run proposes.
|
package/artifacts/constants.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"AUDIT_RETENTION_DAYS": 90,
|
|
3
|
-
"PROTOCOL_VERSION":
|
|
3
|
+
"PROTOCOL_VERSION": 4,
|
|
4
4
|
"PROTOCOL_VERSIONS": [
|
|
5
5
|
{
|
|
6
6
|
"version": 2,
|
|
@@ -10,13 +10,21 @@
|
|
|
10
10
|
{
|
|
11
11
|
"version": 3,
|
|
12
12
|
"bridge_from": "4.0.0",
|
|
13
|
+
"deprecated_at": "2026-09-28"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": 4,
|
|
17
|
+
"bridge_from": "4.1.0",
|
|
13
18
|
"deprecated_at": null
|
|
14
19
|
}
|
|
15
20
|
],
|
|
16
21
|
"PROTOCOL_SUNSET_DAYS": 90,
|
|
17
|
-
"LATEST_BRIDGE_VERSION": "4.
|
|
22
|
+
"LATEST_BRIDGE_VERSION": "4.1.0",
|
|
18
23
|
"CLOSE_ROBOT_DELETED": 4004,
|
|
19
24
|
"CLOSE_TOKEN_ROTATED": 4005,
|
|
25
|
+
"JOB_HEARTBEAT_INTERVAL_MS": 1000,
|
|
26
|
+
"JOB_HEARTBEAT_TIMEOUT_MS": 5000,
|
|
27
|
+
"JOB_OFFLINE_GRACE_MS": 300000,
|
|
20
28
|
"ASSET_UPLOAD_HEADERS": {
|
|
21
29
|
"kind": "x-fleetless-asset-kind",
|
|
22
30
|
"name": "x-fleetless-asset-name",
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
"format": "uuid",
|
|
14
14
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
15
15
|
}
|
|
16
|
+
},
|
|
17
|
+
"error": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"code": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"message": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": [
|
|
30
|
+
"code",
|
|
31
|
+
"message"
|
|
32
|
+
]
|
|
16
33
|
}
|
|
17
34
|
},
|
|
18
35
|
"required": [
|
|
@@ -13,6 +13,24 @@
|
|
|
13
13
|
"format": "uuid",
|
|
14
14
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
15
15
|
}
|
|
16
|
+
},
|
|
17
|
+
"error": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"code": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"message": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": [
|
|
30
|
+
"code",
|
|
31
|
+
"message"
|
|
32
|
+
],
|
|
33
|
+
"additionalProperties": false
|
|
16
34
|
}
|
|
17
35
|
},
|
|
18
36
|
"required": [
|
package/dist/errors.d.ts
CHANGED
|
@@ -49,5 +49,5 @@ export type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
|
|
|
49
49
|
* list is the shared vocabulary, not a closed set, so a new refusal never
|
|
50
50
|
* needs a contracts release before it can be reported honestly.
|
|
51
51
|
*/
|
|
52
|
-
export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
|
|
52
|
+
export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "action_server_lost", "action_failed", "goal_rejected", "goal_send_failed", "result_failed", "goal_uncontrollable", "bridge_disconnected", "config_changed", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
|
|
53
53
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
package/dist/errors.js
CHANGED
|
@@ -71,6 +71,13 @@ export const ERROR_CODES = [
|
|
|
71
71
|
'no_data',
|
|
72
72
|
// Talking to the robot.
|
|
73
73
|
'robot_offline',
|
|
74
|
+
/**
|
|
75
|
+
* The cloud has heard nothing — heartbeat or real progress — from a
|
|
76
|
+
* running job for longer than it tolerates while the bridge is connected:
|
|
77
|
+
* `patience_ms` for a protocol-3 bridge, `JOB_HEARTBEAT_TIMEOUT_MS` for a
|
|
78
|
+
* protocol-4 one once it has heard from the job at all. `job.error.code`
|
|
79
|
+
* on `lost`.
|
|
80
|
+
*/
|
|
74
81
|
'bridge_timeout',
|
|
75
82
|
// Identity and rights. `forbidden` is deliberately the answer both
|
|
76
83
|
// for "your role does not grant this" and for "there is no such slug":
|
|
@@ -140,6 +147,37 @@ export const ERROR_CODES = [
|
|
|
140
147
|
'parameter_invalid',
|
|
141
148
|
/** The bridge could not account for this job after a restart. */
|
|
142
149
|
'job_lost',
|
|
150
|
+
/**
|
|
151
|
+
* The robot's action server vanished mid-goal — the bridge's own liveness
|
|
152
|
+
* check found `server_is_ready()` false for three seconds straight and
|
|
153
|
+
* gave up waiting for it to come back. A `job.error.code` on `lost`: the
|
|
154
|
+
* outcome the goal actually reached is unknown, so `lost` — not `failed` —
|
|
155
|
+
* is the honest state, and this code says why.
|
|
156
|
+
*/
|
|
157
|
+
'action_server_lost',
|
|
158
|
+
/** The action ended with a ROS status other than succeeded; `job.error.code` on `failed`. */
|
|
159
|
+
'action_failed',
|
|
160
|
+
/** The action server rejected the goal outright; `job.error.code` on `failed`. */
|
|
161
|
+
'goal_rejected',
|
|
162
|
+
/** Sending the goal to the action server itself raised; `job.error.code` on `failed`. */
|
|
163
|
+
'goal_send_failed',
|
|
164
|
+
/** Asking the action server for its result raised; `job.error.code` on `failed`. */
|
|
165
|
+
'result_failed',
|
|
166
|
+
/** A goal accepted after its own timeout could not then be cancelled; `job.error.code` on `failed`. */
|
|
167
|
+
'goal_uncontrollable',
|
|
168
|
+
/**
|
|
169
|
+
* The robot stayed offline for longer than `JOB_OFFLINE_GRACE_MS` while a
|
|
170
|
+
* job was running. A late real outcome, if the robot reconnects and the
|
|
171
|
+
* bridge still has it, corrects this — it is not final the way a genuine
|
|
172
|
+
* bridge report is. `job.error.code` on `lost`.
|
|
173
|
+
*/
|
|
174
|
+
'bridge_disconnected',
|
|
175
|
+
/**
|
|
176
|
+
* The job's action or service no longer exists in the published
|
|
177
|
+
* configuration — a republish invalidated it while it was running.
|
|
178
|
+
* `job.error.code` on `cancelled`.
|
|
179
|
+
*/
|
|
180
|
+
'config_changed',
|
|
143
181
|
/** Another user holds this publisher and has not been quiet long enough. */
|
|
144
182
|
'publisher_busy',
|
|
145
183
|
/** A well-formed realtime frame this server does not know — the socket stays open. */
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { SLUG_RULE, ROS_NAME_RULE, ROS_TYPE_NAME_RULE, FIELD_PATH_RULE, slug, ro
|
|
|
3
3
|
export type { ApplyErrorKind, ApplyError } from './common.js';
|
|
4
4
|
export { MCP_PROTOCOL_VERSION, MCP_ENDPOINT_PATH, mcpAppEndpointPath, MCP_APP_PATHS, MCP_TOOL_NAME_MAX, MCP_ASSET_LINK_PATH, MCP_ASSET_LINK_TTL_MS, mcpToolNamePattern, mcpToolKind, mcpExposure, mcpCapabilities, mcpRobotDatasheet, mcpRolePreviewResponse, } from './mcp.js';
|
|
5
5
|
export type { McpAppPaths, McpToolKind, McpExposure, McpCapabilities, McpRobotDatasheet, McpRolePreviewResponse, } from './mcp.js';
|
|
6
|
-
export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDGE_VERSION, protocolStatus, minimumProtocolVersion, sunsetOf, statusFromTable, bridgeHello, cloudHelloOk, cloudHelloError, cloudPing, bridgePong, bridgeLinkMode, datapointFrame, bridgeState, cloudConfig, bridgeConfigApplied, cloudIntrospectRequest, bridgeIntrospect, cloudTypeRequest, bridgeTypeDefinitions, cloudInvoke, cloudCancel, cloudPublish, bridgeJobUpdate, bridgeJobLost, snapshotHeader, cloudCameraStart, cloudCameraStop, bridgeCameraState, SNAPSHOT_MAX_BYTES, CLOSE_ROBOT_DELETED, CLOSE_TOKEN_ROTATED, bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress, activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, } from './protocol.js';
|
|
6
|
+
export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDGE_VERSION, protocolStatus, minimumProtocolVersion, sunsetOf, statusFromTable, bridgeHello, cloudHelloOk, cloudHelloError, cloudPing, bridgePong, bridgeLinkMode, datapointFrame, bridgeState, cloudConfig, bridgeConfigApplied, cloudIntrospectRequest, bridgeIntrospect, cloudTypeRequest, bridgeTypeDefinitions, cloudInvoke, cloudCancel, cloudPublish, bridgeJobUpdate, bridgeJobLost, snapshotHeader, cloudCameraStart, cloudCameraStop, bridgeCameraState, SNAPSHOT_MAX_BYTES, CLOSE_ROBOT_DELETED, CLOSE_TOKEN_ROTATED, bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress, activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, JOB_HEARTBEAT_INTERVAL_MS, JOB_HEARTBEAT_TIMEOUT_MS, JOB_OFFLINE_GRACE_MS, } from './protocol.js';
|
|
7
7
|
export type { ProtocolVersionEntry, ProtocolStatus, BridgeHello, CloudHelloOk, CloudHelloError, CloudPing, BridgePong, BridgeLinkMode, DatapointFrame, BridgeState, CloudConfig, BridgeConfigApplied, CloudIntrospectRequest, BridgeIntrospect, CloudTypeRequest, BridgeTypeDefinitions, CloudInvoke, CloudCancel, CloudPublish, BridgeJobUpdate, BridgeJobLost, SnapshotHeader, CloudCameraStart, CloudCameraStop, BridgeCameraState, ActiveJob, BridgeAssetsAvailable, CloudAssetRequest, BridgeAssetProgress, } from './protocol.js';
|
|
8
8
|
export { jobState, job, jobEvent, busyDetails, publisherBusyDetails, jobQueueFullDetails } from './jobs.js';
|
|
9
9
|
export type { JobState, Job, JobEvent, BusyDetails, PublisherBusyDetails, JobQueueFullDetails, } from './jobs.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDG
|
|
|
5
5
|
// Assets.
|
|
6
6
|
bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress,
|
|
7
7
|
// Addressing.
|
|
8
|
-
activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, } from './protocol.js';
|
|
8
|
+
activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, JOB_HEARTBEAT_INTERVAL_MS, JOB_HEARTBEAT_TIMEOUT_MS, JOB_OFFLINE_GRACE_MS, } from './protocol.js';
|
|
9
9
|
export { jobState, job, jobEvent, busyDetails, publisherBusyDetails, jobQueueFullDetails } from './jobs.js';
|
|
10
10
|
export { JOB_RUN_PAGE_MAX, JOB_RUN_RETENTION_DAYS, jobActor, jobRunKind, jobRun, jobRunQuery, jobRunListResponse, jobRunSummaryQuery, jobRunSummary, } from './jobs.js';
|
|
11
11
|
export { FLEETLESS_FORMAT_VERSION, RESERVED_SLUGS, parameterType, parameterSpec, parameterMap, serviceDescription, parameterDescription, messageTemplate, messageRef, messageBody, messageMap, PLACEHOLDER_RE, placeholderNames, actionConfig, serviceConfig, publisherConfig, cameraConfig, alertCondition, datapointAlert, rateThrottleHz, datapointNumeric, datapointRetention, datapointChart, datapointConfig, lowBandwidthSection, LOW_BANDWIDTH_DEFAULTS, robotConfigDoc, validationIssue, configState, snapshotIntervalSeconds,
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* Bridge <-> cloud protocol, version
|
|
4
|
+
* Bridge <-> cloud protocol, version 4.
|
|
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
|
|
@@ -11,6 +11,18 @@ import { z } from 'zod';
|
|
|
11
11
|
* which names the window and reaches the robot's detail view as
|
|
12
12
|
* `last_hello_error`.
|
|
13
13
|
*
|
|
14
|
+
* **4 (2026-09-28):** the bridge sends a `job_update` heartbeat at
|
|
15
|
+
* `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
|
|
16
|
+
* action said anything new, and reports a vanished action server with
|
|
17
|
+
* `job_lost`'s new optional `error`, `action_server_lost`. The cloud bounds
|
|
18
|
+
* a protocol-4 job's silence by the heartbeat (`JOB_HEARTBEAT_TIMEOUT_MS`)
|
|
19
|
+
* once it has heard from the job at all; `patience_ms` still bounds
|
|
20
|
+
* acceptance, the same as before. Offline tolerance is
|
|
21
|
+
* `JOB_OFFLINE_GRACE_MS` (five minutes), up from the informal one minute a
|
|
22
|
+
* protocol-3 bridge got. A protocol-3 bridge sends no heartbeat and keeps
|
|
23
|
+
* today's behaviour exactly: `patience_ms` alone bounds the whole running
|
|
24
|
+
* job, silence included.
|
|
25
|
+
*
|
|
14
26
|
* **3 (2026-09-22):** the ping carries `latency_ms` and `lag_ms`, the bridge
|
|
15
27
|
* sends `link_mode`, `bridge_state` gains `low_bandwidth`, and the
|
|
16
28
|
* `bridge_pressure` datapoint is gone. A protocol-2 bridge is served until
|
|
@@ -23,7 +35,7 @@ import { z } from 'zod';
|
|
|
23
35
|
* **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
|
|
24
36
|
* beside `message`.
|
|
25
37
|
*/
|
|
26
|
-
export declare const PROTOCOL_VERSION =
|
|
38
|
+
export declare const PROTOCOL_VERSION = 4;
|
|
27
39
|
/** Days between a version's deprecation and its sunset. */
|
|
28
40
|
export declare const PROTOCOL_SUNSET_DAYS = 90;
|
|
29
41
|
export interface ProtocolVersionEntry {
|
|
@@ -36,13 +48,15 @@ export interface ProtocolVersionEntry {
|
|
|
36
48
|
/**
|
|
37
49
|
* Every protocol version the cloud has served, oldest first. A test keeps
|
|
38
50
|
* exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
|
|
39
|
-
* requires
|
|
40
|
-
*
|
|
41
|
-
*
|
|
51
|
+
* requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
|
|
52
|
+
* the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
|
|
53
|
+
* date, so the pull request that moves this window is the one that fails
|
|
54
|
+
* without saying so; and `scripts/verify-version-tag.mjs` requires a dated
|
|
55
|
+
* heading for the tag being released.
|
|
42
56
|
*/
|
|
43
57
|
export declare const PROTOCOL_VERSIONS: readonly ProtocolVersionEntry[];
|
|
44
58
|
/** The newest bridge package. The cloud mails organisations still below it. */
|
|
45
|
-
export declare const LATEST_BRIDGE_VERSION = "4.
|
|
59
|
+
export declare const LATEST_BRIDGE_VERSION = "4.1.0";
|
|
46
60
|
export interface ProtocolStatus {
|
|
47
61
|
status: 'current' | 'deprecated' | 'unsupported';
|
|
48
62
|
/** ISO date, or null for a current or unknown version. */
|
|
@@ -128,6 +142,40 @@ export declare const MAX_PATIENCE_MS = 120000;
|
|
|
128
142
|
* this end bounds what the caller can do to the robot.
|
|
129
143
|
*/
|
|
130
144
|
export declare const MIN_PATIENCE_MS = 1000;
|
|
145
|
+
/**
|
|
146
|
+
* How often a protocol-4 bridge sends a `job_update` heartbeat for every
|
|
147
|
+
* running job — the last known state, whether or not the action itself said
|
|
148
|
+
* anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
|
|
149
|
+
* can be a small multiple of it and still absorb a missed beat or two, rare
|
|
150
|
+
* enough that it costs nothing next to the datapoint traffic a busy robot
|
|
151
|
+
* already sends.
|
|
152
|
+
*/
|
|
153
|
+
export declare const JOB_HEARTBEAT_INTERVAL_MS = 1000;
|
|
154
|
+
/**
|
|
155
|
+
* How long a protocol-4 job may go without a `job_update` — heartbeat or
|
|
156
|
+
* real progress, either counts — before the cloud settles it `lost` with
|
|
157
|
+
* `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
|
|
158
|
+
* slack for an ordinary scheduling jitter, small next to `patience_ms`
|
|
159
|
+
* because it no longer has to cover the acceptance gap too. `patience_ms`
|
|
160
|
+
* bounds only the time from `invoke` to the *first* update on a protocol-4
|
|
161
|
+
* job; every rearm after that uses this constant instead. A protocol-3
|
|
162
|
+
* bridge sends no heartbeat, so this constant does not apply to it —
|
|
163
|
+
* `patience_ms` keeps bounding the whole running job there, exactly as
|
|
164
|
+
* before.
|
|
165
|
+
*/
|
|
166
|
+
export declare const JOB_HEARTBEAT_TIMEOUT_MS = 5000;
|
|
167
|
+
/**
|
|
168
|
+
* How long a running job survives its robot going offline before the cloud
|
|
169
|
+
* gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
|
|
170
|
+
* long enough that an ordinary Wi-Fi dead zone — the case this constant
|
|
171
|
+
* exists for — never costs a job, since a robot with no safety layer of its
|
|
172
|
+
* own (§ Fleetless is not a safety layer) keeps driving through one and the
|
|
173
|
+
* result the cloud is waiting for is often still coming. A robot connected
|
|
174
|
+
* the whole time never reaches this bound at all: while online, silence is
|
|
175
|
+
* `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
|
|
176
|
+
* (protocol 3), never this one's.
|
|
177
|
+
*/
|
|
178
|
+
export declare const JOB_OFFLINE_GRACE_MS = 300000;
|
|
131
179
|
/** Re-exported so consumers keep importing wire names from one place. */
|
|
132
180
|
export { slug } from './common.js';
|
|
133
181
|
/**
|
|
@@ -598,10 +646,23 @@ export type BridgeJobUpdate = z.infer<typeof bridgeJobUpdate>;
|
|
|
598
646
|
* left to enumerate, so it is `hello.active_job_ids` that closes that gap.
|
|
599
647
|
* Both paths end in the same place — the cloud publishes `lost` rather than
|
|
600
648
|
* leaving a job reading "running" because nobody contradicted it.
|
|
649
|
+
*
|
|
650
|
+
* **`error` (since protocol 4) is optional and, when present, applies to
|
|
651
|
+
* every job named in `job_ids`.** A vanished action server is discovered
|
|
652
|
+
* once, by the bridge's own liveness check on that one goal, so a frame
|
|
653
|
+
* naming several jobs at once — plausible if several goals shared the same
|
|
654
|
+
* server — always shares the same cause. Absent means today's behaviour:
|
|
655
|
+
* the cloud settles the job `lost` with no specific code, the same as a
|
|
656
|
+
* protocol-3 bridge's frame, which carries no `error` at all and still
|
|
657
|
+
* parses under this schema unchanged.
|
|
601
658
|
*/
|
|
602
659
|
export declare const bridgeJobLost: z.ZodObject<{
|
|
603
660
|
type: z.ZodLiteral<"job_lost">;
|
|
604
661
|
job_ids: z.ZodArray<z.ZodUUID>;
|
|
662
|
+
error: z.ZodOptional<z.ZodObject<{
|
|
663
|
+
code: z.ZodString;
|
|
664
|
+
message: z.ZodString;
|
|
665
|
+
}, z.core.$strip>>;
|
|
605
666
|
}, z.core.$strip>;
|
|
606
667
|
export type BridgeJobLost = z.infer<typeof bridgeJobLost>;
|
|
607
668
|
/** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
|
package/dist/protocol.js
CHANGED
|
@@ -7,7 +7,7 @@ import { rosGraph, typeDefinition } from './introspection.js';
|
|
|
7
7
|
import { jobState } from './jobs.js';
|
|
8
8
|
import { rosTypeName } from './common.js';
|
|
9
9
|
/**
|
|
10
|
-
* Bridge <-> cloud protocol, version
|
|
10
|
+
* Bridge <-> cloud protocol, version 4.
|
|
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
|
|
@@ -17,6 +17,18 @@ import { rosTypeName } from './common.js';
|
|
|
17
17
|
* which names the window and reaches the robot's detail view as
|
|
18
18
|
* `last_hello_error`.
|
|
19
19
|
*
|
|
20
|
+
* **4 (2026-09-28):** the bridge sends a `job_update` heartbeat at
|
|
21
|
+
* `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
|
|
22
|
+
* action said anything new, and reports a vanished action server with
|
|
23
|
+
* `job_lost`'s new optional `error`, `action_server_lost`. The cloud bounds
|
|
24
|
+
* a protocol-4 job's silence by the heartbeat (`JOB_HEARTBEAT_TIMEOUT_MS`)
|
|
25
|
+
* once it has heard from the job at all; `patience_ms` still bounds
|
|
26
|
+
* acceptance, the same as before. Offline tolerance is
|
|
27
|
+
* `JOB_OFFLINE_GRACE_MS` (five minutes), up from the informal one minute a
|
|
28
|
+
* protocol-3 bridge got. A protocol-3 bridge sends no heartbeat and keeps
|
|
29
|
+
* today's behaviour exactly: `patience_ms` alone bounds the whole running
|
|
30
|
+
* job, silence included.
|
|
31
|
+
*
|
|
20
32
|
* **3 (2026-09-22):** the ping carries `latency_ms` and `lag_ms`, the bridge
|
|
21
33
|
* sends `link_mode`, `bridge_state` gains `low_bandwidth`, and the
|
|
22
34
|
* `bridge_pressure` datapoint is gone. A protocol-2 bridge is served until
|
|
@@ -29,22 +41,25 @@ import { rosTypeName } from './common.js';
|
|
|
29
41
|
* **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
|
|
30
42
|
* beside `message`.
|
|
31
43
|
*/
|
|
32
|
-
export const PROTOCOL_VERSION =
|
|
44
|
+
export const PROTOCOL_VERSION = 4;
|
|
33
45
|
/** Days between a version's deprecation and its sunset. */
|
|
34
46
|
export const PROTOCOL_SUNSET_DAYS = 90;
|
|
35
47
|
/**
|
|
36
48
|
* Every protocol version the cloud has served, oldest first. A test keeps
|
|
37
49
|
* exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
|
|
38
|
-
* requires
|
|
39
|
-
*
|
|
40
|
-
*
|
|
50
|
+
* requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
|
|
51
|
+
* the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
|
|
52
|
+
* date, so the pull request that moves this window is the one that fails
|
|
53
|
+
* without saying so; and `scripts/verify-version-tag.mjs` requires a dated
|
|
54
|
+
* heading for the tag being released.
|
|
41
55
|
*/
|
|
42
56
|
export const PROTOCOL_VERSIONS = [
|
|
43
57
|
{ version: 2, bridge_from: '3.0.0', deprecated_at: '2026-09-22' },
|
|
44
|
-
{ version: 3, bridge_from: '4.0.0', deprecated_at:
|
|
58
|
+
{ version: 3, bridge_from: '4.0.0', deprecated_at: '2026-09-28' },
|
|
59
|
+
{ version: 4, bridge_from: '4.1.0', deprecated_at: null },
|
|
45
60
|
];
|
|
46
61
|
/** The newest bridge package. The cloud mails organisations still below it. */
|
|
47
|
-
export const LATEST_BRIDGE_VERSION = '4.
|
|
62
|
+
export const LATEST_BRIDGE_VERSION = '4.1.0';
|
|
48
63
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
49
64
|
function isoDate(date) {
|
|
50
65
|
return date.toISOString().slice(0, 10);
|
|
@@ -146,6 +161,40 @@ export const MAX_PATIENCE_MS = 120_000;
|
|
|
146
161
|
* this end bounds what the caller can do to the robot.
|
|
147
162
|
*/
|
|
148
163
|
export const MIN_PATIENCE_MS = 1_000;
|
|
164
|
+
/**
|
|
165
|
+
* How often a protocol-4 bridge sends a `job_update` heartbeat for every
|
|
166
|
+
* running job — the last known state, whether or not the action itself said
|
|
167
|
+
* anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
|
|
168
|
+
* can be a small multiple of it and still absorb a missed beat or two, rare
|
|
169
|
+
* enough that it costs nothing next to the datapoint traffic a busy robot
|
|
170
|
+
* already sends.
|
|
171
|
+
*/
|
|
172
|
+
export const JOB_HEARTBEAT_INTERVAL_MS = 1_000;
|
|
173
|
+
/**
|
|
174
|
+
* How long a protocol-4 job may go without a `job_update` — heartbeat or
|
|
175
|
+
* real progress, either counts — before the cloud settles it `lost` with
|
|
176
|
+
* `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
|
|
177
|
+
* slack for an ordinary scheduling jitter, small next to `patience_ms`
|
|
178
|
+
* because it no longer has to cover the acceptance gap too. `patience_ms`
|
|
179
|
+
* bounds only the time from `invoke` to the *first* update on a protocol-4
|
|
180
|
+
* job; every rearm after that uses this constant instead. A protocol-3
|
|
181
|
+
* bridge sends no heartbeat, so this constant does not apply to it —
|
|
182
|
+
* `patience_ms` keeps bounding the whole running job there, exactly as
|
|
183
|
+
* before.
|
|
184
|
+
*/
|
|
185
|
+
export const JOB_HEARTBEAT_TIMEOUT_MS = 5_000;
|
|
186
|
+
/**
|
|
187
|
+
* How long a running job survives its robot going offline before the cloud
|
|
188
|
+
* gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
|
|
189
|
+
* long enough that an ordinary Wi-Fi dead zone — the case this constant
|
|
190
|
+
* exists for — never costs a job, since a robot with no safety layer of its
|
|
191
|
+
* own (§ Fleetless is not a safety layer) keeps driving through one and the
|
|
192
|
+
* result the cloud is waiting for is often still coming. A robot connected
|
|
193
|
+
* the whole time never reaches this bound at all: while online, silence is
|
|
194
|
+
* `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
|
|
195
|
+
* (protocol 3), never this one's.
|
|
196
|
+
*/
|
|
197
|
+
export const JOB_OFFLINE_GRACE_MS = 300_000;
|
|
149
198
|
/** Re-exported so consumers keep importing wire names from one place. */
|
|
150
199
|
export { slug } from './common.js';
|
|
151
200
|
/**
|
|
@@ -440,10 +489,20 @@ export const bridgeJobUpdate = z.object({
|
|
|
440
489
|
* left to enumerate, so it is `hello.active_job_ids` that closes that gap.
|
|
441
490
|
* Both paths end in the same place — the cloud publishes `lost` rather than
|
|
442
491
|
* leaving a job reading "running" because nobody contradicted it.
|
|
492
|
+
*
|
|
493
|
+
* **`error` (since protocol 4) is optional and, when present, applies to
|
|
494
|
+
* every job named in `job_ids`.** A vanished action server is discovered
|
|
495
|
+
* once, by the bridge's own liveness check on that one goal, so a frame
|
|
496
|
+
* naming several jobs at once — plausible if several goals shared the same
|
|
497
|
+
* server — always shares the same cause. Absent means today's behaviour:
|
|
498
|
+
* the cloud settles the job `lost` with no specific code, the same as a
|
|
499
|
+
* protocol-3 bridge's frame, which carries no `error` at all and still
|
|
500
|
+
* parses under this schema unchanged.
|
|
443
501
|
*/
|
|
444
502
|
export const bridgeJobLost = z.object({
|
|
445
503
|
type: z.literal('job_lost'),
|
|
446
504
|
job_ids: z.array(z.uuid()),
|
|
505
|
+
error: z.object({ code: z.string().min(1), message: z.string().min(1) }).optional(),
|
|
447
506
|
});
|
|
448
507
|
/** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
|
|
449
508
|
export const cloudIntrospectRequest = z.object({
|
package/dist/realtime.d.ts
CHANGED
|
@@ -256,8 +256,8 @@ export declare const liveSessionEndReason: z.ZodEnum<{
|
|
|
256
256
|
unknown: "unknown";
|
|
257
257
|
publish_failed: "publish_failed";
|
|
258
258
|
robot_offline: "robot_offline";
|
|
259
|
-
released_by_peer: "released_by_peer";
|
|
260
259
|
config_changed: "config_changed";
|
|
260
|
+
released_by_peer: "released_by_peer";
|
|
261
261
|
revoked: "revoked";
|
|
262
262
|
expired: "expired";
|
|
263
263
|
robot_deleted: "robot_deleted";
|
|
@@ -287,8 +287,8 @@ export declare const liveSessionEvent: z.ZodObject<{
|
|
|
287
287
|
unknown: "unknown";
|
|
288
288
|
publish_failed: "publish_failed";
|
|
289
289
|
robot_offline: "robot_offline";
|
|
290
|
-
released_by_peer: "released_by_peer";
|
|
291
290
|
config_changed: "config_changed";
|
|
291
|
+
released_by_peer: "released_by_peer";
|
|
292
292
|
revoked: "revoked";
|
|
293
293
|
expired: "expired";
|
|
294
294
|
robot_deleted: "robot_deleted";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetless/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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",
|