@xfxstudio/claworld 2026.4.14-testing.1 → 2026.4.16-testing.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/index.js +0 -50
- package/setup-entry.js +0 -6
- package/skills/claworld-a2a-channel-agent/SKILL.md +0 -218
- package/skills/claworld-help/SKILL.md +0 -304
- package/skills/claworld-join-and-chat/SKILL.md +0 -515
- package/skills/claworld-manage-worlds/SKILL.md +0 -283
- package/skills/claworld-manage-worlds/references/world-context-templates.md +0 -145
- package/src/lib/chat-request.js +0 -366
- package/src/lib/public-identity.js +0 -175
- package/src/lib/relay/agent-readable-markdown.js +0 -385
- package/src/lib/relay/kickoff-progress.js +0 -162
- package/src/lib/relay/kickoff-text.js +0 -191
- package/src/lib/relay/shared.js +0 -30
- package/src/lib/runtime-errors.js +0 -149
- package/src/openclaw/index.js +0 -51
- package/src/openclaw/plugin/account-identity.js +0 -73
- package/src/openclaw/plugin/claworld-channel-plugin.js +0 -3483
- package/src/openclaw/plugin/config-schema.js +0 -392
- package/src/openclaw/plugin/lifecycle.js +0 -114
- package/src/openclaw/plugin/managed-config.js +0 -1054
- package/src/openclaw/plugin/onboarding.js +0 -312
- package/src/openclaw/plugin/register-tooling.js +0 -728
- package/src/openclaw/plugin/register.js +0 -1609
- package/src/openclaw/plugin/relay-client-shared.js +0 -146
- package/src/openclaw/plugin/relay-client.js +0 -1469
- package/src/openclaw/plugin/runtime-backup.js +0 -105
- package/src/openclaw/plugin/runtime.js +0 -12
- package/src/openclaw/plugin-version.js +0 -67
- package/src/openclaw/protocol/relay-event-protocol.js +0 -43
- package/src/openclaw/runtime/backend-error-context.js +0 -91
- package/src/openclaw/runtime/canonical-result-builder.js +0 -126
- package/src/openclaw/runtime/demo-session-bootstrap.js +0 -32
- package/src/openclaw/runtime/feedback-helper.js +0 -145
- package/src/openclaw/runtime/inbound-session-router.js +0 -44
- package/src/openclaw/runtime/outbound-session-bridge.js +0 -29
- package/src/openclaw/runtime/product-shell-helper.js +0 -931
- package/src/openclaw/runtime/runtime-path.js +0 -19
- package/src/openclaw/runtime/system-message-orchestrator.js +0 -1
- package/src/openclaw/runtime/tool-contracts.js +0 -939
- package/src/openclaw/runtime/tool-inventory.js +0 -83
- package/src/openclaw/runtime/world-membership-helper.js +0 -320
- package/src/openclaw/runtime/world-moderation-helper.js +0 -508
- package/src/product-shell/contracts/chat-request-approval-policy.js +0 -93
- package/src/product-shell/contracts/world-orchestration.js +0 -734
- package/src/product-shell/orchestration/world-conversation-text.js +0 -229
|
@@ -1,1469 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import WebSocket from 'ws';
|
|
3
|
-
import { resolveClaworldRuntimeConfig } from './config-schema.js';
|
|
4
|
-
import { buildRuntimeAuthHeaders } from './account-identity.js';
|
|
5
|
-
import { buildClaworldRelayClientVersion } from '../plugin-version.js';
|
|
6
|
-
import { createRelayEventProtocol } from '../protocol/relay-event-protocol.js';
|
|
7
|
-
import { createInboundSessionRouter } from '../runtime/inbound-session-router.js';
|
|
8
|
-
import { createOutboundSessionBridge } from '../runtime/outbound-session-bridge.js';
|
|
9
|
-
import { normalizeChatRequestInput } from '../../lib/chat-request.js';
|
|
10
|
-
import {
|
|
11
|
-
buildPublicErrorPayload,
|
|
12
|
-
createRuntimeBoundaryError,
|
|
13
|
-
logRuntimeBoundary,
|
|
14
|
-
} from '../../lib/runtime-errors.js';
|
|
15
|
-
import {
|
|
16
|
-
buildAcceptedAckTimeoutError,
|
|
17
|
-
buildInboundEnvelope,
|
|
18
|
-
buildKeepSilentAckTimeoutError,
|
|
19
|
-
buildKeepSilentFallbackError,
|
|
20
|
-
buildReplyAckTimeoutError,
|
|
21
|
-
buildReplyFallbackError,
|
|
22
|
-
DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
23
|
-
DUPLICATE_CONNECTION_CLOSE_CODE,
|
|
24
|
-
isDeliveryKeptSilentAlreadyApplied,
|
|
25
|
-
isReplyAlreadyApplied,
|
|
26
|
-
normalizeOptionalText,
|
|
27
|
-
normalizeRelayWebSocketUrl,
|
|
28
|
-
requireClientMessageId,
|
|
29
|
-
STALE_CONNECTION_CLOSE_CODE,
|
|
30
|
-
TERMINAL_CLOSE_REASONS,
|
|
31
|
-
} from './relay-client-shared.js';
|
|
32
|
-
|
|
33
|
-
const DELIVERY_VISIBILITY_RETRY_ATTEMPTS = 20;
|
|
34
|
-
const DELIVERY_VISIBILITY_RETRY_DELAY_MS = 10;
|
|
35
|
-
|
|
36
|
-
function isDeliveryVisibilityMiss(result = {}) {
|
|
37
|
-
return Number(result?.status) === 404
|
|
38
|
-
&& normalizeOptionalText(result?.body?.error) === 'delivery_not_found';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function waitForDeliveryVisibilityRetry() {
|
|
42
|
-
await new Promise((resolve) => {
|
|
43
|
-
const timer = setTimeout(resolve, DELIVERY_VISIBILITY_RETRY_DELAY_MS);
|
|
44
|
-
if (typeof timer?.unref === 'function') timer.unref();
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class ClaworldRelayClient extends EventEmitter {
|
|
49
|
-
constructor({
|
|
50
|
-
logger = console,
|
|
51
|
-
inbound = createInboundSessionRouter(),
|
|
52
|
-
outbound = createOutboundSessionBridge(),
|
|
53
|
-
protocol = createRelayEventProtocol(),
|
|
54
|
-
wsFactory = (url) => new WebSocket(url),
|
|
55
|
-
httpFetch = globalThis.fetch,
|
|
56
|
-
} = {}) {
|
|
57
|
-
super();
|
|
58
|
-
this.logger = logger;
|
|
59
|
-
this.inbound = inbound;
|
|
60
|
-
this.outbound = outbound;
|
|
61
|
-
this.protocol = protocol;
|
|
62
|
-
this.wsFactory = wsFactory;
|
|
63
|
-
this.httpFetch = httpFetch;
|
|
64
|
-
this.ws = null;
|
|
65
|
-
this.events = [];
|
|
66
|
-
this.heartbeatTimer = null;
|
|
67
|
-
this.connectionState = 'idle';
|
|
68
|
-
this.runtimeConfig = null;
|
|
69
|
-
this.boundAgentId = null;
|
|
70
|
-
this.serverUrl = null;
|
|
71
|
-
this.connectionParams = null;
|
|
72
|
-
this.reconnectTimer = null;
|
|
73
|
-
this.reconnectAttempts = 0;
|
|
74
|
-
this.manualClose = false;
|
|
75
|
-
this.lastDisconnectInfo = null;
|
|
76
|
-
this.acceptedChatRequests = new Map();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
buildBoundaryContext(extra = null) {
|
|
80
|
-
return {
|
|
81
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
82
|
-
agentId: this.boundAgentId,
|
|
83
|
-
...(extra && typeof extra === 'object' ? extra : {}),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
buildDisconnectInfo(code = null, reason = null, source = 'socket') {
|
|
88
|
-
const reasonText = String(reason || '').trim() || null;
|
|
89
|
-
return {
|
|
90
|
-
code: Number.isInteger(code) ? code : null,
|
|
91
|
-
reason: reasonText,
|
|
92
|
-
source,
|
|
93
|
-
recoverable: !(
|
|
94
|
-
code === DUPLICATE_CONNECTION_CLOSE_CODE
|
|
95
|
-
|| code === STALE_CONNECTION_CLOSE_CODE
|
|
96
|
-
|| TERMINAL_CLOSE_REASONS.has(reasonText)
|
|
97
|
-
),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
clearReconnectTimer() {
|
|
102
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
103
|
-
this.reconnectTimer = null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
resolveReconnectDelayMs() {
|
|
107
|
-
const heartbeatSeconds = Number(this.runtimeConfig?.heartbeatSeconds || 1);
|
|
108
|
-
return Math.min(5000, Math.max(500, Math.floor(heartbeatSeconds * 500)));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
shouldAutoReconnect(disconnectInfo = null) {
|
|
112
|
-
if (this.manualClose) return false;
|
|
113
|
-
if (this.runtimeConfig?.reconnect === false) return false;
|
|
114
|
-
return disconnectInfo?.recoverable !== false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
emitRelayMessage(message, { sessionTarget, fallbackTarget }) {
|
|
118
|
-
this.events.push(message);
|
|
119
|
-
this.emit('event', message);
|
|
120
|
-
if (message.event === 'error') {
|
|
121
|
-
this.emit('relay_error', message);
|
|
122
|
-
this.emit('relay.error', message);
|
|
123
|
-
} else {
|
|
124
|
-
this.emit(message.event, message);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const inboundEnvelope = buildInboundEnvelope(message);
|
|
128
|
-
if (!inboundEnvelope) return;
|
|
129
|
-
const described = this.protocol.describeEvent(inboundEnvelope);
|
|
130
|
-
const route = this.inbound.routeInboundEvent(inboundEnvelope, {
|
|
131
|
-
sessionTarget: sessionTarget || this.runtimeConfig.routing.sessionTarget,
|
|
132
|
-
fallbackTarget: fallbackTarget || this.runtimeConfig.routing.fallbackTarget,
|
|
133
|
-
});
|
|
134
|
-
const runtimeEvent = {
|
|
135
|
-
eventType: inboundEnvelope.eventType,
|
|
136
|
-
protocol: described,
|
|
137
|
-
delivery: inboundEnvelope,
|
|
138
|
-
route,
|
|
139
|
-
raw: message,
|
|
140
|
-
};
|
|
141
|
-
this.emit('runtime_event', runtimeEvent);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
emitBoundaryError(
|
|
145
|
-
label,
|
|
146
|
-
error,
|
|
147
|
-
{
|
|
148
|
-
code = 'relay_runtime_error',
|
|
149
|
-
category = 'runtime',
|
|
150
|
-
publicMessage = 'relay runtime error',
|
|
151
|
-
recoverable = true,
|
|
152
|
-
context = null,
|
|
153
|
-
errorType = 'relay_runtime_error',
|
|
154
|
-
fallbackMessage = null,
|
|
155
|
-
includeStack = false,
|
|
156
|
-
} = {},
|
|
157
|
-
) {
|
|
158
|
-
const normalized = logRuntimeBoundary(this.logger, label, error, this.buildBoundaryContext(context), {
|
|
159
|
-
includeStack,
|
|
160
|
-
fallback: {
|
|
161
|
-
code,
|
|
162
|
-
category,
|
|
163
|
-
publicMessage,
|
|
164
|
-
recoverable,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
const payload = {
|
|
168
|
-
event: 'error',
|
|
169
|
-
data: buildPublicErrorPayload(normalized, {
|
|
170
|
-
errorType,
|
|
171
|
-
fallbackMessage: fallbackMessage || publicMessage,
|
|
172
|
-
exposeMessage: true,
|
|
173
|
-
}),
|
|
174
|
-
};
|
|
175
|
-
this.events.push(payload);
|
|
176
|
-
this.emit('event', payload);
|
|
177
|
-
this.emit('relay_error', payload);
|
|
178
|
-
this.emit('relay.error', payload);
|
|
179
|
-
return normalized;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
buildDisconnectInfoFromError(error, source = 'reconnect') {
|
|
183
|
-
if (error?.close) return error.close;
|
|
184
|
-
return this.buildDisconnectInfo(null, error?.reason || error?.code || error?.message || 'reconnect_failed', source);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
createClosedBeforeAuthError(disconnectInfo, cause = null) {
|
|
188
|
-
const normalized = createRuntimeBoundaryError({
|
|
189
|
-
code: 'relay_ws_closed_before_auth',
|
|
190
|
-
category: 'transport',
|
|
191
|
-
status: 502,
|
|
192
|
-
message: `relay websocket closed before authentication (code=${disconnectInfo?.code ?? 'unknown'}, reason=${disconnectInfo?.reason || ''})`,
|
|
193
|
-
publicMessage: 'relay websocket closed before authentication',
|
|
194
|
-
recoverable: disconnectInfo?.recoverable !== false,
|
|
195
|
-
context: this.buildBoundaryContext({
|
|
196
|
-
stage: 'pre_auth_close',
|
|
197
|
-
closeCode: disconnectInfo?.code ?? null,
|
|
198
|
-
closeReason: disconnectInfo?.reason || null,
|
|
199
|
-
}),
|
|
200
|
-
cause,
|
|
201
|
-
});
|
|
202
|
-
normalized.reason = disconnectInfo?.reason || normalized.code;
|
|
203
|
-
normalized.close = disconnectInfo;
|
|
204
|
-
normalized.fatal = disconnectInfo?.recoverable === false;
|
|
205
|
-
return normalized;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async requestJson(pathName, init = {}, fallback = {}) {
|
|
209
|
-
if (!this.serverUrl) {
|
|
210
|
-
throw createRuntimeBoundaryError({
|
|
211
|
-
code: 'relay_client_not_connected',
|
|
212
|
-
category: 'runtime',
|
|
213
|
-
status: 409,
|
|
214
|
-
message: 'client not connected',
|
|
215
|
-
publicMessage: 'relay client is not connected',
|
|
216
|
-
recoverable: true,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const url = `${this.serverUrl}${pathName}`;
|
|
221
|
-
let response;
|
|
222
|
-
try {
|
|
223
|
-
response = await this.httpFetch(url, init);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
throw createRuntimeBoundaryError({
|
|
226
|
-
code: fallback.code || 'relay_fetch_failed',
|
|
227
|
-
category: 'transport',
|
|
228
|
-
status: 502,
|
|
229
|
-
message: `${fallback.message || 'relay request failed'}: ${error?.message || String(error)}`,
|
|
230
|
-
publicMessage: fallback.publicMessage || 'relay request failed',
|
|
231
|
-
recoverable: true,
|
|
232
|
-
context: {
|
|
233
|
-
url,
|
|
234
|
-
method: init?.method || 'GET',
|
|
235
|
-
},
|
|
236
|
-
cause: error,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let body = null;
|
|
241
|
-
try {
|
|
242
|
-
body = await response.json();
|
|
243
|
-
} catch (error) {
|
|
244
|
-
throw createRuntimeBoundaryError({
|
|
245
|
-
code: 'relay_response_invalid',
|
|
246
|
-
category: 'transport',
|
|
247
|
-
status: 502,
|
|
248
|
-
message: `relay response was not valid JSON: ${error?.message || String(error)}`,
|
|
249
|
-
publicMessage: 'relay response was not valid JSON',
|
|
250
|
-
recoverable: true,
|
|
251
|
-
context: {
|
|
252
|
-
url,
|
|
253
|
-
method: init?.method || 'GET',
|
|
254
|
-
status: response.status,
|
|
255
|
-
},
|
|
256
|
-
cause: error,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return { status: response.status, body };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async requestJsonWithDeliveryVisibilityRetry(pathName, init = {}, fallback = {}) {
|
|
264
|
-
let attempt = 0;
|
|
265
|
-
while (true) {
|
|
266
|
-
const result = await this.requestJson(pathName, init, fallback);
|
|
267
|
-
if (!isDeliveryVisibilityMiss(result) || attempt >= DELIVERY_VISIBILITY_RETRY_ATTEMPTS - 1) {
|
|
268
|
-
return result;
|
|
269
|
-
}
|
|
270
|
-
attempt += 1;
|
|
271
|
-
await waitForDeliveryVisibilityRetry();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async openSocket({
|
|
276
|
-
wsUrl,
|
|
277
|
-
agentId,
|
|
278
|
-
credential,
|
|
279
|
-
clientVersion,
|
|
280
|
-
sessionTarget,
|
|
281
|
-
fallbackTarget,
|
|
282
|
-
}) {
|
|
283
|
-
this.connectionState = this.connectionState === 'reconnecting' ? 'reconnecting' : 'connecting';
|
|
284
|
-
|
|
285
|
-
return await new Promise((resolve, reject) => {
|
|
286
|
-
let settled = false;
|
|
287
|
-
let suppressCloseHandler = false;
|
|
288
|
-
const ws = this.wsFactory(wsUrl);
|
|
289
|
-
this.ws = ws;
|
|
290
|
-
|
|
291
|
-
ws.on('open', () => {
|
|
292
|
-
this.logger.info?.('[claworld:relay-client] websocket open, sending auth', {
|
|
293
|
-
accountId: this.runtimeConfig.accountId,
|
|
294
|
-
agentId,
|
|
295
|
-
clientVersion,
|
|
296
|
-
bridgeProtocol: this.protocol.version,
|
|
297
|
-
});
|
|
298
|
-
this.send({
|
|
299
|
-
type: 'auth',
|
|
300
|
-
agentId,
|
|
301
|
-
credential,
|
|
302
|
-
clientVersion,
|
|
303
|
-
bridgeProtocol: this.protocol.version,
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
ws.on('message', (buf) => {
|
|
308
|
-
let message;
|
|
309
|
-
try {
|
|
310
|
-
message = JSON.parse(String(buf));
|
|
311
|
-
} catch (error) {
|
|
312
|
-
const normalized = this.emitBoundaryError('[claworld:relay-client] invalid relay message', error, {
|
|
313
|
-
code: 'relay_ws_message_invalid',
|
|
314
|
-
category: 'transport',
|
|
315
|
-
publicMessage: 'relay websocket delivered invalid JSON',
|
|
316
|
-
context: { stage: 'message_parse' },
|
|
317
|
-
});
|
|
318
|
-
if (!settled) {
|
|
319
|
-
settled = true;
|
|
320
|
-
this.connectionState = 'error';
|
|
321
|
-
reject(normalized);
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
this.emitRelayMessage(message, { sessionTarget, fallbackTarget });
|
|
328
|
-
|
|
329
|
-
if (message.event === 'auth.ok' && !settled) {
|
|
330
|
-
settled = true;
|
|
331
|
-
this.connectionState = 'authenticated';
|
|
332
|
-
this.reconnectAttempts = 0;
|
|
333
|
-
this.logger.info?.('[claworld:relay-client] auth ok', {
|
|
334
|
-
accountId: this.runtimeConfig.accountId,
|
|
335
|
-
agentId,
|
|
336
|
-
});
|
|
337
|
-
this.startHeartbeatLoop();
|
|
338
|
-
resolve(message);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (message.event === 'error' && !settled && message.data?.code === 'unauthorized') {
|
|
342
|
-
settled = true;
|
|
343
|
-
suppressCloseHandler = true;
|
|
344
|
-
this.connectionState = 'error';
|
|
345
|
-
const authReason = message.data?.reason || message.data?.error || 'unauthorized';
|
|
346
|
-
const authError = createRuntimeBoundaryError({
|
|
347
|
-
code: message.data?.code || 'unauthorized',
|
|
348
|
-
category: 'auth',
|
|
349
|
-
status: 401,
|
|
350
|
-
message: authReason,
|
|
351
|
-
publicMessage: 'relay authentication failed',
|
|
352
|
-
recoverable: true,
|
|
353
|
-
context: this.buildBoundaryContext({
|
|
354
|
-
stage: 'auth',
|
|
355
|
-
reason: authReason,
|
|
356
|
-
}),
|
|
357
|
-
});
|
|
358
|
-
authError.reason = authReason;
|
|
359
|
-
authError.fatal = true;
|
|
360
|
-
authError.close = this.buildDisconnectInfo(null, authReason, 'auth');
|
|
361
|
-
this.logger.error?.('[claworld:relay-client] auth failed', {
|
|
362
|
-
accountId: this.runtimeConfig.accountId,
|
|
363
|
-
agentId,
|
|
364
|
-
error: authError.message,
|
|
365
|
-
code: authError.code,
|
|
366
|
-
});
|
|
367
|
-
reject(authError);
|
|
368
|
-
}
|
|
369
|
-
} catch (error) {
|
|
370
|
-
const normalized = this.emitBoundaryError('[claworld:relay-client] relay message handling failed', error, {
|
|
371
|
-
code: 'relay_ws_message_handling_failed',
|
|
372
|
-
category: 'runtime',
|
|
373
|
-
publicMessage: 'relay websocket message handling failed',
|
|
374
|
-
context: {
|
|
375
|
-
stage: 'message_handle',
|
|
376
|
-
relayEvent: message?.event || null,
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
if (!settled) {
|
|
380
|
-
settled = true;
|
|
381
|
-
suppressCloseHandler = true;
|
|
382
|
-
this.connectionState = 'error';
|
|
383
|
-
reject(normalized);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
ws.on('close', (code, reason) => {
|
|
389
|
-
if (this.ws === ws) this.ws = null;
|
|
390
|
-
this.stopHeartbeatLoop();
|
|
391
|
-
const disconnectInfo = this.buildDisconnectInfo(code, reason);
|
|
392
|
-
this.lastDisconnectInfo = disconnectInfo;
|
|
393
|
-
this.logger.warn?.('[claworld:relay-client] websocket closed', {
|
|
394
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
395
|
-
agentId: this.boundAgentId,
|
|
396
|
-
code: disconnectInfo.code,
|
|
397
|
-
reason: disconnectInfo.reason || '',
|
|
398
|
-
recoverable: disconnectInfo.recoverable,
|
|
399
|
-
});
|
|
400
|
-
this.emit('disconnect', disconnectInfo);
|
|
401
|
-
if (suppressCloseHandler) return;
|
|
402
|
-
|
|
403
|
-
if (!settled) {
|
|
404
|
-
settled = true;
|
|
405
|
-
this.connectionState = disconnectInfo.reason === 'duplicate_connection_replaced' ? 'replaced' : 'error';
|
|
406
|
-
reject(this.createClosedBeforeAuthError(disconnectInfo));
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (this.shouldAutoReconnect(disconnectInfo)) {
|
|
411
|
-
this.connectionState = 'reconnecting';
|
|
412
|
-
this.scheduleReconnect(disconnectInfo);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
this.connectionState = disconnectInfo.reason === 'duplicate_connection_replaced' ? 'replaced' : 'closed';
|
|
417
|
-
this.emit('close', disconnectInfo);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
ws.on('error', (error) => {
|
|
421
|
-
const normalized = logRuntimeBoundary(this.logger, '[claworld:relay-client] websocket error', error, this.buildBoundaryContext({
|
|
422
|
-
stage: settled ? 'runtime_socket_error' : 'connect_socket_error',
|
|
423
|
-
}), {
|
|
424
|
-
includeStack: false,
|
|
425
|
-
fallback: {
|
|
426
|
-
code: settled ? 'relay_ws_runtime_error' : 'relay_ws_connect_failed',
|
|
427
|
-
category: 'transport',
|
|
428
|
-
publicMessage: settled ? 'relay websocket runtime error' : 'relay websocket connection failed',
|
|
429
|
-
recoverable: true,
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
if (!settled) {
|
|
433
|
-
settled = true;
|
|
434
|
-
suppressCloseHandler = true;
|
|
435
|
-
this.connectionState = 'error';
|
|
436
|
-
normalized.reason = normalized.reason || normalized.code || normalized.message;
|
|
437
|
-
reject(normalized);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
scheduleReconnect(disconnectInfo = null) {
|
|
444
|
-
if (!this.shouldAutoReconnect(disconnectInfo)) {
|
|
445
|
-
this.connectionState = 'closed';
|
|
446
|
-
this.emit('close', disconnectInfo || this.buildDisconnectInfo(null, 'reconnect_disabled', 'reconnect'));
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
this.clearReconnectTimer();
|
|
451
|
-
const delayMs = this.resolveReconnectDelayMs();
|
|
452
|
-
const attempt = this.reconnectAttempts + 1;
|
|
453
|
-
this.reconnectAttempts = attempt;
|
|
454
|
-
this.logger.warn?.('[claworld:relay-client] scheduling reconnect', {
|
|
455
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
456
|
-
agentId: this.boundAgentId,
|
|
457
|
-
attempt,
|
|
458
|
-
delayMs,
|
|
459
|
-
reason: disconnectInfo?.reason || null,
|
|
460
|
-
});
|
|
461
|
-
this.emit('reconnecting', { attempt, delayMs, disconnectInfo });
|
|
462
|
-
|
|
463
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
464
|
-
this.reconnectTimer = null;
|
|
465
|
-
if (this.manualClose || !this.connectionParams) return;
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
await this.openSocket(this.connectionParams);
|
|
469
|
-
this.emit('reconnected', { attempt, disconnectInfo });
|
|
470
|
-
} catch (error) {
|
|
471
|
-
const nextDisconnect = this.buildDisconnectInfoFromError(error);
|
|
472
|
-
this.lastDisconnectInfo = nextDisconnect;
|
|
473
|
-
if (error?.fatal || !this.shouldAutoReconnect(nextDisconnect)) {
|
|
474
|
-
this.connectionState = nextDisconnect.reason === 'duplicate_connection_replaced' ? 'replaced' : 'error';
|
|
475
|
-
this.emit('close', nextDisconnect);
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
this.connectionState = 'reconnecting';
|
|
479
|
-
this.scheduleReconnect(nextDisconnect);
|
|
480
|
-
}
|
|
481
|
-
}, delayMs);
|
|
482
|
-
|
|
483
|
-
if (typeof this.reconnectTimer.unref === 'function') this.reconnectTimer.unref();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async connect({
|
|
487
|
-
config,
|
|
488
|
-
agentId,
|
|
489
|
-
credential = null,
|
|
490
|
-
clientVersion = buildClaworldRelayClientVersion(),
|
|
491
|
-
sessionTarget,
|
|
492
|
-
fallbackTarget,
|
|
493
|
-
} = {}) {
|
|
494
|
-
if (!agentId) {
|
|
495
|
-
throw createRuntimeBoundaryError({
|
|
496
|
-
code: 'relay_agent_id_required',
|
|
497
|
-
category: 'input',
|
|
498
|
-
status: 400,
|
|
499
|
-
message: 'agentId is required',
|
|
500
|
-
publicMessage: 'agentId is required',
|
|
501
|
-
recoverable: true,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
this.runtimeConfig = resolveClaworldRuntimeConfig(config);
|
|
506
|
-
this.boundAgentId = agentId;
|
|
507
|
-
this.serverUrl = this.runtimeConfig.serverUrl;
|
|
508
|
-
this.manualClose = false;
|
|
509
|
-
this.clearReconnectTimer();
|
|
510
|
-
|
|
511
|
-
const wsUrl = normalizeRelayWebSocketUrl(this.runtimeConfig.serverUrl);
|
|
512
|
-
this.connectionParams = {
|
|
513
|
-
wsUrl,
|
|
514
|
-
agentId,
|
|
515
|
-
credential,
|
|
516
|
-
clientVersion,
|
|
517
|
-
sessionTarget,
|
|
518
|
-
fallbackTarget,
|
|
519
|
-
};
|
|
520
|
-
this.connectionState = 'connecting';
|
|
521
|
-
|
|
522
|
-
this.logger.info?.('[claworld:relay-client] connecting websocket', {
|
|
523
|
-
accountId: this.runtimeConfig.accountId,
|
|
524
|
-
agentId,
|
|
525
|
-
wsUrl,
|
|
526
|
-
reconnect: this.runtimeConfig.reconnect !== false,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
return await this.openSocket(this.connectionParams);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
startHeartbeatLoop() {
|
|
533
|
-
this.stopHeartbeatLoop();
|
|
534
|
-
const intervalMs = this.runtimeConfig.heartbeatSeconds * 1000;
|
|
535
|
-
this.heartbeatTimer = setInterval(() => {
|
|
536
|
-
try {
|
|
537
|
-
this.sendHeartbeat();
|
|
538
|
-
} catch (error) {
|
|
539
|
-
logRuntimeBoundary(this.logger, '[claworld:relay-client] heartbeat failed', error, this.buildBoundaryContext({
|
|
540
|
-
stage: 'heartbeat',
|
|
541
|
-
}), {
|
|
542
|
-
includeStack: false,
|
|
543
|
-
fallback: {
|
|
544
|
-
code: 'relay_heartbeat_failed',
|
|
545
|
-
category: 'transport',
|
|
546
|
-
publicMessage: 'relay heartbeat failed',
|
|
547
|
-
recoverable: true,
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
}, intervalMs);
|
|
552
|
-
if (typeof this.heartbeatTimer.unref === 'function') this.heartbeatTimer.unref();
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
stopHeartbeatLoop() {
|
|
556
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
557
|
-
this.heartbeatTimer = null;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
send(payload) {
|
|
561
|
-
if (!this.ws || this.ws.readyState !== 1) {
|
|
562
|
-
throw createRuntimeBoundaryError({
|
|
563
|
-
code: 'relay_ws_not_connected',
|
|
564
|
-
category: 'transport',
|
|
565
|
-
status: 409,
|
|
566
|
-
message: 'relay websocket is not connected',
|
|
567
|
-
publicMessage: 'relay websocket is not connected',
|
|
568
|
-
recoverable: true,
|
|
569
|
-
context: this.buildBoundaryContext({
|
|
570
|
-
stage: 'send',
|
|
571
|
-
}),
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
this.ws.send(JSON.stringify(payload));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
sendHeartbeat() {
|
|
578
|
-
this.send({ type: 'heartbeat' });
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
sendAccepted({ deliveryId, sessionKey, source = 'runtime_dispatch' } = {}) {
|
|
582
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
583
|
-
if (!normalizedDeliveryId) {
|
|
584
|
-
throw createRuntimeBoundaryError({
|
|
585
|
-
code: 'relay_delivery_id_required',
|
|
586
|
-
category: 'input',
|
|
587
|
-
status: 400,
|
|
588
|
-
message: 'deliveryId is required to acknowledge relay delivery acceptance',
|
|
589
|
-
publicMessage: 'deliveryId is required',
|
|
590
|
-
recoverable: true,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
const envelope = {
|
|
594
|
-
deliveryId: normalizedDeliveryId,
|
|
595
|
-
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
596
|
-
source: normalizeOptionalText(source) || 'runtime_dispatch',
|
|
597
|
-
};
|
|
598
|
-
this.send({
|
|
599
|
-
type: 'accepted',
|
|
600
|
-
deliveryId: envelope.deliveryId,
|
|
601
|
-
sessionKey: envelope.sessionKey,
|
|
602
|
-
payload: {
|
|
603
|
-
source: envelope.source,
|
|
604
|
-
},
|
|
605
|
-
});
|
|
606
|
-
return envelope;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
sendReply({ deliveryId, sessionKey, replyText, source = 'subagent' } = {}) {
|
|
610
|
-
const envelope = this.outbound.createReplyEnvelope({
|
|
611
|
-
deliveryId,
|
|
612
|
-
sessionKey,
|
|
613
|
-
replyText,
|
|
614
|
-
source,
|
|
615
|
-
});
|
|
616
|
-
this.send({
|
|
617
|
-
type: 'reply',
|
|
618
|
-
deliveryId: envelope.deliveryId,
|
|
619
|
-
sessionKey: envelope.sessionKey,
|
|
620
|
-
payload: {
|
|
621
|
-
...envelope.payload,
|
|
622
|
-
},
|
|
623
|
-
});
|
|
624
|
-
return envelope;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
replyToDelivery({ deliveryId, sessionKey, replyText, source = 'subagent' } = {}) {
|
|
628
|
-
return this.sendReply({
|
|
629
|
-
deliveryId,
|
|
630
|
-
sessionKey,
|
|
631
|
-
replyText,
|
|
632
|
-
source,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
sendKeepSilent({ deliveryId, sessionKey, reason = null, source = 'openclaw-autochain' } = {}) {
|
|
637
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
638
|
-
if (!normalizedDeliveryId) {
|
|
639
|
-
throw createRuntimeBoundaryError({
|
|
640
|
-
code: 'relay_delivery_id_required',
|
|
641
|
-
category: 'input',
|
|
642
|
-
status: 400,
|
|
643
|
-
message: 'deliveryId is required to mark relay delivery kept_silent',
|
|
644
|
-
publicMessage: 'deliveryId is required',
|
|
645
|
-
recoverable: true,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
const envelope = {
|
|
649
|
-
deliveryId: normalizedDeliveryId,
|
|
650
|
-
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
651
|
-
reason: normalizeOptionalText(reason) || 'no_renderable_reply',
|
|
652
|
-
source: normalizeOptionalText(source) || 'openclaw-autochain',
|
|
653
|
-
};
|
|
654
|
-
this.send({
|
|
655
|
-
type: 'kept_silent',
|
|
656
|
-
deliveryId: envelope.deliveryId,
|
|
657
|
-
sessionKey: envelope.sessionKey,
|
|
658
|
-
payload: {
|
|
659
|
-
reason: envelope.reason,
|
|
660
|
-
source: envelope.source,
|
|
661
|
-
},
|
|
662
|
-
});
|
|
663
|
-
return envelope;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
waitForReplyAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
|
|
667
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
668
|
-
if (!normalizedDeliveryId) {
|
|
669
|
-
return Promise.reject(createRuntimeBoundaryError({
|
|
670
|
-
code: 'relay_delivery_id_required',
|
|
671
|
-
category: 'input',
|
|
672
|
-
status: 400,
|
|
673
|
-
message: 'deliveryId is required to wait for relay reply acknowledgement',
|
|
674
|
-
publicMessage: 'deliveryId is required',
|
|
675
|
-
recoverable: true,
|
|
676
|
-
}));
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return new Promise((resolve, reject) => {
|
|
680
|
-
let settled = false;
|
|
681
|
-
let timeout = null;
|
|
682
|
-
|
|
683
|
-
const cleanup = () => {
|
|
684
|
-
if (timeout) clearTimeout(timeout);
|
|
685
|
-
this.off('reply.accepted', onReplyAccepted);
|
|
686
|
-
this.off('disconnect', onDisconnect);
|
|
687
|
-
this.off('close', onDisconnect);
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
const settleResolve = (value) => {
|
|
691
|
-
if (settled) return;
|
|
692
|
-
settled = true;
|
|
693
|
-
cleanup();
|
|
694
|
-
resolve(value);
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
const settleReject = (error) => {
|
|
698
|
-
if (settled) return;
|
|
699
|
-
settled = true;
|
|
700
|
-
cleanup();
|
|
701
|
-
reject(error);
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
const onReplyAccepted = (message = {}) => {
|
|
705
|
-
const repliedDeliveryId = normalizeOptionalText(message?.data?.repliedDeliveryId);
|
|
706
|
-
if (repliedDeliveryId !== normalizedDeliveryId) return;
|
|
707
|
-
settleResolve(message);
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
const onDisconnect = (info = {}) => {
|
|
711
|
-
settleReject(createRuntimeBoundaryError({
|
|
712
|
-
code: 'relay_reply_ack_disconnected',
|
|
713
|
-
category: 'transport',
|
|
714
|
-
status: 502,
|
|
715
|
-
message: `relay websocket closed before reply acknowledgement for ${normalizedDeliveryId}`,
|
|
716
|
-
publicMessage: 'relay websocket closed before reply acknowledgement',
|
|
717
|
-
recoverable: true,
|
|
718
|
-
context: this.buildBoundaryContext({
|
|
719
|
-
stage: 'reply_ack_wait',
|
|
720
|
-
deliveryId: normalizedDeliveryId,
|
|
721
|
-
closeCode: info?.code ?? null,
|
|
722
|
-
closeReason: info?.reason || null,
|
|
723
|
-
}),
|
|
724
|
-
}));
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
this.on('reply.accepted', onReplyAccepted);
|
|
728
|
-
this.on('disconnect', onDisconnect);
|
|
729
|
-
this.on('close', onDisconnect);
|
|
730
|
-
|
|
731
|
-
timeout = setTimeout(() => {
|
|
732
|
-
settleReject(buildReplyAckTimeoutError({
|
|
733
|
-
deliveryId: normalizedDeliveryId,
|
|
734
|
-
timeoutMs,
|
|
735
|
-
context: this.buildBoundaryContext({
|
|
736
|
-
stage: 'reply_ack_wait',
|
|
737
|
-
deliveryId: normalizedDeliveryId,
|
|
738
|
-
}),
|
|
739
|
-
}));
|
|
740
|
-
}, timeoutMs);
|
|
741
|
-
if (typeof timeout.unref === 'function') timeout.unref();
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
waitForAcceptedAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
|
|
746
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
747
|
-
if (!normalizedDeliveryId) {
|
|
748
|
-
return Promise.reject(createRuntimeBoundaryError({
|
|
749
|
-
code: 'relay_delivery_id_required',
|
|
750
|
-
category: 'input',
|
|
751
|
-
status: 400,
|
|
752
|
-
message: 'deliveryId is required to wait for relay delivery acceptance acknowledgement',
|
|
753
|
-
publicMessage: 'deliveryId is required',
|
|
754
|
-
recoverable: true,
|
|
755
|
-
}));
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return new Promise((resolve, reject) => {
|
|
759
|
-
let settled = false;
|
|
760
|
-
let timeout = null;
|
|
761
|
-
|
|
762
|
-
const cleanup = () => {
|
|
763
|
-
if (timeout) clearTimeout(timeout);
|
|
764
|
-
this.off('delivery.accepted', onAccepted);
|
|
765
|
-
this.off('disconnect', onDisconnect);
|
|
766
|
-
this.off('close', onDisconnect);
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
const settleResolve = (value) => {
|
|
770
|
-
if (settled) return;
|
|
771
|
-
settled = true;
|
|
772
|
-
cleanup();
|
|
773
|
-
resolve(value);
|
|
774
|
-
};
|
|
775
|
-
|
|
776
|
-
const settleReject = (error) => {
|
|
777
|
-
if (settled) return;
|
|
778
|
-
settled = true;
|
|
779
|
-
cleanup();
|
|
780
|
-
reject(error);
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
const onAccepted = (message = {}) => {
|
|
784
|
-
const acceptedDeliveryId = normalizeOptionalText(message?.data?.acceptedDeliveryId);
|
|
785
|
-
if (acceptedDeliveryId !== normalizedDeliveryId) return;
|
|
786
|
-
settleResolve(message);
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const onDisconnect = (info = {}) => {
|
|
790
|
-
settleReject(createRuntimeBoundaryError({
|
|
791
|
-
code: 'relay_delivery_accept_ack_disconnected',
|
|
792
|
-
category: 'transport',
|
|
793
|
-
status: 502,
|
|
794
|
-
message: `relay websocket closed before delivery acceptance acknowledgement for ${normalizedDeliveryId}`,
|
|
795
|
-
publicMessage: 'relay websocket closed before delivery acceptance acknowledgement',
|
|
796
|
-
recoverable: true,
|
|
797
|
-
context: this.buildBoundaryContext({
|
|
798
|
-
stage: 'delivery_accept_ack_wait',
|
|
799
|
-
deliveryId: normalizedDeliveryId,
|
|
800
|
-
closeCode: info?.code ?? null,
|
|
801
|
-
closeReason: info?.reason || null,
|
|
802
|
-
}),
|
|
803
|
-
}));
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
this.on('delivery.accepted', onAccepted);
|
|
807
|
-
this.on('disconnect', onDisconnect);
|
|
808
|
-
this.on('close', onDisconnect);
|
|
809
|
-
|
|
810
|
-
timeout = setTimeout(() => {
|
|
811
|
-
settleReject(buildAcceptedAckTimeoutError({
|
|
812
|
-
deliveryId: normalizedDeliveryId,
|
|
813
|
-
timeoutMs,
|
|
814
|
-
context: this.buildBoundaryContext({
|
|
815
|
-
stage: 'delivery_accept_ack_wait',
|
|
816
|
-
deliveryId: normalizedDeliveryId,
|
|
817
|
-
}),
|
|
818
|
-
}));
|
|
819
|
-
}, timeoutMs);
|
|
820
|
-
if (typeof timeout.unref === 'function') timeout.unref();
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
waitForKeepSilentAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
|
|
825
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
826
|
-
if (!normalizedDeliveryId) {
|
|
827
|
-
return Promise.reject(createRuntimeBoundaryError({
|
|
828
|
-
code: 'relay_delivery_id_required',
|
|
829
|
-
category: 'input',
|
|
830
|
-
status: 400,
|
|
831
|
-
message: 'deliveryId is required to wait for relay kept_silent acknowledgement',
|
|
832
|
-
publicMessage: 'deliveryId is required',
|
|
833
|
-
recoverable: true,
|
|
834
|
-
}));
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return new Promise((resolve, reject) => {
|
|
838
|
-
let settled = false;
|
|
839
|
-
let timeout = null;
|
|
840
|
-
|
|
841
|
-
const cleanup = () => {
|
|
842
|
-
if (timeout) clearTimeout(timeout);
|
|
843
|
-
this.off('kept_silent.accepted', onKeptSilentAccepted);
|
|
844
|
-
this.off('disconnect', onDisconnect);
|
|
845
|
-
this.off('close', onDisconnect);
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
const settleResolve = (value) => {
|
|
849
|
-
if (settled) return;
|
|
850
|
-
settled = true;
|
|
851
|
-
cleanup();
|
|
852
|
-
resolve(value);
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
const settleReject = (error) => {
|
|
856
|
-
if (settled) return;
|
|
857
|
-
settled = true;
|
|
858
|
-
cleanup();
|
|
859
|
-
reject(error);
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const onKeptSilentAccepted = (message = {}) => {
|
|
863
|
-
const keptSilentDeliveryId = normalizeOptionalText(message?.data?.keptSilentDeliveryId);
|
|
864
|
-
if (keptSilentDeliveryId !== normalizedDeliveryId) return;
|
|
865
|
-
settleResolve(message);
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
const onDisconnect = (info = {}) => {
|
|
869
|
-
settleReject(createRuntimeBoundaryError({
|
|
870
|
-
code: 'relay_kept_silent_ack_disconnected',
|
|
871
|
-
category: 'transport',
|
|
872
|
-
status: 502,
|
|
873
|
-
message: `relay websocket closed before kept_silent acknowledgement for ${normalizedDeliveryId}`,
|
|
874
|
-
publicMessage: 'relay websocket closed before kept_silent acknowledgement',
|
|
875
|
-
recoverable: true,
|
|
876
|
-
context: this.buildBoundaryContext({
|
|
877
|
-
stage: 'kept_silent_ack_wait',
|
|
878
|
-
deliveryId: normalizedDeliveryId,
|
|
879
|
-
closeCode: info?.code ?? null,
|
|
880
|
-
closeReason: info?.reason || null,
|
|
881
|
-
}),
|
|
882
|
-
}));
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
this.on('kept_silent.accepted', onKeptSilentAccepted);
|
|
886
|
-
this.on('disconnect', onDisconnect);
|
|
887
|
-
this.on('close', onDisconnect);
|
|
888
|
-
|
|
889
|
-
timeout = setTimeout(() => {
|
|
890
|
-
settleReject(buildKeepSilentAckTimeoutError({
|
|
891
|
-
deliveryId: normalizedDeliveryId,
|
|
892
|
-
timeoutMs,
|
|
893
|
-
context: this.buildBoundaryContext({
|
|
894
|
-
stage: 'kept_silent_ack_wait',
|
|
895
|
-
deliveryId: normalizedDeliveryId,
|
|
896
|
-
}),
|
|
897
|
-
}));
|
|
898
|
-
}, timeoutMs);
|
|
899
|
-
if (typeof timeout.unref === 'function') timeout.unref();
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
async replyToDeliveryHttp({ deliveryId, replyText, source = 'subagent' } = {}) {
|
|
904
|
-
const envelope = this.outbound.createReplyEnvelope({
|
|
905
|
-
deliveryId,
|
|
906
|
-
sessionKey: null,
|
|
907
|
-
replyText,
|
|
908
|
-
source,
|
|
909
|
-
});
|
|
910
|
-
const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(envelope.deliveryId)}/reply`, {
|
|
911
|
-
method: 'POST',
|
|
912
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
913
|
-
body: JSON.stringify({
|
|
914
|
-
fromAgentId: this.boundAgentId,
|
|
915
|
-
payload: {
|
|
916
|
-
...envelope.payload,
|
|
917
|
-
},
|
|
918
|
-
}),
|
|
919
|
-
}, {
|
|
920
|
-
code: 'relay_reply_fallback_failed',
|
|
921
|
-
message: 'failed to submit relay reply fallback',
|
|
922
|
-
publicMessage: 'failed to submit relay reply fallback',
|
|
923
|
-
});
|
|
924
|
-
return {
|
|
925
|
-
...result,
|
|
926
|
-
envelope,
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
async acceptDeliveryHttp({ deliveryId, sessionKey = null, source = 'runtime_dispatch' } = {}) {
|
|
931
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
932
|
-
const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/accepted`, {
|
|
933
|
-
method: 'POST',
|
|
934
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
935
|
-
body: JSON.stringify({
|
|
936
|
-
fromAgentId: this.boundAgentId,
|
|
937
|
-
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
938
|
-
source: normalizeOptionalText(source) || 'runtime_dispatch',
|
|
939
|
-
}),
|
|
940
|
-
}, {
|
|
941
|
-
code: 'relay_delivery_accept_fallback_failed',
|
|
942
|
-
message: 'failed to submit relay delivery acceptance fallback',
|
|
943
|
-
publicMessage: 'failed to submit relay delivery acceptance fallback',
|
|
944
|
-
});
|
|
945
|
-
return {
|
|
946
|
-
...result,
|
|
947
|
-
envelope: {
|
|
948
|
-
deliveryId: normalizedDeliveryId,
|
|
949
|
-
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
950
|
-
source: normalizeOptionalText(source) || 'runtime_dispatch',
|
|
951
|
-
},
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async sendReplyAndWaitForAck({
|
|
956
|
-
deliveryId,
|
|
957
|
-
sessionKey,
|
|
958
|
-
replyText,
|
|
959
|
-
source = 'subagent',
|
|
960
|
-
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
961
|
-
httpFallback = true,
|
|
962
|
-
} = {}) {
|
|
963
|
-
const ackPromise = this.waitForReplyAck({
|
|
964
|
-
deliveryId,
|
|
965
|
-
timeoutMs,
|
|
966
|
-
});
|
|
967
|
-
const envelope = this.sendReply({
|
|
968
|
-
deliveryId,
|
|
969
|
-
sessionKey,
|
|
970
|
-
replyText,
|
|
971
|
-
source,
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
try {
|
|
975
|
-
const ack = await ackPromise;
|
|
976
|
-
return {
|
|
977
|
-
ok: true,
|
|
978
|
-
envelope,
|
|
979
|
-
ack,
|
|
980
|
-
transport: 'websocket',
|
|
981
|
-
fallbackUsed: false,
|
|
982
|
-
};
|
|
983
|
-
} catch (error) {
|
|
984
|
-
if (!httpFallback) throw error;
|
|
985
|
-
|
|
986
|
-
this.logger.warn?.('[claworld:relay-client] reply websocket acknowledgement failed; attempting HTTP fallback', {
|
|
987
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
988
|
-
agentId: this.boundAgentId,
|
|
989
|
-
deliveryId: envelope.deliveryId,
|
|
990
|
-
sessionKey: envelope.sessionKey,
|
|
991
|
-
error: error?.message || String(error),
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
const fallbackResult = await this.replyToDeliveryHttp({
|
|
995
|
-
deliveryId: envelope.deliveryId,
|
|
996
|
-
replyText,
|
|
997
|
-
source,
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
|
|
1001
|
-
return {
|
|
1002
|
-
ok: true,
|
|
1003
|
-
envelope,
|
|
1004
|
-
ack: {
|
|
1005
|
-
event: 'reply.accepted',
|
|
1006
|
-
data: fallbackResult.body,
|
|
1007
|
-
},
|
|
1008
|
-
transport: 'http',
|
|
1009
|
-
fallbackUsed: true,
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (isReplyAlreadyApplied(fallbackResult, envelope.deliveryId)) {
|
|
1014
|
-
return {
|
|
1015
|
-
ok: true,
|
|
1016
|
-
envelope,
|
|
1017
|
-
ack: {
|
|
1018
|
-
event: 'reply.accepted',
|
|
1019
|
-
data: {
|
|
1020
|
-
...(fallbackResult.body && typeof fallbackResult.body === 'object' ? fallbackResult.body : {}),
|
|
1021
|
-
repliedDeliveryId: envelope.deliveryId,
|
|
1022
|
-
},
|
|
1023
|
-
},
|
|
1024
|
-
transport: 'http-already-applied',
|
|
1025
|
-
fallbackUsed: true,
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
throw buildReplyFallbackError({
|
|
1030
|
-
deliveryId: envelope.deliveryId,
|
|
1031
|
-
status: fallbackResult.status,
|
|
1032
|
-
body: fallbackResult.body,
|
|
1033
|
-
context: this.buildBoundaryContext({
|
|
1034
|
-
stage: 'reply_fallback',
|
|
1035
|
-
deliveryId: envelope.deliveryId,
|
|
1036
|
-
sessionKey: envelope.sessionKey,
|
|
1037
|
-
fallbackFrom: error?.code || error?.message || null,
|
|
1038
|
-
}),
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
async submitDeliveryReply({
|
|
1044
|
-
deliveryId,
|
|
1045
|
-
sessionKey,
|
|
1046
|
-
replyText,
|
|
1047
|
-
source = 'subagent',
|
|
1048
|
-
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
1049
|
-
httpFallback = true,
|
|
1050
|
-
} = {}) {
|
|
1051
|
-
return await this.sendReplyAndWaitForAck({
|
|
1052
|
-
deliveryId,
|
|
1053
|
-
sessionKey,
|
|
1054
|
-
replyText,
|
|
1055
|
-
source,
|
|
1056
|
-
timeoutMs,
|
|
1057
|
-
httpFallback,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
async sendAcceptedAndWaitForAck({
|
|
1062
|
-
deliveryId,
|
|
1063
|
-
sessionKey,
|
|
1064
|
-
source = 'runtime_dispatch',
|
|
1065
|
-
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
1066
|
-
httpFallback = true,
|
|
1067
|
-
} = {}) {
|
|
1068
|
-
const ackPromise = this.waitForAcceptedAck({
|
|
1069
|
-
deliveryId,
|
|
1070
|
-
timeoutMs,
|
|
1071
|
-
});
|
|
1072
|
-
const envelope = this.sendAccepted({
|
|
1073
|
-
deliveryId,
|
|
1074
|
-
sessionKey,
|
|
1075
|
-
source,
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
try {
|
|
1079
|
-
const ack = await ackPromise;
|
|
1080
|
-
return {
|
|
1081
|
-
ok: true,
|
|
1082
|
-
envelope,
|
|
1083
|
-
ack,
|
|
1084
|
-
transport: 'websocket',
|
|
1085
|
-
fallbackUsed: false,
|
|
1086
|
-
};
|
|
1087
|
-
} catch (error) {
|
|
1088
|
-
if (!httpFallback) throw error;
|
|
1089
|
-
|
|
1090
|
-
this.logger.warn?.('[claworld:relay-client] delivery acceptance websocket acknowledgement failed; attempting HTTP fallback', {
|
|
1091
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
1092
|
-
agentId: this.boundAgentId,
|
|
1093
|
-
deliveryId: envelope.deliveryId,
|
|
1094
|
-
sessionKey: envelope.sessionKey,
|
|
1095
|
-
error: error?.message || String(error),
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
const fallbackResult = await this.acceptDeliveryHttp({
|
|
1099
|
-
deliveryId: envelope.deliveryId,
|
|
1100
|
-
sessionKey: envelope.sessionKey,
|
|
1101
|
-
source: envelope.source,
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
|
|
1105
|
-
return {
|
|
1106
|
-
ok: true,
|
|
1107
|
-
envelope,
|
|
1108
|
-
ack: {
|
|
1109
|
-
event: 'delivery.accepted',
|
|
1110
|
-
data: fallbackResult.body,
|
|
1111
|
-
},
|
|
1112
|
-
transport: 'http',
|
|
1113
|
-
fallbackUsed: true,
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
throw buildReplyFallbackError({
|
|
1118
|
-
deliveryId: envelope.deliveryId,
|
|
1119
|
-
status: fallbackResult.status,
|
|
1120
|
-
body: fallbackResult.body,
|
|
1121
|
-
context: this.buildBoundaryContext({
|
|
1122
|
-
stage: 'delivery_accept_fallback',
|
|
1123
|
-
deliveryId: envelope.deliveryId,
|
|
1124
|
-
sessionKey: envelope.sessionKey,
|
|
1125
|
-
fallbackFrom: error?.code || error?.message || null,
|
|
1126
|
-
}),
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
async keepDeliverySilentHttp({ deliveryId, reason = null, source = 'openclaw-autochain' } = {}) {
|
|
1132
|
-
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
1133
|
-
const result = await this.requestJsonWithDeliveryVisibilityRetry(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/kept-silent`, {
|
|
1134
|
-
method: 'POST',
|
|
1135
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
1136
|
-
body: JSON.stringify({
|
|
1137
|
-
fromAgentId: this.boundAgentId,
|
|
1138
|
-
reason: normalizeOptionalText(reason) || 'no_renderable_reply',
|
|
1139
|
-
source: normalizeOptionalText(source) || 'openclaw-autochain',
|
|
1140
|
-
}),
|
|
1141
|
-
}, {
|
|
1142
|
-
code: 'relay_kept_silent_fallback_failed',
|
|
1143
|
-
message: 'failed to submit relay kept_silent fallback',
|
|
1144
|
-
publicMessage: 'failed to submit relay kept_silent fallback',
|
|
1145
|
-
});
|
|
1146
|
-
return {
|
|
1147
|
-
...result,
|
|
1148
|
-
envelope: {
|
|
1149
|
-
deliveryId: normalizedDeliveryId,
|
|
1150
|
-
reason: normalizeOptionalText(reason) || 'no_renderable_reply',
|
|
1151
|
-
source: normalizeOptionalText(source) || 'openclaw-autochain',
|
|
1152
|
-
},
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
async sendKeepSilentAndWaitForAck({
|
|
1157
|
-
deliveryId,
|
|
1158
|
-
sessionKey,
|
|
1159
|
-
reason = null,
|
|
1160
|
-
source = 'openclaw-autochain',
|
|
1161
|
-
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
1162
|
-
httpFallback = true,
|
|
1163
|
-
} = {}) {
|
|
1164
|
-
const ackPromise = this.waitForKeepSilentAck({
|
|
1165
|
-
deliveryId,
|
|
1166
|
-
timeoutMs,
|
|
1167
|
-
});
|
|
1168
|
-
const envelope = this.sendKeepSilent({
|
|
1169
|
-
deliveryId,
|
|
1170
|
-
sessionKey,
|
|
1171
|
-
reason,
|
|
1172
|
-
source,
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
try {
|
|
1176
|
-
const ack = await ackPromise;
|
|
1177
|
-
return {
|
|
1178
|
-
ok: true,
|
|
1179
|
-
envelope,
|
|
1180
|
-
ack,
|
|
1181
|
-
transport: 'websocket',
|
|
1182
|
-
fallbackUsed: false,
|
|
1183
|
-
};
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
if (!httpFallback) throw error;
|
|
1186
|
-
|
|
1187
|
-
this.logger.warn?.('[claworld:relay-client] kept_silent websocket acknowledgement failed; attempting HTTP fallback', {
|
|
1188
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
1189
|
-
agentId: this.boundAgentId,
|
|
1190
|
-
deliveryId: envelope.deliveryId,
|
|
1191
|
-
sessionKey: envelope.sessionKey,
|
|
1192
|
-
error: error?.message || String(error),
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
|
-
const fallbackResult = await this.keepDeliverySilentHttp({
|
|
1196
|
-
deliveryId: envelope.deliveryId,
|
|
1197
|
-
reason: envelope.reason,
|
|
1198
|
-
source: envelope.source,
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
|
|
1202
|
-
return {
|
|
1203
|
-
ok: true,
|
|
1204
|
-
envelope,
|
|
1205
|
-
ack: {
|
|
1206
|
-
event: 'kept_silent.accepted',
|
|
1207
|
-
data: fallbackResult.body,
|
|
1208
|
-
},
|
|
1209
|
-
transport: 'http',
|
|
1210
|
-
fallbackUsed: true,
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
if (isDeliveryKeptSilentAlreadyApplied(fallbackResult, envelope.deliveryId)) {
|
|
1215
|
-
return {
|
|
1216
|
-
ok: true,
|
|
1217
|
-
envelope,
|
|
1218
|
-
ack: {
|
|
1219
|
-
event: 'kept_silent.accepted',
|
|
1220
|
-
data: {
|
|
1221
|
-
...(fallbackResult.body && typeof fallbackResult.body === 'object' ? fallbackResult.body : {}),
|
|
1222
|
-
keptSilentDeliveryId: envelope.deliveryId,
|
|
1223
|
-
},
|
|
1224
|
-
},
|
|
1225
|
-
transport: 'http-already-applied',
|
|
1226
|
-
fallbackUsed: true,
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
throw buildKeepSilentFallbackError({
|
|
1231
|
-
deliveryId: envelope.deliveryId,
|
|
1232
|
-
status: fallbackResult.status,
|
|
1233
|
-
body: fallbackResult.body,
|
|
1234
|
-
context: this.buildBoundaryContext({
|
|
1235
|
-
stage: 'kept_silent_fallback',
|
|
1236
|
-
deliveryId: envelope.deliveryId,
|
|
1237
|
-
sessionKey: envelope.sessionKey,
|
|
1238
|
-
fallbackFrom: error?.code || error?.message || null,
|
|
1239
|
-
}),
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
async submitDeliveryKeptSilent({
|
|
1245
|
-
deliveryId,
|
|
1246
|
-
sessionKey,
|
|
1247
|
-
reason = null,
|
|
1248
|
-
source = 'openclaw-autochain',
|
|
1249
|
-
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
1250
|
-
httpFallback = true,
|
|
1251
|
-
} = {}) {
|
|
1252
|
-
return await this.sendKeepSilentAndWaitForAck({
|
|
1253
|
-
deliveryId,
|
|
1254
|
-
sessionKey,
|
|
1255
|
-
reason,
|
|
1256
|
-
source,
|
|
1257
|
-
timeoutMs,
|
|
1258
|
-
httpFallback,
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
async createChatRequest({ fromAgentId, displayName, agentCode, requestContext = {} } = {}) {
|
|
1263
|
-
const normalized = normalizeChatRequestInput({ requestContext, source: 'direct_lookup' });
|
|
1264
|
-
const normalizedDisplayName = normalizeOptionalText(displayName);
|
|
1265
|
-
const normalizedAgentCode = normalizeOptionalText(agentCode)?.toUpperCase() || null;
|
|
1266
|
-
return await this.requestJson('/v1/chat-requests', {
|
|
1267
|
-
method: 'POST',
|
|
1268
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
1269
|
-
body: JSON.stringify({
|
|
1270
|
-
fromAgentId,
|
|
1271
|
-
...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
|
|
1272
|
-
...(normalizedAgentCode ? { agentCode: normalizedAgentCode } : {}),
|
|
1273
|
-
kickoffBrief: normalized.kickoffBrief || null,
|
|
1274
|
-
openingMessage: normalized.openingMessage || null,
|
|
1275
|
-
worldId: normalized.conversation?.worldId || null,
|
|
1276
|
-
requestContext: requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
|
|
1277
|
-
? requestContext
|
|
1278
|
-
: undefined,
|
|
1279
|
-
}),
|
|
1280
|
-
}, {
|
|
1281
|
-
code: 'relay_request_create_failed',
|
|
1282
|
-
message: 'failed to create relay chat request',
|
|
1283
|
-
publicMessage: 'failed to create relay chat request',
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
async acceptChatRequest(requestId, { actorAgentId, ...options } = {}) {
|
|
1288
|
-
const result = await this.requestJson(`/v1/chat-requests/${requestId}/accept`, {
|
|
1289
|
-
method: 'POST',
|
|
1290
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
1291
|
-
body: JSON.stringify({ actorAgentId, ...options }),
|
|
1292
|
-
}, {
|
|
1293
|
-
code: 'relay_request_accept_failed',
|
|
1294
|
-
message: 'failed to accept relay chat request',
|
|
1295
|
-
publicMessage: 'failed to accept relay chat request',
|
|
1296
|
-
});
|
|
1297
|
-
if (result.status < 400 && requestId) {
|
|
1298
|
-
const kickoff = result.body?.kickoff && typeof result.body.kickoff === 'object' && !Array.isArray(result.body.kickoff)
|
|
1299
|
-
? { ...result.body.kickoff }
|
|
1300
|
-
: null;
|
|
1301
|
-
this.acceptedChatRequests.set(requestId, {
|
|
1302
|
-
requestId,
|
|
1303
|
-
sessionKey: kickoff?.sessionKey || null,
|
|
1304
|
-
conversationKey: kickoff?.conversationKey || null,
|
|
1305
|
-
kickoff,
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
return result;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
async rejectChatRequest(requestId, { actorAgentId, ...options } = {}) {
|
|
1312
|
-
return await this.requestJson(`/v1/chat-requests/${requestId}/reject`, {
|
|
1313
|
-
method: 'POST',
|
|
1314
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
1315
|
-
body: JSON.stringify({ actorAgentId, ...options }),
|
|
1316
|
-
}, {
|
|
1317
|
-
code: 'relay_request_reject_failed',
|
|
1318
|
-
message: 'failed to reject relay chat request',
|
|
1319
|
-
publicMessage: 'failed to reject relay chat request',
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
async deliverMessage({ fromAgentId, targetAgentId, clientMessageId = null, payload = {}, conversation = {} } = {}) {
|
|
1324
|
-
const resolvedClientMessageId = requireClientMessageId(clientMessageId);
|
|
1325
|
-
const result = await this.requestJson('/v1/messages', {
|
|
1326
|
-
method: 'POST',
|
|
1327
|
-
headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
|
|
1328
|
-
body: JSON.stringify({ fromAgentId, targetAgentId, clientMessageId: resolvedClientMessageId, payload, conversation }),
|
|
1329
|
-
}, {
|
|
1330
|
-
code: 'relay_message_delivery_failed',
|
|
1331
|
-
message: 'failed to deliver relay message',
|
|
1332
|
-
publicMessage: 'failed to deliver relay message',
|
|
1333
|
-
});
|
|
1334
|
-
return {
|
|
1335
|
-
...result,
|
|
1336
|
-
clientMessageId: resolvedClientMessageId,
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
waitFor(eventNameOrPredicate, timeoutMs = 8000) {
|
|
1341
|
-
const isPredicate = typeof eventNameOrPredicate === 'function';
|
|
1342
|
-
const predicate = isPredicate
|
|
1343
|
-
? eventNameOrPredicate
|
|
1344
|
-
: (event) => event.event === eventNameOrPredicate;
|
|
1345
|
-
|
|
1346
|
-
return new Promise((resolve, reject) => {
|
|
1347
|
-
const existing = this.events.find(predicate);
|
|
1348
|
-
if (existing) return resolve(existing);
|
|
1349
|
-
|
|
1350
|
-
const started = Date.now();
|
|
1351
|
-
const timer = setInterval(() => {
|
|
1352
|
-
const found = this.events.find(predicate);
|
|
1353
|
-
if (found) {
|
|
1354
|
-
clearInterval(timer);
|
|
1355
|
-
resolve(found);
|
|
1356
|
-
} else if (Date.now() - started > timeoutMs) {
|
|
1357
|
-
clearInterval(timer);
|
|
1358
|
-
const eventName = isPredicate ? 'predicate event' : eventNameOrPredicate;
|
|
1359
|
-
reject(new Error(`timed out waiting for ${eventName}`));
|
|
1360
|
-
}
|
|
1361
|
-
}, 100);
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
async establishConversation({ fromAgentId, displayName = null, agentCode = null, requestContext = {}, openingPayload = {} } = {}) {
|
|
1366
|
-
const normalizedRequestContext = requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
|
|
1367
|
-
? { ...requestContext }
|
|
1368
|
-
: {};
|
|
1369
|
-
const normalizedOpeningPayload = openingPayload && typeof openingPayload === 'object' && !Array.isArray(openingPayload)
|
|
1370
|
-
? { ...openingPayload }
|
|
1371
|
-
: {};
|
|
1372
|
-
if (Object.keys(normalizedOpeningPayload).length > 0) {
|
|
1373
|
-
normalizedRequestContext.openingPayload = normalizedOpeningPayload;
|
|
1374
|
-
if (!normalizedRequestContext.message && typeof normalizedOpeningPayload.text === 'string' && normalizedOpeningPayload.text.trim()) {
|
|
1375
|
-
normalizedRequestContext.message = normalizedOpeningPayload.text.trim();
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
const requestResult = await this.createChatRequest({
|
|
1380
|
-
fromAgentId,
|
|
1381
|
-
displayName,
|
|
1382
|
-
agentCode,
|
|
1383
|
-
requestContext: normalizedRequestContext,
|
|
1384
|
-
});
|
|
1385
|
-
if (requestResult.status !== 201) {
|
|
1386
|
-
throw new Error(`failed to create chat request: ${JSON.stringify(requestResult.body)}`);
|
|
1387
|
-
}
|
|
1388
|
-
const requestId = requestResult.body?.chatRequest?.chatRequestId || requestResult.body?.requestId || null;
|
|
1389
|
-
return {
|
|
1390
|
-
requestId,
|
|
1391
|
-
sessionKey: null,
|
|
1392
|
-
conversationKey: null,
|
|
1393
|
-
openAcceptedConversation: async ({ timeoutMs = 15000 } = {}) => {
|
|
1394
|
-
const acceptedRequest = this.acceptedChatRequests.get(requestId) || null;
|
|
1395
|
-
if (acceptedRequest?.kickoff || acceptedRequest?.conversationKey || acceptedRequest?.sessionKey) {
|
|
1396
|
-
return {
|
|
1397
|
-
requestId,
|
|
1398
|
-
sessionKey: acceptedRequest?.sessionKey || acceptedRequest?.kickoff?.sessionKey || null,
|
|
1399
|
-
conversationKey: acceptedRequest?.conversationKey || acceptedRequest?.kickoff?.conversationKey || null,
|
|
1400
|
-
kickoff: acceptedRequest?.kickoff || null,
|
|
1401
|
-
delivery: null,
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
const deliveryEvent = await this.waitFor(
|
|
1405
|
-
(event) => event.event === 'delivery'
|
|
1406
|
-
&& (
|
|
1407
|
-
event.data?.metadata?.kickoffRequestId === requestId
|
|
1408
|
-
),
|
|
1409
|
-
timeoutMs,
|
|
1410
|
-
);
|
|
1411
|
-
const delivery = deliveryEvent?.data && typeof deliveryEvent.data === 'object' && !Array.isArray(deliveryEvent.data)
|
|
1412
|
-
? deliveryEvent.data
|
|
1413
|
-
: {};
|
|
1414
|
-
const metadata = delivery.metadata && typeof delivery.metadata === 'object' && !Array.isArray(delivery.metadata)
|
|
1415
|
-
? delivery.metadata
|
|
1416
|
-
: {};
|
|
1417
|
-
const kickoff = {
|
|
1418
|
-
status: 'delivered',
|
|
1419
|
-
deliveryId: delivery.deliveryId || null,
|
|
1420
|
-
sessionKey: delivery.sessionKey || metadata.sessionKey || null,
|
|
1421
|
-
conversationKey: delivery.conversationKey || null,
|
|
1422
|
-
};
|
|
1423
|
-
return {
|
|
1424
|
-
requestId,
|
|
1425
|
-
sessionKey: kickoff.sessionKey,
|
|
1426
|
-
conversationKey: kickoff.conversationKey,
|
|
1427
|
-
kickoff,
|
|
1428
|
-
delivery: deliveryEvent,
|
|
1429
|
-
};
|
|
1430
|
-
},
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
snapshot() {
|
|
1435
|
-
return {
|
|
1436
|
-
connectionState: this.connectionState,
|
|
1437
|
-
boundAgentId: this.boundAgentId,
|
|
1438
|
-
eventCount: this.events.length,
|
|
1439
|
-
heartbeatSeconds: this.runtimeConfig?.heartbeatSeconds || null,
|
|
1440
|
-
hasActiveSocket: Boolean(this.ws && this.ws.readyState === 1),
|
|
1441
|
-
reconnectEnabled: this.runtimeConfig?.reconnect !== false,
|
|
1442
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
1443
|
-
lastDisconnectCode: this.lastDisconnectInfo?.code ?? null,
|
|
1444
|
-
lastDisconnectReason: this.lastDisconnectInfo?.reason || null,
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
async close(reason = 'manual_close') {
|
|
1449
|
-
this.manualClose = true;
|
|
1450
|
-
this.clearReconnectTimer();
|
|
1451
|
-
this.stopHeartbeatLoop();
|
|
1452
|
-
if (!this.ws) return { closed: false, reason: 'not_connected' };
|
|
1453
|
-
const ws = this.ws;
|
|
1454
|
-
this.ws = null;
|
|
1455
|
-
await new Promise((resolve) => {
|
|
1456
|
-
if (ws.readyState === 3) return resolve();
|
|
1457
|
-
ws.once('close', resolve);
|
|
1458
|
-
ws.close(1000, reason);
|
|
1459
|
-
});
|
|
1460
|
-
this.connectionState = 'closed';
|
|
1461
|
-
return { closed: true, reason };
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
export function createClaworldRelayClient(options = {}) {
|
|
1466
|
-
return new ClaworldRelayClient(options);
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
export { normalizeRelayWebSocketUrl };
|