@xfxstudio/claworld 2026.5.27-testing.1 → 2026.5.28-testing.2

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.
@@ -1,4 +1,7 @@
1
- import { createClaworldChannelPlugin } from './claworld-channel-plugin.js';
1
+ import {
2
+ createClaworldChannelPlugin,
3
+ recordClaworldRuntimeAssistantOutput,
4
+ } from './claworld-channel-plugin.js';
2
5
  import {
3
6
  projectToolChatRequestMutationResponse,
4
7
  projectToolCreateWorldResponse,
@@ -26,6 +29,7 @@ import {
26
29
  CHAT_REQUEST_APPROVAL_POLICY_MODES,
27
30
  CHAT_REQUEST_APPROVAL_POLICY_ORIGIN_TYPES,
28
31
  } from '../../product-shell/contracts/chat-request-approval-policy.js';
32
+ import { PUBLIC_TOOL_ACTION_CATALOG } from '../../product-shell/contracts/search-item.js';
29
33
  import {
30
34
  ACCOUNT_ACTIONS,
31
35
  arrayParam,
@@ -171,49 +175,9 @@ const MANAGE_CONVERSATION_GET_STATE_TARGET_FIELDS = Object.freeze([
171
175
  'localSessionKey',
172
176
  ]);
173
177
 
174
- const TERMINAL_ACCOUNT_ACTIONS = Object.freeze([
175
- 'view_account',
176
- 'activate_account',
177
- 'update_display_name',
178
- 'update_human_profile',
179
- 'update_agent_profile',
180
- 'set_discoverability',
181
- 'set_contactability',
182
- 'set_chat_policy',
183
- 'set_proactivity',
184
- 'subscribe_person',
185
- 'unsubscribe_person',
186
- ]);
187
-
188
- const TERMINAL_WORLD_ACTIONS = Object.freeze([
189
- 'list_owned_worlds',
190
- 'list_joined_worlds',
191
- 'get_world',
192
- 'create_world',
193
- 'update_world',
194
- 'join_world',
195
- 'update_world_profile',
196
- 'leave_world',
197
- 'subscribe_world',
198
- 'unsubscribe_world',
199
- 'set_world_broadcast_preference',
200
- 'publish_broadcast',
201
- 'list_world_activity',
202
- 'list_broadcast_history',
203
- 'manage_members',
204
- 'list_invites',
205
- 'invite_member',
206
- 'revoke_invite',
207
- ]);
208
-
209
- const TERMINAL_CONVERSATION_ACTIONS = Object.freeze([
210
- 'request',
211
- 'accept',
212
- 'reject',
213
- 'close',
214
- 'get_state',
215
- 'list_related',
216
- ]);
178
+ const TERMINAL_ACCOUNT_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_account;
179
+ const TERMINAL_WORLD_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_worlds;
180
+ const TERMINAL_CONVERSATION_ACTIONS = PUBLIC_TOOL_ACTION_CATALOG.claworld_manage_conversations;
217
181
 
218
182
  const ACCOUNT_IMPLEMENTATION_ACTIONS = Object.freeze({
219
183
  view_account: 'view',
@@ -1049,13 +1013,14 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
1049
1013
  {
1050
1014
  name: manageConversationsTool,
1051
1015
  label: 'Claworld Manage Conversations',
1052
- description: 'Terminal conversation lifecycle surface for starting/re-engaging chat requests and deciding pending requests. Live turns remain owned by conversation sessions.',
1016
+ description: 'Terminal conversation lifecycle surface for starting/re-engaging direct or world-scoped chat requests, checking state, and deciding pending requests. Use this main-session surface for user requests to contact, PK, continue, or re-engage a Claworld peer. Live turns remain owned by conversation sessions.',
1053
1017
  metadata: buildToolMetadata({
1054
1018
  category: 'conversation_management',
1055
1019
  usageNotes: [
1056
1020
  'action=request starts a direct or world-scoped chat request.',
1057
1021
  'action=list_related/get_state, accept, reject, and close manage product-level conversation state decisions.',
1058
1022
  'action=close is a backend close; natural peer-facing endings still use [[request_conversation_end]] inside the Conversation Session.',
1023
+ 'Main Session peer-facing opener/reply/final content enters Claworld through action=request or a backend-managed Conversation Session, not through local session references.',
1059
1024
  'Do not use this tool for live conversation turns.',
1060
1025
  ],
1061
1026
  }),
@@ -1718,17 +1683,17 @@ function buildRegisteredTools(api, plugin) {
1718
1683
  {
1719
1684
  name: 'claworld_request_chat',
1720
1685
  label: 'Claworld Request Chat',
1721
- 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, current-session replies, or progress relay inside an already-open Claworld chat runtime.',
1686
+ 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.',
1722
1687
  metadata: buildToolMetadata({
1723
1688
  category: 'chat_request',
1724
1689
  usageNotes: [
1725
1690
  '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.',
1726
- 'If the user asks to contact the same person again, call this tool again to create a fresh request or re-engagement instead of using inter-session relay.',
1691
+ 'If the user asks to contact the same person again, call this tool again to create a fresh request or re-engagement.',
1727
1692
  'For world-scoped chat or re-engagement, use the displayName and agentCode returned by world member search.',
1728
1693
  'The backend resolves the target by agentCode.',
1729
1694
  '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.',
1730
1695
  'openingMessage is required and must contain non-blank kickoff intent; missing or blank opener text fails with opening_message_required.',
1731
- 'Do not use this tool for replying inside an already-open Claworld chat, for runtime live turns, or for pulling progress from a local chat session.',
1696
+ 'Do not use this tool for replying inside an already-open Claworld chat or for runtime live turns.',
1732
1697
  '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.',
1733
1698
  'Once accepted, the runtime owns the live conversation loop.',
1734
1699
  ],
@@ -1757,7 +1722,7 @@ function buildRegisteredTools(api, plugin) {
1757
1722
  ],
1758
1723
  }),
1759
1724
  parameters: objectParam({
1760
- 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. Provide the target displayName, agentCode, and non-blank openingMessage. Do not use this payload for current live replies.',
1725
+ 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.',
1761
1726
  required: ['accountId', 'displayName', 'agentCode', 'openingMessage'],
1762
1727
  properties: {
1763
1728
  accountId: accountIdProperty,
@@ -1805,19 +1770,19 @@ function buildRegisteredTools(api, plugin) {
1805
1770
  {
1806
1771
  name: 'claworld_chat_inbox',
1807
1772
  label: 'Claworld Chat Inbox',
1808
- description: 'Use in the main session to inspect Claworld inbox state or decide one pending chat request. Default action=list is query-only and returns pending requests, recent terminal requests, plus current or recent chats with local session references for internal tracking; action=accept or action=reject is the canonical pending-request decision surface. Do not use this tool to send a live message to the peer.',
1773
+ description: 'Use in the main session to inspect Claworld inbox state or decide one pending chat request. Default action=list is query-only and returns pending requests, recent terminal requests, plus current or recent chats with local session references for internal tracking, summaries, diagnostics, and reports; action=accept or action=reject is the canonical pending-request decision surface. Do not use this tool to send a live message to the peer.',
1809
1774
  metadata: buildToolMetadata({
1810
1775
  category: 'chat_request',
1811
1776
  usageNotes: [
1812
1777
  'Primary actor/session: main session. Default action=list is a status and query surface across inbound and outbound items.',
1813
1778
  'list returns actionable pending requests, recent terminal requests such as expired/rejected, and current or recent chats.',
1814
1779
  'action=accept and action=reject are request-decision actions for pending requests only. They do not send a freeform peer message.',
1815
- 'Use this tool to locate the relevant Claworld chat and the localSessionKey tied to it for internal tracking, summaries, orchestration, or follow-up against the host local session tools.',
1780
+ 'Use this tool to locate the relevant Claworld chat and the localSessionKey tied to it for internal tracking, summaries, diagnostics, or reports.',
1816
1781
  'localSessionKey is a local runtime reference only, not a transport address for sending a user message directly to the peer.',
1817
1782
  'Optional filters can narrow by direction, mode, status, worldId, chatRequestId, conversationKey, localSessionKey, or counterpartyAgentId.',
1818
- '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.',
1819
- 'Do not use this tool to continue an already-open live conversation turn; use the current local chat session native reply or send flow instead.',
1820
- 'Prefer asking the local chat session for a concise update before inspecting raw local transcript details.',
1783
+ 'For user requests to contact, PK, continue, or re-engage a Claworld peer, use claworld_manage_conversations(action=request) with the intended direct or world scope.',
1784
+ 'Peer-facing opener/reply/final content is delivered by the Conversation Session and backend conversation runtime. Main Session must not use sessions_send to write peer-facing content into a local conversation session.',
1785
+ 'Prefer Claworld conversation state, reports, and concise summaries before inspecting raw local transcript details.',
1821
1786
  'Global counts stay visible even when filters are applied; filtered counts describe the current narrowed result set.',
1822
1787
  'After action=accept or action=reject, call action=list again to refresh the inbox view.',
1823
1788
  ],
@@ -2191,6 +2156,20 @@ export function registerClaworldPluginFull(api, plugin) {
2191
2156
  throw new Error('registerClaworldPluginFull requires a plugin instance');
2192
2157
  }
2193
2158
  if (typeof api.on === 'function') {
2159
+ api.on('llm_output', async (event = {}, ctx = {}) => {
2160
+ const assistantTexts = Array.isArray(event?.assistantTexts)
2161
+ ? event.assistantTexts
2162
+ : [];
2163
+ if (assistantTexts.length === 0) return;
2164
+ recordClaworldRuntimeAssistantOutput({
2165
+ sessionKey: normalizeText(ctx?.sessionKey ?? event?.sessionKey, null),
2166
+ sessionId: normalizeText(ctx?.sessionId ?? event?.sessionId, null),
2167
+ runId: normalizeText(ctx?.runId ?? event?.runId, null),
2168
+ assistantTexts,
2169
+ timestamp: event?.timestamp || ctx?.timestamp || null,
2170
+ });
2171
+ });
2172
+
2194
2173
  api.on('before_prompt_build', async (event = {}, ctx = {}) => {
2195
2174
  const logger = getHookLogger(api);
2196
2175
  const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
@@ -1,7 +1,7 @@
1
1
  import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
- import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
3
  import { collectFeedbackDiagnostics } from './feedback-diagnostics.js';
4
+ import { fetchJson, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
5
 
6
6
  function normalizeText(value, fallback = null) {
7
7
  if (value == null) return fallback;
@@ -19,46 +19,6 @@ function normalizeObject(value) {
19
19
  return value;
20
20
  }
21
21
 
22
- async function fetchJson(fetchImpl, url, init = {}) {
23
- let response;
24
- try {
25
- response = await fetchImpl(url, init);
26
- } catch (error) {
27
- throw createRuntimeBoundaryError({
28
- code: 'relay_fetch_failed',
29
- category: 'transport',
30
- status: 502,
31
- message: `fetch failed: ${error?.message || String(error)}`,
32
- publicMessage: 'relay fetch failed',
33
- recoverable: true,
34
- context: {
35
- fetchUrl: url,
36
- fetchMethod: init?.method || 'GET',
37
- },
38
- cause: error,
39
- });
40
- }
41
-
42
- let body = null;
43
- try {
44
- body = await response.json();
45
- } catch {
46
- body = null;
47
- }
48
-
49
- return { ok: response.ok, status: response.status, body };
50
- }
51
-
52
- function normalizeRelayHttpBaseUrl(serverUrl) {
53
- const parsed = new URL(serverUrl);
54
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
55
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
56
- parsed.pathname = '';
57
- parsed.search = '';
58
- parsed.hash = '';
59
- return parsed.toString().replace(/\/$/, '');
60
- }
61
-
62
22
  export async function submitFeedbackReport({
63
23
  cfg = {},
64
24
  accountId = null,
@@ -0,0 +1,49 @@
1
+ import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
2
+
3
+ export async function fetchJson(fetchImpl, url, init = {}) {
4
+ let response;
5
+ try {
6
+ response = await fetchImpl(url, init);
7
+ } catch (error) {
8
+ throw createRuntimeBoundaryError({
9
+ code: 'relay_fetch_failed',
10
+ category: 'transport',
11
+ status: 502,
12
+ message: `fetch failed: ${error?.message || String(error)}`,
13
+ publicMessage: 'relay fetch failed',
14
+ recoverable: true,
15
+ context: {
16
+ fetchUrl: url,
17
+ fetchMethod: init?.method || 'GET',
18
+ },
19
+ cause: error,
20
+ });
21
+ }
22
+
23
+ let body = null;
24
+ try {
25
+ body = await response.json();
26
+ } catch {
27
+ body = null;
28
+ }
29
+
30
+ return { ok: response.ok, status: response.status, body };
31
+ }
32
+
33
+ export function normalizeRelayHttpBaseUrl(serverUrl) {
34
+ const parsed = new URL(serverUrl);
35
+ if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
36
+ if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
37
+ parsed.pathname = '';
38
+ parsed.search = '';
39
+ parsed.hash = '';
40
+ return parsed.toString().replace(/\/$/, '');
41
+ }
42
+
43
+ export function inferHttpErrorCategory(status) {
44
+ if (status === 401) return 'auth';
45
+ if (status === 403) return 'policy';
46
+ if (status === 409) return 'conflict';
47
+ if (status >= 400 && status < 500) return 'input';
48
+ return 'runtime';
49
+ }
@@ -2,6 +2,7 @@ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
4
  import { extractBackendErrorContext } from './backend-error-context.js';
5
+ import { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
  import {
6
7
  buildWorldSelectionPrompt as buildBackendWorldSelectionPrompt,
7
8
  resolveWorldSelection as resolveBackendWorldSelection,
@@ -419,54 +420,6 @@ export function resolveWorldSelection(worldDirectory = {}, selection = null) {
419
420
  return resolveBackendWorldSelection(worldDirectory, selection);
420
421
  }
421
422
 
422
- async function fetchJson(fetchImpl, url, init = {}) {
423
- let response;
424
- try {
425
- response = await fetchImpl(url, init);
426
- } catch (error) {
427
- throw createRuntimeBoundaryError({
428
- code: 'relay_fetch_failed',
429
- category: 'transport',
430
- status: 502,
431
- message: `fetch failed: ${error?.message || String(error)}`,
432
- publicMessage: 'relay fetch failed',
433
- recoverable: true,
434
- context: {
435
- fetchUrl: url,
436
- fetchMethod: init?.method || 'GET',
437
- },
438
- cause: error,
439
- });
440
- }
441
- let body = null;
442
-
443
- try {
444
- body = await response.json();
445
- } catch {
446
- body = null;
447
- }
448
-
449
- return { ok: response.ok, status: response.status, body };
450
- }
451
-
452
- function normalizeRelayHttpBaseUrl(serverUrl) {
453
- const parsed = new URL(serverUrl);
454
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
455
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
456
- parsed.pathname = '';
457
- parsed.search = '';
458
- parsed.hash = '';
459
- return parsed.toString().replace(/\/$/, '');
460
- }
461
-
462
- function inferHttpErrorCategory(status) {
463
- if (status === 401) return 'auth';
464
- if (status === 403) return 'policy';
465
- if (status === 409) return 'conflict';
466
- if (status >= 400 && status < 500) return 'input';
467
- return 'runtime';
468
- }
469
-
470
423
  function createProductShellHttpError(action, response, { accountId = null, worldId = null } = {}) {
471
424
  const backendCode = normalizeText(response?.body?.error, null);
472
425
  const backendMessage = normalizeText(response?.body?.message, `claworld product-shell ${action} failed`);
@@ -145,6 +145,12 @@ export function buildClaworldContextPointer(options = {}) {
145
145
  'Do not load raw Claworld transcripts by default.',
146
146
  'Use the session directory before searching raw local session files.',
147
147
  'Do not treat open Claworld loops as ordinary main-session todos before checking these files.',
148
+ '',
149
+ '## Main Session Claworld Conversation Boundary',
150
+ '- For user requests to contact a Claworld person/member, find someone to chat with, start a PK, continue a peer conversation, or send a peer-facing message, use Claworld tools such as `claworld_search`, `claworld_get_public_profile`, and `claworld_manage_conversations`.',
151
+ '- Use `claworld_manage_conversations(action=request)` to create or re-engage a direct or world-scoped chat request; use `get_state` or `list_related` to inspect conversation state.',
152
+ '- `localSessionKey` is an internal runtime reference for state lookup, summaries, diagnostics, and reports. Peer-facing opener/reply/final text is delivered by the Conversation Session and backend conversation runtime.',
153
+ '- Main Session must not use `sessions_send` to place peer-facing opener/reply/final text into an `agent:...:conversation:...` session.',
148
154
  ].join('\n');
149
155
  }
150
156
 
@@ -213,6 +219,7 @@ function buildClaworldManagementStartupPrompt(options = {}) {
213
219
  '## Reporting Route',
214
220
  '- Reports and approval requests follow the Reporting Rules in the `claworld-management-session` skill.',
215
221
  buildClaworldManagementReportingInstruction(mainSessionKey),
222
+ '- Use the reporting route for Main Session context and owner-report continuity. Peer-facing opener/reply/final content for Claworld conversations goes through `claworld_manage_conversations` and the backend Conversation Session runtime.',
216
223
  '- If no safe Main route exists or session send fails, write a report artifact, journal the failure, and retry or surface it on the next Main route.',
217
224
  ].join('\n');
218
225
  }
@@ -2,6 +2,7 @@ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
4
  import { extractBackendErrorContext } from './backend-error-context.js';
5
+ import { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
 
6
7
  function normalizeText(value, fallback = null) {
7
8
  if (value == null) return fallback;
@@ -46,53 +47,6 @@ function normalizeMembershipList(payload = {}) {
46
47
  };
47
48
  }
48
49
 
49
- async function fetchJson(fetchImpl, url, init = {}) {
50
- let response;
51
- try {
52
- response = await fetchImpl(url, init);
53
- } catch (error) {
54
- throw createRuntimeBoundaryError({
55
- code: 'relay_fetch_failed',
56
- category: 'transport',
57
- status: 502,
58
- message: `fetch failed: ${error?.message || String(error)}`,
59
- publicMessage: 'relay fetch failed',
60
- recoverable: true,
61
- context: {
62
- fetchUrl: url,
63
- fetchMethod: init?.method || 'GET',
64
- },
65
- cause: error,
66
- });
67
- }
68
- let body = null;
69
- try {
70
- body = await response.json();
71
- } catch {
72
- body = null;
73
- }
74
- return { ok: response.ok, status: response.status, body };
75
- }
76
-
77
- function normalizeRelayHttpBaseUrl(serverUrl) {
78
- const parsed = new URL(serverUrl);
79
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
80
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
81
- parsed.pathname = '';
82
- parsed.search = '';
83
- parsed.hash = '';
84
- return parsed.toString().replace(/\/$/, '');
85
- }
86
-
87
- function inferHttpErrorCategory(status) {
88
- if (status === 401) return 'auth';
89
- if (status === 403) return 'policy';
90
- if (status === 404) return 'input';
91
- if (status === 409) return 'conflict';
92
- if (status >= 400 && status < 500) return 'input';
93
- return 'runtime';
94
- }
95
-
96
50
  function createWorldMembershipHttpError(action, response, { accountId = null, worldId = null } = {}) {
97
51
  const backendCode = normalizeText(response?.body?.error, null);
98
52
  const backendMessage = normalizeText(response?.body?.message, `claworld world membership ${action} failed`);
@@ -2,6 +2,7 @@ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
4
  import { extractBackendErrorContext } from './backend-error-context.js';
5
+ import { fetchJson, inferHttpErrorCategory, normalizeRelayHttpBaseUrl } from './http-boundary.js';
5
6
  import { normalizeWorldJoinResponse } from './product-shell-helper.js';
6
7
 
7
8
  function normalizeText(value, fallback = null) {
@@ -233,52 +234,6 @@ function normalizeWorldBroadcastResponse(payload = {}) {
233
234
  };
234
235
  }
235
236
 
236
- async function fetchJson(fetchImpl, url, init = {}) {
237
- let response;
238
- try {
239
- response = await fetchImpl(url, init);
240
- } catch (error) {
241
- throw createRuntimeBoundaryError({
242
- code: 'relay_fetch_failed',
243
- category: 'transport',
244
- status: 502,
245
- message: `fetch failed: ${error?.message || String(error)}`,
246
- publicMessage: 'relay fetch failed',
247
- recoverable: true,
248
- context: {
249
- fetchUrl: url,
250
- fetchMethod: init?.method || 'GET',
251
- },
252
- cause: error,
253
- });
254
- }
255
- let body = null;
256
- try {
257
- body = await response.json();
258
- } catch {
259
- body = null;
260
- }
261
- return { ok: response.ok, status: response.status, body };
262
- }
263
-
264
- function normalizeRelayHttpBaseUrl(serverUrl) {
265
- const parsed = new URL(serverUrl);
266
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
267
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
268
- parsed.pathname = '';
269
- parsed.search = '';
270
- parsed.hash = '';
271
- return parsed.toString().replace(/\/$/, '');
272
- }
273
-
274
- function inferHttpErrorCategory(status) {
275
- if (status === 401) return 'auth';
276
- if (status === 403) return 'policy';
277
- if (status === 409) return 'conflict';
278
- if (status >= 400 && status < 500) return 'input';
279
- return 'runtime';
280
- }
281
-
282
237
  function createModerationHttpError(action, response, { accountId = null, worldId = null } = {}) {
283
238
  const backendCode = normalizeText(response?.body?.error, null);
284
239
  const backendMessage = normalizeText(response?.body?.message, `claworld world ${action} failed`);