@xfxstudio/claworld 0.2.6 → 0.2.7

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 (27) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/skills/claworld-help/SKILL.md +2 -2
  4. package/skills/claworld-join-and-chat/SKILL.md +18 -9
  5. package/src/lib/chat-request.js +19 -0
  6. package/src/lib/relay/kickoff-text.js +6 -1
  7. package/src/openclaw/installer/core.js +16 -2
  8. package/src/openclaw/plugin/claworld-channel-plugin.js +164 -12
  9. package/src/openclaw/plugin/config-schema.js +9 -4
  10. package/src/openclaw/plugin/register.js +38 -18
  11. package/src/openclaw/plugin/relay-client.js +502 -1
  12. package/src/openclaw/runtime/demo-session-bootstrap.js +1 -2
  13. package/src/openclaw/runtime/tool-contracts.js +39 -0
  14. package/src/openclaw/runtime/tool-inventory.js +1 -1
  15. package/src/openclaw/runtime/world-moderation-helper.js +8 -12
  16. package/src/product-shell/catalog/default-world-catalog.js +3 -3
  17. package/src/product-shell/contracts/world-manifest.js +0 -24
  18. package/src/product-shell/contracts/world-orchestration.js +0 -4
  19. package/src/product-shell/index.js +0 -5
  20. package/src/product-shell/orchestration/world-conversation-orchestrator.js +0 -2
  21. package/src/product-shell/orchestration/world-conversation-text.js +0 -2
  22. package/src/product-shell/social/chat-request-routes.js +4 -1
  23. package/src/product-shell/social/chat-request-service.js +163 -15
  24. package/src/product-shell/worlds/world-admin-service.js +0 -20
  25. package/src/product-shell/worlds/world-authorization.js +15 -1
  26. package/src/product-shell/worlds/world-text.js +0 -2
  27. package/src/product-shell/results/result-service.js +0 -21
@@ -1,6 +1,6 @@
1
1
  import { createClaworldChannelPlugin } from './claworld-channel-plugin.js';
2
2
  import {
3
- projectToolChatRequestListResponse,
3
+ projectToolChatInboxResponse,
4
4
  projectToolChatRequestMutationResponse,
5
5
  projectToolCreateWorldResponse,
6
6
  projectToolManagedWorldResponse,
@@ -19,6 +19,8 @@ import {
19
19
  normalizeRuntimeBoundaryError,
20
20
  } from '../../lib/runtime-errors.js';
21
21
 
22
+ const INTERNAL_REQUESTER_SESSION_KEY_PARAM = '__claworldRequesterSessionKey';
23
+
22
24
  function normalizeText(value, fallback = null) {
23
25
  if (value == null) return fallback;
24
26
  const normalized = String(value).trim();
@@ -166,6 +168,7 @@ async function resolveToolContext(api, plugin, params = {}) {
166
168
  accountId,
167
169
  runtimeConfig,
168
170
  agentId: normalizeText(params.agentId, runtimeConfig.relay?.agentId || null),
171
+ requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
169
172
  });
170
173
  }
171
174
 
@@ -175,6 +178,7 @@ async function resolveToolContext(api, plugin, params = {}) {
175
178
  accountId,
176
179
  runtimeConfig,
177
180
  agentId,
181
+ requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
178
182
  };
179
183
  }
180
184
 
@@ -567,7 +571,7 @@ function buildRegisteredTools(api, plugin) {
567
571
  input: {
568
572
  accountId: 'claworld',
569
573
  worldId: 'ugc-weekend-debate-club',
570
- worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
574
+ worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.',
571
575
  },
572
576
  outcome: 'Returns the updated managed-world projection when the current agent is the owner.',
573
577
  },
