@xfxstudio/claworld 0.2.24 → 2026.4.14-testing.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
+ import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
+ import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
+ import { extractBackendErrorContext } from './backend-error-context.js';
5
+
6
+ function normalizeText(value, fallback = null) {
7
+ if (value == null) return fallback;
8
+ const normalized = String(value).trim();
9
+ return normalized || fallback;
10
+ }
11
+
12
+ function normalizeOptionalBoolean(value, fallback = null) {
13
+ if (typeof value === 'boolean') return value;
14
+ return fallback;
15
+ }
16
+
17
+ function normalizeWorldRole(worldRole, fallback = null) {
18
+ const normalized = normalizeText(worldRole, fallback);
19
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
20
+ }
21
+
22
+ function normalizeManagedWorldMembership(payload = {}) {
23
+ return {
24
+ membershipId: normalizeText(payload.membershipId, null),
25
+ worldId: normalizeText(payload.worldId, null),
26
+ displayName: normalizeText(payload.displayName, null),
27
+ worldContextText: normalizeText(payload.worldContextText, null),
28
+ ownerAgentId: normalizeText(payload.ownerAgentId, null),
29
+ enabled: normalizeOptionalBoolean(payload.enabled, null),
30
+ worldStatus: normalizeText(payload.worldStatus, null),
31
+ worldRole: normalizeWorldRole(payload.worldRole, null),
32
+ membershipStatus: normalizeText(payload.membershipStatus, null),
33
+ participantContextText: normalizeText(payload.participantContextText, null),
34
+ joinedAt: normalizeText(payload.joinedAt, null),
35
+ updatedAt: normalizeText(payload.updatedAt, null),
36
+ nextAction: normalizeText(payload.nextAction, null),
37
+ };
38
+ }
39
+
40
+ function normalizeMembershipList(payload = {}) {
41
+ return {
42
+ items: Array.isArray(payload.items)
43
+ ? payload.items.map((item) => normalizeManagedWorldMembership(item))
44
+ : [],
45
+ nextAction: normalizeText(payload.nextAction, null),
46
+ };
47
+ }
48
+
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
+ function createWorldMembershipHttpError(action, response, { accountId = null, worldId = null } = {}) {
97
+ const backendCode = normalizeText(response?.body?.error, null);
98
+ const backendMessage = normalizeText(response?.body?.message, `claworld world membership ${action} failed`);
99
+
100
+ return createRuntimeBoundaryError({
101
+ code: backendCode || `claworld_world_membership_${action}_failed`,
102
+ category: inferHttpErrorCategory(response?.status),
103
+ status: response?.status ?? 500,
104
+ message: `claworld world membership ${action} failed: ${response?.status ?? 500}`,
105
+ publicMessage: backendMessage,
106
+ recoverable: Number(response?.status) >= 400 && Number(response?.status) < 500,
107
+ context: {
108
+ action: `world_membership_${action}`,
109
+ accountId,
110
+ ...(worldId ? { worldId } : {}),
111
+ httpStatus: response?.status ?? 500,
112
+ ...extractBackendErrorContext(response?.body),
113
+ },
114
+ });
115
+ }
116
+
117
+ export async function fetchWorldMemberships({
118
+ cfg = {},
119
+ accountId = null,
120
+ runtimeConfig = null,
121
+ agentId = null,
122
+ status = null,
123
+ includeInactive = false,
124
+ includeDisabled = true,
125
+ fetchImpl,
126
+ logger = console,
127
+ } = {}) {
128
+ if (typeof fetchImpl !== 'function') {
129
+ throw new Error('fetch is unavailable for claworld world membership helper');
130
+ }
131
+
132
+ const resolvedAgentId = normalizeText(agentId, null);
133
+ if (!resolvedAgentId) {
134
+ throw new Error('claworld world membership helper requires agentId');
135
+ }
136
+
137
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
138
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
139
+ const requestUrl = new URL(`${baseUrl}/v1/world-memberships`);
140
+ requestUrl.searchParams.set('agentId', resolvedAgentId);
141
+ if (normalizeText(status, null)) requestUrl.searchParams.set('status', normalizeText(status, null));
142
+ if (includeInactive) requestUrl.searchParams.set('includeInactive', 'true');
143
+ requestUrl.searchParams.set('includeDisabled', includeDisabled ? 'true' : 'false');
144
+ const result = await fetchJson(fetchImpl, requestUrl.toString(), {
145
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
146
+ accept: 'application/json',
147
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
148
+ }),
149
+ });
150
+
151
+ if (!result.ok) {
152
+ logger.error?.('[claworld:membership] world memberships fetch failed', {
153
+ status: result.status,
154
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
155
+ body: result.body,
156
+ });
157
+ throw createWorldMembershipHttpError('list', result, {
158
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
159
+ });
160
+ }
161
+
162
+ return normalizeMembershipList(result.body);
163
+ }
164
+
165
+ export async function fetchWorldMembership({
166
+ cfg = {},
167
+ accountId = null,
168
+ runtimeConfig = null,
169
+ agentId = null,
170
+ worldId = null,
171
+ includeDisabled = true,
172
+ fetchImpl,
173
+ logger = console,
174
+ } = {}) {
175
+ if (typeof fetchImpl !== 'function') {
176
+ throw new Error('fetch is unavailable for claworld world membership helper');
177
+ }
178
+
179
+ const resolvedAgentId = normalizeText(agentId, null);
180
+ if (!resolvedAgentId) {
181
+ throw new Error('claworld world membership helper requires agentId');
182
+ }
183
+ const resolvedWorldId = normalizeText(worldId, null);
184
+ if (!resolvedWorldId) {
185
+ throw new Error('claworld world membership helper requires worldId');
186
+ }
187
+
188
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
189
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
190
+ const requestUrl = new URL(`${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership`);
191
+ requestUrl.searchParams.set('agentId', resolvedAgentId);
192
+ requestUrl.searchParams.set('includeDisabled', includeDisabled ? 'true' : 'false');
193
+ const result = await fetchJson(fetchImpl, requestUrl.toString(), {
194
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
195
+ accept: 'application/json',
196
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
197
+ }),
198
+ });
199
+
200
+ if (!result.ok) {
201
+ logger.error?.('[claworld:membership] world membership fetch failed', {
202
+ status: result.status,
203
+ worldId: resolvedWorldId,
204
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
205
+ body: result.body,
206
+ });
207
+ throw createWorldMembershipHttpError('get', result, {
208
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
209
+ worldId: resolvedWorldId,
210
+ });
211
+ }
212
+
213
+ return normalizeManagedWorldMembership(result.body);
214
+ }
215
+
216
+ export async function updateWorldMembershipProfile({
217
+ cfg = {},
218
+ accountId = null,
219
+ runtimeConfig = null,
220
+ agentId = null,
221
+ worldId = null,
222
+ participantContextText = null,
223
+ fetchImpl,
224
+ logger = console,
225
+ } = {}) {
226
+ if (typeof fetchImpl !== 'function') {
227
+ throw new Error('fetch is unavailable for claworld world membership helper');
228
+ }
229
+
230
+ const resolvedAgentId = normalizeText(agentId, null);
231
+ if (!resolvedAgentId) {
232
+ throw new Error('claworld world membership helper requires agentId');
233
+ }
234
+ const resolvedWorldId = normalizeText(worldId, null);
235
+ if (!resolvedWorldId) {
236
+ throw new Error('claworld world membership helper requires worldId');
237
+ }
238
+
239
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
240
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
241
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership`, {
242
+ method: 'PATCH',
243
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
244
+ accept: 'application/json',
245
+ 'content-type': 'application/json',
246
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
247
+ }),
248
+ body: JSON.stringify({
249
+ agentId: resolvedAgentId,
250
+ participantContextText: normalizeText(participantContextText, null),
251
+ }),
252
+ });
253
+
254
+ if (!result.ok) {
255
+ logger.error?.('[claworld:membership] world membership profile update failed', {
256
+ status: result.status,
257
+ worldId: resolvedWorldId,
258
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
259
+ body: result.body,
260
+ });
261
+ throw createWorldMembershipHttpError('update_profile', result, {
262
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
263
+ worldId: resolvedWorldId,
264
+ });
265
+ }
266
+
267
+ return normalizeManagedWorldMembership(result.body);
268
+ }
269
+
270
+ export async function leaveWorldMembership({
271
+ cfg = {},
272
+ accountId = null,
273
+ runtimeConfig = null,
274
+ agentId = null,
275
+ worldId = null,
276
+ fetchImpl,
277
+ logger = console,
278
+ } = {}) {
279
+ if (typeof fetchImpl !== 'function') {
280
+ throw new Error('fetch is unavailable for claworld world membership helper');
281
+ }
282
+
283
+ const resolvedAgentId = normalizeText(agentId, null);
284
+ if (!resolvedAgentId) {
285
+ throw new Error('claworld world membership helper requires agentId');
286
+ }
287
+ const resolvedWorldId = normalizeText(worldId, null);
288
+ if (!resolvedWorldId) {
289
+ throw new Error('claworld world membership helper requires worldId');
290
+ }
291
+
292
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
293
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
294
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/membership/leave`, {
295
+ method: 'POST',
296
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
297
+ accept: 'application/json',
298
+ 'content-type': 'application/json',
299
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
300
+ }),
301
+ body: JSON.stringify({
302
+ agentId: resolvedAgentId,
303
+ }),
304
+ });
305
+
306
+ if (!result.ok) {
307
+ logger.error?.('[claworld:membership] world membership leave failed', {
308
+ status: result.status,
309
+ worldId: resolvedWorldId,
310
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
311
+ body: result.body,
312
+ });
313
+ throw createWorldMembershipHttpError('leave', result, {
314
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
315
+ worldId: resolvedWorldId,
316
+ });
317
+ }
318
+
319
+ return normalizeManagedWorldMembership(result.body);
320
+ }
@@ -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 { normalizeWorldJoinResponse } from './product-shell-helper.js';
5
6
 
