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.
Files changed (93) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +2 -2
  6. package/dist/src/cli/bootstrap.d.ts.map +1 -1
  7. package/dist/src/cli/bootstrap.js +2 -0
  8. package/dist/src/cli/bootstrap.js.map +1 -1
  9. package/dist/src/cli/commands/connect.d.ts +3 -0
  10. package/dist/src/cli/commands/connect.d.ts.map +1 -0
  11. package/dist/src/cli/commands/connect.js +18 -0
  12. package/dist/src/cli/commands/connect.js.map +1 -0
  13. package/dist/src/cli/lib/auth-ssh.d.ts.map +1 -1
  14. package/dist/src/cli/lib/auth-ssh.js +22 -270
  15. package/dist/src/cli/lib/auth-ssh.js.map +1 -1
  16. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  17. package/dist/src/cli/lib/broker-lifecycle.js +33 -0
  18. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  19. package/dist/src/cli/lib/connect-daytona.d.ts +15 -0
  20. package/dist/src/cli/lib/connect-daytona.d.ts.map +1 -0
  21. package/dist/src/cli/lib/connect-daytona.js +217 -0
  22. package/dist/src/cli/lib/connect-daytona.js.map +1 -0
  23. package/dist/src/cli/lib/ssh-interactive.d.ts +41 -0
  24. package/dist/src/cli/lib/ssh-interactive.d.ts.map +1 -0
  25. package/dist/src/cli/lib/ssh-interactive.js +320 -0
  26. package/dist/src/cli/lib/ssh-interactive.js.map +1 -0
  27. package/install.sh +2 -1
  28. package/package.json +13 -10
  29. package/packages/acp-bridge/package.json +2 -2
  30. package/packages/config/dist/cli-auth-config.d.ts +2 -0
  31. package/packages/config/dist/cli-auth-config.d.ts.map +1 -1
  32. package/packages/config/dist/cli-auth-config.js +1 -0
  33. package/packages/config/dist/cli-auth-config.js.map +1 -1
  34. package/packages/config/package.json +1 -1
  35. package/packages/config/src/cli-auth-config.ts +3 -0
  36. package/packages/hooks/package.json +4 -4
  37. package/packages/memory/package.json +2 -2
  38. package/packages/openclaw/dist/__tests__/gateway-control.test.js +13 -13
  39. package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -1
  40. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts +2 -0
  41. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts.map +1 -0
  42. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js +369 -0
  43. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js.map +1 -0
  44. package/packages/openclaw/dist/__tests__/gateway-threads.test.js +57 -16
  45. package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
  46. package/packages/openclaw/dist/__tests__/ws-client.test.js +2 -0
  47. package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -1
  48. package/packages/openclaw/dist/config.d.ts +2 -0
  49. package/packages/openclaw/dist/config.d.ts.map +1 -1
  50. package/packages/openclaw/dist/config.js +99 -12
  51. package/packages/openclaw/dist/config.js.map +1 -1
  52. package/packages/openclaw/dist/gateway.d.ts +56 -2
  53. package/packages/openclaw/dist/gateway.d.ts.map +1 -1
  54. package/packages/openclaw/dist/gateway.js +819 -127
  55. package/packages/openclaw/dist/gateway.js.map +1 -1
  56. package/packages/openclaw/dist/runtime/openclaw-config.d.ts +2 -0
  57. package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -1
  58. package/packages/openclaw/dist/runtime/openclaw-config.js +1 -1
  59. package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -1
  60. package/packages/openclaw/dist/runtime/setup.d.ts +0 -2
  61. package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -1
  62. package/packages/openclaw/dist/runtime/setup.js +53 -8
  63. package/packages/openclaw/dist/runtime/setup.js.map +1 -1
  64. package/packages/openclaw/dist/types.d.ts +28 -0
  65. package/packages/openclaw/dist/types.d.ts.map +1 -1
  66. package/packages/openclaw/package.json +2 -2
  67. package/packages/openclaw/skill/SKILL.md +150 -44
  68. package/packages/openclaw/src/__tests__/gateway-control.test.ts +14 -14
  69. package/packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts +467 -0
  70. package/packages/openclaw/src/__tests__/gateway-threads.test.ts +73 -23
  71. package/packages/openclaw/src/__tests__/ws-client.test.ts +71 -51
  72. package/packages/openclaw/src/config.ts +121 -12
  73. package/packages/openclaw/src/gateway.ts +1155 -252
  74. package/packages/openclaw/src/runtime/openclaw-config.ts +3 -1
  75. package/packages/openclaw/src/runtime/setup.ts +57 -16
  76. package/packages/openclaw/src/types.ts +31 -0
  77. package/packages/openclaw/test/vitest.setup.ts +1 -0
  78. package/packages/policy/package.json +2 -2
  79. package/packages/sdk/dist/__tests__/unit.test.js +131 -129
  80. package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
  81. package/packages/sdk/dist/relay.js +1 -1
  82. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  83. package/packages/sdk/dist/workflows/runner.js +5 -3
  84. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  85. package/packages/sdk/package.json +2 -2
  86. package/packages/sdk/src/__tests__/unit.test.ts +142 -157
  87. package/packages/sdk/src/relay.ts +1 -1
  88. package/packages/sdk/src/workflows/runner.ts +12 -9
  89. package/packages/sdk-py/pyproject.toml +1 -1
  90. package/packages/telemetry/package.json +1 -1
  91. package/packages/trajectory/package.json +2 -2
  92. package/packages/user-directory/package.json +2 -2
  93. 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: {"category":"communication","api_base":"https://api.relaycast.dev"}
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 (Join Existing Workspace)
70
+ ## 1) Setup (Create New Workspace)
67
71
 
