@xfxstudio/claworld 2026.7.1-testing.1 → 2026.7.2-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/skills/claworld-help/SKILL.md +8 -4
- package/src/openclaw/plugin/managed-config.js +16 -1
- package/src/openclaw/plugin/register.js +18 -117
- package/src/openclaw/plugin/relay-client.js +203 -68
- package/src/openclaw/runtime/working-memory.js +0 -1
package/openclaw.plugin.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"name": "Claworld Persona Relay",
|
|
19
19
|
"description": "Claworld relay world channel plugin for OpenClaw.",
|
|
20
|
-
"version": "2026.7.
|
|
20
|
+
"version": "2026.7.2-testing.1",
|
|
21
21
|
"configSchema": {
|
|
22
22
|
"type": "object",
|
|
23
23
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -122,13 +122,17 @@ Useful questions:
|
|
|
122
122
|
|
|
123
123
|
Use `details` for the developer-facing summary: concise evidence, relevant observations, why this looks like product/runtime work, and anything the human specifically cares about. Use `reproductionSteps` for repeatable steps. Use `context` and `runtimeContext` for lookup metadata.
|
|
124
124
|
|
|
125
|
-
Use a direct HTTP POST to the configured Claworld backend feedback URL
|
|
125
|
+
Use a direct HTTP POST to the configured Claworld backend feedback URL. Read the
|
|
126
|
+
active Claworld channel/account configuration first and use its configured
|
|
127
|
+
backend when present:
|
|
126
128
|
|
|
127
129
|
```text
|
|
128
|
-
|
|
130
|
+
<configured Claworld server URL>/v1/feedback
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
For a fresh setup with no configured backend yet, use the package default:
|
|
134
|
+
testing packages default to `https://staging.claworld.love`, and stable packages
|
|
135
|
+
default to `https://claworld.love`.
|
|
132
136
|
|
|
133
137
|
The `accountId`, `apiKey`, and app token come from the active Claworld channel/account configuration. Do not print secrets to the human. If an app token is configured, send it as `Authorization: Bearer <appToken>` and `x-claworld-app-token: <appToken>`. If an API key is configured, send `x-api-key: <apiKey>`.
|
|
134
138
|
|
|
@@ -137,7 +141,7 @@ The clean authenticated path is an app token that resolves to the account's back
|
|
|
137
141
|
Example:
|
|
138
142
|
|
|
139
143
|
```bash
|
|
140
|
-
CLAWORLD_SERVER_URL="${CLAWORLD_SERVER_URL:-https://
|
|
144
|
+
CLAWORLD_SERVER_URL="${CLAWORLD_SERVER_URL:-${CONFIGURED_CLAWORLD_SERVER_URL:-https://claworld.love}}"
|
|
141
145
|
|
|
142
146
|
headers=(-H "content-type: application/json")
|
|
143
147
|
if [ -n "${CLAWORLD_APP_TOKEN:-}" ]; then
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
CLAWORLD_PLUGIN_CURRENT_VERSION,
|
|
5
|
+
normalizeClaworldPluginVersion,
|
|
6
|
+
} from '../plugin-version.js';
|
|
3
7
|
import {
|
|
4
8
|
CLAWORLD_MINIMAL_OPENCLAW_TOOL_NAMES,
|
|
5
9
|
CLAWORLD_PUBLIC_TOOL_NAMES,
|
|
@@ -7,7 +11,18 @@ import {
|
|
|
7
11
|
CLAWORLD_TOOL_PROFILES,
|
|
8
12
|
} from '../runtime/tool-inventory.js';
|
|
9
13
|
|
|
10
|
-
export const
|
|
14
|
+
export const CLAWORLD_STAGING_SERVER_URL = 'https://staging.claworld.love';
|
|
15
|
+
export const CLAWORLD_PRODUCTION_SERVER_URL = 'https://claworld.love';
|
|
16
|
+
export function isClaworldTestingPluginVersion(version = CLAWORLD_PLUGIN_CURRENT_VERSION) {
|
|
17
|
+
const normalized = normalizeClaworldPluginVersion(version, null);
|
|
18
|
+
return Boolean(normalized && /-testing(?:\.|$)/.test(normalized));
|
|
19
|
+
}
|
|
20
|
+
export function resolveDefaultClaworldServerUrl(version = CLAWORLD_PLUGIN_CURRENT_VERSION) {
|
|
21
|
+
return isClaworldTestingPluginVersion(version)
|
|
22
|
+
? CLAWORLD_STAGING_SERVER_URL
|
|
23
|
+
: CLAWORLD_PRODUCTION_SERVER_URL;
|
|
24
|
+
}
|
|
25
|
+
export const DEFAULT_CLAWORLD_SERVER_URL = resolveDefaultClaworldServerUrl();
|
|
11
26
|
export const DEFAULT_CLAWORLD_API_KEY = 'local-test';
|
|
12
27
|
export const DEFAULT_CLAWORLD_AGENT_ID = 'main';
|
|
13
28
|
export const DEFAULT_CLAWORLD_ACCOUNT_ID = 'claworld';
|
|
@@ -1128,11 +1128,25 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
|
|
|
1128
1128
|
async execute(toolCallId, params = {}) {
|
|
1129
1129
|
const action = normalizeTerminalConversationAction(params.action, 'list_related', { throwOnInvalid: true });
|
|
1130
1130
|
if (action === 'request') {
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1131
|
+
const context = await resolveToolContext(api, plugin, params, {
|
|
1132
|
+
requiredPublicIdentityCapability: 'request chat',
|
|
1133
|
+
});
|
|
1134
|
+
const payload = await plugin.helpers.social.requestChat({
|
|
1135
|
+
...context,
|
|
1136
|
+
displayName: params.displayName,
|
|
1137
|
+
agentCode: params.agentCode,
|
|
1138
|
+
openingMessage: resolveConversationOpeningMessage(params),
|
|
1139
|
+
message: params.message || null,
|
|
1140
|
+
text: params.text || null,
|
|
1141
|
+
kickoffBrief: params.kickoffBrief || null,
|
|
1142
|
+
openingPayload: params.openingPayload || null,
|
|
1143
|
+
worldId: params.worldId || null,
|
|
1144
|
+
});
|
|
1145
|
+
return buildToolResult({
|
|
1146
|
+
...projectToolChatRequestMutationResponse(payload, { accountId: context.accountId }),
|
|
1147
|
+
tool: manageConversationsTool,
|
|
1148
|
+
action,
|
|
1134
1149
|
});
|
|
1135
|
-
return rewriteToolResultName(result, manageConversationsTool, action);
|
|
1136
1150
|
}
|
|
1137
1151
|
if (action === 'list_related' || action === 'get_state') {
|
|
1138
1152
|
const filters = normalizeManageConversationInboxQuery(params, action);
|
|
@@ -1749,119 +1763,6 @@ function buildRegisteredTools(api, plugin) {
|
|
|
1749
1763
|
}));
|
|
1750
1764
|
},
|
|
1751
1765
|
},
|
|
1752
|
-
{
|
|
1753
|
-
name: 'claworld_request_chat',
|
|
1754
|
-
label: 'Claworld Request Chat',
|
|
1755
|
-
description: 'Use in the main session to create a new Claworld chat request or re-engage a selected public identity. Do not use for live conversation turns or current-session replies.',
|
|
1756
|
-
metadata: buildToolMetadata({
|
|
1757
|
-
category: 'chat_request',
|
|
1758
|
-
usageNotes: [
|
|
1759
|
-
'Primary actor/session: main session only. Use this tool when the user wants to start a new request or re-engage someone after an earlier request or chat went silent or ended.',
|
|
1760
|
-
'If the user asks to contact the same person again, call this tool again to create a fresh request or re-engagement.',
|
|
1761
|
-
'For world-scoped chat or re-engagement, use the displayName and agentCode returned by world member search.',
|
|
1762
|
-
'The backend resolves the target by agentCode.',
|
|
1763
|
-
'If the current displayName for that agentCode no longer matches, the tool can still route by the current owner and return an explicit warning with the current displayName.',
|
|
1764
|
-
'openingMessage is required and must contain non-blank kickoff intent; missing or blank opener text fails with opening_message_required.',
|
|
1765
|
-
'Do not use this tool for replying inside an already-open Claworld chat or for runtime live turns.',
|
|
1766
|
-
'After creation, use claworld_chat_inbox to inspect pending, expired, rejected, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
|
|
1767
|
-
'Once accepted, the runtime owns the live conversation loop.',
|
|
1768
|
-
],
|
|
1769
|
-
examples: [
|
|
1770
|
-
{
|
|
1771
|
-
title: 'Request chat with a world member',
|
|
1772
|
-
input: {
|
|
1773
|
-
accountId: 'claworld',
|
|
1774
|
-
worldId: 'dating-demo-world',
|
|
1775
|
-
displayName: 'Runtime Peer',
|
|
1776
|
-
agentCode: 'ZX82QP',
|
|
1777
|
-
openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
|
|
1778
|
-
},
|
|
1779
|
-
outcome: 'Creates one pending world-scoped chat request or re-engagement request.',
|
|
1780
|
-
},
|
|
1781
|
-
{
|
|
1782
|
-
title: 'Re-engage a known public identity',
|
|
1783
|
-
input: {
|
|
1784
|
-
accountId: 'claworld',
|
|
1785
|
-
displayName: 'Runtime Peer',
|
|
1786
|
-
agentCode: 'ZX82QP',
|
|
1787
|
-
openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
|
|
1788
|
-
},
|
|
1789
|
-
outcome: 'Creates one pending direct chat request toward the known public identity, including re-engagement after silence or a prior ended request.',
|
|
1790
|
-
},
|
|
1791
|
-
],
|
|
1792
|
-
}),
|
|
1793
|
-
parameters: objectParam({
|
|
1794
|
-
description: 'In the main session, create a new direct or world-scoped chat request, or re-engage a previously silent or ended relationship, for one target agent. Use this for user requests to contact, PK, continue, or send peer-facing Claworld conversation content. Provide the target displayName, agentCode, and non-blank openingMessage. Do not use this payload for current live replies.',
|
|
1795
|
-
required: ['accountId', 'displayName', 'agentCode', 'openingMessage'],
|
|
1796
|
-
properties: {
|
|
1797
|
-
accountId: accountIdProperty,
|
|
1798
|
-
displayName: stringParam({
|
|
1799
|
-
description: 'Target public displayName for the request or re-engagement target.',
|
|
1800
|
-
minLength: 1,
|
|
1801
|
-
examples: ['Runtime Peer'],
|
|
1802
|
-
}),
|
|
1803
|
-
agentCode: stringParam({
|
|
1804
|
-
description: 'Target public agentCode. The backend resolves the target by this code and verifies the displayName still matches. Use public identity, not local session references.',
|
|
1805
|
-
minLength: 1,
|
|
1806
|
-
examples: ['ZX82QP'],
|
|
1807
|
-
}),
|
|
1808
|
-
openingMessage: stringParam({
|
|
1809
|
-
description: 'Required non-blank request or re-engagement brief that the backend uses when the peer accepts. Missing or blank opener text fails with opening_message_required. This is kickoff intent, not a live runtime reply payload.',
|
|
1810
|
-
minLength: 1,
|
|
1811
|
-
examples: ['Hi, want to compare trail-running routes in Shanghai?'],
|
|
1812
|
-
}),
|
|
1813
|
-
message: stringParam({
|
|
1814
|
-
description: 'Alias for openingMessage.',
|
|
1815
|
-
minLength: 1,
|
|
1816
|
-
}),
|
|
1817
|
-
text: stringParam({
|
|
1818
|
-
description: 'Alias for openingMessage.',
|
|
1819
|
-
minLength: 1,
|
|
1820
|
-
}),
|
|
1821
|
-
kickoffBrief: objectParam({
|
|
1822
|
-
description: 'Structured request kickoff brief. text/openingMessage/message are accepted as opener aliases.',
|
|
1823
|
-
properties: {
|
|
1824
|
-
text: stringParam({ description: 'Request/re-engagement kickoff message.', minLength: 1 }),
|
|
1825
|
-
openingMessage: stringParam({ description: 'Alias for kickoff brief text.', minLength: 1 }),
|
|
1826
|
-
message: stringParam({ description: 'Alias for kickoff brief text.', minLength: 1 }),
|
|
1827
|
-
},
|
|
1828
|
-
}),
|
|
1829
|
-
openingPayload: objectParam({
|
|
1830
|
-
description: 'Optional structured opening payload. text is accepted as opener alias.',
|
|
1831
|
-
properties: {
|
|
1832
|
-
text: stringParam({ description: 'Request/re-engagement kickoff message.', minLength: 1 }),
|
|
1833
|
-
},
|
|
1834
|
-
}),
|
|
1835
|
-
worldId: worldIdProperty,
|
|
1836
|
-
},
|
|
1837
|
-
examples: [
|
|
1838
|
-
{
|
|
1839
|
-
accountId: 'claworld',
|
|
1840
|
-
worldId: 'dating-demo-world',
|
|
1841
|
-
displayName: 'Runtime Peer',
|
|
1842
|
-
agentCode: 'ZX82QP',
|
|
1843
|
-
openingMessage: 'Hi, want to compare trail-running routes in Shanghai?',
|
|
1844
|
-
},
|
|
1845
|
-
],
|
|
1846
|
-
}),
|
|
1847
|
-
async execute(_toolCallId, params = {}) {
|
|
1848
|
-
const context = await resolveToolContext(api, plugin, params, {
|
|
1849
|
-
requiredPublicIdentityCapability: 'request chat',
|
|
1850
|
-
});
|
|
1851
|
-
const payload = await plugin.helpers.social.requestChat({
|
|
1852
|
-
...context,
|
|
1853
|
-
displayName: params.displayName,
|
|
1854
|
-
agentCode: params.agentCode,
|
|
1855
|
-
openingMessage: resolveConversationOpeningMessage(params),
|
|
1856
|
-
message: params.message || null,
|
|
1857
|
-
text: params.text || null,
|
|
1858
|
-
kickoffBrief: params.kickoffBrief || null,
|
|
1859
|
-
openingPayload: params.openingPayload || null,
|
|
1860
|
-
worldId: params.worldId || null,
|
|
1861
|
-
});
|
|
1862
|
-
return buildToolResult(projectToolChatRequestMutationResponse(payload, { accountId: context.accountId }));
|
|
1863
|
-
},
|
|
1864
|
-
},
|
|
1865
1766
|
{
|
|
1866
1767
|
name: 'claworld_chat_inbox',
|
|
1867
1768
|
label: 'Claworld Chat Inbox',
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
|
|
33
33
|
const DELIVERY_VISIBILITY_RETRY_ATTEMPTS = 20;
|
|
34
34
|
const DELIVERY_VISIBILITY_RETRY_DELAY_MS = 10;
|
|
35
|
+
const DEFAULT_RELAY_AUTH_TIMEOUT_MS = 30_000;
|
|
36
|
+
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1_000;
|
|
37
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 60_000;
|
|
38
|
+
const DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
|
|
35
39
|
|
|
36
40
|
function isDeliveryVisibilityMiss(result = {}) {
|
|
37
41
|
return Number(result?.status) === 404
|
|
@@ -53,6 +57,11 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
53
57
|
protocol = createRelayEventProtocol(),
|
|
54
58
|
wsFactory = (url) => new WebSocket(url),
|
|
55
59
|
httpFetch = globalThis.fetch,
|
|
60
|
+
authTimeoutMs = DEFAULT_RELAY_AUTH_TIMEOUT_MS,
|
|
61
|
+
reconnectBaseDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS,
|
|
62
|
+
reconnectMaxDelayMs = DEFAULT_RECONNECT_MAX_DELAY_MS,
|
|
63
|
+
reconnectJitterRatio = DEFAULT_RECONNECT_JITTER_RATIO,
|
|
64
|
+
random = Math.random,
|
|
56
65
|
} = {}) {
|
|
57
66
|
super();
|
|
58
67
|
this.logger = logger;
|
|
@@ -61,6 +70,11 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
61
70
|
this.protocol = protocol;
|
|
62
71
|
this.wsFactory = wsFactory;
|
|
63
72
|
this.httpFetch = httpFetch;
|
|
73
|
+
this.authTimeoutMs = Math.max(1, Math.floor(Number(authTimeoutMs) || DEFAULT_RELAY_AUTH_TIMEOUT_MS));
|
|
74
|
+
this.reconnectBaseDelayMs = Math.max(1, Math.floor(Number(reconnectBaseDelayMs) || DEFAULT_RECONNECT_BASE_DELAY_MS));
|
|
75
|
+
this.reconnectMaxDelayMs = Math.max(this.reconnectBaseDelayMs, Math.floor(Number(reconnectMaxDelayMs) || DEFAULT_RECONNECT_MAX_DELAY_MS));
|
|
76
|
+
this.reconnectJitterRatio = Math.max(0, Math.min(1, Number(reconnectJitterRatio) || 0));
|
|
77
|
+
this.random = typeof random === 'function' ? random : Math.random;
|
|
64
78
|
this.ws = null;
|
|
65
79
|
this.events = [];
|
|
66
80
|
this.heartbeatTimer = null;
|
|
@@ -103,9 +117,17 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
103
117
|
this.reconnectTimer = null;
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
resolveReconnectDelayMs() {
|
|
107
|
-
const
|
|
108
|
-
|
|
120
|
+
resolveReconnectDelayMs(attempt = this.reconnectAttempts + 1) {
|
|
121
|
+
const normalizedAttempt = Math.max(1, Math.floor(Number(attempt) || 1));
|
|
122
|
+
const exponent = Math.min(normalizedAttempt - 1, 10);
|
|
123
|
+
const baseDelayMs = Math.min(
|
|
124
|
+
this.reconnectMaxDelayMs,
|
|
125
|
+
this.reconnectBaseDelayMs * (2 ** exponent),
|
|
126
|
+
);
|
|
127
|
+
const jitterWindowMs = Math.floor(baseDelayMs * this.reconnectJitterRatio);
|
|
128
|
+
if (jitterWindowMs <= 0) return baseDelayMs;
|
|
129
|
+
const jitterMs = Math.floor(Math.max(0, Math.min(1, Number(this.random()) || 0)) * jitterWindowMs);
|
|
130
|
+
return Math.min(this.reconnectMaxDelayMs, baseDelayMs + jitterMs);
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
shouldAutoReconnect(disconnectInfo = null) {
|
|
@@ -285,9 +307,59 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
285
307
|
return await new Promise((resolve, reject) => {
|
|
286
308
|
let settled = false;
|
|
287
309
|
let suppressCloseHandler = false;
|
|
310
|
+
let authTimer = null;
|
|
288
311
|
const ws = this.wsFactory(wsUrl);
|
|
289
312
|
this.ws = ws;
|
|
290
313
|
|
|
314
|
+
const clearAuthTimer = () => {
|
|
315
|
+
if (authTimer) clearTimeout(authTimer);
|
|
316
|
+
authTimer = null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const closeSocketAfterFailedAuth = (reason = 'auth_failed') => {
|
|
320
|
+
try {
|
|
321
|
+
if (this.ws === ws) this.ws = null;
|
|
322
|
+
if (ws.readyState !== 3) ws.close(1008, reason);
|
|
323
|
+
} catch {
|
|
324
|
+
// No-op.
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const settleAuthResolve = (message) => {
|
|
329
|
+
if (settled) return;
|
|
330
|
+
settled = true;
|
|
331
|
+
clearAuthTimer();
|
|
332
|
+
resolve(message);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const settleAuthReject = (error, { suppressClose = false, closeReason = null } = {}) => {
|
|
336
|
+
if (settled) return;
|
|
337
|
+
settled = true;
|
|
338
|
+
clearAuthTimer();
|
|
339
|
+
if (suppressClose) suppressCloseHandler = true;
|
|
340
|
+
this.connectionState = 'error';
|
|
341
|
+
if (closeReason) closeSocketAfterFailedAuth(closeReason);
|
|
342
|
+
reject(error);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
authTimer = setTimeout(() => {
|
|
346
|
+
const timeoutError = createRuntimeBoundaryError({
|
|
347
|
+
code: 'relay_auth_timeout',
|
|
348
|
+
category: 'transport',
|
|
349
|
+
status: 504,
|
|
350
|
+
message: `timed out waiting for relay authentication after ${this.authTimeoutMs}ms`,
|
|
351
|
+
publicMessage: 'relay authentication timed out',
|
|
352
|
+
recoverable: true,
|
|
353
|
+
context: this.buildBoundaryContext({
|
|
354
|
+
stage: 'auth',
|
|
355
|
+
timeoutMs: this.authTimeoutMs,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
timeoutError.reason = 'auth_timeout';
|
|
359
|
+
timeoutError.close = this.buildDisconnectInfo(null, 'auth_timeout', 'auth');
|
|
360
|
+
settleAuthReject(timeoutError, { suppressClose: true, closeReason: 'auth_timeout' });
|
|
361
|
+
}, this.authTimeoutMs);
|
|
362
|
+
|
|
291
363
|
ws.on('open', () => {
|
|
292
364
|
this.logger.info?.('[claworld:relay-client] websocket open, sending auth', {
|
|
293
365
|
accountId: this.runtimeConfig.accountId,
|
|
@@ -295,13 +367,17 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
295
367
|
clientVersion,
|
|
296
368
|
bridgeProtocol: this.protocol.version,
|
|
297
369
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
370
|
+
try {
|
|
371
|
+
this.send({
|
|
372
|
+
type: 'auth',
|
|
373
|
+
agentId,
|
|
374
|
+
credential,
|
|
375
|
+
clientVersion,
|
|
376
|
+
bridgeProtocol: this.protocol.version,
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
settleAuthReject(error, { suppressClose: true, closeReason: 'auth_send_failed' });
|
|
380
|
+
}
|
|
305
381
|
});
|
|
306
382
|
|
|
307
383
|
ws.on('message', (buf) => {
|
|
@@ -316,9 +392,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
316
392
|
context: { stage: 'message_parse' },
|
|
317
393
|
});
|
|
318
394
|
if (!settled) {
|
|
319
|
-
|
|
320
|
-
this.connectionState = 'error';
|
|
321
|
-
reject(normalized);
|
|
395
|
+
settleAuthReject(normalized, { suppressClose: true, closeReason: 'message_parse_failed' });
|
|
322
396
|
}
|
|
323
397
|
return;
|
|
324
398
|
}
|
|
@@ -327,7 +401,6 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
327
401
|
this.emitRelayMessage(message, { sessionTarget, fallbackTarget });
|
|
328
402
|
|
|
329
403
|
if (message.event === 'auth.ok' && !settled) {
|
|
330
|
-
settled = true;
|
|
331
404
|
this.connectionState = 'authenticated';
|
|
332
405
|
this.reconnectAttempts = 0;
|
|
333
406
|
this.logger.info?.('[claworld:relay-client] auth ok', {
|
|
@@ -335,13 +408,10 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
335
408
|
agentId,
|
|
336
409
|
});
|
|
337
410
|
this.startHeartbeatLoop();
|
|
338
|
-
|
|
411
|
+
settleAuthResolve(message);
|
|
339
412
|
}
|
|
340
413
|
|
|
341
414
|
if (message.event === 'error' && !settled && message.data?.code === 'unauthorized') {
|
|
342
|
-
settled = true;
|
|
343
|
-
suppressCloseHandler = true;
|
|
344
|
-
this.connectionState = 'error';
|
|
345
415
|
const authReason = message.data?.reason || message.data?.error || 'unauthorized';
|
|
346
416
|
const authError = createRuntimeBoundaryError({
|
|
347
417
|
code: message.data?.code || 'unauthorized',
|
|
@@ -364,7 +434,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
364
434
|
error: authError.message,
|
|
365
435
|
code: authError.code,
|
|
366
436
|
});
|
|
367
|
-
|
|
437
|
+
settleAuthReject(authError, { suppressClose: true, closeReason: authReason });
|
|
368
438
|
}
|
|
369
439
|
} catch (error) {
|
|
370
440
|
const normalized = this.emitBoundaryError('[claworld:relay-client] relay message handling failed', error, {
|
|
@@ -377,10 +447,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
377
447
|
},
|
|
378
448
|
});
|
|
379
449
|
if (!settled) {
|
|
380
|
-
|
|
381
|
-
suppressCloseHandler = true;
|
|
382
|
-
this.connectionState = 'error';
|
|
383
|
-
reject(normalized);
|
|
450
|
+
settleAuthReject(normalized, { suppressClose: true, closeReason: 'message_handling_failed' });
|
|
384
451
|
}
|
|
385
452
|
}
|
|
386
453
|
});
|
|
@@ -402,6 +469,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
402
469
|
|
|
403
470
|
if (!settled) {
|
|
404
471
|
settled = true;
|
|
472
|
+
clearAuthTimer();
|
|
405
473
|
this.connectionState = disconnectInfo.reason === 'duplicate_connection_replaced' ? 'replaced' : 'error';
|
|
406
474
|
reject(this.createClosedBeforeAuthError(disconnectInfo));
|
|
407
475
|
return;
|
|
@@ -430,11 +498,8 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
430
498
|
},
|
|
431
499
|
});
|
|
432
500
|
if (!settled) {
|
|
433
|
-
settled = true;
|
|
434
|
-
suppressCloseHandler = true;
|
|
435
|
-
this.connectionState = 'error';
|
|
436
501
|
normalized.reason = normalized.reason || normalized.code || normalized.message;
|
|
437
|
-
|
|
502
|
+
settleAuthReject(normalized, { suppressClose: true, closeReason: 'connect_error' });
|
|
438
503
|
}
|
|
439
504
|
});
|
|
440
505
|
});
|
|
@@ -448,8 +513,8 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
448
513
|
}
|
|
449
514
|
|
|
450
515
|
this.clearReconnectTimer();
|
|
451
|
-
const delayMs = this.resolveReconnectDelayMs();
|
|
452
516
|
const attempt = this.reconnectAttempts + 1;
|
|
517
|
+
const delayMs = this.resolveReconnectDelayMs(attempt);
|
|
453
518
|
this.reconnectAttempts = attempt;
|
|
454
519
|
this.logger.warn?.('[claworld:relay-client] scheduling reconnect', {
|
|
455
520
|
accountId: this.runtimeConfig?.accountId || null,
|
|
@@ -793,7 +858,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
793
858
|
});
|
|
794
859
|
}
|
|
795
860
|
|
|
796
|
-
waitForAcceptedAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
|
|
861
|
+
waitForAcceptedAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS, signal = null } = {}) {
|
|
797
862
|
const normalizedDeliveryId = normalizeOptionalText(deliveryId);
|
|
798
863
|
if (!normalizedDeliveryId) {
|
|
799
864
|
return Promise.reject(createRuntimeBoundaryError({
|
|
@@ -805,6 +870,20 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
805
870
|
recoverable: true,
|
|
806
871
|
}));
|
|
807
872
|
}
|
|
873
|
+
if (signal?.aborted) {
|
|
874
|
+
return Promise.reject(createRuntimeBoundaryError({
|
|
875
|
+
code: 'relay_delivery_accept_ack_wait_cancelled',
|
|
876
|
+
category: 'transport',
|
|
877
|
+
status: 499,
|
|
878
|
+
message: `relay delivery acceptance acknowledgement wait cancelled for ${normalizedDeliveryId}`,
|
|
879
|
+
publicMessage: 'relay delivery acceptance acknowledgement wait cancelled',
|
|
880
|
+
recoverable: true,
|
|
881
|
+
context: this.buildBoundaryContext({
|
|
882
|
+
stage: 'delivery_accept_ack_wait',
|
|
883
|
+
deliveryId: normalizedDeliveryId,
|
|
884
|
+
}),
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
808
887
|
|
|
809
888
|
return new Promise((resolve, reject) => {
|
|
810
889
|
let settled = false;
|
|
@@ -815,6 +894,7 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
815
894
|
this.off('delivery.accepted', onAccepted);
|
|
816
895
|
this.off('disconnect', onDisconnect);
|
|
817
896
|
this.off('close', onDisconnect);
|
|
897
|
+
signal?.removeEventListener('abort', onAbort);
|
|
818
898
|
};
|
|
819
899
|
|
|
820
900
|
const settleResolve = (value) => {
|
|
@@ -854,9 +934,25 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
854
934
|
}));
|
|
855
935
|
};
|
|
856
936
|
|
|
937
|
+
const onAbort = () => {
|
|
938
|
+
settleReject(createRuntimeBoundaryError({
|
|
939
|
+
code: 'relay_delivery_accept_ack_wait_cancelled',
|
|
940
|
+
category: 'transport',
|
|
941
|
+
status: 499,
|
|
942
|
+
message: `relay delivery acceptance acknowledgement wait cancelled for ${normalizedDeliveryId}`,
|
|
943
|
+
publicMessage: 'relay delivery acceptance acknowledgement wait cancelled',
|
|
944
|
+
recoverable: true,
|
|
945
|
+
context: this.buildBoundaryContext({
|
|
946
|
+
stage: 'delivery_accept_ack_wait',
|
|
947
|
+
deliveryId: normalizedDeliveryId,
|
|
948
|
+
}),
|
|
949
|
+
}));
|
|
950
|
+
};
|
|
951
|
+
|
|
857
952
|
this.on('delivery.accepted', onAccepted);
|
|
858
953
|
this.on('disconnect', onDisconnect);
|
|
859
954
|
this.on('close', onDisconnect);
|
|
955
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
860
956
|
|
|
861
957
|
timeout = setTimeout(() => {
|
|
862
958
|
settleReject(buildAcceptedAckTimeoutError({
|
|
@@ -1097,6 +1193,52 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
1097
1193
|
};
|
|
1098
1194
|
}
|
|
1099
1195
|
|
|
1196
|
+
async submitAcceptedHttpFallback({
|
|
1197
|
+
deliveryId,
|
|
1198
|
+
sessionKey,
|
|
1199
|
+
source = 'runtime_dispatch',
|
|
1200
|
+
error = null,
|
|
1201
|
+
} = {}) {
|
|
1202
|
+
this.logger.warn?.('[claworld:relay-client] delivery acceptance websocket transport failed; attempting HTTP fallback', {
|
|
1203
|
+
accountId: this.runtimeConfig?.accountId || null,
|
|
1204
|
+
agentId: this.boundAgentId,
|
|
1205
|
+
deliveryId: normalizeOptionalText(deliveryId),
|
|
1206
|
+
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
1207
|
+
error: error?.message || String(error),
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
const fallbackResult = await this.acceptDeliveryHttp({
|
|
1211
|
+
deliveryId,
|
|
1212
|
+
sessionKey,
|
|
1213
|
+
source,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
|
|
1217
|
+
return {
|
|
1218
|
+
ok: true,
|
|
1219
|
+
envelope: fallbackResult.envelope,
|
|
1220
|
+
ack: {
|
|
1221
|
+
event: 'delivery.accepted',
|
|
1222
|
+
data: fallbackResult.body,
|
|
1223
|
+
},
|
|
1224
|
+
transport: 'http',
|
|
1225
|
+
fallbackUsed: true,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
throw buildReplyFallbackError({
|
|
1230
|
+
deliveryId: fallbackResult.envelope.deliveryId,
|
|
1231
|
+
status: fallbackResult.status,
|
|
1232
|
+
body: fallbackResult.body,
|
|
1233
|
+
context: this.buildBoundaryContext({
|
|
1234
|
+
stage: 'delivery_accept_fallback',
|
|
1235
|
+
deliveryId: fallbackResult.envelope.deliveryId,
|
|
1236
|
+
sessionKey: normalizeOptionalText(sessionKey) || null,
|
|
1237
|
+
fallbackFrom: error?.code || error?.message || null,
|
|
1238
|
+
}),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1100
1242
|
async sendReplyAndWaitForAck({
|
|
1101
1243
|
deliveryId,
|
|
1102
1244
|
sessionKey,
|
|
@@ -1190,15 +1332,40 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
1190
1332
|
timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
|
|
1191
1333
|
httpFallback = true,
|
|
1192
1334
|
} = {}) {
|
|
1335
|
+
if (httpFallback && (!this.ws || this.ws.readyState !== 1)) {
|
|
1336
|
+
return await this.submitAcceptedHttpFallback({
|
|
1337
|
+
deliveryId,
|
|
1338
|
+
sessionKey,
|
|
1339
|
+
source,
|
|
1340
|
+
error: this.buildWsNotConnectedError('delivery_accept_send'),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const ackAbortController = new AbortController();
|
|
1193
1345
|
const ackPromise = this.waitForAcceptedAck({
|
|
1194
1346
|
deliveryId,
|
|
1195
1347
|
timeoutMs,
|
|
1348
|
+
signal: ackAbortController.signal,
|
|
1196
1349
|
});
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1350
|
+
let envelope;
|
|
1351
|
+
|
|
1352
|
+
try {
|
|
1353
|
+
envelope = this.sendAccepted({
|
|
1354
|
+
deliveryId,
|
|
1355
|
+
sessionKey,
|
|
1356
|
+
source,
|
|
1357
|
+
});
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
ackAbortController.abort();
|
|
1360
|
+
void ackPromise.catch(() => {});
|
|
1361
|
+
if (!httpFallback) throw error;
|
|
1362
|
+
return await this.submitAcceptedHttpFallback({
|
|
1363
|
+
deliveryId,
|
|
1364
|
+
sessionKey,
|
|
1365
|
+
source,
|
|
1366
|
+
error,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1202
1369
|
|
|
1203
1370
|
try {
|
|
1204
1371
|
const ack = await ackPromise;
|
|
@@ -1212,43 +1379,11 @@ export class ClaworldRelayClient extends EventEmitter {
|
|
|
1212
1379
|
} catch (error) {
|
|
1213
1380
|
if (!httpFallback) throw error;
|
|
1214
1381
|
|
|
1215
|
-
this.
|
|
1216
|
-
accountId: this.runtimeConfig?.accountId || null,
|
|
1217
|
-
agentId: this.boundAgentId,
|
|
1218
|
-
deliveryId: envelope.deliveryId,
|
|
1219
|
-
sessionKey: envelope.sessionKey,
|
|
1220
|
-
error: error?.message || String(error),
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
const fallbackResult = await this.acceptDeliveryHttp({
|
|
1382
|
+
return await this.submitAcceptedHttpFallback({
|
|
1224
1383
|
deliveryId: envelope.deliveryId,
|
|
1225
1384
|
sessionKey: envelope.sessionKey,
|
|
1226
1385
|
source: envelope.source,
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
|
|
1230
|
-
return {
|
|
1231
|
-
ok: true,
|
|
1232
|
-
envelope,
|
|
1233
|
-
ack: {
|
|
1234
|
-
event: 'delivery.accepted',
|
|
1235
|
-
data: fallbackResult.body,
|
|
1236
|
-
},
|
|
1237
|
-
transport: 'http',
|
|
1238
|
-
fallbackUsed: true,
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
throw buildReplyFallbackError({
|
|
1243
|
-
deliveryId: envelope.deliveryId,
|
|
1244
|
-
status: fallbackResult.status,
|
|
1245
|
-
body: fallbackResult.body,
|
|
1246
|
-
context: this.buildBoundaryContext({
|
|
1247
|
-
stage: 'delivery_accept_fallback',
|
|
1248
|
-
deliveryId: envelope.deliveryId,
|
|
1249
|
-
sessionKey: envelope.sessionKey,
|
|
1250
|
-
fallbackFrom: error?.code || error?.message || null,
|
|
1251
|
-
}),
|
|
1386
|
+
error,
|
|
1252
1387
|
});
|
|
1253
1388
|
}
|
|
1254
1389
|
}
|
|
@@ -1402,7 +1402,6 @@ export function buildClaworldToolMaintenanceEvent({
|
|
|
1402
1402
|
const conversationToolNames = new Set([
|
|
1403
1403
|
'claworld_manage_conversations',
|
|
1404
1404
|
'claworld_chat_inbox',
|
|
1405
|
-
'claworld_request_chat',
|
|
1406
1405
|
]);
|
|
1407
1406
|
const localSessionKey = firstText(
|
|
1408
1407
|
conversationToolNames.has(normalizedToolName) ? resultConversationSessionKey : null,
|