@@ -582,7 +586,7 @@ function buildRegisteredTools(api, plugin) {
582
586
  worldContextText: stringParam({
583
587
  description: 'Replacement canonical world context text for the owned world.',
584
588
  minLength: 1,
585
- examples: ['世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.'],
589
+ examples: ['世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.'],
586
590
  }),
587
591
  displayName: stringParam({
588
592
  description: 'Optional new display name for the owned world.',
@@ -598,7 +602,7 @@ function buildRegisteredTools(api, plugin) {
598
602
  {
599
603
  accountId: 'claworld',
600
604
  worldId: 'ugc-weekend-debate-club',
601
- worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
605
+ worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.',
602
606
  },
603
607
  ],
604
608
  }),
@@ -626,7 +630,7 @@ function buildRegisteredTools(api, plugin) {
626
630
  category: 'chat_request',
627
631
  usageNotes: [
628
632
  'For world-scoped chat, use the targetAgentId returned by claworld_join_world.',
629
- 'After creation, use claworld_list_chat_requests or wait for the peer to accept.',
633
+ 'After creation, use claworld_chat_inbox or wait for the peer to accept.',
630
634
  'Once accepted, the runtime owns the live conversation loop.',
631
635
  ],
632
636
  examples: [
@@ -680,33 +684,36 @@ function buildRegisteredTools(api, plugin) {
680
684
  },
681
685
  },
682
686
  {
683
- name: 'claworld_list_chat_requests',
684
- label: 'Claworld List Chat Requests',
685
- description: 'Canonical review tool for pending chat requests after request_chat or before accept_chat_request.',
687
+ name: 'claworld_chat_inbox',
688
+ label: 'Claworld Chat Inbox',
689
+ description: 'Canonical chat inbox tool. Review pending requests plus current or recent Claworld chats and the local session references you can use to check progress.',
686
690
  metadata: buildToolMetadata({
687
691
  category: 'chat_request',
688
692
  usageNotes: [
689
- 'Use to review inbound requests waiting for acceptance.',
690
- 'Use direction=outbound when checking whether a previously-created request is still pending.',
693
+ 'Use to review pending inbound requests waiting for acceptance.',
694
+ 'Use to locate the relevant Claworld chat and the local sessionKey tied to it.',
695
+ 'If the user asks about one chat, first locate it here, then use your local session-send tool to ask that local session for a progress update or short summary.',
696
+ 'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
697
+ 'Use direction=outbound when focusing on chats you initiated.',
691
698
  ],
692
699
  examples: [
693
700
  {
694
- title: 'Review inbound requests',
701
+ title: 'Review inbound chat state',
695
702
  input: {
696
703
  accountId: 'claworld',
697
704
  direction: 'inbound',
698
705
  },
699
- outcome: 'Returns the current pending or accepted chat requests for the current account.',
706
+ outcome: 'Returns pending requests plus related chats for the current account.',
700
707
  },
701
708
  ],
702
709
  }),
703
710
  parameters: objectParam({
704
- description: 'List direct or world-scoped chat requests for the current account.',
711
+ description: 'Review overall Claworld chat state for the current account before deciding whether to accept a request or query one local chat session for progress.',
705
712
  required: ['accountId'],
706
713
  properties: {
707
714
  accountId: accountIdProperty,
708
715
  direction: stringParam({
709
- description: 'Filter to inbound requests you can review or outbound requests you previously created.',
716
+ description: 'Filter to inbound or outbound chat state from the current account perspective.',
710
717
  enumValues: ['inbound', 'outbound'],
711
718
  examples: ['inbound'],
712
719
  }),
@@ -720,11 +727,11 @@ function buildRegisteredTools(api, plugin) {
720
727
  }),
721
728
  async execute(_toolCallId, params = {}) {
722
729
  const context = await resolveToolContext(api, plugin, params);
723
- const payload = await plugin.helpers.social.listChatRequests({
730
+ const payload = await plugin.helpers.social.listChatInbox({
724
731
  ...context,
725
732
  direction: params.direction || null,
726
733
  });
727
- return buildToolResult(projectToolChatRequestListResponse(payload, { accountId: context.accountId }));
734
+ return buildToolResult(projectToolChatInboxResponse(payload, { accountId: context.accountId }));
728
735
  },
729
736
  },
730
737
  {
@@ -734,7 +741,7 @@ function buildRegisteredTools(api, plugin) {
734
741
  metadata: buildToolMetadata({
735
742
  category: 'chat_request',
736
743
  usageNotes: [
737
- 'Use the chatRequestId returned by claworld_list_chat_requests.',
744
+ 'Use the chatRequestId returned by claworld_chat_inbox pendingRequests.',
738
745
  'After acceptance, do not try to send a separate raw message tool call; wait for runtime-owned live conversation.',
739
746
  ],
740
747
  examples: [
@@ -754,7 +761,7 @@ function buildRegisteredTools(api, plugin) {
754
761
  properties: {
755
762
  accountId: accountIdProperty,
756
763
  chatRequestId: stringParam({
757
- description: 'Canonical chat request id returned by claworld_list_chat_requests.',
764
+ description: 'Canonical chat request id returned by claworld_chat_inbox pendingRequests.',
758
765
  minLength: 1,
759
766
  examples: ['req_demo_1'],
760
767
  }),
@@ -1007,6 +1014,19 @@ export function registerClaworldPluginFull(api, plugin) {
1007
1014
  if (!plugin) {
1008
1015
  throw new Error('registerClaworldPluginFull requires a plugin instance');
1009
1016
  }
1017
+ if (typeof api.on === 'function') {
1018
+ api.on('before_tool_call', async (event, ctx) => {
1019
+ if (event?.toolName !== 'claworld_request_chat') return;
1020
+ const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
1021
+ if (!requesterSessionKey) return;
1022
+ return {
1023
+ params: {
1024
+ ...(event?.params && typeof event.params === 'object' && !Array.isArray(event.params) ? event.params : {}),
1025
+ [INTERNAL_REQUESTER_SESSION_KEY_PARAM]: requesterSessionKey,
1026
+ },
1027
+ };
1028
+ });
1029
+ }
1010
1030
  if (typeof api.registerHttpRoute === 'function') {
1011
1031
  api.registerHttpRoute(buildClaworldStatusRoute(plugin));
1012
1032
  }
@@ -15,6 +15,7 @@ import {
15
15
  const DUPLICATE_CONNECTION_CLOSE_CODE = 4001;
16
16
  const STALE_CONNECTION_CLOSE_CODE = 4002;
17
17
  const TERMINAL_CLOSE_REASONS = new Set(['duplicate_connection_replaced', 'stale_connection']);
18
+ const DEFAULT_REPLY_ACK_TIMEOUT_MS = 5000;
18
19
 
19
20
  function normalizeRelayWebSocketUrl(serverUrl) {
20
21
  const parsed = new URL(serverUrl);
@@ -49,6 +50,90 @@ function buildInboundEnvelope(message = {}) {
49
50
  };
50
51
  }
51
52
 
53
+ function normalizeOptionalText(value) {
54
+ if (value == null) return null;
55
+ const normalized = String(value).trim();
56
+ return normalized || null;
57
+ }
58
+
59
+ function buildReplyAckTimeoutError({ deliveryId, timeoutMs, context = {} } = {}) {
60
+ return createRuntimeBoundaryError({
61
+ code: 'relay_reply_ack_timeout',
62
+ category: 'transport',
63
+ status: 504,
64
+ message: `timed out waiting for relay reply acknowledgement for ${deliveryId || 'unknown-delivery'} after ${timeoutMs}ms`,
65
+ publicMessage: 'relay reply acknowledgement timed out',
66
+ recoverable: true,
67
+ context,
68
+ });
69
+ }
70
+
71
+ function buildReplyFallbackError({
72
+ deliveryId,
73
+ status,
74
+ body,
75
+ context = {},
76
+ } = {}) {
77
+ return createRuntimeBoundaryError({
78
+ code: normalizeOptionalText(body?.code) || normalizeOptionalText(body?.error) || 'relay_reply_fallback_failed',
79
+ category: status >= 500 ? 'runtime' : 'transport',
80
+ status: Number.isInteger(status) ? status : 502,
81
+ message: normalizeOptionalText(body?.message) || normalizeOptionalText(body?.reason) || 'relay reply fallback failed',
82
+ publicMessage: 'relay reply fallback failed',
83
+ recoverable: status >= 500,
84
+ context: {
85
+ deliveryId: normalizeOptionalText(deliveryId),
86
+ ...context,
87
+ },
88
+ });
89
+ }
90
+
91
+ function buildKeepSilentAckTimeoutError({ deliveryId, timeoutMs, context = {} } = {}) {
92
+ return createRuntimeBoundaryError({
93
+ code: 'relay_kept_silent_ack_timeout',
94
+ category: 'transport',
95
+ status: 504,
96
+ message: `timed out waiting for relay kept_silent acknowledgement for ${deliveryId || 'unknown-delivery'} after ${timeoutMs}ms`,
97
+ publicMessage: 'relay kept_silent acknowledgement timed out',
98
+ recoverable: true,
99
+ context,
100
+ });
101
+ }
102
+
103
+ function buildKeepSilentFallbackError({
104
+ deliveryId,
105
+ status,
106
+ body,
107
+ context = {},
108
+ } = {}) {
109
+ return createRuntimeBoundaryError({
110
+ code: normalizeOptionalText(body?.code) || normalizeOptionalText(body?.error) || 'relay_kept_silent_fallback_failed',
111
+ category: status >= 500 ? 'runtime' : 'transport',
112
+ status: Number.isInteger(status) ? status : 502,
113
+ message: normalizeOptionalText(body?.message) || normalizeOptionalText(body?.reason) || 'relay kept_silent fallback failed',
114
+ publicMessage: 'relay kept_silent fallback failed',
115
+ recoverable: status >= 500,
116
+ context: {
117
+ deliveryId: normalizeOptionalText(deliveryId),
118
+ ...context,
119
+ },
120
+ });
121
+ }
122
+
123
+ function isReplyAlreadyApplied(result = null, deliveryId = null) {
124
+ if (!result || result.status !== 409) return false;
125
+ if (normalizeOptionalText(result.body?.reason) !== 'delivery_not_replyable') return false;
126
+ if (normalizeOptionalText(result.body?.delivery?.deliveryId) !== normalizeOptionalText(deliveryId)) return false;
127
+ return normalizeOptionalText(result.body?.delivery?.status) === 'replied';
128
+ }
129
+
130
+ function isDeliveryKeptSilentAlreadyApplied(result = null, deliveryId = null) {
131
+ if (!result || result.status !== 409) return false;
132
+ if (normalizeOptionalText(result.body?.reason) !== 'delivery_not_replyable') return false;
133
+ if (normalizeOptionalText(result.body?.delivery?.deliveryId) !== normalizeOptionalText(deliveryId)) return false;
134
+ return normalizeOptionalText(result.body?.delivery?.status) === 'kept_silent';
135
+ }
136
+
52
137
  export class ClaworldRelayClient extends EventEmitter {
53
138
  constructor({
54
139
  logger = console,
@@ -479,7 +564,7 @@ export class ClaworldRelayClient extends EventEmitter {
479
564
  config,
480
565
  agentId,
481
566
  credential = null,
482
- clientVersion = 'claworld-plugin/0.1.5',
567
+ clientVersion = 'claworld-plugin/0.2.7',
483
568
  sessionTarget,
484
569
  fallbackTarget,
485
570
  } = {}) {
@@ -597,6 +682,419 @@ export class ClaworldRelayClient extends EventEmitter {
597
682
  });
598
683
  }
599
684
 
685
+ sendKeepSilent({ deliveryId, sessionKey, reason = null, source = 'openclaw-autochain' } = {}) {
686
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
687
+ if (!normalizedDeliveryId) {
688
+ throw createRuntimeBoundaryError({
689
+ code: 'relay_delivery_id_required',
690
+ category: 'input',
691
+ status: 400,
692
+ message: 'deliveryId is required to mark relay delivery kept_silent',
693
+ publicMessage: 'deliveryId is required',
694
+ recoverable: true,
695
+ });
696
+ }
697
+ const envelope = {
698
+ deliveryId: normalizedDeliveryId,
699
+ sessionKey: normalizeOptionalText(sessionKey) || null,
700
+ reason: normalizeOptionalText(reason) || 'no_renderable_reply',
701
+ source: normalizeOptionalText(source) || 'openclaw-autochain',
702
+ };
703
+ this.send({
704
+ type: 'kept_silent',
705
+ deliveryId: envelope.deliveryId,
706
+ sessionKey: envelope.sessionKey,
707
+ payload: {
708
+ reason: envelope.reason,
709
+ source: envelope.source,
710
+ },
711
+ });
712
+ return envelope;
713
+ }
714
+
715
+ waitForReplyAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
716
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
717
+ if (!normalizedDeliveryId) {
718
+ return Promise.reject(createRuntimeBoundaryError({
719
+ code: 'relay_delivery_id_required',
720
+ category: 'input',
721
+ status: 400,
722
+ message: 'deliveryId is required to wait for relay reply acknowledgement',
723
+ publicMessage: 'deliveryId is required',
724
+ recoverable: true,
725
+ }));
726
+ }
727
+
728
+ return new Promise((resolve, reject) => {
729
+ let settled = false;
730
+ let timeout = null;
731
+
732
+ const cleanup = () => {
733
+ if (timeout) clearTimeout(timeout);
734
+ this.off('reply.accepted', onReplyAccepted);
735
+ this.off('disconnect', onDisconnect);
736
+ this.off('close', onDisconnect);
737
+ };
738
+
739
+ const settleResolve = (value) => {
740
+ if (settled) return;
741
+ settled = true;
742
+ cleanup();
743
+ resolve(value);
744
+ };
745
+
746
+ const settleReject = (error) => {
747
+ if (settled) return;
748
+ settled = true;
749
+ cleanup();
750
+ reject(error);
751
+ };
752
+
753
+ const onReplyAccepted = (message = {}) => {
754
+ const repliedDeliveryId = normalizeOptionalText(message?.data?.repliedDeliveryId);
755
+ if (repliedDeliveryId !== normalizedDeliveryId) return;
756
+ settleResolve(message);
757
+ };
758
+
759
+ const onDisconnect = (info = {}) => {
760
+ settleReject(createRuntimeBoundaryError({
761
+ code: 'relay_reply_ack_disconnected',
762
+ category: 'transport',
763
+ status: 502,
764
+ message: `relay websocket closed before reply acknowledgement for ${normalizedDeliveryId}`,
765
+ publicMessage: 'relay websocket closed before reply acknowledgement',
766
+ recoverable: true,
767
+ context: this.buildBoundaryContext({
768
+ stage: 'reply_ack_wait',
769
+ deliveryId: normalizedDeliveryId,
770
+ closeCode: info?.code ?? null,
771
+ closeReason: info?.reason || null,
772
+ }),
773
+ }));
774
+ };
775
+
776
+ this.on('reply.accepted', onReplyAccepted);
777
+ this.on('disconnect', onDisconnect);
778
+ this.on('close', onDisconnect);
779
+
780
+ timeout = setTimeout(() => {
781
+ settleReject(buildReplyAckTimeoutError({
782
+ deliveryId: normalizedDeliveryId,
783
+ timeoutMs,
784
+ context: this.buildBoundaryContext({
785
+ stage: 'reply_ack_wait',
786
+ deliveryId: normalizedDeliveryId,
787
+ }),
788
+ }));
789
+ }, timeoutMs);
790
+ if (typeof timeout.unref === 'function') timeout.unref();
791
+ });
792
+ }
793
+
794
+ waitForKeepSilentAck({ deliveryId, timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS } = {}) {
795
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
796
+ if (!normalizedDeliveryId) {
797
+ return Promise.reject(createRuntimeBoundaryError({
798
+ code: 'relay_delivery_id_required',
799
+ category: 'input',
800
+ status: 400,
801
+ message: 'deliveryId is required to wait for relay kept_silent acknowledgement',
802
+ publicMessage: 'deliveryId is required',
803
+ recoverable: true,
804
+ }));
805
+ }
806
+
807
+ return new Promise((resolve, reject) => {
808
+ let settled = false;
809
+ let timeout = null;
810
+
811
+ const cleanup = () => {
812
+ if (timeout) clearTimeout(timeout);
813
+ this.off('kept_silent.accepted', onKeptSilentAccepted);
814
+ this.off('disconnect', onDisconnect);
815
+ this.off('close', onDisconnect);
816
+ };
817
+
818
+ const settleResolve = (value) => {
819
+ if (settled) return;
820
+ settled = true;
821
+ cleanup();
822
+ resolve(value);
823
+ };
824
+
825
+ const settleReject = (error) => {
826
+ if (settled) return;
827
+ settled = true;
828
+ cleanup();
829
+ reject(error);
830
+ };
831
+
832
+ const onKeptSilentAccepted = (message = {}) => {
833
+ const keptSilentDeliveryId = normalizeOptionalText(message?.data?.keptSilentDeliveryId);
834
+ if (keptSilentDeliveryId !== normalizedDeliveryId) return;
835
+ settleResolve(message);
836
+ };
837
+
838
+ const onDisconnect = (info = {}) => {
839
+ settleReject(createRuntimeBoundaryError({
840
+ code: 'relay_kept_silent_ack_disconnected',
841
+ category: 'transport',
842
+ status: 502,
843
+ message: `relay websocket closed before kept_silent acknowledgement for ${normalizedDeliveryId}`,
844
+ publicMessage: 'relay websocket closed before kept_silent acknowledgement',
845
+ recoverable: true,
846
+ context: this.buildBoundaryContext({
847
+ stage: 'kept_silent_ack_wait',
848
+ deliveryId: normalizedDeliveryId,
849
+ closeCode: info?.code ?? null,
850
+ closeReason: info?.reason || null,
851
+ }),
852
+ }));
853
+ };
854
+
855
+ this.on('kept_silent.accepted', onKeptSilentAccepted);
856
+ this.on('disconnect', onDisconnect);
857
+ this.on('close', onDisconnect);
858
+
859
+ timeout = setTimeout(() => {
860
+ settleReject(buildKeepSilentAckTimeoutError({
861
+ deliveryId: normalizedDeliveryId,
862
+ timeoutMs,
863
+ context: this.buildBoundaryContext({
864
+ stage: 'kept_silent_ack_wait',
865
+ deliveryId: normalizedDeliveryId,
866
+ }),
867
+ }));
868
+ }, timeoutMs);
869
+ if (typeof timeout.unref === 'function') timeout.unref();
870
+ });
871
+ }
872
+
873
+ async replyToDeliveryHttp({ deliveryId, replyText, source = 'subagent' } = {}) {
874
+ const envelope = this.outbound.createReplyEnvelope({
875
+ deliveryId,
876
+ sessionKey: null,
877
+ replyText,
878
+ source,
879
+ });
880
+ const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(envelope.deliveryId)}/reply`, {
881
+ method: 'POST',
882
+ headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
883
+ body: JSON.stringify({
884
+ fromAgentId: this.boundAgentId,
885
+ payload: {
886
+ ...envelope.payload,
887
+ },
888
+ }),
889
+ }, {
890
+ code: 'relay_reply_fallback_failed',
891
+ message: 'failed to submit relay reply fallback',
892
+ publicMessage: 'failed to submit relay reply fallback',
893
+ });
894
+ return {
895
+ ...result,
896
+ envelope,
897
+ };
898
+ }
899
+
900
+ async sendReplyAndWaitForAck({
901
+ deliveryId,
902
+ sessionKey,
903
+ replyText,
904
+ source = 'subagent',
905
+ timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
906
+ httpFallback = true,
907
+ } = {}) {
908
+ const envelope = this.sendReply({
909
+ deliveryId,
910
+ sessionKey,
911
+ replyText,
912
+ source,
913
+ });
914
+
915
+ try {
916
+ const ack = await this.waitForReplyAck({
917
+ deliveryId: envelope.deliveryId,
918
+ timeoutMs,
919
+ });
920
+ return {
921
+ ok: true,
922
+ envelope,
923
+ ack,
924
+ transport: 'websocket',
925
+ fallbackUsed: false,
926
+ };
927
+ } catch (error) {
928
+ if (!httpFallback) throw error;
929
+
930
+ this.logger.warn?.('[claworld:relay-client] reply websocket acknowledgement failed; attempting HTTP fallback', {
931
+ accountId: this.runtimeConfig?.accountId || null,
932
+ agentId: this.boundAgentId,
933
+ deliveryId: envelope.deliveryId,
934
+ sessionKey: envelope.sessionKey,
935
+ error: error?.message || String(error),
936
+ });
937
+
938
+ const fallbackResult = await this.replyToDeliveryHttp({
939
+ deliveryId: envelope.deliveryId,
940
+ replyText,
941
+ source,
942
+ });
943
+
944
+ if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
945
+ return {
946
+ ok: true,
947
+ envelope,
948
+ ack: {
949
+ event: 'reply.accepted',
950
+ data: fallbackResult.body,
951
+ },
952
+ transport: 'http',
953
+ fallbackUsed: true,
954
+ };
955
+ }
956
+
957
+ if (isReplyAlreadyApplied(fallbackResult, envelope.deliveryId)) {
958
+ return {
959
+ ok: true,
960
+ envelope,
961
+ ack: {
962
+ event: 'reply.accepted',
963
+ data: {
964
+ ...(fallbackResult.body && typeof fallbackResult.body === 'object' ? fallbackResult.body : {}),
965
+ repliedDeliveryId: envelope.deliveryId,
966
+ },
967
+ },
968
+ transport: 'http-already-applied',
969
+ fallbackUsed: true,
970
+ };
971
+ }
972
+
973
+ throw buildReplyFallbackError({
974
+ deliveryId: envelope.deliveryId,
975
+ status: fallbackResult.status,
976
+ body: fallbackResult.body,
977
+ context: this.buildBoundaryContext({
978
+ stage: 'reply_fallback',
979
+ deliveryId: envelope.deliveryId,
980
+ sessionKey: envelope.sessionKey,
981
+ }),
982
+ });
983
+ }
984
+ }
985
+
986
+ async keepDeliverySilentHttp({ deliveryId, reason = null, source = 'openclaw-autochain' } = {}) {
987
+ const normalizedDeliveryId = normalizeOptionalText(deliveryId);
988
+ const result = await this.requestJson(`/v1/deliveries/${encodeURIComponent(normalizedDeliveryId)}/kept-silent`, {
989
+ method: 'POST',
990
+ headers: buildRuntimeAuthHeaders(this.runtimeConfig, { 'content-type': 'application/json' }),
991
+ body: JSON.stringify({
992
+ fromAgentId: this.boundAgentId,
993
+ reason: normalizeOptionalText(reason) || 'no_renderable_reply',
994
+ source: normalizeOptionalText(source) || 'openclaw-autochain',
995
+ }),
996
+ }, {
997
+ code: 'relay_kept_silent_fallback_failed',
998
+ message: 'failed to submit relay kept_silent fallback',
999
+ publicMessage: 'failed to submit relay kept_silent fallback',
1000
+ });
1001
+ return {
1002
+ ...result,
1003
+ envelope: {
1004
+ deliveryId: normalizedDeliveryId,
1005
+ reason: normalizeOptionalText(reason) || 'no_renderable_reply',
1006
+ source: normalizeOptionalText(source) || 'openclaw-autochain',
1007
+ },
1008
+ };
1009
+ }
1010
+
1011
+ async sendKeepSilentAndWaitForAck({
1012
+ deliveryId,
1013
+ sessionKey,
1014
+ reason = null,
1015
+ source = 'openclaw-autochain',
1016
+ timeoutMs = DEFAULT_REPLY_ACK_TIMEOUT_MS,
1017
+ httpFallback = true,
1018
+ } = {}) {
1019
+ const envelope = this.sendKeepSilent({
1020
+ deliveryId,
1021
+ sessionKey,
1022
+ reason,
1023
+ source,
1024
+ });
1025
+
1026
+ try {
1027
+ const ack = await this.waitForKeepSilentAck({
1028
+ deliveryId: envelope.deliveryId,
1029
+ timeoutMs,
1030
+ });
1031
+ return {
1032
+ ok: true,
1033
+ envelope,
1034
+ ack,
1035
+ transport: 'websocket',
1036
+ fallbackUsed: false,
1037
+ };
1038
+ } catch (error) {
1039
+ if (!httpFallback) throw error;
1040
+
1041
+ this.logger.warn?.('[claworld:relay-client] kept_silent websocket acknowledgement failed; attempting HTTP fallback', {
1042
+ accountId: this.runtimeConfig?.accountId || null,
1043
+ agentId: this.boundAgentId,
1044
+ deliveryId: envelope.deliveryId,
1045
+ sessionKey: envelope.sessionKey,
1046
+ error: error?.message || String(error),
1047
+ });
1048
+
1049
+ const fallbackResult = await this.keepDeliverySilentHttp({
1050
+ deliveryId: envelope.deliveryId,
1051
+ reason: envelope.reason,
1052
+ source: envelope.source,
1053
+ });
1054
+
1055
+ if (fallbackResult.status >= 200 && fallbackResult.status < 300) {
1056
+ return {
1057
+ ok: true,
1058
+ envelope,
1059
+ ack: {
1060
+ event: 'kept_silent.accepted',
1061
+ data: fallbackResult.body,
1062
+ },
1063
+ transport: 'http',
1064
+ fallbackUsed: true,
1065
+ };
1066
+ }
1067
+
1068
+ if (isDeliveryKeptSilentAlreadyApplied(fallbackResult, envelope.deliveryId)) {
1069
+ return {
1070
+ ok: true,
1071
+ envelope,
1072
+ ack: {
1073
+ event: 'kept_silent.accepted',
1074
+ data: {
1075
+ ...(fallbackResult.body && typeof fallbackResult.body === 'object' ? fallbackResult.body : {}),
1076
+ keptSilentDeliveryId: envelope.deliveryId,
1077
+ },
1078
+ },
1079
+ transport: 'http-already-applied',
1080
+ fallbackUsed: true,
1081
+ };
1082
+ }
1083
+
1084
+ throw buildKeepSilentFallbackError({
1085
+ deliveryId: envelope.deliveryId,
1086
+ status: fallbackResult.status,
1087
+ body: fallbackResult.body,
1088
+ context: this.buildBoundaryContext({
1089
+ stage: 'kept_silent_fallback',
1090
+ deliveryId: envelope.deliveryId,
1091
+ sessionKey: envelope.sessionKey,
1092
+ fallbackFrom: error?.code || error?.message || null,
1093
+ }),
1094
+ });
1095
+ }
1096
+ }
1097
+
600
1098
  async createChatRequest({ fromAgentId, toAddress, requestContext = {} } = {}) {
601
1099
  const target = await this.requestJson(`/v1/agents/resolve?address=${encodeURIComponent(toAddress)}`, {
602
1100
  method: 'GET',
@@ -618,6 +1116,9 @@ export class ClaworldRelayClient extends EventEmitter {
618
1116
  kickoffBrief: normalized.kickoffBrief || null,
619
1117
  openingMessage: normalized.openingMessage || null,
620
1118
  worldId: normalized.conversation?.worldId || null,
1119
+ requestContext: requestContext && typeof requestContext === 'object' && !Array.isArray(requestContext)
1120
+ ? requestContext
1121
+ : undefined,
621
1122
  }),
622
1123
  }, {
623
1124
  code: 'relay_request_create_failed',