68
72
  ```bash
69
- npx -y @agent-relay/openclaw@latest setup rk_live_YOUR_WORKSPACE_KEY --name my-claw
73
+ npx -y @agent-relay/openclaw@latest setup --name my-claw
70
74
  ```
71
75
 
72
- Expected signals:
73
- - `Agent "my-claw" registered with token` (when token is returned)
74
- - `MCP server configured in openclaw.json`
75
- - `Inbound gateway started in background`
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 (Create New Workspace)
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
- This prints a new `rk_live_...` key. Share invite URL:
92
+ Expected signals:
86
93
 
87
- ```text
88
- https://agentrelay.dev/openclaw?invite_token=rk_live_YOUR_WORKSPACE_KEY
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 | Likely Cause | Fix |
397
- |---|---|---|
398
- | `Pairing rejected` with requestId in logs | device not approved | run `openclaw devices approve <requestId>` from the log output |
399
- | `pairing-required` after restart | `device.json` deleted or `OPENCLAW_HOME` changed | check `~/.openclaw/workspace/relaycast/device.json` exists; re-approve if needed |
400
- | Polling works, injection fails | local WS auth/topology issue | run full recovery runbook above |
401
- | Setup succeeds but no MCP tools | `mcporter` missing from PATH | install/verify `mcporter`, re-run setup |
402
- | `Not registered` in mcporter calls | missing/cleared `RELAY_AGENT_TOKEN` | restore token in `~/.mcporter/mcporter.json` and retry |
403
- | `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 |
404
- | 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) |
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 | Auth Profile | Primary Payload | Fallback | Notes |
419
- |---|---|---|---|---|
420
- | `~/.openclaw/` (standard) | `default` | v3 (with platform/deviceFamily) | v2 | Current OpenClaw server supports v3 natively |
421
- | `~/.clawdbot/` (marketplace image) | `clawdbot-v1` | v2 (no platform/deviceFamily) | v3 | Older gateway only supports v2; v3↔v2 fallback handles upgrades |
422
- | `OPENCLAW_WS_AUTH_COMPAT=clawdbot` | `clawdbot-v1` | v2 | v3 | Manual override for non-standard installations |
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 | Value | Purpose |
461
- |---|---|---|
462
- | `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. |
463
- | `tools.exec.ask` | `off` | Disables interactive approval prompts. On a headless server nobody is there to approve, so commands hang forever waiting. |
464
- | `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. |
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 | Likely Cause | Fix |
479
- |---|---|---|
480
- | Agent chats but can't execute anything | Sandbox default policies | Set all three execution policies above |
481
- | Commands hang forever | `tools.exec.ask` still on (waiting for approval) | Set `tools.exec.ask off` and restart |
482
- | Network calls fail from agent | `tools.exec.security` not set to `full` | Set `tools.exec.security full` and restart |
483
- | Commands fail silently | `tools.exec.host` not set to `gateway` | Set `tools.exec.host gateway` and restart |
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) Optional Direct API (curl)
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
- ## 13) Minimal Onboarding Recipe
602
+ ## 14) Minimal Onboarding Recipe
499
603
 
500
604
  Invite URL:
605
+
501
606
  ```text
502
- https://agentrelay.dev/openclaw?invite_token=rk_live_YOUR_WORKSPACE_KEY
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 { createServer, type Server as HttpServer, type IncomingMessage, type ServerResponse } from 'node:http';
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
  });