agent-relay 3.0.2 → 3.1.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/README.md +8 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +273 -56
- package/dist/src/cli/commands/core.d.ts +2 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +9 -2
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +87 -28
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/package.json +9 -9
- package/packages/acp-bridge/README.md +50 -67
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/README.md +78 -0
- package/packages/openclaw/bin/relay-openclaw.mjs +2 -0
- package/packages/openclaw/bridge/bridge.mjs +305 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js +320 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/naming.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/naming.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/naming.test.js +21 -0
- package/packages/openclaw/dist/__tests__/naming.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js +126 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js.map +1 -0
- package/packages/openclaw/dist/auth/converter.d.ts +28 -0
- package/packages/openclaw/dist/auth/converter.d.ts.map +1 -0
- package/packages/openclaw/dist/auth/converter.js +64 -0
- package/packages/openclaw/dist/auth/converter.js.map +1 -0
- package/packages/openclaw/dist/cli.d.ts +2 -0
- package/packages/openclaw/dist/cli.d.ts.map +1 -0
- package/packages/openclaw/dist/cli.js +230 -0
- package/packages/openclaw/dist/cli.js.map +1 -0
- package/packages/openclaw/dist/config.d.ts +27 -0
- package/packages/openclaw/dist/config.d.ts.map +1 -0
- package/packages/openclaw/dist/config.js +97 -0
- package/packages/openclaw/dist/config.js.map +1 -0
- package/packages/openclaw/dist/control.d.ts +22 -0
- package/packages/openclaw/dist/control.d.ts.map +1 -0
- package/packages/openclaw/dist/control.js +58 -0
- package/packages/openclaw/dist/control.js.map +1 -0
- package/packages/openclaw/dist/gateway.d.ts +71 -0
- package/packages/openclaw/dist/gateway.d.ts.map +1 -0
- package/packages/openclaw/dist/gateway.js +785 -0
- package/packages/openclaw/dist/gateway.js.map +1 -0
- package/packages/openclaw/dist/identity/contract.d.ts +11 -0
- package/packages/openclaw/dist/identity/contract.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/contract.js +40 -0
- package/packages/openclaw/dist/identity/contract.js.map +1 -0
- package/packages/openclaw/dist/identity/files.d.ts +33 -0
- package/packages/openclaw/dist/identity/files.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/files.js +145 -0
- package/packages/openclaw/dist/identity/files.js.map +1 -0
- package/packages/openclaw/dist/identity/model.d.ts +11 -0
- package/packages/openclaw/dist/identity/model.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/model.js +28 -0
- package/packages/openclaw/dist/identity/model.js.map +1 -0
- package/packages/openclaw/dist/identity/naming.d.ts +5 -0
- package/packages/openclaw/dist/identity/naming.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/naming.js +7 -0
- package/packages/openclaw/dist/identity/naming.js.map +1 -0
- package/packages/openclaw/dist/index.d.ts +20 -0
- package/packages/openclaw/dist/index.d.ts.map +1 -0
- package/packages/openclaw/dist/index.js +27 -0
- package/packages/openclaw/dist/index.js.map +1 -0
- package/packages/openclaw/dist/inject.d.ts +14 -0
- package/packages/openclaw/dist/inject.d.ts.map +1 -0
- package/packages/openclaw/dist/inject.js +66 -0
- package/packages/openclaw/dist/inject.js.map +1 -0
- package/packages/openclaw/dist/mcp/server.d.ts +8 -0
- package/packages/openclaw/dist/mcp/server.d.ts.map +1 -0
- package/packages/openclaw/dist/mcp/server.js +105 -0
- package/packages/openclaw/dist/mcp/server.js.map +1 -0
- package/packages/openclaw/dist/mcp/tools.d.ts +17 -0
- package/packages/openclaw/dist/mcp/tools.d.ts.map +1 -0
- package/packages/openclaw/dist/mcp/tools.js +145 -0
- package/packages/openclaw/dist/mcp/tools.js.map +1 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts +20 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/openclaw-config.js +50 -0
- package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -0
- package/packages/openclaw/dist/runtime/patch.d.ts +24 -0
- package/packages/openclaw/dist/runtime/patch.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/patch.js +92 -0
- package/packages/openclaw/dist/runtime/patch.js.map +1 -0
- package/packages/openclaw/dist/runtime/setup.d.ts +26 -0
- package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/setup.js +58 -0
- package/packages/openclaw/dist/runtime/setup.js.map +1 -0
- package/packages/openclaw/dist/setup.d.ts +29 -0
- package/packages/openclaw/dist/setup.d.ts.map +1 -0
- package/packages/openclaw/dist/setup.js +300 -0
- package/packages/openclaw/dist/setup.js.map +1 -0
- package/packages/openclaw/dist/spawn/docker.d.ts +58 -0
- package/packages/openclaw/dist/spawn/docker.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/docker.js +222 -0
- package/packages/openclaw/dist/spawn/docker.js.map +1 -0
- package/packages/openclaw/dist/spawn/manager.d.ts +45 -0
- package/packages/openclaw/dist/spawn/manager.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/manager.js +140 -0
- package/packages/openclaw/dist/spawn/manager.js.map +1 -0
- package/packages/openclaw/dist/spawn/process.d.ts +16 -0
- package/packages/openclaw/dist/spawn/process.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/process.js +241 -0
- package/packages/openclaw/dist/spawn/process.js.map +1 -0
- package/packages/openclaw/dist/spawn/types.d.ts +42 -0
- package/packages/openclaw/dist/spawn/types.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/types.js +2 -0
- package/packages/openclaw/dist/spawn/types.js.map +1 -0
- package/packages/openclaw/dist/types.d.ts +37 -0
- package/packages/openclaw/dist/types.d.ts.map +1 -0
- package/packages/openclaw/dist/types.js +2 -0
- package/packages/openclaw/dist/types.js.map +1 -0
- package/packages/openclaw/package.json +63 -0
- package/packages/openclaw/skill/SKILL.md +194 -0
- package/packages/openclaw/src/__tests__/gateway-threads.test.ts +384 -0
- package/packages/openclaw/src/__tests__/naming.test.ts +24 -0
- package/packages/openclaw/src/__tests__/spawn-manager.test.ts +152 -0
- package/packages/openclaw/src/auth/converter.ts +90 -0
- package/packages/openclaw/src/cli.ts +269 -0
- package/packages/openclaw/src/config.ts +124 -0
- package/packages/openclaw/src/control.ts +100 -0
- package/packages/openclaw/src/gateway.ts +941 -0
- package/packages/openclaw/src/identity/contract.ts +44 -0
- package/packages/openclaw/src/identity/files.ts +198 -0
- package/packages/openclaw/src/identity/model.ts +27 -0
- package/packages/openclaw/src/identity/naming.ts +6 -0
- package/packages/openclaw/src/index.ts +59 -0
- package/packages/openclaw/src/inject.ts +77 -0
- package/packages/openclaw/src/mcp/server.ts +121 -0
- package/packages/openclaw/src/mcp/tools.ts +174 -0
- package/packages/openclaw/src/runtime/openclaw-config.ts +64 -0
- package/packages/openclaw/src/runtime/patch.ts +103 -0
- package/packages/openclaw/src/runtime/setup.ts +89 -0
- package/packages/openclaw/src/setup.ts +336 -0
- package/packages/openclaw/src/spawn/docker.ts +261 -0
- package/packages/openclaw/src/spawn/manager.ts +181 -0
- package/packages/openclaw/src/spawn/process.ts +272 -0
- package/packages/openclaw/src/spawn/types.ts +43 -0
- package/packages/openclaw/src/types.ts +38 -0
- package/packages/openclaw/templates/SOUL.md.template +34 -0
- package/packages/openclaw/tsconfig.json +12 -0
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +169 -64
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/integration.test.js +5 -4
- package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +34 -3
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +120 -10
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/protocol.d.ts +7 -1
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +47 -11
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +114 -23
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +71 -36
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +1 -1
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
- package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
- package/packages/sdk/src/__tests__/integration.test.ts +5 -4
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
- package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
- package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
- package/packages/sdk/src/client.ts +171 -14
- package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
- package/packages/sdk/src/protocol.ts +7 -2
- package/packages/sdk/src/relay.ts +196 -34
- package/packages/sdk/src/workflows/runner.ts +73 -42
- package/packages/sdk/src/workflows/schema.json +1 -1
- package/packages/sdk/src/workflows/types.ts +1 -1
- package/packages/sdk/vitest.config.ts +1 -0
- package/packages/sdk-py/README.md +89 -102
- package/packages/sdk-py/agent_relay/__init__.py +16 -19
- package/packages/sdk-py/pyproject.toml +5 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
- package/packages/sdk-py/src/agent_relay/client.py +776 -0
- package/packages/sdk-py/src/agent_relay/models.py +27 -0
- package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
- package/packages/sdk-py/src/agent_relay/relay.py +860 -0
- package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
- 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
- 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/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
- package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { RelayCast } from '@relaycast/sdk';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { SpawnManager } from './spawn/manager.js';
|
|
6
|
+
function normalizeChannelName(channel) {
|
|
7
|
+
return channel.startsWith('#') ? channel.slice(1) : channel;
|
|
8
|
+
}
|
|
9
|
+
function generateDeviceIdentity() {
|
|
10
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
11
|
+
// Extract raw 32-byte public key from SPKI DER (12-byte header for Ed25519)
|
|
12
|
+
const rawPublicBytes = publicKey.export({ type: 'spki', format: 'der' }).subarray(12);
|
|
13
|
+
const deviceId = createHash('sha256').update(rawPublicBytes).digest('hex');
|
|
14
|
+
const publicKeyB64 = Buffer.from(rawPublicBytes).toString('base64url');
|
|
15
|
+
return {
|
|
16
|
+
publicKeyB64,
|
|
17
|
+
privateKeyObj: privateKey,
|
|
18
|
+
deviceId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function signConnectPayload(device, params) {
|
|
22
|
+
// v3 payload format: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
|
|
23
|
+
const payload = [
|
|
24
|
+
'v3',
|
|
25
|
+
device.deviceId,
|
|
26
|
+
params.clientId,
|
|
27
|
+
params.clientMode,
|
|
28
|
+
params.role,
|
|
29
|
+
params.scopes.join(','),
|
|
30
|
+
String(params.signedAt),
|
|
31
|
+
params.token || '',
|
|
32
|
+
params.nonce,
|
|
33
|
+
params.platform,
|
|
34
|
+
params.deviceFamily,
|
|
35
|
+
].join('|');
|
|
36
|
+
const payloadBytes = Buffer.from(payload, 'utf-8');
|
|
37
|
+
// Ed25519 sign — no hash algorithm needed (null), it's built into Ed25519
|
|
38
|
+
const signature = sign(null, payloadBytes, device.privateKeyObj);
|
|
39
|
+
return Buffer.from(signature).toString('base64url');
|
|
40
|
+
}
|
|
41
|
+
class OpenClawGatewayClient {
|
|
42
|
+
ws = null;
|
|
43
|
+
authenticated = false;
|
|
44
|
+
device;
|
|
45
|
+
token;
|
|
46
|
+
port;
|
|
47
|
+
pendingRpcs = new Map();
|
|
48
|
+
rpcIdCounter = 0;
|
|
49
|
+
reconnectTimer = null;
|
|
50
|
+
stopped = false;
|
|
51
|
+
connectPromise = null;
|
|
52
|
+
connectResolve = null;
|
|
53
|
+
connectReject = null;
|
|
54
|
+
connectTimeout = null;
|
|
55
|
+
/** Default timeout for initial connection (30 seconds). */
|
|
56
|
+
static CONNECT_TIMEOUT_MS = 30_000;
|
|
57
|
+
constructor(token, port) {
|
|
58
|
+
this.token = token;
|
|
59
|
+
this.port = port;
|
|
60
|
+
this.device = generateDeviceIdentity();
|
|
61
|
+
}
|
|
62
|
+
/** Connect and authenticate. Resolves when chat.send is ready, rejects on timeout or error. */
|
|
63
|
+
async connect() {
|
|
64
|
+
if (this.authenticated && this.ws?.readyState === WebSocket.OPEN)
|
|
65
|
+
return;
|
|
66
|
+
// Cancel any pending reconnect timer to prevent orphaned WebSocket connections
|
|
67
|
+
if (this.reconnectTimer) {
|
|
68
|
+
clearTimeout(this.reconnectTimer);
|
|
69
|
+
this.reconnectTimer = null;
|
|
70
|
+
}
|
|
71
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
72
|
+
this.connectResolve = resolve;
|
|
73
|
+
this.connectReject = reject;
|
|
74
|
+
// Set up timeout to prevent indefinite hanging
|
|
75
|
+
this.connectTimeout = setTimeout(() => {
|
|
76
|
+
this.connectTimeout = null;
|
|
77
|
+
if (!this.authenticated) {
|
|
78
|
+
const err = new Error(`Connection to OpenClaw gateway timed out after ${OpenClawGatewayClient.CONNECT_TIMEOUT_MS}ms`);
|
|
79
|
+
this.connectReject?.(err);
|
|
80
|
+
this.connectReject = null;
|
|
81
|
+
this.connectResolve = null;
|
|
82
|
+
}
|
|
83
|
+
}, OpenClawGatewayClient.CONNECT_TIMEOUT_MS);
|
|
84
|
+
});
|
|
85
|
+
this.doConnect();
|
|
86
|
+
return this.connectPromise;
|
|
87
|
+
}
|
|
88
|
+
clearConnectTimeout() {
|
|
89
|
+
if (this.connectTimeout) {
|
|
90
|
+
clearTimeout(this.connectTimeout);
|
|
91
|
+
this.connectTimeout = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
doConnect() {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
this.ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.warn(`[openclaw-ws] Connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
this.scheduleReconnect();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.ws.on('open', () => {
|
|
106
|
+
console.log('[openclaw-ws] Connected to OpenClaw gateway');
|
|
107
|
+
});
|
|
108
|
+
this.ws.on('message', (data) => {
|
|
109
|
+
this.handleMessage(data.toString());
|
|
110
|
+
});
|
|
111
|
+
this.ws.on('close', (code, reason) => {
|
|
112
|
+
console.warn(`[openclaw-ws] Disconnected: ${code} ${reason.toString()}`);
|
|
113
|
+
const wasAuthenticated = this.authenticated;
|
|
114
|
+
this.authenticated = false;
|
|
115
|
+
// Reject all pending RPCs
|
|
116
|
+
for (const [id, pending] of this.pendingRpcs) {
|
|
117
|
+
clearTimeout(pending.timer);
|
|
118
|
+
pending.resolve(false);
|
|
119
|
+
this.pendingRpcs.delete(id);
|
|
120
|
+
}
|
|
121
|
+
// If we weren't authenticated yet, reject the connect promise
|
|
122
|
+
if (!wasAuthenticated && this.connectReject) {
|
|
123
|
+
this.clearConnectTimeout();
|
|
124
|
+
const err = new Error(`WebSocket closed before authentication (code=${code})`);
|
|
125
|
+
this.connectReject(err);
|
|
126
|
+
this.connectReject = null;
|
|
127
|
+
this.connectResolve = null;
|
|
128
|
+
}
|
|
129
|
+
if (!this.stopped) {
|
|
130
|
+
this.scheduleReconnect();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.ws.on('error', (err) => {
|
|
134
|
+
console.warn(`[openclaw-ws] Error: ${err.message}`);
|
|
135
|
+
// If we weren't authenticated yet, reject the connect promise
|
|
136
|
+
if (!this.authenticated && this.connectReject) {
|
|
137
|
+
this.clearConnectTimeout();
|
|
138
|
+
this.connectReject(err);
|
|
139
|
+
this.connectReject = null;
|
|
140
|
+
this.connectResolve = null;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
handleMessage(raw) {
|
|
145
|
+
let msg;
|
|
146
|
+
try {
|
|
147
|
+
msg = JSON.parse(raw);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Handle connect.challenge — sign and respond
|
|
153
|
+
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
154
|
+
const payload = msg.payload;
|
|
155
|
+
console.log('[openclaw-ws] Received connect.challenge, signing...');
|
|
156
|
+
const signedAt = Date.now();
|
|
157
|
+
const clientId = 'cli';
|
|
158
|
+
const clientMode = 'cli';
|
|
159
|
+
const platform = process.platform === 'darwin' ? 'macos' : 'linux';
|
|
160
|
+
const deviceFamily = 'cli';
|
|
161
|
+
const role = 'operator';
|
|
162
|
+
const scopes = ['operator.read', 'operator.write'];
|
|
163
|
+
const signature = signConnectPayload(this.device, {
|
|
164
|
+
clientId,
|
|
165
|
+
clientMode,
|
|
166
|
+
platform,
|
|
167
|
+
deviceFamily,
|
|
168
|
+
role,
|
|
169
|
+
scopes,
|
|
170
|
+
signedAt,
|
|
171
|
+
token: this.token,
|
|
172
|
+
nonce: payload.nonce,
|
|
173
|
+
});
|
|
174
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
175
|
+
console.warn('[openclaw-ws] WebSocket not open when trying to send connect');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.ws.send(JSON.stringify({
|
|
179
|
+
type: 'req',
|
|
180
|
+
id: 'connect-1',
|
|
181
|
+
method: 'connect',
|
|
182
|
+
params: {
|
|
183
|
+
minProtocol: 3,
|
|
184
|
+
maxProtocol: 3,
|
|
185
|
+
client: {
|
|
186
|
+
id: clientId,
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
platform,
|
|
189
|
+
mode: clientMode,
|
|
190
|
+
deviceFamily,
|
|
191
|
+
},
|
|
192
|
+
role,
|
|
193
|
+
scopes,
|
|
194
|
+
caps: [],
|
|
195
|
+
commands: [],
|
|
196
|
+
permissions: {},
|
|
197
|
+
auth: { token: this.token },
|
|
198
|
+
locale: 'en-US',
|
|
199
|
+
userAgent: 'relaycast-gateway/1.0.0',
|
|
200
|
+
device: {
|
|
201
|
+
id: this.device.deviceId,
|
|
202
|
+
publicKey: this.device.publicKeyB64,
|
|
203
|
+
signature,
|
|
204
|
+
signedAt,
|
|
205
|
+
nonce: payload.nonce,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Handle connect response
|
|
212
|
+
if (msg.type === 'res' && msg.id === 'connect-1') {
|
|
213
|
+
this.clearConnectTimeout();
|
|
214
|
+
if (msg.ok) {
|
|
215
|
+
console.log('[openclaw-ws] Authenticated successfully');
|
|
216
|
+
this.authenticated = true;
|
|
217
|
+
this.connectResolve?.();
|
|
218
|
+
this.connectResolve = null;
|
|
219
|
+
this.connectReject = null;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.warn(`[openclaw-ws] Auth rejected: ${JSON.stringify(msg.error ?? msg)}`);
|
|
223
|
+
// Reject the connect promise on auth failure
|
|
224
|
+
const errMsg = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
|
|
225
|
+
this.connectReject?.(new Error(`OpenClaw gateway auth failed: ${errMsg}`));
|
|
226
|
+
this.connectReject = null;
|
|
227
|
+
this.connectResolve = null;
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Handle RPC responses
|
|
232
|
+
const id = msg.id;
|
|
233
|
+
if (id && this.pendingRpcs.has(id)) {
|
|
234
|
+
const pending = this.pendingRpcs.get(id);
|
|
235
|
+
clearTimeout(pending.timer);
|
|
236
|
+
this.pendingRpcs.delete(id);
|
|
237
|
+
if (msg.ok === false || msg.error) {
|
|
238
|
+
console.warn(`[openclaw-ws] RPC ${id} error: ${JSON.stringify(msg.error ?? msg)}`);
|
|
239
|
+
pending.resolve(false);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const result = msg.payload;
|
|
243
|
+
console.log(`[openclaw-ws] RPC ${id} ok: runId=${result?.runId ?? 'n/a'} status=${result?.status ?? 'n/a'}`);
|
|
244
|
+
pending.resolve(true);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Log other events at debug level
|
|
249
|
+
if (msg.type === 'event') {
|
|
250
|
+
// chat events, tick events, etc. — ignore silently
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Send a chat.send RPC. Returns true if accepted. */
|
|
254
|
+
async sendChatMessage(text, idempotencyKey) {
|
|
255
|
+
if (!this.authenticated || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
256
|
+
// Try to reconnect
|
|
257
|
+
try {
|
|
258
|
+
await this.connect();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
if (!this.authenticated)
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const id = `chat-${++this.rpcIdCounter}-${Date.now()}`;
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
console.warn(`[openclaw-ws] chat.send ${id} timed out`);
|
|
270
|
+
this.pendingRpcs.delete(id);
|
|
271
|
+
resolve(false);
|
|
272
|
+
}, 15_000);
|
|
273
|
+
this.pendingRpcs.set(id, { resolve, timer });
|
|
274
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
this.pendingRpcs.delete(id);
|
|
277
|
+
resolve(false);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.ws.send(JSON.stringify({
|
|
281
|
+
type: 'req',
|
|
282
|
+
id,
|
|
283
|
+
method: 'chat.send',
|
|
284
|
+
params: {
|
|
285
|
+
sessionKey: 'agent:main:main',
|
|
286
|
+
message: text,
|
|
287
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
288
|
+
},
|
|
289
|
+
}));
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
scheduleReconnect() {
|
|
293
|
+
if (this.stopped || this.reconnectTimer)
|
|
294
|
+
return;
|
|
295
|
+
console.log('[openclaw-ws] Reconnecting in 3s...');
|
|
296
|
+
this.reconnectTimer = setTimeout(() => {
|
|
297
|
+
this.reconnectTimer = null;
|
|
298
|
+
this.doConnect();
|
|
299
|
+
}, 3_000);
|
|
300
|
+
}
|
|
301
|
+
async disconnect() {
|
|
302
|
+
this.stopped = true;
|
|
303
|
+
this.clearConnectTimeout();
|
|
304
|
+
if (this.reconnectTimer) {
|
|
305
|
+
clearTimeout(this.reconnectTimer);
|
|
306
|
+
this.reconnectTimer = null;
|
|
307
|
+
}
|
|
308
|
+
for (const [id, pending] of this.pendingRpcs) {
|
|
309
|
+
clearTimeout(pending.timer);
|
|
310
|
+
pending.resolve(false);
|
|
311
|
+
this.pendingRpcs.delete(id);
|
|
312
|
+
}
|
|
313
|
+
if (this.ws) {
|
|
314
|
+
try {
|
|
315
|
+
this.ws.close();
|
|
316
|
+
}
|
|
317
|
+
catch { }
|
|
318
|
+
this.ws = null;
|
|
319
|
+
}
|
|
320
|
+
this.authenticated = false;
|
|
321
|
+
// Clear any pending connect promise
|
|
322
|
+
this.connectReject = null;
|
|
323
|
+
this.connectResolve = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// InboundGateway
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
export class InboundGateway {
|
|
330
|
+
relaySender;
|
|
331
|
+
relayAgentClient = null;
|
|
332
|
+
relaycast;
|
|
333
|
+
config;
|
|
334
|
+
fallbackPollMs;
|
|
335
|
+
dedupeTtlMs;
|
|
336
|
+
running = false;
|
|
337
|
+
pollTimer = null;
|
|
338
|
+
unsubscribeHandlers = [];
|
|
339
|
+
seenMessageIds = new Map();
|
|
340
|
+
processingMessageIds = new Set();
|
|
341
|
+
channelCursor = new Map();
|
|
342
|
+
/** Persistent WebSocket client for the local OpenClaw gateway. */
|
|
343
|
+
openclawClient = null;
|
|
344
|
+
/** Spawn manager — lives in the gateway so spawned processes survive MCP server restarts. */
|
|
345
|
+
spawnManager;
|
|
346
|
+
/** HTTP control server for spawn/list/release commands. */
|
|
347
|
+
controlServer = null;
|
|
348
|
+
/** Port the control server listens on. */
|
|
349
|
+
controlPort = 0;
|
|
350
|
+
/** Default control port for the gateway's spawn API. */
|
|
351
|
+
static DEFAULT_CONTROL_PORT = 18790;
|
|
352
|
+
constructor(options) {
|
|
353
|
+
this.config = {
|
|
354
|
+
...options.config,
|
|
355
|
+
channels: options.config.channels.map(normalizeChannelName),
|
|
356
|
+
};
|
|
357
|
+
this.relaySender = options.relaySender ?? null;
|
|
358
|
+
this.relaycast = new RelayCast({
|
|
359
|
+
apiKey: this.config.apiKey,
|
|
360
|
+
baseUrl: this.config.baseUrl,
|
|
361
|
+
});
|
|
362
|
+
const fallbackPollMs = Number(process.env.RELAYCAST_FALLBACK_POLL_MS ?? 15000);
|
|
363
|
+
this.fallbackPollMs = Number.isFinite(fallbackPollMs) && fallbackPollMs >= 1000
|
|
364
|
+
? Math.floor(fallbackPollMs)
|
|
365
|
+
: 15000;
|
|
366
|
+
const dedupeTtlMs = Number(process.env.RELAYCAST_DEDUPE_TTL_MS ?? 15 * 60 * 1000);
|
|
367
|
+
this.dedupeTtlMs = Number.isFinite(dedupeTtlMs) && dedupeTtlMs >= 1000
|
|
368
|
+
? Math.floor(dedupeTtlMs)
|
|
369
|
+
: 15 * 60 * 1000;
|
|
370
|
+
const parentDepth = Number(process.env.OPENCLAW_SPAWN_DEPTH || 0);
|
|
371
|
+
this.spawnManager = new SpawnManager({ spawnDepth: parentDepth + 1 });
|
|
372
|
+
}
|
|
373
|
+
/** Start the gateway — register agent, subscribe for realtime events, and run fallback polling. */
|
|
374
|
+
async start() {
|
|
375
|
+
if (this.running)
|
|
376
|
+
return;
|
|
377
|
+
this.running = true;
|
|
378
|
+
// Connect to the local OpenClaw gateway WebSocket (persistent connection)
|
|
379
|
+
const token = this.config.openclawGatewayToken ?? process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
380
|
+
const port = this.config.openclawGatewayPort ?? 18789;
|
|
381
|
+
if (token) {
|
|
382
|
+
this.openclawClient = new OpenClawGatewayClient(token, port);
|
|
383
|
+
try {
|
|
384
|
+
await this.openclawClient.connect();
|
|
385
|
+
console.log('[gateway] OpenClaw gateway WebSocket client ready');
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
console.warn(`[gateway] OpenClaw gateway WS failed (will retry per message): ${err instanceof Error ? err.message : String(err)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.warn('[gateway] No OPENCLAW_GATEWAY_TOKEN — local delivery disabled');
|
|
393
|
+
}
|
|
394
|
+
// Register with a viewer- prefixed name so we don't collide with the
|
|
395
|
+
// container broker's agent registration (which uses the bare clawName).
|
|
396
|
+
const viewerName = `viewer-${this.config.clawName}`;
|
|
397
|
+
const registered = await this.relaycast.agents.registerOrGet({
|
|
398
|
+
name: viewerName,
|
|
399
|
+
type: 'agent',
|
|
400
|
+
persona: 'Relaycast inbound gateway for OpenClaw',
|
|
401
|
+
});
|
|
402
|
+
this.relayAgentClient = this.relaycast.as(registered.token);
|
|
403
|
+
// Connect first, then register handlers. The SDK requires connect()
|
|
404
|
+
// before subscribe() can be called.
|
|
405
|
+
this.relayAgentClient.connect();
|
|
406
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.connected(() => {
|
|
407
|
+
console.log(`[gateway] Relaycast WebSocket connected, subscribing to channels: ${this.config.channels.join(', ')}`);
|
|
408
|
+
this.relayAgentClient?.subscribe(this.config.channels);
|
|
409
|
+
}));
|
|
410
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.messageCreated((event) => {
|
|
411
|
+
console.log(`[gateway] Realtime message from @${event.message?.agentName} in #${event.channel}`);
|
|
412
|
+
void this.handleRealtimeMessage(event);
|
|
413
|
+
}));
|
|
414
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.threadReply((event) => {
|
|
415
|
+
console.log(`[gateway] Thread reply from @${event.message?.agentName} in #${event.channel} (parent: ${event.parentId})`);
|
|
416
|
+
void this.handleRealtimeThreadReply(event);
|
|
417
|
+
}));
|
|
418
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.reconnecting((attempt) => {
|
|
419
|
+
console.warn(`[gateway] Relaycast reconnecting (attempt ${attempt})`);
|
|
420
|
+
}));
|
|
421
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.disconnected(() => {
|
|
422
|
+
console.warn(`[gateway] Relaycast disconnected`);
|
|
423
|
+
}));
|
|
424
|
+
this.unsubscribeHandlers.push(this.relayAgentClient.on.error(() => {
|
|
425
|
+
console.warn(`[gateway] Relaycast socket error`);
|
|
426
|
+
}));
|
|
427
|
+
await this.ensureChannelMembership();
|
|
428
|
+
// Also subscribe explicitly in case the `connected` event already fired
|
|
429
|
+
// before we registered the handler above.
|
|
430
|
+
try {
|
|
431
|
+
this.relayAgentClient.subscribe(this.config.channels);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// Will subscribe on next connected event
|
|
435
|
+
}
|
|
436
|
+
// Initial catch-up in case messages arrived before realtime subscription was active.
|
|
437
|
+
await this.pollMessages();
|
|
438
|
+
// Keep a low-frequency poll as recovery/backfill only.
|
|
439
|
+
this.pollTimer = setInterval(() => {
|
|
440
|
+
void this.pollMessages();
|
|
441
|
+
}, this.fallbackPollMs);
|
|
442
|
+
console.log(`[gateway] Realtime listening on channels: ${this.config.channels.join(', ')}`);
|
|
443
|
+
// Start spawn control HTTP server
|
|
444
|
+
await this.startControlServer();
|
|
445
|
+
}
|
|
446
|
+
/** Stop the gateway — clean up websocket and relay clients. */
|
|
447
|
+
async stop() {
|
|
448
|
+
this.running = false;
|
|
449
|
+
if (this.pollTimer) {
|
|
450
|
+
clearInterval(this.pollTimer);
|
|
451
|
+
this.pollTimer = null;
|
|
452
|
+
}
|
|
453
|
+
for (const unsubscribe of this.unsubscribeHandlers) {
|
|
454
|
+
try {
|
|
455
|
+
unsubscribe();
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Best effort
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this.unsubscribeHandlers = [];
|
|
462
|
+
if (this.relayAgentClient) {
|
|
463
|
+
try {
|
|
464
|
+
await this.relayAgentClient.disconnect();
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Best effort
|
|
468
|
+
}
|
|
469
|
+
this.relayAgentClient = null;
|
|
470
|
+
}
|
|
471
|
+
if (this.openclawClient) {
|
|
472
|
+
await this.openclawClient.disconnect();
|
|
473
|
+
this.openclawClient = null;
|
|
474
|
+
}
|
|
475
|
+
// Stop control server and release all spawns
|
|
476
|
+
if (this.controlServer) {
|
|
477
|
+
this.controlServer.close();
|
|
478
|
+
this.controlServer = null;
|
|
479
|
+
}
|
|
480
|
+
await this.spawnManager.releaseAll();
|
|
481
|
+
this.processingMessageIds.clear();
|
|
482
|
+
this.channelCursor.clear();
|
|
483
|
+
this.seenMessageIds.clear();
|
|
484
|
+
}
|
|
485
|
+
cleanupSeenMap(nowMs) {
|
|
486
|
+
for (const [id, seenAt] of this.seenMessageIds.entries()) {
|
|
487
|
+
if (nowMs - seenAt > this.dedupeTtlMs) {
|
|
488
|
+
this.seenMessageIds.delete(id);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
isSeen(messageId) {
|
|
493
|
+
const nowMs = Date.now();
|
|
494
|
+
this.cleanupSeenMap(nowMs);
|
|
495
|
+
return this.seenMessageIds.has(messageId);
|
|
496
|
+
}
|
|
497
|
+
markSeen(messageId) {
|
|
498
|
+
const nowMs = Date.now();
|
|
499
|
+
this.cleanupSeenMap(nowMs);
|
|
500
|
+
this.seenMessageIds.set(messageId, nowMs);
|
|
501
|
+
}
|
|
502
|
+
async ensureChannelMembership() {
|
|
503
|
+
if (!this.relayAgentClient)
|
|
504
|
+
return;
|
|
505
|
+
for (const channel of this.config.channels) {
|
|
506
|
+
try {
|
|
507
|
+
await this.relayAgentClient.channels.join(channel);
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
try {
|
|
511
|
+
await this.relayAgentClient.channels.create({ name: channel });
|
|
512
|
+
await this.relayAgentClient.channels.join(channel);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Non-fatal
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async handleRealtimeMessage(event) {
|
|
521
|
+
const channel = normalizeChannelName(event.channel);
|
|
522
|
+
if (!this.config.channels.includes(channel))
|
|
523
|
+
return;
|
|
524
|
+
const messageId = event.message?.id;
|
|
525
|
+
if (!messageId)
|
|
526
|
+
return;
|
|
527
|
+
const inbound = {
|
|
528
|
+
id: messageId,
|
|
529
|
+
channel,
|
|
530
|
+
from: event.message.agentName,
|
|
531
|
+
text: event.message.text,
|
|
532
|
+
timestamp: new Date().toISOString(),
|
|
533
|
+
};
|
|
534
|
+
await this.handleInbound(inbound);
|
|
535
|
+
}
|
|
536
|
+
async handleRealtimeThreadReply(event) {
|
|
537
|
+
const channel = normalizeChannelName(event.channel);
|
|
538
|
+
if (!this.config.channels.includes(channel))
|
|
539
|
+
return;
|
|
540
|
+
const messageId = event.message?.id;
|
|
541
|
+
if (!messageId)
|
|
542
|
+
return;
|
|
543
|
+
const inbound = {
|
|
544
|
+
id: messageId,
|
|
545
|
+
channel,
|
|
546
|
+
from: event.message.agentName,
|
|
547
|
+
text: event.message.text,
|
|
548
|
+
timestamp: new Date().toISOString(),
|
|
549
|
+
threadParentId: event.parentId,
|
|
550
|
+
};
|
|
551
|
+
await this.handleInbound(inbound);
|
|
552
|
+
}
|
|
553
|
+
normalizePolledMessage(channel, message) {
|
|
554
|
+
return {
|
|
555
|
+
id: message.id,
|
|
556
|
+
channel,
|
|
557
|
+
from: message.agentName,
|
|
558
|
+
text: message.text,
|
|
559
|
+
timestamp: message.createdAt,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** Poll channels for catch-up/recovery only. */
|
|
563
|
+
async pollMessages() {
|
|
564
|
+
if (!this.running)
|
|
565
|
+
return;
|
|
566
|
+
for (const channel of this.config.channels) {
|
|
567
|
+
try {
|
|
568
|
+
const after = this.channelCursor.get(channel);
|
|
569
|
+
const query = { limit: 50 };
|
|
570
|
+
if (after) {
|
|
571
|
+
query.after = after;
|
|
572
|
+
}
|
|
573
|
+
const messages = await this.relaycast.messages.list(channel, query);
|
|
574
|
+
const ordered = [...messages].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
575
|
+
for (const message of ordered) {
|
|
576
|
+
await this.handleInbound(this.normalizePolledMessage(channel, message));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
console.warn(`[gateway] Poll error for #${channel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async handleInbound(message) {
|
|
585
|
+
if (!this.running)
|
|
586
|
+
return;
|
|
587
|
+
if (this.processingMessageIds.has(message.id) || this.isSeen(message.id))
|
|
588
|
+
return;
|
|
589
|
+
// Avoid echo loops — skip messages from this claw or its viewer identity.
|
|
590
|
+
const viewerName = `viewer-${this.config.clawName}`;
|
|
591
|
+
if (message.from === this.config.clawName || message.from === viewerName) {
|
|
592
|
+
this.channelCursor.set(normalizeChannelName(message.channel), message.id);
|
|
593
|
+
this.markSeen(message.id);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// Mark as seen immediately to prevent duplicate delivery from concurrent
|
|
597
|
+
// realtime + poll paths processing the same message.
|
|
598
|
+
this.markSeen(message.id);
|
|
599
|
+
this.processingMessageIds.add(message.id);
|
|
600
|
+
console.log(`[gateway] Delivering message ${message.id} from @${message.from}: "${message.text}"`);
|
|
601
|
+
try {
|
|
602
|
+
const result = await this.onMessage(message);
|
|
603
|
+
console.log(`[gateway] Delivery result: ${result.method} ok=${result.ok}${result.error ? ' error=' + result.error : ''}`);
|
|
604
|
+
this.channelCursor.set(normalizeChannelName(message.channel), message.id);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
this.processingMessageIds.delete(message.id);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** Format delivery text with channel, sender, and optional thread prefix. */
|
|
611
|
+
formatDeliveryText(message) {
|
|
612
|
+
const threadPrefix = message.threadParentId ? '[thread] ' : '';
|
|
613
|
+
return `${threadPrefix}[relaycast:${message.channel}] @${message.from}: ${message.text}`;
|
|
614
|
+
}
|
|
615
|
+
/** Handle an inbound Relaycast message. */
|
|
616
|
+
async onMessage(message) {
|
|
617
|
+
// Try primary delivery via the shared relay sender (no extra broker spawned).
|
|
618
|
+
if (this.relaySender) {
|
|
619
|
+
const ok = await this.deliverViaRelaySender(message);
|
|
620
|
+
if (ok) {
|
|
621
|
+
return { ok: true, method: 'relay_sdk' };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Deliver via persistent OpenClaw gateway WebSocket connection
|
|
625
|
+
if (this.openclawClient) {
|
|
626
|
+
const text = this.formatDeliveryText(message);
|
|
627
|
+
const ok = await this.openclawClient.sendChatMessage(text, message.id);
|
|
628
|
+
if (ok) {
|
|
629
|
+
return { ok: true, method: 'gateway_ws' };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
console.warn(`[gateway] Failed to deliver message ${message.id} from @${message.from}`);
|
|
633
|
+
return { ok: false, method: 'failed', error: 'All delivery methods failed' };
|
|
634
|
+
}
|
|
635
|
+
/** Deliver via the caller-provided relay sender (shared broker). */
|
|
636
|
+
async deliverViaRelaySender(message) {
|
|
637
|
+
if (!this.relaySender)
|
|
638
|
+
return false;
|
|
639
|
+
const input = {
|
|
640
|
+
to: this.config.clawName,
|
|
641
|
+
text: this.formatDeliveryText(message),
|
|
642
|
+
from: message.from,
|
|
643
|
+
data: {
|
|
644
|
+
source: 'relaycast',
|
|
645
|
+
channel: message.channel,
|
|
646
|
+
messageId: message.id,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
const result = await this.relaySender.sendMessage(input);
|
|
651
|
+
return Boolean(result.event_id) && result.event_id !== 'unsupported_operation';
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
// Spawn control HTTP server
|
|
659
|
+
// -------------------------------------------------------------------------
|
|
660
|
+
async startControlServer() {
|
|
661
|
+
const port = Number(process.env.RELAYCAST_CONTROL_PORT) || InboundGateway.DEFAULT_CONTROL_PORT;
|
|
662
|
+
this.controlServer = createServer((req, res) => {
|
|
663
|
+
void this.handleControlRequest(req, res);
|
|
664
|
+
});
|
|
665
|
+
return new Promise((resolve) => {
|
|
666
|
+
this.controlServer.listen(port, '127.0.0.1', () => {
|
|
667
|
+
this.controlPort = port;
|
|
668
|
+
console.log(`[gateway] Spawn control API listening on http://127.0.0.1:${port}`);
|
|
669
|
+
resolve();
|
|
670
|
+
});
|
|
671
|
+
this.controlServer.on('error', (err) => {
|
|
672
|
+
console.warn(`[gateway] Control server failed to start on port ${port}: ${err.message}`);
|
|
673
|
+
this.controlServer = null;
|
|
674
|
+
resolve(); // Non-fatal
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async handleControlRequest(req, res) {
|
|
679
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
680
|
+
const path = url.pathname;
|
|
681
|
+
// CORS for local callers
|
|
682
|
+
res.setHeader('Content-Type', 'application/json');
|
|
683
|
+
if (req.method === 'GET' && path === '/health') {
|
|
684
|
+
res.writeHead(200);
|
|
685
|
+
res.end(JSON.stringify({
|
|
686
|
+
ok: true,
|
|
687
|
+
status: 'running',
|
|
688
|
+
active: this.spawnManager.size,
|
|
689
|
+
uptime: process.uptime(),
|
|
690
|
+
}));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (req.method === 'POST' && path === '/spawn') {
|
|
694
|
+
const body = await readBody(req);
|
|
695
|
+
try {
|
|
696
|
+
const args = JSON.parse(body);
|
|
697
|
+
const name = args.name;
|
|
698
|
+
if (!name) {
|
|
699
|
+
res.writeHead(400);
|
|
700
|
+
res.end(JSON.stringify({ ok: false, error: '"name" is required' }));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const relayApiKey = this.config.apiKey;
|
|
704
|
+
const spawnOpts = {
|
|
705
|
+
name,
|
|
706
|
+
relayApiKey,
|
|
707
|
+
role: args.role || undefined,
|
|
708
|
+
model: args.model || undefined,
|
|
709
|
+
channels: args.channels || undefined,
|
|
710
|
+
systemPrompt: args.system_prompt || undefined,
|
|
711
|
+
relayBaseUrl: this.config.baseUrl,
|
|
712
|
+
workspaceId: args.workspace_id || process.env.OPENCLAW_WORKSPACE_ID,
|
|
713
|
+
};
|
|
714
|
+
const handle = await this.spawnManager.spawn(spawnOpts);
|
|
715
|
+
res.writeHead(200);
|
|
716
|
+
res.end(JSON.stringify({
|
|
717
|
+
ok: true,
|
|
718
|
+
name: handle.displayName,
|
|
719
|
+
agentName: handle.agentName,
|
|
720
|
+
id: handle.id,
|
|
721
|
+
gatewayPort: handle.gatewayPort,
|
|
722
|
+
active: this.spawnManager.size,
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
res.writeHead(500);
|
|
727
|
+
res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (req.method === 'GET' && path === '/list') {
|
|
732
|
+
const handles = this.spawnManager.list();
|
|
733
|
+
res.writeHead(200);
|
|
734
|
+
res.end(JSON.stringify({
|
|
735
|
+
ok: true,
|
|
736
|
+
active: handles.length,
|
|
737
|
+
claws: handles.map(h => ({
|
|
738
|
+
name: h.displayName,
|
|
739
|
+
agentName: h.agentName,
|
|
740
|
+
id: h.id,
|
|
741
|
+
gatewayPort: h.gatewayPort,
|
|
742
|
+
})),
|
|
743
|
+
}));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (req.method === 'POST' && path === '/release') {
|
|
747
|
+
const body = await readBody(req);
|
|
748
|
+
try {
|
|
749
|
+
const args = JSON.parse(body);
|
|
750
|
+
const name = args.name;
|
|
751
|
+
const id = args.id;
|
|
752
|
+
if (!name && !id) {
|
|
753
|
+
res.writeHead(400);
|
|
754
|
+
res.end(JSON.stringify({ ok: false, error: 'Provide "name" or "id"' }));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
let released = false;
|
|
758
|
+
if (id) {
|
|
759
|
+
released = await this.spawnManager.release(id);
|
|
760
|
+
}
|
|
761
|
+
else if (name) {
|
|
762
|
+
released = await this.spawnManager.releaseByName(name);
|
|
763
|
+
}
|
|
764
|
+
res.writeHead(200);
|
|
765
|
+
res.end(JSON.stringify({ ok: released, active: this.spawnManager.size }));
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
res.writeHead(500);
|
|
769
|
+
res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
res.writeHead(404);
|
|
774
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function readBody(req) {
|
|
778
|
+
return new Promise((resolve, reject) => {
|
|
779
|
+
const chunks = [];
|
|
780
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
781
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
782
|
+
req.on('error', (err) => reject(err));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
//# sourceMappingURL=gateway.js.map
|