@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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/claworld-help/SKILL.md +2 -2
- package/skills/claworld-join-and-chat/SKILL.md +18 -9
- package/src/lib/chat-request.js +19 -0
- package/src/lib/relay/kickoff-text.js +6 -1
- package/src/openclaw/installer/core.js +16 -2
- package/src/openclaw/plugin/claworld-channel-plugin.js +164 -12
- package/src/openclaw/plugin/config-schema.js +9 -4
- package/src/openclaw/plugin/register.js +38 -18
- package/src/openclaw/plugin/relay-client.js +502 -1
- package/src/openclaw/runtime/demo-session-bootstrap.js +1 -2
- package/src/openclaw/runtime/tool-contracts.js +39 -0
- package/src/openclaw/runtime/tool-inventory.js +1 -1
- package/src/openclaw/runtime/world-moderation-helper.js +8 -12
- package/src/product-shell/catalog/default-world-catalog.js +3 -3
- package/src/product-shell/contracts/world-manifest.js +0 -24
- package/src/product-shell/contracts/world-orchestration.js +0 -4
- package/src/product-shell/index.js +0 -5
- package/src/product-shell/orchestration/world-conversation-orchestrator.js +0 -2
- package/src/product-shell/orchestration/world-conversation-text.js +0 -2
- package/src/product-shell/social/chat-request-routes.js +4 -1
- package/src/product-shell/social/chat-request-service.js +163 -15
- package/src/product-shell/worlds/world-admin-service.js +0 -20
- package/src/product-shell/worlds/world-authorization.js +15 -1
- package/src/product-shell/worlds/world-text.js +0 -2
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
684
|
-
label: 'Claworld
|
|
685
|
-
description: 'Canonical
|
|
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
|
|
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
|
|
701
|
+
title: 'Review inbound chat state',
|
|
695
702
|
input: {
|
|
696
703
|
accountId: 'claworld',
|
|
697
704
|
direction: 'inbound',
|
|
698
705
|
},
|
|
699
|
-
outcome: 'Returns
|
|
706
|
+
outcome: 'Returns pending requests plus related chats for the current account.',
|
|
700
707
|
},
|
|
701
708
|
],
|
|
702
709
|
}),
|
|
703
710
|
parameters: objectParam({
|
|
704
|
-
description: '
|
|
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
|
|
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.
|
|
730
|
+
const payload = await plugin.helpers.social.listChatInbox({
|
|
724
731
|
...context,
|
|
725
732
|
direction: params.direction || null,
|
|
726
733
|
});
|
|
727
|
-
return buildToolResult(
|
|
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
|
|
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
|
|
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.
|
|
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',
|