6
7
  function normalizeText(value, fallback = null) {
7
8
  if (value == null) return fallback;
@@ -68,6 +69,29 @@ function normalizeWorldRole(worldRole, fallback = null) {
68
69
  return ['owner', 'member'].includes(normalized) ? normalized : fallback;
69
70
  }
70
71
 
72
+ function normalizeBroadcastAudience(value, fallback = 'members') {
73
+ const normalized = normalizeText(value, fallback);
74
+ if (normalized === 'admins') return 'admins';
75
+ if (normalized === 'admins_and_owner') return 'admins_and_owner';
76
+ return 'members';
77
+ }
78
+
79
+ function normalizeBroadcastReplyPolicy(value, fallback = 'zero') {
80
+ const normalized = normalizeText(value, fallback);
81
+ if (normalized === 'at_most_one') return 'at_most_one';
82
+ return 'zero';
83
+ }
84
+
85
+ function normalizeWorldBroadcastConfig(broadcast = null) {
86
+ if (!broadcast || typeof broadcast !== 'object' || Array.isArray(broadcast)) return null;
87
+ return {
88
+ enabled: normalizeOptionalBoolean(broadcast.enabled, null),
89
+ audience: normalizeBroadcastAudience(broadcast.audience, 'members'),
90
+ replyPolicy: normalizeBroadcastReplyPolicy(broadcast.replyPolicy, 'zero'),
91
+ excludeSelf: normalizeOptionalBoolean(broadcast.excludeSelf, null),
92
+ };
93
+ }
94
+
71
95
  function normalizeManagedWorld(payload = {}) {
72
96
  return {
73
97
  worldId: normalizeText(payload.worldId, null),
@@ -81,10 +105,25 @@ function normalizeManagedWorld(payload = {}) {
81
105
  createdAt: normalizeText(payload.createdAt, null),
82
106
  updatedAt: normalizeText(payload.updatedAt, null),
83
107
  participantContextField: normalizeParticipantContextFieldPayload(payload.participantContextField),
108
+ broadcast: normalizeWorldBroadcastConfig(payload.broadcast),
84
109
  stats: normalizeWorldStats(payload.stats),
85
110
  };
86
111
  }
87
112
 
113
+ function normalizeCreatedWorld(payload = {}) {
114
+ const world = normalizeManagedWorld(payload);
115
+ return {
116
+ ...world,
117
+ ownerJoin:
118
+ payload.ownerJoin && typeof payload.ownerJoin === 'object'
119
+ ? normalizeWorldJoinResponse(payload.ownerJoin, {
120
+ worldId: world.worldId,
121
+ agentId: world.ownerAgentId,
122
+ })
123
+ : null,
124
+ };
125
+ }
126
+
88
127
  function normalizeOwnedWorldSummary(payload = {}) {
89
128
  return {
90
129
  worldId: normalizeText(payload.worldId, null),
@@ -96,10 +135,62 @@ function normalizeOwnedWorldSummary(payload = {}) {
96
135
  worldRole: normalizeWorldRole(payload.worldRole, null),
97
136
  createdAt: normalizeText(payload.createdAt, null),
98
137
  updatedAt: normalizeText(payload.updatedAt, null),
138
+ broadcast: normalizeWorldBroadcastConfig(payload.broadcast),
99
139
  stats: normalizeWorldStats(payload.stats),
100
140
  };
101
141
  }
102
142
 
143
+ function normalizeWorldBroadcastRequestItem(item = {}) {
144
+ return {
145
+ agentId: normalizeText(item.agentId, null),
146
+ status: normalizeText(item.status, null),
147
+ verdict: normalizeText(item.verdict, null),
148
+ chatRequest: item.chatRequest && typeof item.chatRequest === 'object' && !Array.isArray(item.chatRequest)
149
+ ? item.chatRequest
150
+ : null,
151
+ kickoff: item.kickoff && typeof item.kickoff === 'object' && !Array.isArray(item.kickoff)
152
+ ? item.kickoff
153
+ : null,
154
+ };
155
+ }
156
+
157
+ function normalizeWorldBroadcastFailureItem(item = {}) {
158
+ return {
159
+ agentId: normalizeText(item.agentId, null),
160
+ status: normalizeText(item.status, 'failed'),
161
+ httpStatus: normalizeOptionalInteger(item.httpStatus, null),
162
+ error: normalizeText(item.error, null),
163
+ reason: normalizeText(item.reason, null),
164
+ message: normalizeText(item.message, null),
165
+ };
166
+ }
167
+
168
+ function normalizeWorldBroadcastResponse(payload = {}) {
169
+ return {
170
+ status: normalizeText(payload.status, null),
171
+ worldId: normalizeText(payload.worldId, null),
172
+ senderAgentId: normalizeText(payload.senderAgentId, null),
173
+ senderRole: normalizeWorldRole(payload.senderRole, null),
174
+ audience: normalizeBroadcastAudience(payload.audience, 'members'),
175
+ excludeSelf: normalizeOptionalBoolean(payload.excludeSelf, null),
176
+ eligibility: normalizeText(payload.eligibility, null),
177
+ broadcastId: normalizeText(payload.broadcastId, null),
178
+ totalTargets: normalizeOptionalInteger(payload.totalTargets, null),
179
+ createdCount: normalizeOptionalInteger(payload.createdCount, null),
180
+ failedCount: normalizeOptionalInteger(payload.failedCount, null),
181
+ pendingCount: normalizeOptionalInteger(payload.pendingCount, null),
182
+ autoAcceptedCount: normalizeOptionalInteger(payload.autoAcceptedCount, null),
183
+ rejectedCount: normalizeOptionalInteger(payload.rejectedCount, null),
184
+ nextAction: normalizeText(payload.nextAction, null),
185
+ requests: Array.isArray(payload.requests)
186
+ ? payload.requests.map((item) => normalizeWorldBroadcastRequestItem(item))
187
+ : [],
188
+ failures: Array.isArray(payload.failures)
189
+ ? payload.failures.map((item) => normalizeWorldBroadcastFailureItem(item))
190
+ : [],
191
+ };
192
+ }
193
+
103
194
  async function fetchJson(fetchImpl, url, init = {}) {
104
195
  let response;
105
196
  try {
@@ -174,6 +265,7 @@ export async function createModeratedWorld({
174
265
  agentId = null,
175
266
  displayName = null,
176
267
  worldContextText = null,
268
+ participantContextText = null,
177
269
  enabled = true,
178
270
  fetchImpl,
179
271
  logger = console,
@@ -200,6 +292,7 @@ export async function createModeratedWorld({
200
292
  agentId: resolvedAgentId,
201
293
  displayName,
202
294
  worldContextText,
295
+ participantContextText: normalizeText(participantContextText, null),
203
296
  enabled,
204
297
  }),
205
298
  });
@@ -215,7 +308,7 @@ export async function createModeratedWorld({
215
308
  });
216
309
  }
217
310
 
218
- return normalizeManagedWorld(created.body);
311
+ return normalizeCreatedWorld(created.body);
219
312
  }
220
313
 
221
314
  export async function fetchOwnedWorlds({
@@ -349,3 +442,67 @@ export async function manageModeratedWorld({
349
442
 
350
443
  return normalizeManagedWorld(result.body);
351
444
  }
445
+
446
+ export async function broadcastModeratedWorld({
447
+ cfg = {},
448
+ accountId = null,
449
+ runtimeConfig = null,
450
+ agentId = null,
451
+ worldId = null,
452
+ announcementText = null,
453
+ audience = null,
454
+ excludeSelf = null,
455
+ fetchImpl,
456
+ logger = console,
457
+ } = {}) {
458
+ if (typeof fetchImpl !== 'function') {
459
+ throw new Error('fetch is unavailable for claworld world broadcast helper');
460
+ }
461
+
462
+ const resolvedAgentId = normalizeText(agentId, null);
463
+ if (!resolvedAgentId) {
464
+ throw new Error('claworld world broadcast helper requires agentId');
465
+ }
466
+ const resolvedWorldId = normalizeText(worldId, null);
467
+ if (!resolvedWorldId) {
468
+ throw new Error('claworld world broadcast helper requires worldId');
469
+ }
470
+ const resolvedAnnouncementText = normalizeText(announcementText, null);
471
+ if (!resolvedAnnouncementText) {
472
+ throw new Error('claworld world broadcast helper requires announcementText');
473
+ }
474
+
475
+ const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
476
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
477
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/worlds/${encodeURIComponent(resolvedWorldId)}/broadcast`, {
478
+ method: 'POST',
479
+ headers: buildRuntimeAuthHeaders(resolvedRuntimeConfig, {
480
+ accept: 'application/json',
481
+ 'content-type': 'application/json',
482
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
483
+ }),
484
+ body: JSON.stringify({
485
+ agentId: resolvedAgentId,
486
+ payload: {
487
+ text: resolvedAnnouncementText,
488
+ },
489
+ ...(normalizeText(audience, null) ? { audience: normalizeText(audience, null) } : {}),
490
+ ...(typeof excludeSelf === 'boolean' ? { excludeSelf } : {}),
491
+ }),
492
+ });
493
+
494
+ if (!result.ok) {
495
+ logger.error?.('[claworld:moderation] world broadcast failed', {
496
+ status: result.status,
497
+ worldId: resolvedWorldId,
498
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
499
+ body: result.body,
500
+ });
501
+ throw createModerationHttpError('broadcast', result, {
502
+ accountId: resolvedRuntimeConfig.accountId || accountId || null,
503
+ worldId: resolvedWorldId,
504
+ });
505
+ }
506
+
507
+ return normalizeWorldBroadcastResponse(result.body);
508
+ }
@@ -259,6 +259,11 @@ function normalizeDeliveryReason(reason = {}) {
259
259
  };
260
260
  }
261
261
 
262
+ function normalizeWorldRole(worldRole, fallback = null) {
263
+ const normalized = normalizeText(worldRole, fallback);
264
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
265
+ }
266
+
262
267
  function normalizeCandidate(candidate = {}, index = 0) {
263
268
  const normalizedRank = normalizeNumber(candidate.rank, null);
264
269
  const displayName = normalizeText(
@@ -280,6 +285,7 @@ function normalizeCandidate(candidate = {}, index = 0) {
280
285
  return {
281
286
  candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
282
287
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
288
+ worldRole: normalizeWorldRole(candidate.worldRole, null),
283
289
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
284
290
  online: candidate.online === true,
285
291
  displayName,
@@ -622,6 +628,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
622
628
  .filter(Boolean);
623
629
  const deliveryReasonSummary = sentenceCase(candidate.deliveryReason.summary, '');
624
630
  const availabilitySummary = candidate.online === true ? 'Online now.' : 'Currently offline.';
631
+ const roleSummary = candidate.worldRole ? `World role: ${candidate.worldRole}.` : null;
625
632
  const scoreSummary = candidate.score == null
626
633
  ? null
627
634
  : `Score ${candidate.score}${candidate.rank == null ? '' : `, rank ${candidate.rank}`}.`;
@@ -631,6 +638,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
631
638
  optionalFieldSummary.length > 0 ? `Optional context: ${optionalFieldSummary.join('; ')}.` : null,
632
639
  compatibilitySummary.length > 0 ? compatibilitySummary.join(' ') : null,
633
640
  deliveryReasonSummary || null,
641
+ roleSummary,
634
642
  availabilitySummary,
635
643
  scoreSummary,
636
644
  ].filter(Boolean).join(' ');
@@ -639,6 +647,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
639
647
  candidateId: candidate.candidateId,
640
648
  sourceMembershipId: candidate.sourceMembershipId,
641
649
  online: candidate.online === true,
650
+ worldRole: candidate.worldRole,
642
651
  agentCode: candidate.agentCode,
643
652
  requestChat: candidate.requestChat,
644
653
  displayName: name,