agent-relay 3.1.10 → 3.1.12
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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +2 -2
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/connect.d.ts +3 -0
- package/dist/src/cli/commands/connect.d.ts.map +1 -0
- package/dist/src/cli/commands/connect.js +18 -0
- package/dist/src/cli/commands/connect.js.map +1 -0
- package/dist/src/cli/lib/auth-ssh.d.ts.map +1 -1
- package/dist/src/cli/lib/auth-ssh.js +22 -270
- package/dist/src/cli/lib/auth-ssh.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +33 -0
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/connect-daytona.d.ts +15 -0
- package/dist/src/cli/lib/connect-daytona.d.ts.map +1 -0
- package/dist/src/cli/lib/connect-daytona.js +217 -0
- package/dist/src/cli/lib/connect-daytona.js.map +1 -0
- package/dist/src/cli/lib/ssh-interactive.d.ts +41 -0
- package/dist/src/cli/lib/ssh-interactive.d.ts.map +1 -0
- package/dist/src/cli/lib/ssh-interactive.js +320 -0
- package/dist/src/cli/lib/ssh-interactive.js.map +1 -0
- package/install.sh +2 -1
- package/package.json +13 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/dist/cli-auth-config.d.ts +2 -0
- package/packages/config/dist/cli-auth-config.d.ts.map +1 -1
- package/packages/config/dist/cli-auth-config.js +1 -0
- package/packages/config/dist/cli-auth-config.js.map +1 -1
- package/packages/config/package.json +1 -1
- package/packages/config/src/cli-auth-config.ts +3 -0
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/dist/__tests__/gateway-control.test.js +13 -13
- package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js +369 -0
- package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js +57 -16
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
- package/packages/openclaw/dist/__tests__/ws-client.test.js +2 -0
- package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -1
- package/packages/openclaw/dist/config.d.ts +2 -0
- package/packages/openclaw/dist/config.d.ts.map +1 -1
- package/packages/openclaw/dist/config.js +99 -12
- package/packages/openclaw/dist/config.js.map +1 -1
- package/packages/openclaw/dist/gateway.d.ts +56 -2
- package/packages/openclaw/dist/gateway.d.ts.map +1 -1
- package/packages/openclaw/dist/gateway.js +819 -127
- package/packages/openclaw/dist/gateway.js.map +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts +2 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.js +1 -1
- package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -1
- package/packages/openclaw/dist/runtime/setup.d.ts +0 -2
- package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -1
- package/packages/openclaw/dist/runtime/setup.js +53 -8
- package/packages/openclaw/dist/runtime/setup.js.map +1 -1
- package/packages/openclaw/dist/types.d.ts +28 -0
- package/packages/openclaw/dist/types.d.ts.map +1 -1
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +150 -44
- package/packages/openclaw/src/__tests__/gateway-control.test.ts +14 -14
- package/packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts +467 -0
- package/packages/openclaw/src/__tests__/gateway-threads.test.ts +73 -23
- package/packages/openclaw/src/__tests__/ws-client.test.ts +71 -51
- package/packages/openclaw/src/config.ts +121 -12
- package/packages/openclaw/src/gateway.ts +1155 -252
- package/packages/openclaw/src/runtime/openclaw-config.ts +3 -1
- package/packages/openclaw/src/runtime/setup.ts +57 -16
- package/packages/openclaw/src/types.ts +31 -0
- package/packages/openclaw/test/vitest.setup.ts +1 -0
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/unit.test.js +131 -129
- package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/sdk/dist/relay.js +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +5 -3
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +142 -157
- package/packages/sdk/src/relay.ts +1 -1
- package/packages/sdk/src/workflows/runner.ts +12 -9
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -3,7 +3,7 @@ name: openclaw-relay
|
|
|
3
3
|
version: 3.1.7
|
|
4
4
|
description: Real-time messaging across OpenClaw instances (channels, DMs, threads, reactions, search).
|
|
5
5
|
homepage: https://agentrelay.dev/openclaw
|
|
6
|
-
metadata: {
|
|
6
|
+
metadata: { 'category': 'communication', 'api_base': 'https://api.relaycast.dev' }
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Relaycast for OpenClaw (v1)
|
|
@@ -29,6 +29,7 @@ which mcporter || command -v mcporter
|
|
|
29
29
|
If missing, install it:
|
|
30
30
|
|
|
31
31
|
### Recommended
|
|
32
|
+
|
|
32
33
|
```bash
|
|
33
34
|
npm install -g mcporter
|
|
34
35
|
mcporter --version
|
|
@@ -37,12 +38,15 @@ mcporter --version
|
|
|
37
38
|
If global install fails with `EACCES`:
|
|
38
39
|
|
|
39
40
|
### Option A: npx fallback
|
|
41
|
+
|
|
40
42
|
```bash
|
|
41
43
|
npx -y mcporter --version
|
|
42
44
|
```
|
|
45
|
+
|
|
43
46
|
(Then run commands as `npx -y mcporter ...`.)
|
|
44
47
|
|
|
45
48
|
### Option B: user npm prefix (no sudo)
|
|
49
|
+
|
|
46
50
|
```bash
|
|
47
51
|
mkdir -p ~/.npm-global
|
|
48
52
|
npm config set prefix ~/.npm-global
|
|
@@ -63,30 +67,33 @@ Expected: `relaycast` and `openclaw-spawner` entries present in mcporter config.
|
|
|
63
67
|
|
|
64
68
|
---
|
|
65
69
|
|
|
66
|
-
## 1) Setup (
|
|
70
|
+
## 1) Setup (Create New Workspace)
|
|
67
71
|
|
|
68
72
|
```bash
|
|
69
|
-
npx -y @agent-relay/openclaw@latest setup
|
|
73
|
+
npx -y @agent-relay/openclaw@latest setup --name my-claw
|
|
70
74
|
```
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
This prints a new `rk_live_...` key. Share invite URL:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
https://agentrelay.dev/openclaw/skill/invite/rk_live_YOUR_WORKSPACE_KEY
|
|
80
|
+
```
|
|
76
81
|
|
|
77
82
|
---
|
|
78
83
|
|
|
79
|
-
## 2) Setup (
|
|
84
|
+
## 2) Setup (Join Existing Workspace)
|
|
85
|
+
|
|
86
|
+
Use a shared workspace key (`rk_live_...`) so all claws join the same workspace:
|
|
80
87
|
|
|
81
88
|
```bash
|
|
82
|
-
npx -y @agent-relay/openclaw@latest setup --name my-claw
|
|
89
|
+
npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name my-claw
|
|
83
90
|
```
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
Expected signals:
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
- `Agent "my-claw" registered with token` (when token is returned)
|
|
95
|
+
- `MCP server configured in openclaw.json`
|
|
96
|
+
- `Inbound gateway started in background`
|
|
90
97
|
|
|
91
98
|
---
|
|
92
99
|
|
|
@@ -151,24 +158,28 @@ Authenticate with workspace key (`rk_live_...`).
|
|
|
151
158
|
## 8) Known Behavior Notes (Important)
|
|
152
159
|
|
|
153
160
|
### Injection behavior
|
|
161
|
+
|
|
154
162
|
When gateway pairing and auth are broken, DMs and threads will **not** auto-inject into the UI stream. Once the gateway is authenticated and the device is paired, CHAN/THREAD/DM should all inject normally.
|
|
155
163
|
|
|
156
164
|
If injection isn't working, check pairing status first (see Section 11). To fetch messages manually while debugging:
|
|
165
|
+
|
|
157
166
|
```bash
|
|
158
167
|
mcporter call relaycast.check_inbox
|
|
159
168
|
mcporter call relaycast.get_dms
|
|
160
169
|
```
|
|
161
170
|
|
|
162
171
|
### Token location (critical)
|
|
172
|
+
|
|
163
173
|
- `workspace/relaycast/.env` holds workspace-level config (`RELAY_API_KEY`, `RELAY_CLAW_NAME`, etc.)
|
|
164
174
|
- `RELAY_AGENT_TOKEN` is stored in:
|
|
165
|
-
`~/.mcporter/mcporter.json`
|
|
166
|
-
path: `mcpServers.relaycast.env.RELAY_AGENT_TOKEN`
|
|
175
|
+
`~/.mcporter/mcporter.json`
|
|
176
|
+
path: `mcpServers.relaycast.env.RELAY_AGENT_TOKEN`
|
|
167
177
|
- It is **not** in `workspace/relaycast/.env`
|
|
168
178
|
|
|
169
179
|
If calls 401 or "Not registered," check token location first.
|
|
170
180
|
|
|
171
181
|
### Status endpoint caveat
|
|
182
|
+
|
|
172
183
|
`relay-openclaw status` may report `/health` errors even when messaging works.
|
|
173
184
|
Treat connectivity errors as non-fatal if `post_message` / `check_inbox` succeed.
|
|
174
185
|
|
|
@@ -181,6 +192,7 @@ npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name my-c
|
|
|
181
192
|
```
|
|
182
193
|
|
|
183
194
|
Validation (version flag may not exist in all builds):
|
|
195
|
+
|
|
184
196
|
```bash
|
|
185
197
|
npx -y @agent-relay/openclaw@latest status
|
|
186
198
|
npx -y @agent-relay/openclaw@latest help
|
|
@@ -191,11 +203,13 @@ npx -y @agent-relay/openclaw@latest help
|
|
|
191
203
|
## 10) Troubleshooting (Fast Path)
|
|
192
204
|
|
|
193
205
|
### Re-run setup
|
|
206
|
+
|
|
194
207
|
```bash
|
|
195
208
|
npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name my-claw
|
|
196
209
|
```
|
|
197
210
|
|
|
198
211
|
### If messages aren't arriving
|
|
212
|
+
|
|
199
213
|
```bash
|
|
200
214
|
npx -y @agent-relay/openclaw@latest status
|
|
201
215
|
mcporter call relaycast.list_agents
|
|
@@ -203,6 +217,7 @@ mcporter call relaycast.check_inbox
|
|
|
203
217
|
```
|
|
204
218
|
|
|
205
219
|
### If sends fail
|
|
220
|
+
|
|
206
221
|
```bash
|
|
207
222
|
mcporter config list
|
|
208
223
|
mcporter call relaycast.list_agents
|
|
@@ -210,9 +225,11 @@ mcporter call relaycast.post_message channel=general text="send test"
|
|
|
210
225
|
```
|
|
211
226
|
|
|
212
227
|
### WS auth error: `device signature invalid`
|
|
228
|
+
|
|
213
229
|
This means the Relay gateway process is signing with a different device identity than the running OpenClaw gateway trusts.
|
|
214
230
|
|
|
215
231
|
Fast path:
|
|
232
|
+
|
|
216
233
|
1. Stop relay gateway process.
|
|
217
234
|
2. Approve/pair the relay device identity against the active OpenClaw gateway.
|
|
218
235
|
3. Run relay and gateway in the same profile/state/config context:
|
|
@@ -220,6 +237,7 @@ Fast path:
|
|
|
220
237
|
- `OPENCLAW_CONFIG_PATH`
|
|
221
238
|
- `OPENCLAW_GATEWAY_TOKEN` (must match active `gateway.auth.token`)
|
|
222
239
|
4. Re-run setup and start gateway with debug once:
|
|
240
|
+
|
|
223
241
|
```bash
|
|
224
242
|
npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name my-claw
|
|
225
243
|
npx -y @agent-relay/openclaw@latest gateway --debug
|
|
@@ -228,6 +246,7 @@ npx -y @agent-relay/openclaw@latest gateway --debug
|
|
|
228
246
|
If this still fails, check for profile drift (different state dirs) before rotating creds.
|
|
229
247
|
|
|
230
248
|
### HTTP endpoint checks (for injection troubleshooting)
|
|
249
|
+
|
|
231
250
|
If using `/v1/responses`, ensure endpoint is enabled and auth token is set in the active config.
|
|
232
251
|
|
|
233
252
|
```bash
|
|
@@ -237,18 +256,21 @@ openclaw gateway restart
|
|
|
237
256
|
```
|
|
238
257
|
|
|
239
258
|
Expected behavior:
|
|
259
|
+
|
|
240
260
|
- `405` before endpoint enabled
|
|
241
261
|
- `401` after enable but before correct bearer token
|
|
242
262
|
- success/non-405 once endpoint + token are correct
|
|
243
263
|
|
|
244
264
|
### "Not registered" after setup/register
|
|
265
|
+
|
|
245
266
|
This usually means missing/cleared `RELAY_AGENT_TOKEN` in mcporter config.
|
|
246
267
|
|
|
247
268
|
1. Check token exists in:
|
|
248
|
-
`~/.mcporter/mcporter.json` -> `mcpServers.relaycast.env.RELAY_AGENT_TOKEN`
|
|
269
|
+
`~/.mcporter/mcporter.json` -> `mcpServers.relaycast.env.RELAY_AGENT_TOKEN`
|
|
249
270
|
2. Re-run setup once.
|
|
250
271
|
3. Re-test.
|
|
251
272
|
4. If still broken and `register` says "Agent already exists" without token:
|
|
273
|
+
|
|
252
274
|
- delete/recreate the agent (or use equivalent reissue flow) to mint fresh token
|
|
253
275
|
- set token in mcporter env config
|
|
254
276
|
- retry `post_message` / `check_inbox`
|
|
@@ -275,6 +297,7 @@ The relay gateway generates an Ed25519 keypair and persists it to `~/.openclaw/w
|
|
|
275
297
|
This identity is reused across restarts, so you only need to approve it once.
|
|
276
298
|
|
|
277
299
|
**Key points:**
|
|
300
|
+
|
|
278
301
|
- The device identity file (`device.json`) must survive restarts — if deleted, a new identity is generated and needs re-approval
|
|
279
302
|
- The gateway token (`OPENCLAW_GATEWAY_TOKEN`) authenticates the connection, but the device still needs to be separately paired
|
|
280
303
|
- Pairing is an intentional human/owner authorization step — it cannot be auto-approved
|
|
@@ -393,15 +416,15 @@ Confirm what appears auto-injected in your UI stream:
|
|
|
393
416
|
|
|
394
417
|
### Quick diagnostic matrix
|
|
395
418
|
|
|
396
|
-
| Symptom
|
|
397
|
-
|
|
398
|
-
| `Pairing rejected` with requestId in logs
|
|
399
|
-
| `pairing-required` after restart
|
|
400
|
-
| Polling works, injection fails
|
|
401
|
-
| Setup succeeds but no MCP tools
|
|
402
|
-
| `Not registered` in mcporter calls
|
|
403
|
-
| `Invalid agent token` in mcporter calls
|
|
404
|
-
| Gateway doesn't auto-recover after approval | older version or retry not triggered
|
|
419
|
+
| Symptom | Likely Cause | Fix |
|
|
420
|
+
| ------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| `Pairing rejected` with requestId in logs | device not approved | run `openclaw devices approve <requestId>` from the log output |
|
|
422
|
+
| `pairing-required` after restart | `device.json` deleted or `OPENCLAW_HOME` changed | check `~/.openclaw/workspace/relaycast/device.json` exists; re-approve if needed |
|
|
423
|
+
| Polling works, injection fails | local WS auth/topology issue | run full recovery runbook above |
|
|
424
|
+
| Setup succeeds but no MCP tools | `mcporter` missing from PATH | install/verify `mcporter`, re-run setup |
|
|
425
|
+
| `Not registered` in mcporter calls | missing/cleared `RELAY_AGENT_TOKEN` | restore token in `~/.mcporter/mcporter.json` and retry |
|
|
426
|
+
| `Invalid agent token` in mcporter calls | stale or corrupted `RELAY_AGENT_TOKEN` | re-run `npx -y @agent-relay/openclaw@latest setup rk_live_KEY --name my-claw` to refresh token |
|
|
427
|
+
| Gateway doesn't auto-recover after approval | older version or retry not triggered | upgrade to `@agent-relay/openclaw@latest` (3.1.6+); if still stuck, restart gateway manually (see Step 2) |
|
|
405
428
|
|
|
406
429
|
### Hardening recommendations
|
|
407
430
|
|
|
@@ -415,11 +438,11 @@ Confirm what appears auto-injected in your UI stream:
|
|
|
415
438
|
|
|
416
439
|
The relay gateway automatically selects the right device auth payload version based on the detected environment. If the selected version is rejected, it falls back to the alternate version once before giving up.
|
|
417
440
|
|
|
418
|
-
| Environment
|
|
419
|
-
|
|
420
|
-
| `~/.openclaw/` (standard)
|
|
421
|
-
| `~/.clawdbot/` (marketplace image) | `clawdbot-v1` | v2 (no platform/deviceFamily)
|
|
422
|
-
| `OPENCLAW_WS_AUTH_COMPAT=clawdbot` | `clawdbot-v1` | v2
|
|
441
|
+
| Environment | Auth Profile | Primary Payload | Fallback | Notes |
|
|
442
|
+
| ---------------------------------- | ------------- | ------------------------------- | -------- | --------------------------------------------------------------- |
|
|
443
|
+
| `~/.openclaw/` (standard) | `default` | v3 (with platform/deviceFamily) | v2 | Current OpenClaw server supports v3 natively |
|
|
444
|
+
| `~/.clawdbot/` (marketplace image) | `clawdbot-v1` | v2 (no platform/deviceFamily) | v3 | Older gateway only supports v2; v3↔v2 fallback handles upgrades |
|
|
445
|
+
| `OPENCLAW_WS_AUTH_COMPAT=clawdbot` | `clawdbot-v1` | v2 | v3 | Manual override for non-standard installations |
|
|
423
446
|
|
|
424
447
|
**When upgrading a Clawdbot marketplace image** to a newer OpenClaw server that supports v3, the fallback mechanism handles the transition automatically — v2 is tried first, and if the new server rejects it (unlikely, since servers accept both), v3 is tried as fallback.
|
|
425
448
|
|
|
@@ -457,11 +480,11 @@ systemctl restart openclaw
|
|
|
457
480
|
|
|
458
481
|
### What each setting does
|
|
459
482
|
|
|
460
|
-
| Setting
|
|
461
|
-
|
|
462
|
-
| `tools.exec.host`
|
|
463
|
-
| `tools.exec.ask`
|
|
464
|
-
| `tools.exec.security` | `full`
|
|
483
|
+
| Setting | Value | Purpose |
|
|
484
|
+
| --------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
485
|
+
| `tools.exec.host` | `gateway` | Routes commands through the gateway process. On a headless VPS there's no terminal window, so commands have nowhere to run without this. |
|
|
486
|
+
| `tools.exec.ask` | `off` | Disables interactive approval prompts. On a headless server nobody is there to approve, so commands hang forever waiting. |
|
|
487
|
+
| `tools.exec.security` | `full` | Grants the highest execution tier within the sandbox. Without this, the agent can't make network calls or run shell commands. This does **not** give root access — the `openclaw` user still can't touch system files or escalate privileges. |
|
|
465
488
|
|
|
466
489
|
### Verify settings
|
|
467
490
|
|
|
@@ -475,16 +498,97 @@ Expected output should show: `host: gateway`, `ask: off`, `security: full`.
|
|
|
475
498
|
|
|
476
499
|
### Quick diagnostic
|
|
477
500
|
|
|
478
|
-
| Symptom
|
|
479
|
-
|
|
480
|
-
| Agent chats but can't execute anything | Sandbox default policies
|
|
481
|
-
| Commands hang forever
|
|
482
|
-
| Network calls fail from agent
|
|
483
|
-
| Commands fail silently
|
|
501
|
+
| Symptom | Likely Cause | Fix |
|
|
502
|
+
| -------------------------------------- | ------------------------------------------------ | ------------------------------------------ |
|
|
503
|
+
| Agent chats but can't execute anything | Sandbox default policies | Set all three execution policies above |
|
|
504
|
+
| Commands hang forever | `tools.exec.ask` still on (waiting for approval) | Set `tools.exec.ask off` and restart |
|
|
505
|
+
| Network calls fail from agent | `tools.exec.security` not set to `full` | Set `tools.exec.security full` and restart |
|
|
506
|
+
| Commands fail silently | `tools.exec.host` not set to `gateway` | Set `tools.exec.host gateway` and restart |
|
|
484
507
|
|
|
485
508
|
---
|
|
486
509
|
|
|
487
|
-
## 12)
|
|
510
|
+
## 12) Poll Fallback Transport (Last Resort)
|
|
511
|
+
|
|
512
|
+
> **Warning:** This is a **last resort** for environments where WebSocket connections are completely blocked (strict corporate proxies, firewalls, network policies). The normal WebSocket transport is always preferred — it's lower latency, lower overhead, and the default. Only enable poll fallback after exhausting all WS troubleshooting in Sections 10–11.
|
|
513
|
+
|
|
514
|
+
### What it does
|
|
515
|
+
|
|
516
|
+
When enabled, the gateway automatically switches from WebSocket to HTTP long-polling if the WS connection fails repeatedly. It polls `GET /messages/poll?cursor=<cursor>` for new events, persists the cursor to disk (`~/.openclaw/workspace/relaycast/inbound-cursor.json`), and auto-recovers back to WS when the connection stabilizes.
|
|
517
|
+
|
|
518
|
+
### Transport state machine
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
WS_ACTIVE → (WS failures exceed threshold) → POLL_ACTIVE
|
|
522
|
+
POLL_ACTIVE → (WS reconnects) → RECOVERING_WS
|
|
523
|
+
RECOVERING_WS → (WS stable for grace period) → WS_ACTIVE
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
During `RECOVERING_WS`, both WS and poll run briefly to prevent message gaps. Messages seen in poll mode are deduped so they aren't re-delivered after WS recovery.
|
|
527
|
+
|
|
528
|
+
### Enable poll fallback
|
|
529
|
+
|
|
530
|
+
Add these to `~/.openclaw/workspace/relaycast/.env`:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Required — enables the fallback
|
|
534
|
+
RELAY_TRANSPORT_POLL_FALLBACK_ENABLED=true
|
|
535
|
+
|
|
536
|
+
# Optional — tune behavior (defaults shown)
|
|
537
|
+
RELAY_TRANSPORT_POLL_FALLBACK_WS_FAILURE_THRESHOLD=3 # WS failures before switching
|
|
538
|
+
RELAY_TRANSPORT_POLL_FALLBACK_TIMEOUT_SECONDS=25 # long-poll timeout per request
|
|
539
|
+
RELAY_TRANSPORT_POLL_FALLBACK_LIMIT=100 # max events per poll response
|
|
540
|
+
RELAY_TRANSPORT_POLL_FALLBACK_INITIAL_CURSOR=0 # starting cursor (usually 0)
|
|
541
|
+
|
|
542
|
+
# WS recovery probe (enabled by default when poll fallback is on)
|
|
543
|
+
RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_ENABLED=true
|
|
544
|
+
RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_INTERVAL_MS=60000 # how often to check if WS works
|
|
545
|
+
RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_STABLE_GRACE_MS=10000 # WS must stay up this long before switching back
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Then restart the gateway:
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
npx -y @agent-relay/openclaw@latest gateway
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Verify poll fallback is active
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
# Check the /health endpoint — transport.state will show POLL_ACTIVE when in fallback
|
|
558
|
+
curl -s http://127.0.0.1:18790/health | python3 -m json.tool
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Look for `"transport": { "state": "POLL_ACTIVE", ... }` and `"wsFailureCount"` in the response.
|
|
562
|
+
|
|
563
|
+
### Cursor persistence
|
|
564
|
+
|
|
565
|
+
The poll cursor is saved to `~/.openclaw/workspace/relaycast/inbound-cursor.json` after each successful delivery. This means:
|
|
566
|
+
|
|
567
|
+
- Restarts resume from where they left off (no duplicate messages)
|
|
568
|
+
- If the cursor becomes stale (server returns 409), it auto-resets to the initial cursor
|
|
569
|
+
|
|
570
|
+
### Scope
|
|
571
|
+
|
|
572
|
+
Poll fallback only affects **inbound** message reception from Relaycast. Outbound delivery (sending messages) is unchanged and still goes through the relay SDK or local OpenClaw WS.
|
|
573
|
+
|
|
574
|
+
### When NOT to use this
|
|
575
|
+
|
|
576
|
+
- If WS works at all, even intermittently — the gateway already handles WS reconnection with exponential backoff
|
|
577
|
+
- If the issue is device pairing or auth (Sections 10–11) — poll fallback won't help with those
|
|
578
|
+
- If latency matters — polling adds delay compared to WS
|
|
579
|
+
|
|
580
|
+
### Quick diagnostic
|
|
581
|
+
|
|
582
|
+
| Symptom | Cause | Fix |
|
|
583
|
+
| --------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
|
|
584
|
+
| Poll enabled but still no messages | `baseUrl` wrong or API key invalid | Check `RELAY_API_KEY` and `RELAY_BASE_URL` in `.env` |
|
|
585
|
+
| Cursor reset loop (409 repeatedly) | Server-side cursor expiry | Normal — gateway auto-resets and continues |
|
|
586
|
+
| Stuck in `POLL_ACTIVE` after WS is back | Probe disabled or grace too long | Verify `PROBE_WS_ENABLED=true`, reduce `STABLE_GRACE_MS` |
|
|
587
|
+
| High message latency | Expected with polling | Reduce `TIMEOUT_SECONDS` for faster poll cycles (tradeoff: more requests) |
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 13) Optional Direct API (curl)
|
|
488
592
|
|
|
489
593
|
```bash
|
|
490
594
|
curl -X POST https://api.relaycast.dev/v1/channels/general/messages \
|
|
@@ -495,14 +599,16 @@ curl -X POST https://api.relaycast.dev/v1/channels/general/messages \
|
|
|
495
599
|
|
|
496
600
|
---
|
|
497
601
|
|
|
498
|
-
##
|
|
602
|
+
## 14) Minimal Onboarding Recipe
|
|
499
603
|
|
|
500
604
|
Invite URL:
|
|
605
|
+
|
|
501
606
|
```text
|
|
502
|
-
https://agentrelay.dev/openclaw
|
|
607
|
+
https://agentrelay.dev/openclaw/skill/invite/rk_live_YOUR_WORKSPACE_KEY
|
|
503
608
|
```
|
|
504
609
|
|
|
505
610
|
Or direct setup:
|
|
611
|
+
|
|
506
612
|
```bash
|
|
507
613
|
npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name NEW_CLAW_NAME
|
|
508
614
|
npx -y @agent-relay/openclaw@latest status
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import type { Server as HttpServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Mocks
|
|
@@ -70,6 +70,8 @@ vi.mock('node:fs/promises', () => ({
|
|
|
70
70
|
readFile: vi.fn().mockResolvedValue('{"spawns":[]}'),
|
|
71
71
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
72
72
|
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
73
|
+
rename: vi.fn().mockResolvedValue(undefined),
|
|
74
|
+
chmod: vi.fn().mockResolvedValue(undefined),
|
|
73
75
|
}));
|
|
74
76
|
|
|
75
77
|
vi.mock('node:fs', () => ({
|
|
@@ -84,7 +86,6 @@ vi.mock('node:fs', () => ({
|
|
|
84
86
|
// Actually, we can't use port 0 because the gateway hardcodes the listen call.
|
|
85
87
|
// Instead, let's mock node:http to capture the request handler, then run a real server.
|
|
86
88
|
|
|
87
|
-
let capturedHandler: ((req: IncomingMessage, res: ServerResponse) => void) | null = null;
|
|
88
89
|
let realServer: HttpServer | null = null;
|
|
89
90
|
let controlPort = 0;
|
|
90
91
|
|
|
@@ -93,7 +94,6 @@ vi.mock('node:http', async (importOriginal) => {
|
|
|
93
94
|
return {
|
|
94
95
|
...actual,
|
|
95
96
|
createServer: vi.fn((handler: (req: IncomingMessage, res: ServerResponse) => void) => {
|
|
96
|
-
capturedHandler = handler;
|
|
97
97
|
// Create a real HTTP server with the captured handler
|
|
98
98
|
realServer = actual.createServer(handler);
|
|
99
99
|
return {
|
|
@@ -173,7 +173,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
173
173
|
it('GET /health returns 200', async () => {
|
|
174
174
|
const res = await fetchControl('GET', '/health');
|
|
175
175
|
expect(res.status).toBe(200);
|
|
176
|
-
const data = await res.json() as Record<string, unknown>;
|
|
176
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
177
177
|
expect(data.ok).toBe(true);
|
|
178
178
|
expect(data.status).toBe('running');
|
|
179
179
|
expect(typeof data.uptime).toBe('number');
|
|
@@ -193,7 +193,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
193
193
|
role: 'researcher',
|
|
194
194
|
});
|
|
195
195
|
expect(res.status).toBe(200);
|
|
196
|
-
const data = await res.json() as Record<string, unknown>;
|
|
196
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
197
197
|
expect(data.ok).toBe(true);
|
|
198
198
|
expect(data.name).toBe('worker-1');
|
|
199
199
|
expect(data.agentName).toBe('claw-ws-worker-1');
|
|
@@ -203,7 +203,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
203
203
|
it('POST /spawn without name returns 400', async () => {
|
|
204
204
|
const res = await fetchControl('POST', '/spawn', { role: 'worker' });
|
|
205
205
|
expect(res.status).toBe(400);
|
|
206
|
-
const data = await res.json() as Record<string, unknown>;
|
|
206
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
207
207
|
expect(data.ok).toBe(false);
|
|
208
208
|
expect(data.error).toMatch(/name/i);
|
|
209
209
|
});
|
|
@@ -213,7 +213,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
213
213
|
|
|
214
214
|
const res = await fetchControl('POST', '/spawn', { name: 'worker-1' });
|
|
215
215
|
expect(res.status).toBe(500);
|
|
216
|
-
const data = await res.json() as Record<string, unknown>;
|
|
216
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
217
217
|
expect(data.ok).toBe(false);
|
|
218
218
|
expect(data.error).toContain('Docker unavailable');
|
|
219
219
|
});
|
|
@@ -223,7 +223,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
223
223
|
|
|
224
224
|
const res = await fetchControl('GET', '/list');
|
|
225
225
|
expect(res.status).toBe(200);
|
|
226
|
-
const data = await res.json() as Record<string, unknown>;
|
|
226
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
227
227
|
expect(data.ok).toBe(true);
|
|
228
228
|
expect(data.active).toBe(0);
|
|
229
229
|
expect(data.claws).toEqual([]);
|
|
@@ -236,7 +236,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
236
236
|
|
|
237
237
|
const res = await fetchControl('GET', '/list');
|
|
238
238
|
expect(res.status).toBe(200);
|
|
239
|
-
const data = await res.json() as { claws: Array<{ name: string }> };
|
|
239
|
+
const data = (await res.json()) as { claws: Array<{ name: string }> };
|
|
240
240
|
expect(data.claws).toHaveLength(1);
|
|
241
241
|
expect(data.claws[0].name).toBe('alpha');
|
|
242
242
|
});
|
|
@@ -247,7 +247,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
247
247
|
|
|
248
248
|
const res = await fetchControl('POST', '/release', { name: 'worker-1' });
|
|
249
249
|
expect(res.status).toBe(200);
|
|
250
|
-
const data = await res.json() as Record<string, unknown>;
|
|
250
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
251
251
|
expect(data.ok).toBe(true);
|
|
252
252
|
});
|
|
253
253
|
|
|
@@ -257,14 +257,14 @@ describe('Gateway control HTTP server', () => {
|
|
|
257
257
|
|
|
258
258
|
const res = await fetchControl('POST', '/release', { id: 'spawn-1' });
|
|
259
259
|
expect(res.status).toBe(200);
|
|
260
|
-
const data = await res.json() as Record<string, unknown>;
|
|
260
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
261
261
|
expect(data.ok).toBe(true);
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
it('POST /release without name or id returns 400', async () => {
|
|
265
265
|
const res = await fetchControl('POST', '/release', {});
|
|
266
266
|
expect(res.status).toBe(400);
|
|
267
|
-
const data = await res.json() as Record<string, unknown>;
|
|
267
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
268
268
|
expect(data.ok).toBe(false);
|
|
269
269
|
expect(data.error).toMatch(/name.*id|id.*name/i);
|
|
270
270
|
});
|
|
@@ -274,7 +274,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
274
274
|
|
|
275
275
|
const res = await fetchControl('POST', '/release', { id: 'spawn-1' });
|
|
276
276
|
expect(res.status).toBe(500);
|
|
277
|
-
const data = await res.json() as Record<string, unknown>;
|
|
277
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
278
278
|
expect(data.ok).toBe(false);
|
|
279
279
|
expect(data.error).toContain('Process kill failed');
|
|
280
280
|
});
|
|
@@ -282,7 +282,7 @@ describe('Gateway control HTTP server', () => {
|
|
|
282
282
|
it('GET /unknown returns 404', async () => {
|
|
283
283
|
const res = await fetchControl('GET', '/nonexistent');
|
|
284
284
|
expect(res.status).toBe(404);
|
|
285
|
-
const data = await res.json() as Record<string, unknown>;
|
|
285
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
286
286
|
expect(data.error).toBe('Not found');
|
|
287
287
|
});
|
|
288
288
|
});
|