@xfxstudio/claworld 0.2.9 → 0.2.10-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/openclaw.plugin.json +7 -63
  3. package/package.json +6 -2
  4. package/skills/claworld-help/SKILL.md +5 -1
  5. package/skills/claworld-join-and-chat/SKILL.md +21 -1
  6. package/skills/claworld-manage-worlds/SKILL.md +81 -10
  7. package/src/lib/agent-profile.js +8 -3
  8. package/src/lib/chat-request.js +0 -1
  9. package/src/lib/policy.js +2 -6
  10. package/src/lib/public-identity.js +175 -0
  11. package/src/lib/relay/kickoff-text.js +1 -0
  12. package/src/openclaw/installer/cli.js +46 -1
  13. package/src/openclaw/installer/constants.js +1 -0
  14. package/src/openclaw/installer/core.js +234 -3
  15. package/src/openclaw/installer/doctor.js +2 -2
  16. package/src/openclaw/plugin/account-identity.js +1 -2
  17. package/src/openclaw/plugin/claworld-channel-plugin.js +270 -255
  18. package/src/openclaw/plugin/config-schema.js +9 -23
  19. package/src/openclaw/plugin/managed-config.js +284 -79
  20. package/src/openclaw/plugin/onboarding.js +22 -42
  21. package/src/openclaw/plugin/register.js +109 -10
  22. package/src/openclaw/plugin/relay-client.js +233 -17
  23. package/src/openclaw/runtime/backend-error-context.js +91 -0
  24. package/src/openclaw/runtime/feedback-helper.js +1 -2
  25. package/src/openclaw/runtime/product-shell-helper.js +43 -9
  26. package/src/openclaw/runtime/tool-contracts.js +26 -3
  27. package/src/openclaw/runtime/tool-inventory.js +7 -0
  28. package/src/openclaw/runtime/world-moderation-helper.js +3 -19
  29. package/src/product-shell/contracts/candidate-feed.js +7 -0
  30. package/src/product-shell/contracts/world-manifest.js +0 -1
  31. package/src/product-shell/contracts/world-orchestration.js +10 -1
  32. package/src/product-shell/conversation-feedback/conversation-feedback-service.js +261 -0
  33. package/src/product-shell/feedback/feedback-routes.js +0 -1
  34. package/src/product-shell/feedback/feedback-service.js +4 -9
  35. package/src/product-shell/index.js +40 -7
  36. package/src/product-shell/matching/matchmaking-service.js +22 -1
  37. package/src/product-shell/membership/membership-service.js +5 -1
  38. package/src/product-shell/onboarding/onboarding-service.js +10 -21
  39. package/src/product-shell/profile/public-identity-routes.js +60 -0
  40. package/src/product-shell/profile/public-identity-service.js +190 -0
  41. package/src/product-shell/search/search-service.js +9 -2
  42. package/src/product-shell/social/chat-request-service.js +22 -7
  43. package/src/product-shell/social/friend-routes.js +1 -1
  44. package/src/product-shell/social/friend-service.js +16 -19
  45. package/src/product-shell/social/social-routes.js +2 -2
  46. package/src/product-shell/social/social-service.js +31 -35
  47. package/src/product-shell/worlds/world-admin-service.js +31 -10
  48. package/src/product-shell/worlds/world-broadcast-service.js +2 -2
  49. package/src/lib/agent-address.js +0 -46
@@ -7,27 +7,16 @@ import {
7
7
  resolveClaworldManagedRuntimeOptions,
8
8
  } from './managed-config.js';
9
9
  import {
10
- LOCAL_AGENT_BOOTSTRAP_SCHEMA,
11
10
  defaultClaworldAccountId,
12
11
  inspectClaworldChannelAccount,
13
12
  listClaworldAccountIds,
14
13
  } from './config-schema.js';
15
- import { parseAgentHandle } from '../../lib/agent-address.js';
16
14
  import {
17
15
  buildManagedOnboardingStatus as buildClaworldOnboardingStatus,
18
16
  inspectManagedClaworldInstall,
19
17
  seedManagedWorkspace as ensureManagedWorkspaceSeed,
20
18
  } from '../installer/core.js';
21
19
 
22
- const LOCAL_AGENT_CODE_REGEX = new RegExp(
23
- LOCAL_AGENT_BOOTSTRAP_SCHEMA?.properties?.agentCode?.pattern || '^[A-Za-z0-9._:+~-]+$',
24
- 'i',
25
- );
26
-
27
- function isCanonicalAgentHandle(value) {
28
- return Boolean(parseAgentHandle(value)?.canonical);
29
- }
30
-
31
20
  function collectUnsupportedSetupFlags(input = {}) {
32
21
  const unsupported = [];
33
22
  const flagMap = [
@@ -69,7 +58,7 @@ function validateClaworldSetupInput({ cfg = {}, accountId = null, input = {} } =
69
58
  const unsupportedFlags = collectUnsupportedSetupFlags(input);
70
59
  if (unsupportedFlags.length > 0) {
71
60
  return (
72
- 'Claworld setup only supports --name, --http-url/--url, --app-token, and --code. ' +
61
+ 'Claworld setup only supports --name, --http-url/--url, and --app-token. ' +
73
62
  `Unsupported flag(s): ${unsupportedFlags.join(', ')}.`
74
63
  );
75
64
  }
@@ -92,24 +81,15 @@ function validateClaworldSetupInput({ cfg = {}, accountId = null, input = {} } =
92
81
  }
93
82
  }
94
83
 
95
- const explicitRegistrationAgentCode = normalizeText(input.code, null);
96
- const existingRegistrationAgentCode = normalizeText(
97
- inspected?.registration?.agentCode,
98
- normalizeText(inspected?.localAgent?.agentCode, null),
84
+ const registrationDisplayName = normalizeText(
85
+ input.name,
86
+ normalizeText(
87
+ inspected?.registration?.displayName,
88
+ normalizeText(inspected?.localAgent?.displayName, null),
89
+ ),
99
90
  );
100
- const registrationAgentCode = explicitRegistrationAgentCode
101
- || (isCanonicalAgentHandle(existingRegistrationAgentCode) ? existingRegistrationAgentCode : null);
102
- if (!appToken && !explicitRegistrationAgentCode && existingRegistrationAgentCode && !isCanonicalAgentHandle(existingRegistrationAgentCode)) {
103
- return 'Existing Claworld registration.agentCode is legacy/raw. Re-run setup with --code <local@namespace> so the namespace stays explicit.';
104
- }
105
- if (!appToken && !registrationAgentCode) {
106
- return 'Claworld identity handle is required unless you already have an appToken. Use --code <local@namespace> or --app-token <token>.';
107
- }
108
- if (!appToken && explicitRegistrationAgentCode && !LOCAL_AGENT_CODE_REGEX.test(explicitRegistrationAgentCode)) {
109
- return 'Claworld identity handle must match ^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$.';
110
- }
111
- if (!appToken && explicitRegistrationAgentCode && !isCanonicalAgentHandle(explicitRegistrationAgentCode)) {
112
- return 'Claworld identity handle must use canonical local@namespace syntax (for example "xiaofafa@robin").';
91
+ if (!appToken && !registrationDisplayName) {
92
+ return 'Claworld public display name is required unless you already have an appToken. Use --name <display-name> or --app-token <token>.';
113
93
  }
114
94
 
115
95
  return null;
@@ -124,12 +104,12 @@ function currentManagedIdentityInput({ cfg = {}, accountId = null } = {}) {
124
104
  };
125
105
  }
126
106
 
127
- const currentCode = normalizeText(
128
- inspected?.registration?.agentCode,
129
- normalizeText(inspected?.localAgent?.agentCode, null),
107
+ const currentDisplayName = normalizeText(
108
+ inspected?.registration?.displayName,
109
+ normalizeText(inspected?.localAgent?.displayName, null),
130
110
  );
131
- return isCanonicalAgentHandle(currentCode)
132
- ? { code: currentCode }
111
+ return currentDisplayName
112
+ ? { name: currentDisplayName }
133
113
  : {};
134
114
  }
135
115
 
@@ -139,18 +119,18 @@ async function collectManagedIdentityInput({ cfg = {}, prompter, accountId = nul
139
119
  return currentInput;
140
120
  }
141
121
 
142
- const code = await prompter.text({
143
- message: 'Choose a unique Claworld identity handle',
144
- initialValue: currentInput.code || '',
145
- placeholder: 'xiaofafa@robin',
122
+ const name = await prompter.text({
123
+ message: 'Choose the public display name to bootstrap this Claworld agent',
124
+ initialValue: currentInput.name || '',
125
+ placeholder: 'Claworld Agent',
146
126
  validate: (value) => {
147
- const message = validateClaworldSetupInput({ input: { code: value } });
127
+ const message = validateClaworldSetupInput({ input: { name: value } });
148
128
  return message || undefined;
149
129
  },
150
130
  });
151
131
 
152
132
  return {
153
- code,
133
+ name,
154
134
  };
155
135
  }
156
136
 
@@ -207,8 +187,8 @@ async function applyManagedOnboardingConfig({
207
187
  const noteLines = [
208
188
  `Bound local agent/account: ${managedOptions.agentId}`,
209
189
  `Remote backend: ${managedOptions.serverUrl}`,
210
- managedOptions.registrationAgentCode
211
- ? `Bootstrap mode: registration (${managedOptions.registrationAgentCode})`
190
+ managedOptions.registrationDisplayName
191
+ ? `Bootstrap mode: registration (${managedOptions.registrationDisplayName})`
212
192
  : 'Bootstrap mode: appToken/manual binding',
213
193
  managedOptions.manageWorkspace
214
194
  ? 'This flow refreshes plugin-side config and the dedicated claworld workspace contract. It does not start a backend service.'
@@ -18,6 +18,11 @@ import {
18
18
  logRuntimeBoundary,
19
19
  normalizeRuntimeBoundaryError,
20
20
  } from '../../lib/runtime-errors.js';
21
+ import {
22
+ normalizeBackendFieldError,
23
+ normalizeBackendMissingField,
24
+ normalizeBackendPublicIdentity,
25
+ } from '../runtime/backend-error-context.js';
21
26
 
22
27
  const INTERNAL_REQUESTER_SESSION_KEY_PARAM = '__claworldRequesterSessionKey';
23
28
 
@@ -52,14 +57,30 @@ function buildPublicToolErrorExtras(error) {
52
57
  const backendCode = normalizeText(context.backendCode, null);
53
58
  const backendMessage = normalizeText(context.backendMessage, null);
54
59
  const fieldErrors = Array.isArray(context.fieldErrors)
55
- ? context.fieldErrors.map((fieldError) => normalizePublicFieldError(fieldError)).filter(Boolean)
60
+ ? context.fieldErrors
61
+ .map((fieldError) => normalizeBackendFieldError(fieldError) || normalizePublicFieldError(fieldError))
62
+ .filter(Boolean)
63
+ : [];
64
+ const requiredAction = normalizeText(context.requiredAction, null);
65
+ const nextAction = normalizeText(context.nextAction, null);
66
+ const nextTool = normalizeText(context.nextTool, null);
67
+ const missingFields = Array.isArray(context.missingFields)
68
+ ? context.missingFields
69
+ .map((field) => normalizeBackendMissingField(field))
70
+ .filter(Boolean)
56
71
  : [];
72
+ const publicIdentity = normalizeBackendPublicIdentity(context.publicIdentity);
57
73
 
58
74
  const extra = {
59
75
  ...(Number.isInteger(httpStatus) && httpStatus > 0 ? { httpStatus } : {}),
60
76
  ...(backendCode ? { backendCode } : {}),
61
77
  ...(backendMessage ? { backendMessage } : {}),
62
78
  ...(fieldErrors.length > 0 ? { fieldErrors } : {}),
79
+ ...(requiredAction ? { requiredAction } : {}),
80
+ ...(nextAction ? { nextAction } : {}),
81
+ ...(nextTool ? { nextTool } : {}),
82
+ ...(missingFields.length > 0 ? { missingFields } : {}),
83
+ ...(publicIdentity ? { publicIdentity } : {}),
63
84
  };
64
85
 
65
86
  return Object.keys(extra).length > 0 ? extra : null;
@@ -553,7 +574,7 @@ function buildRegisteredTools(api, plugin) {
553
574
  ...context,
554
575
  displayName: params.displayName,
555
576
  worldContextText: params.worldContextText,
556
- enabled: params.enabled === true,
577
+ enabled: typeof params.enabled === 'boolean' ? params.enabled : true,
557
578
  });
558
579
  return buildToolResult(projectToolCreateWorldResponse(payload, { accountId: context.accountId }));
559
580
  },
@@ -1009,10 +1030,6 @@ function buildRegisteredTools(api, plugin) {
1009
1030
  description: 'Optional peer agentId related to the issue.',
1010
1031
  examples: ['agt_runtime_candidate'],
1011
1032
  }),
1012
- targetAgentCode: stringParam({
1013
- description: 'Optional compatibility agentCode for the peer.',
1014
- examples: ['runtimecandidate@relay.local'],
1015
- }),
1016
1033
  tags: arrayParam({
1017
1034
  description: 'Short labels used for moderation and triage filtering.',
1018
1035
  maxItems: 10,
@@ -1110,6 +1127,15 @@ function buildRegisteredTools(api, plugin) {
1110
1127
  accountId,
1111
1128
  runtimeConfig,
1112
1129
  });
1130
+ const pairedAgentId = payload.runtimeConfig?.relay?.agentId || payload.relayAgent?.agentId || null;
1131
+ const publicIdentity = pairedAgentId
1132
+ ? await plugin.runtime.productShell.profile.getPublicIdentity({
1133
+ cfg,
1134
+ accountId,
1135
+ runtimeConfig,
1136
+ agentId: pairedAgentId,
1137
+ })
1138
+ : null;
1113
1139
  return buildToolResult({
1114
1140
  status: payload.status,
1115
1141
  reason: payload.reason || null,
@@ -1117,17 +1143,90 @@ function buildRegisteredTools(api, plugin) {
1117
1143
  bindingSource: payload.bindingSource || null,
1118
1144
  relay: {
1119
1145
  agentId: payload.runtimeConfig?.relay?.agentId || payload.relayAgent?.agentId || null,
1120
- agentCode: payload.relayAgent?.agentCode || null,
1121
- relayLocalCode: payload.relayAgent?.relayLocalCode || null,
1122
- address: payload.relayAgent?.address || null,
1123
- domain: payload.relayAgent?.domain || null,
1124
1146
  displayName: payload.relayAgent?.displayName || null,
1125
1147
  discoverable: payload.relayAgent?.discoverable ?? null,
1126
1148
  contactable: payload.relayAgent?.contactable ?? null,
1127
1149
  online: payload.relayAgent?.online ?? null,
1128
1150
  resolved: payload.relayAgent?.resolved ?? null,
1129
1151
  },
1152
+ publicIdentity: publicIdentity
1153
+ ? {
1154
+ status: publicIdentity.status || null,
1155
+ ready: publicIdentity.ready ?? null,
1156
+ displayIdentity: publicIdentity.publicIdentity?.displayIdentity || null,
1157
+ displayName: publicIdentity.publicIdentity?.displayName || null,
1158
+ code: publicIdentity.publicIdentity?.code || null,
1159
+ nextAction: publicIdentity.nextAction || null,
1160
+ nextTool: publicIdentity.nextTool || null,
1161
+ missingFields: Array.isArray(publicIdentity.missingFields) ? publicIdentity.missingFields : [],
1162
+ recommendedDisplayName: publicIdentity.recommendedDisplayName || null,
1163
+ feedbackSummary: publicIdentity.feedbackSummary && typeof publicIdentity.feedbackSummary === 'object'
1164
+ ? {
1165
+ totalLikesReceived: Number(publicIdentity.feedbackSummary.totalLikesReceived || 0),
1166
+ totalDislikesReceived: Number(publicIdentity.feedbackSummary.totalDislikesReceived || 0),
1167
+ totalLikesGiven: Number(publicIdentity.feedbackSummary.totalLikesGiven || 0),
1168
+ totalDislikesGiven: Number(publicIdentity.feedbackSummary.totalDislikesGiven || 0),
1169
+ }
1170
+ : null,
1171
+ }
1172
+ : null,
1173
+ });
1174
+ },
1175
+ },
1176
+ {
1177
+ name: 'claworld_get_public_identity',
1178
+ label: 'Claworld Get Public Identity',
1179
+ description: 'Read the current public identity state for the paired Claworld agent. Use this before world join or request-chat if you need to confirm whether public naming is complete.',
1180
+ metadata: buildToolMetadata({
1181
+ category: 'bootstrap',
1182
+ usageNotes: [
1183
+ 'Use when pair_agent indicates public identity is still pending.',
1184
+ 'Use before requesting a public-name confirmation from the user.',
1185
+ ],
1186
+ }),
1187
+ parameters: objectParam({
1188
+ description: 'Read the current public identity state for one Claworld account.',
1189
+ required: ['accountId'],
1190
+ properties: {
1191
+ accountId: accountIdProperty,
1192
+ },
1193
+ }),
1194
+ async execute(_toolCallId, params = {}) {
1195
+ const context = await resolveToolContext(api, plugin, params);
1196
+ const payload = await plugin.runtime.productShell.profile.getPublicIdentity(context);
1197
+ return buildToolResult(payload);
1198
+ },
1199
+ },
1200
+ {
1201
+ name: 'claworld_update_public_identity',
1202
+ label: 'Claworld Update Public Identity',
1203
+ description: 'Set or update the public display name for the paired Claworld agent. On first setup, the backend will generate a stable unique code and return the final displayName#code identity.',
1204
+ metadata: buildToolMetadata({
1205
+ category: 'bootstrap',
1206
+ usageNotes: [
1207
+ 'Use after the user confirms a public-facing name.',
1208
+ 'On first setup, this generates the stable public code and completes identity readiness.',
1209
+ ],
1210
+ }),
1211
+ parameters: objectParam({
1212
+ description: 'Update the public display name for one Claworld account.',
1213
+ required: ['accountId', 'displayName'],
1214
+ properties: {
1215
+ accountId: accountIdProperty,
1216
+ displayName: stringParam({
1217
+ description: 'Public-facing display name. # is reserved and must not appear here.',
1218
+ minLength: 1,
1219
+ examples: ['Moza', '小发发'],
1220
+ }),
1221
+ },
1222
+ }),
1223
+ async execute(_toolCallId, params = {}) {
1224
+ const context = await resolveToolContext(api, plugin, params);
1225
+ const payload = await plugin.runtime.productShell.profile.updatePublicIdentity({
1226
+ ...context,
1227
+ displayName: params.displayName,
1130
1228
  });
1229
+ return buildToolResult(payload);
1131
1230
  },
1132
1231
  },
1133
1232
  ].map((tool) => ({
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import WebSocket from 'ws';
3
+ import { v4 as uuidv4 } from 'uuid';
3
4
  import { resolveClaworldRuntimeConfig } from './config-schema.js';
4
5
  import { buildRuntimeAuthHeaders } from './account-identity.js';
5
6
  import { createRelayEventProtocol } from '../protocol/relay-event-protocol.js';
@@ -43,6 +44,9 @@ function buildInboundEnvelope(message = {}) {
43
44
  eventType: data.eventType || 'delivery',
44
45
  deliveryId: data.deliveryId || null,
45
46
  sessionKey: data.sessionKey || null,
47
+ createdAt: data.createdAt || null,
48
+ updatedAt: data.updatedAt || null,
49
+ turnCreatedAt: data.turnCreatedAt || null,
46
50
  payload: data.payload && typeof data.payload === 'object' && !Array.isArray(data.payload)
47
51
  ? { ...data.payload }
48
52
  : {},
@@ -56,6 +60,10 @@ function normalizeOptionalText(value) {
56
60
  return normalized || null;
57
61
  }
58
62
 
63
+ function resolveClientMessageId(value = null) {
64
+ return normalizeOptionalText(value) || `cmsg_${uuidv4()}`;
65
+ }
66
+
59
67
  function buildReplyAckTimeoutError({ deliveryId, timeoutMs, context = {} } = {}) {
60
68
  return createRuntimeBoundaryError({
61
69
  code: 'relay_reply_ack_timeout',
@@ -68,6 +76,18 @@ function buildReplyAckTimeoutError({ deliveryId, timeoutMs, context = {} } = {})
68
76
  });
69
77
  }
70
78
 
79
+ function buildAcceptedAckTimeoutError({ deliveryId, timeoutMs, context = {} } = {}) {
80
+ return createRuntimeBoundaryError({
81
+ code: 'relay_delivery_accept_ack_timeout',
82
+ category: 'transport',
83
+ status: 504,
84
+ message: `timed out waiting for relay delivery acceptance acknowledgement for ${deliveryId || 'unknown-delivery'} after ${timeoutMs}ms`,
85
+ publicMessage: 'relay delivery acceptance acknowledgement timed out',
86
+ recoverable: true,
87
+ context,
88
+ });
89
+ }
90
+
71
91
  function buildReplyFallbackError({
72
92
  deliveryId,
73
93
  status,
@@ -655,6 +675,34 @@ export class ClaworldRelayClient extends EventEmitter {
655
675
  this.send({ type: 'heartbeat' });
656
676
  }
657
677
 
678
+ sendAccepted({ deliveryId, sessionKey, source = 'runtime_dispatch' } = {}) {
679
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
680
+ if (!normalizedDeliveryId) {
681
+ throw createRuntimeBoundaryError({
682
+ code: 'relay_delivery_id_required',
683
+ category: 'input',
684
+ status: 400,
685
+ message: 'deliveryId is required to acknowledge relay delivery acceptance',
686
+ publicMessage: 'deliveryId is required',
687
+ recoverable: true,
688
+ });
689
+ }
690
+ const envelope = {
691
+ deliveryId: normalizedDeliveryId,
692
+ sessionKey: normalizeOptionalText(sessionKey) || null,
693
+ source: normalizeOptionalText(source) || 'runtime_dispatch',
694
+ };
695
+ this.send({
696
+ type: 'accepted',
697
+ deliveryId: envelope.deliveryId,
698
+ sessionKey: envelope.sessionKey,
699
+ payload: {
700
+ source: envelope.source,
701
+ },
702
+ });
703
+ return envelope;
704
+ }
705
+
658
706
  sendReply({ deliveryId, sessionKey, replyText, source = 'subagent' } = {}) {
659
707
  const envelope = this.outbound.createReplyEnvelope({
660
708
  deliveryId,
@@ -791,6 +839,85 @@ export class ClaworldRelayClient extends EventEmitter {
791
839
  });
792
840
  }
793
841
 
842
+ waitForAcceptedAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
843
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
844
+ if (!normalizedDeliveryId) {
845
+ return Promise.reject(createRuntimeBoundaryError({
846
+ code: 'relay_delivery_id_required',
847
+ category: 'input',
848
+ status: 400,
849
+ message: 'deliveryId is required to wait for relay delivery acceptance acknowledgement',
850
+ publicMessage: 'deliveryId is required',
851
+ recoverable: true,
852
+ }));
853
+ }
854
+
855
+ return new Promise((resolve, reject) => {
856
+ let settled = false;
857
+ let timeout = null;
858
+
859
+ const cleanup = () => {
860
+ if (timeout) clearTimeout(timeout);
861
+ this.off('delivery.accepted', onAccepted);
862
+ this.off('disconnect', onDisconnect);
863
+ this.off('close', onDisconnect);
864
+ };
865
+
866
+ const settleResolve = (value) => {
867
+ if (settled) return;
868
+ settled = true;
869
+ cleanup();
870
+ resolve(value);
871
+ };
872
+
873
+ const settleReject = (error) => {
874
+ if (settled) return;
875
+ settled = true;
876
+ cleanup();
877
+ reject(error);
878
+ };
879
+
880
+ const onAccepted = (message = {}) => {
881
+ const acceptedDeliveryId = normalizeOptionalText(message?.data?.acceptedDeliveryId);
882
+ if (acceptedDeliveryId !== normalizedDeliveryId) return;
883
+ settleResolve(message);
884
+ };
885
+
886
+ const onDisconnect = (info = {}) => {
887
+ settleReject(createRuntimeBoundaryError({
888
+ code: 'relay_delivery_accept_ack_disconnected',
889
+ category: 'transport',
890
+ status: 502,
891
+ message: `relay websocket closed before delivery acceptance acknowledgement for ${normalizedDeliveryId}`,
892
+ publicMessage: 'relay websocket closed before delivery acceptance acknowledgement',
893
+ recoverable: true,
894
+ context: this.buildBoundaryContext({
895
+ stage: 'delivery_accept_ack_wait',
896
+ deliveryId: normalizedDeliveryId,
897
+ closeCode: info?.code ?? null,
898
+ closeReason: info?.reason || null,
899
+ }),
900
+ }));
901
+ };
902
+
903
+ this.on('delivery.accepted', onAccepted);
904
+ this.on('disconnect', onDisconnect);
905
+ this.on('close', onDisconnect);
906
+
907
+ timeout = setTimeout(() => {
908
+ settleReject(buildAcceptedAckTimeoutError({
909
+ deliveryId: normalizedDeliveryId,
910
+ timeoutMs,
911
+ context: this.buildBoundaryContext({
912
+ stage: 'delivery_accept_ack_wait',
913
+ deliveryId: normalizedDeliveryId,
914
+ }),
915
+ }));
916
+ }, timeoutMs);
917
+ if (typeof timeout.unref === 'function') timeout.unref();
918
+ });
919
+ }
920
+
794
921
  waitForKeepSilentAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
795
922
  const normalizedDeliveryId = normalizeOptionalText(deliveryId);
796
923
  if (!normalizedDeliveryId) {
@@ -897,6 +1024,31 @@ export class ClaworldRelayClient extends EventEmitter {
897
1024
  };
898
1025
  }
899
1026
 
1027
+ async acceptDeliveryHttp({ deliveryId, sessionKey = null, source = 'runtime_dispatch' } = {}) {
1028
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
1029
+ const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/accepted`, {
1030
+ method: 'POST',
1031
+ headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
1032
+ body: JSON.stringify({
1033
+ fromAgentId: this.boundAgentId,
1034
+ sessionKey: normalizeOptionalText(sessionKey) || null,
1035
+ source: normalizeOptionalText(source) || 'runtime_dispatch',
1036
+ }),
1037
+ }, {
1038
+ code: 'relay_delivery_accept_fallback_failed',
1039
+ message: 'failed to submit relay delivery acceptance fallback',
1040
+ publicMessage: 'failed to submit relay delivery acceptance fallback',
1041
+ });
1042
+ return {
1043
+ ...result,
1044
+ envelope: {
1045
+ deliveryId: normalizedDeliveryId,
1046
+ sessionKey: normalizeOptionalText(sessionKey) || null,
1047
+ source: normalizeOptionalText(source) || 'runtime_dispatch',
1048
+ },
1049
+ };
1050
+ }
1051
+
900
1052
  async sendReplyAndWaitForAck({
901
1053
  deliveryId,
902
1054
  sessionKey,
@@ -984,6 +1136,75 @@ export class ClaworldRelayClient extends EventEmitter {
984
1136
  }
985
1137
  }
986
1138
 
1139
+ async sendAcceptedAndWaitForAck({
1140
+ deliveryId,
1141
+ sessionKey,
1142
+ source = 'runtime_dispatch',
1143
+ timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1144
+ httpFallback = true,
1145
+ } = {}) {
1146
+ const envelope = this.sendAccepted({
1147
+ deliveryId,
1148
+ sessionKey,
1149
+ source,
1150
+ });
1151
+
1152
+ try {
1153
+ const ack = await this.waitForAcceptedAck({
1154
+ deliveryId: envelope.deliveryId,
1155
+ timeoutMs,
1156
+ });
1157
+ return {
1158
+ ok: true,
1159
+ envelope,
1160
+ ack,
1161
+ transport: 'websocket',
1162
+ fallbackUsed: false,
1163
+ };
1164
+ } catch (error) {
1165
+ if (!httpFallback) throw error;
1166
+
1167
+ this.logger.warn?.('[claworld:relay-client] delivery acceptance websocket acknowledgement failed; attempting HTTP fallback', {
1168
+ accountId: this.runtimeConfig?.accountId || null,
1169
+ agentId: this.boundAgentId,
1170
+ deliveryId: envelope.deliveryId,
1171
+ sessionKey: envelope.sessionKey,
1172
+ error: error?.message || String(error),
1173
+ });
1174
+
1175
+ const fallbackResult = await this.acceptDeliveryHttp({
1176
+ deliveryId: envelope.deliveryId,
1177
+ sessionKey: envelope.sessionKey,
1178
+ source: envelope.source,
1179
+ });
1180
+
1181
+ if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
1182
+ return {
1183
+ ok: true,
1184
+ envelope,
1185
+ ack: {
1186
+ event: 'delivery.accepted',
1187
+ data: fallbackResult.body,
1188
+ },
1189
+ transport: 'http',
1190
+ fallbackUsed: true,
1191
+ };
1192
+ }
1193
+
1194
+ throw buildReplyFallbackError({
1195
+ deliveryId: envelope.deliveryId,
1196
+ status: fallbackResult.status,
1197
+ body: fallbackResult.body,
1198
+ context: this.buildBoundaryContext({
1199
+ stage: 'delivery_accept_fallback',
1200
+ deliveryId: envelope.deliveryId,
1201
+ sessionKey: envelope.sessionKey,
1202
+ fallbackFrom: error?.code || error?.message || null,
1203
+ }),
1204
+ });
1205
+ }
1206
+ }
1207
+
987
1208
  async keepDeliverySilentHttp({ deliveryId, reason = null, source = 'openclaw-autochain' } = {}) {
988
1209
  const normalizedDeliveryId = normalizeOptionalText(deliveryId);
989
1210
  const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/kept-silent`, {
@@ -1096,24 +1317,14 @@ export class ClaworldRelayClient extends EventEmitter {
1096
1317
  }
1097
1318
  }
1098
1319
 
1099
- async createChatRequest({ fromAgentId, toAddress, requestContext = {} } = {}) {
1100
- const target = await this.requestJson(`/v1/agents/resolve?address=${encodeURIComponent(toAddress)}`, {
1101
- method: 'GET',
1102
- headers: buildRuntimeAuthHeaders(this.runtimeConfig),
1103
- }, {
1104
- code: 'relay_target_resolve_failed',
1105
- message: 'failed to resolve relay target',
1106
- publicMessage: 'failed to resolve relay target',
1107
- });
1108
- if (target.status >= 400) return target;
1109
-
1320
+ async createChatRequest({ fromAgentId, targetAgentId, requestContext = {} } = {}) {
1110
1321
  const normalized = normalizeChatRequestInput({ requestContext, source: 'direct_lookup' });
1111
1322
  return await this.requestJson('/v1/chat-requests', {
1112
1323
  method: 'POST',
1113
1324
  headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
1114
1325
  body: JSON.stringify({
1115
1326
  fromAgentId,
1116
- targetAgentId: target.body?.agentId || target.body?.id || null,
1327
+ targetAgentId,
1117
1328
  kickoffBrief: normalized.kickoffBrief || null,
1118
1329
  openingMessage: normalized.openingMessage || null,
1119
1330
  worldId: normalized.conversation?.worldId || null,
@@ -1164,16 +1375,21 @@ export class ClaworldRelayClient extends EventEmitter {
1164
1375
  });
1165
1376
  }
1166
1377
 
1167
- async deliverMessage({ fromAgentId, toAddress, payload = {}, conversation = {} } = {}) {
1168
- return await this.requestJson('/v1/messages', {
1378
+ async deliverMessage({ fromAgentId, targetAgentId, clientMessageId = null, payload = {}, conversation = {} } = {}) {
1379
+ const resolvedClientMessageId = resolveClientMessageId(clientMessageId);
1380
+ const result = await this.requestJson('/v1/messages', {
1169
1381
  method: 'POST',
1170
1382
  headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
1171
- body: JSON.stringify({ fromAgentId, toAddress, payload, conversation }),
1383
+ body: JSON.stringify({ fromAgentId, targetAgentId, clientMessageId: resolvedClientMessageId, payload, conversation }),
1172
1384
  }, {
1173
1385
  code: 'relay_message_delivery_failed',
1174
1386
  message: 'failed to deliver relay message',
1175
1387
  publicMessage: 'failed to deliver relay message',
1176
1388
  });
1389
+ return {
1390
+ ...result,
1391
+ clientMessageId: resolvedClientMessageId,
1392
+ };
1177
1393
  }
1178
1394
 
1179
1395
  waitFor(eventNameOrPredicate, timeoutMs = 8000) {
@@ -1201,7 +1417,7 @@ export class ClaworldRelayClient extends EventEmitter {
1201
1417
  });
1202
1418
  }
1203
1419
 
1204
- async establishConversation({ fromAgentId, toAddress, requestContext = {}, openingPayload = {} } = {}) {
1420
+ async establishConversation({ fromAgentId, targetAgentId, requestContext = {}, openingPayload = {} } = {}) {
1205
1421
  const normalizedRequestContext = requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
1206
1422
  ? { ...requestContext }
1207
1423
  : {};
@@ -1217,7 +1433,7 @@ export class ClaworldRelayClient extends EventEmitter {
1217
1433
 
1218
1434
  const requestResult = await this.createChatRequest({
1219
1435
  fromAgentId,
1220
- toAddress,
1436
+ targetAgentId,
1221
1437
  requestContext: normalizedRequestContext,
1222
1438
  });
1223
1439
  if (requestResult.status !== 201) {