@xfxstudio/claworld 0.2.23-beta.0 → 0.2.23-beta.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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.23-beta.0",
11
+ "version": "0.2.23-beta.2",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.2.23-beta.0",
3
+ "version": "0.2.23-beta.2",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,4 +1,5 @@
1
1
  import { createKickoffBrief } from './relay/kickoff-text.js';
2
+ import { normalizeAcceptedChatKickoffRecord } from './relay/kickoff-progress.js';
2
3
 
3
4
  function normalizeText(value, fallback = null) {
4
5
  if (value == null) return fallback;
@@ -358,7 +359,7 @@ export function normalizeStoredChatRequest(input = {}, { defaultSource = 'chat_r
358
359
  if (acceptedByAgentId) normalized.acceptedByAgentId = acceptedByAgentId;
359
360
  const approvalGrantId = normalizeText(input.approvalGrantId, null);
360
361
  if (approvalGrantId) normalized.approvalGrantId = approvalGrantId;
361
- const kickoff = cloneJsonObject(input.kickoff);
362
+ const kickoff = normalizeAcceptedChatKickoffRecord(cloneJsonObject(input.kickoff));
362
363
  if (kickoff) normalized.kickoff = kickoff;
363
364
 
364
365
  return normalized;
@@ -0,0 +1,162 @@
1
+ import { normalizeOptionalText } from './shared.js';
2
+
3
+ function normalizeAcceptedChatKickoffField(value, fallback = null) {
4
+ return normalizeOptionalText(value) || fallback;
5
+ }
6
+
7
+ export function normalizeAcceptedChatKickoffRecord(kickoff = null, { fallbackStatus = null } = {}) {
8
+ if (!kickoff || typeof kickoff !== 'object' || Array.isArray(kickoff)) return null;
9
+
10
+ const normalized = {
11
+ ...kickoff,
12
+ };
13
+
14
+ const normalizedStatus = normalizeAcceptedChatKickoffField(normalized.status, fallbackStatus);
15
+ const normalizedReason = normalizeAcceptedChatKickoffField(normalized.reason, null);
16
+ const normalizedDeliveredAt = normalizeAcceptedChatKickoffField(normalized.deliveredAt, null);
17
+ const normalizedSenderKickoffDeliveredAt = normalizeAcceptedChatKickoffField(
18
+ normalized.senderKickoffDeliveredAt,
19
+ normalizedDeliveredAt,
20
+ );
21
+ const normalizedOpenerAcceptedAt = normalizeAcceptedChatKickoffField(normalized.openerAcceptedAt, null);
22
+ const normalizedOpenerDeliveredAt = normalizeAcceptedChatKickoffField(normalized.openerDeliveredAt, null);
23
+ const normalizedLiveChatEstablishedAt = normalizeAcceptedChatKickoffField(normalized.liveChatEstablishedAt, null);
24
+ const normalizedTurnId = normalizeAcceptedChatKickoffField(normalized.turnId, null);
25
+ const normalizedDeliveryId = normalizeAcceptedChatKickoffField(normalized.deliveryId, null);
26
+ const normalizedConversationKey = normalizeAcceptedChatKickoffField(normalized.conversationKey, null);
27
+ const normalizedOpenerTurnId = normalizeAcceptedChatKickoffField(normalized.openerTurnId, null);
28
+ const normalizedOpenerDeliveryId = normalizeAcceptedChatKickoffField(normalized.openerDeliveryId, null);
29
+ const normalizedFailedAt = normalizeAcceptedChatKickoffField(normalized.failedAt, null);
30
+
31
+ if (normalizedStatus) normalized.status = normalizedStatus;
32
+ else delete normalized.status;
33
+ if (normalizedReason) normalized.reason = normalizedReason;
34
+ else delete normalized.reason;
35
+ if (normalizedDeliveredAt) normalized.deliveredAt = normalizedDeliveredAt;
36
+ else delete normalized.deliveredAt;
37
+ if (normalizedSenderKickoffDeliveredAt) normalized.senderKickoffDeliveredAt = normalizedSenderKickoffDeliveredAt;
38
+ else delete normalized.senderKickoffDeliveredAt;
39
+ if (normalizedOpenerAcceptedAt) normalized.openerAcceptedAt = normalizedOpenerAcceptedAt;
40
+ else delete normalized.openerAcceptedAt;
41
+ if (normalizedOpenerDeliveredAt) normalized.openerDeliveredAt = normalizedOpenerDeliveredAt;
42
+ else delete normalized.openerDeliveredAt;
43
+ if (normalizedLiveChatEstablishedAt) normalized.liveChatEstablishedAt = normalizedLiveChatEstablishedAt;
44
+ else delete normalized.liveChatEstablishedAt;
45
+ if (normalizedTurnId) normalized.turnId = normalizedTurnId;
46
+ else delete normalized.turnId;
47
+ if (normalizedDeliveryId) normalized.deliveryId = normalizedDeliveryId;
48
+ else delete normalized.deliveryId;
49
+ if (normalizedConversationKey) normalized.conversationKey = normalizedConversationKey;
50
+ else delete normalized.conversationKey;
51
+ if (normalizedOpenerTurnId) normalized.openerTurnId = normalizedOpenerTurnId;
52
+ else delete normalized.openerTurnId;
53
+ if (normalizedOpenerDeliveryId) normalized.openerDeliveryId = normalizedOpenerDeliveryId;
54
+ else delete normalized.openerDeliveryId;
55
+ if (normalizedFailedAt) normalized.failedAt = normalizedFailedAt;
56
+ else delete normalized.failedAt;
57
+
58
+ const hasEstablishedEvidence = Boolean(
59
+ normalized.openerDeliveredAt
60
+ || normalized.liveChatEstablishedAt,
61
+ );
62
+
63
+ if (hasEstablishedEvidence && (!normalized.status || ['queued', 'sent'].includes(normalized.status))) {
64
+ normalized.status = 'established';
65
+ }
66
+
67
+ if (normalized.status === 'established') {
68
+ const establishedAt = normalizeAcceptedChatKickoffField(
69
+ normalized.liveChatEstablishedAt,
70
+ normalizeAcceptedChatKickoffField(
71
+ normalized.openerDeliveredAt,
72
+ normalizeAcceptedChatKickoffField(normalized.openerAcceptedAt, null),
73
+ ),
74
+ );
75
+ if (!normalized.openerDeliveredAt && normalized.openerAcceptedAt) {
76
+ normalized.openerDeliveredAt = normalized.openerAcceptedAt;
77
+ }
78
+ if (!normalized.liveChatEstablishedAt && establishedAt) {
79
+ normalized.liveChatEstablishedAt = establishedAt;
80
+ }
81
+ if (String(normalized.reason || '').startsWith('queued_')) {
82
+ delete normalized.reason;
83
+ }
84
+ }
85
+
86
+ return normalized;
87
+ }
88
+
89
+ export async function markAcceptedChatKickoffFailureWithDeps(deps, {
90
+ requestId = null,
91
+ reason = 'accepted_chat_kickoff_failed',
92
+ turnId = null,
93
+ conversationKey = null,
94
+ } = {}) {
95
+ const { store, pushToAgent } = deps;
96
+ const normalizedRequestId = normalizeOptionalText(requestId);
97
+ if (!normalizedRequestId) return null;
98
+ const request = store.getChatRequest(normalizedRequestId);
99
+ if (!request) return null;
100
+
101
+ request.kickoff = normalizeAcceptedChatKickoffRecord({
102
+ ...(request.kickoff && typeof request.kickoff === 'object' && !Array.isArray(request.kickoff) ? request.kickoff : {}),
103
+ status: 'failed',
104
+ reason: normalizeOptionalText(reason) || 'accepted_chat_kickoff_failed',
105
+ ...(normalizeOptionalText(turnId) ? { turnId: normalizeOptionalText(turnId) } : {}),
106
+ ...(normalizeOptionalText(conversationKey) ? { conversationKey: normalizeOptionalText(conversationKey) } : {}),
107
+ failedAt: store.now(),
108
+ });
109
+ if (store.markChatRequestUpdated) {
110
+ await store.markChatRequestUpdated();
111
+ }
112
+ await pushToAgent(request.fromAgentId, 'request.updated', request);
113
+ await pushToAgent(request.toAgentId, 'request.updated', request);
114
+ return request;
115
+ }
116
+
117
+ export async function markAcceptedChatKickoffProgressWithDeps(deps, {
118
+ requestId = null,
119
+ status = null,
120
+ reason = null,
121
+ turnId = null,
122
+ deliveryId = null,
123
+ conversationKey = null,
124
+ senderKickoffDeliveredAt = null,
125
+ openerAcceptedAt = null,
126
+ openerDeliveredAt = null,
127
+ liveChatEstablishedAt = null,
128
+ openerTurnId = null,
129
+ openerDeliveryId = null,
130
+ } = {}) {
131
+ const { store, pushToAgent } = deps;
132
+ const normalizedRequestId = normalizeOptionalText(requestId);
133
+ if (!normalizedRequestId) return null;
134
+ const request = store.getChatRequest(normalizedRequestId);
135
+ if (!request) return null;
136
+
137
+ request.kickoff = normalizeAcceptedChatKickoffRecord({
138
+ ...(request.kickoff && typeof request.kickoff === 'object' && !Array.isArray(request.kickoff) ? request.kickoff : {}),
139
+ ...(normalizeOptionalText(status) ? { status: normalizeOptionalText(status) } : {}),
140
+ ...(normalizeOptionalText(reason) ? { reason: normalizeOptionalText(reason) } : {}),
141
+ ...(normalizeOptionalText(turnId) ? { turnId: normalizeOptionalText(turnId) } : {}),
142
+ ...(normalizeOptionalText(deliveryId) ? { deliveryId: normalizeOptionalText(deliveryId) } : {}),
143
+ ...(normalizeOptionalText(conversationKey) ? { conversationKey: normalizeOptionalText(conversationKey) } : {}),
144
+ ...(normalizeOptionalText(senderKickoffDeliveredAt)
145
+ ? {
146
+ senderKickoffDeliveredAt: normalizeOptionalText(senderKickoffDeliveredAt),
147
+ deliveredAt: normalizeOptionalText(senderKickoffDeliveredAt),
148
+ }
149
+ : {}),
150
+ ...(normalizeOptionalText(openerAcceptedAt) ? { openerAcceptedAt: normalizeOptionalText(openerAcceptedAt) } : {}),
151
+ ...(normalizeOptionalText(openerDeliveredAt) ? { openerDeliveredAt: normalizeOptionalText(openerDeliveredAt) } : {}),
152
+ ...(normalizeOptionalText(liveChatEstablishedAt) ? { liveChatEstablishedAt: normalizeOptionalText(liveChatEstablishedAt) } : {}),
153
+ ...(normalizeOptionalText(openerTurnId) ? { openerTurnId: normalizeOptionalText(openerTurnId) } : {}),
154
+ ...(normalizeOptionalText(openerDeliveryId) ? { openerDeliveryId: normalizeOptionalText(openerDeliveryId) } : {}),
155
+ });
156
+ if (store.markChatRequestUpdated) {
157
+ await store.markChatRequestUpdated();
158
+ }
159
+ await pushToAgent(request.fromAgentId, 'request.updated', request);
160
+ await pushToAgent(request.toAgentId, 'request.updated', request);
161
+ return request;
162
+ }
@@ -0,0 +1,30 @@
1
+ export function normalizePositiveInteger(value, fallback) {
2
+ const normalized = Number(value);
3
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
4
+ return Math.floor(normalized);
5
+ }
6
+
7
+ export function normalizeOptionalText(value) {
8
+ if (typeof value !== 'string') return null;
9
+ const normalized = value.trim();
10
+ return normalized || null;
11
+ }
12
+
13
+ export function cloneJsonObject(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
15
+ try {
16
+ const cloned = JSON.parse(JSON.stringify(value));
17
+ if (!cloned || typeof cloned !== 'object' || Array.isArray(cloned)) return null;
18
+ return cloned;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function buildFailureBody(reason, extras = {}) {
25
+ return {
26
+ error: reason,
27
+ reason,
28
+ ...extras,
29
+ };
30
+ }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import claworldPackageJson from '../../../package.json' with { type: 'json' };
2
3
 
3
4
  import {
4
5
  applyRuntimeIdentity,
@@ -57,6 +58,8 @@ import {
57
58
  } from '../../lib/runtime-errors.js';
58
59
  import { PUBLIC_IDENTITY_STATUS } from '../../lib/public-identity.js';
59
60
 
61
+ const CLAWORLD_PLUGIN_VERSION = claworldPackageJson.version;
62
+
60
63
  function normalizeRelayHttpBaseUrl(serverUrl) {
61
64
  const parsed = new URL(serverUrl);
62
65
  if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
@@ -174,6 +177,20 @@ const CLAWORLD_RELAY_OPERATIONAL_NOTICE_PATTERNS = [
174
177
  /^⚠️\s*Agent failed before reply:/i,
175
178
  ];
176
179
 
180
+ // Older/runtime-variant OpenClaw hosts may surface provider/runtime failures as
181
+ // plain final text without setting `isError`. Keep this fallback at the bridge
182
+ // boundary so business logic never has to guess.
183
+ const CLAWORLD_RELAY_RUNTIME_ERROR_PATTERNS = [
184
+ /^⚠️\s*Agent failed before reply:/i,
185
+ /^LLM request failed:/i,
186
+ /^LLM request timed out\./i,
187
+ /^LLM request unauthorized\./i,
188
+ /^The AI service is temporarily overloaded\./i,
189
+ /^The AI service returned an error\./i,
190
+ /^⚠️\s*API rate limit reached\./i,
191
+ /^⚠️\s*.+\s+returned a billing error\b/i,
192
+ ];
193
+
177
194
  const CLAWORLD_RELAY_OPERATIONAL_SUFFIX_PATTERNS = [
178
195
  /^Usage:\s+.+\s+in\s+\/\s+.+\s+out(?:\s+·\s+est\s+.+)?$/i,
179
196
  ];
@@ -201,18 +218,21 @@ function classifyRelayContinuationText(text) {
201
218
  if (!normalized) {
202
219
  return {
203
220
  text: '',
204
- operational: Boolean(String(text || '').trim()),
221
+ operationalNotice: Boolean(String(text || '').trim()),
222
+ runtimeError: false,
205
223
  };
206
224
  }
207
225
  if (CLAWORLD_RELAY_OPERATIONAL_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized))) {
208
226
  return {
209
227
  text: '',
210
- operational: true,
228
+ operationalNotice: true,
229
+ runtimeError: false,
211
230
  };
212
231
  }
213
232
  return {
214
233
  text: normalized,
215
- operational: false,
234
+ operationalNotice: false,
235
+ runtimeError: false,
216
236
  };
217
237
  }
218
238
 
@@ -220,6 +240,45 @@ function sanitizeRelayContinuationText(text) {
220
240
  return classifyRelayContinuationText(text).text;
221
241
  }
222
242
 
243
+ function classifyRelayContinuationPayload(payload = {}) {
244
+ const rawText = String(payload?.text ?? payload?.body ?? '').trim();
245
+ const normalized = stripRelayOperationalSuffix(rawText);
246
+ const textClassification = classifyRelayContinuationText(rawText);
247
+ const runtimeError = payload?.isError === true
248
+ || CLAWORLD_RELAY_RUNTIME_ERROR_PATTERNS.some((pattern) => pattern.test(normalized));
249
+ if (runtimeError) {
250
+ return {
251
+ text: '',
252
+ previewText: normalized,
253
+ operationalNotice: false,
254
+ runtimeError: true,
255
+ nonRenderable: true,
256
+ };
257
+ }
258
+ return {
259
+ text: textClassification.text,
260
+ previewText: normalized,
261
+ operationalNotice: textClassification.operationalNotice,
262
+ runtimeError: false,
263
+ nonRenderable: textClassification.operationalNotice,
264
+ };
265
+ }
266
+
267
+ function resolveRelaySilentReason(runtimeOutputSummary = {}, continuation = {}) {
268
+ const counts = runtimeOutputSummary?.counts || {};
269
+ if (Number(counts.runtimeErrorFinal || 0) > 0) {
270
+ return 'runtime_failed_before_reply';
271
+ }
272
+ if (Number(counts.operationalNotice || 0) > 0 && Number(counts.nonRenderableFinal || 0) === Number(counts.final || 0)) {
273
+ return 'operational_notice_only';
274
+ }
275
+ const normalizedSource = normalizePluginOptionalText(continuation?.source);
276
+ if (normalizedSource && normalizedSource !== 'none') {
277
+ return normalizedSource;
278
+ }
279
+ return 'no_renderable_reply';
280
+ }
281
+
223
282
  function previewRuntimeOutputText(text, maxLength = 120) {
224
283
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
225
284
  if (!normalized) return '';
@@ -1269,7 +1328,9 @@ function createDeliveryReplyDispatcher({
1269
1328
  reasoningEnd: 0,
1270
1329
  compactionStart: 0,
1271
1330
  compactionEnd: 0,
1331
+ nonRenderableFinal: 0,
1272
1332
  operationalNotice: 0,
1333
+ runtimeErrorFinal: 0,
1273
1334
  },
1274
1335
  previews: {
1275
1336
  final: [],
@@ -1278,6 +1339,7 @@ function createDeliveryReplyDispatcher({
1278
1339
  partial: [],
1279
1340
  reasoning: [],
1280
1341
  operationalNotice: [],
1342
+ runtimeErrorFinal: [],
1281
1343
  },
1282
1344
  relayContinuationSource: 'none',
1283
1345
  relayContinuationPreview: null,
@@ -1288,14 +1350,21 @@ function createDeliveryReplyDispatcher({
1288
1350
  runtimeOutputSummary.counts[kind] += 1;
1289
1351
  const text = String(payload?.text ?? payload?.body ?? '').trim();
1290
1352
  if (kind === 'final') {
1291
- if (text) {
1292
- finalTexts.push(text);
1293
- appendRuntimeOutputPreview(runtimeOutputSummary.previews.final, text);
1294
- const classified = classifyRelayContinuationText(text);
1295
- if (classified.operational) {
1296
- runtimeOutputSummary.counts.operationalNotice += 1;
1297
- appendRuntimeOutputPreview(runtimeOutputSummary.previews.operationalNotice, text);
1298
- }
1353
+ const classified = classifyRelayContinuationPayload(payload);
1354
+ if (classified.text) {
1355
+ finalTexts.push(classified.text);
1356
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.final, classified.text);
1357
+ }
1358
+ if (classified.nonRenderable) {
1359
+ runtimeOutputSummary.counts.nonRenderableFinal += 1;
1360
+ }
1361
+ if (classified.operationalNotice) {
1362
+ runtimeOutputSummary.counts.operationalNotice += 1;
1363
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.operationalNotice, classified.previewText || text);
1364
+ }
1365
+ if (classified.runtimeError) {
1366
+ runtimeOutputSummary.counts.runtimeErrorFinal += 1;
1367
+ appendRuntimeOutputPreview(runtimeOutputSummary.previews.runtimeErrorFinal, classified.previewText || text);
1299
1368
  }
1300
1369
  return;
1301
1370
  }
@@ -1411,21 +1480,35 @@ function createDeliveryReplyDispatcher({
1411
1480
  const markDispatchIdle = async () => {
1412
1481
  await dispatchApi.dispatcher.waitForIdle?.();
1413
1482
  if (!replied && !suppressed) {
1414
- const continuation = buildRelayContinuationText({
1483
+ const allowPartialFallback = (
1484
+ runtimeOutputSummary.counts.final > 0
1485
+ && finalTexts.length === 0
1486
+ && blockTexts.length === 0
1487
+ && runtimeOutputSummary.counts.nonRenderableFinal === 0
1488
+ );
1489
+ const safeContinuation = buildRelayContinuationText({
1415
1490
  finalTexts,
1416
1491
  blockTexts,
1417
1492
  partialText: partialContinuationText,
1418
- allowPartialFallback:
1419
- runtimeOutputSummary.counts.final > 0 && finalTexts.length === 0 && blockTexts.length === 0,
1493
+ allowPartialFallback,
1420
1494
  });
1421
- runtimeOutputSummary.relayContinuationSource = continuation.source;
1422
- runtimeOutputSummary.relayContinuationPreview = continuation.text
1423
- ? previewRuntimeOutputText(continuation.text)
1495
+ runtimeOutputSummary.relayContinuationSource = safeContinuation.source;
1496
+ runtimeOutputSummary.relayContinuationPreview = safeContinuation.text
1497
+ ? previewRuntimeOutputText(safeContinuation.text)
1424
1498
  : null;
1425
- if (continuation.text) {
1426
- await flushReply(continuation.text);
1499
+ if (safeContinuation.text) {
1500
+ await flushReply(safeContinuation.text);
1427
1501
  } else {
1428
- await flushKeptSilent(continuation.source);
1502
+ const silentReason = resolveRelaySilentReason(runtimeOutputSummary, safeContinuation);
1503
+ if (runtimeOutputSummary.counts.runtimeErrorFinal > 0) {
1504
+ logger.warn?.(`[claworld:${runtimeAccountId}] runtime produced non-renderable error finals; returning kept_silent`, {
1505
+ deliveryId,
1506
+ sessionKey,
1507
+ localAgentId,
1508
+ runtimeOutputSummary,
1509
+ });
1510
+ }
1511
+ await flushKeptSilent(silentReason);
1429
1512
  }
1430
1513
  }
1431
1514
  await dispatchApi.markDispatchIdle?.();
@@ -1470,13 +1553,14 @@ function createDeliveryReplyDispatcher({
1470
1553
  final: [...runtimeOutputSummary.previews.final],
1471
1554
  block: [...runtimeOutputSummary.previews.block],
1472
1555
  tool: [...runtimeOutputSummary.previews.tool],
1473
- partial: [...runtimeOutputSummary.previews.partial],
1474
- reasoning: [...runtimeOutputSummary.previews.reasoning],
1475
- operationalNotice: [...runtimeOutputSummary.previews.operationalNotice],
1476
- },
1477
- relayContinuationSource: runtimeOutputSummary.relayContinuationSource,
1478
- relayContinuationPreview: runtimeOutputSummary.relayContinuationPreview,
1479
- replyTransport,
1556
+ partial: [...runtimeOutputSummary.previews.partial],
1557
+ reasoning: [...runtimeOutputSummary.previews.reasoning],
1558
+ operationalNotice: [...runtimeOutputSummary.previews.operationalNotice],
1559
+ runtimeErrorFinal: [...runtimeOutputSummary.previews.runtimeErrorFinal],
1560
+ },
1561
+ relayContinuationSource: runtimeOutputSummary.relayContinuationSource,
1562
+ relayContinuationPreview: runtimeOutputSummary.relayContinuationPreview,
1563
+ replyTransport,
1480
1564
  replyFallbackUsed,
1481
1565
  keptSilentTransport,
1482
1566
  keptSilentFallbackUsed,
@@ -1749,8 +1833,8 @@ async function maybeBridgeRuntimeDelivery({
1749
1833
  && metadata.allowReply !== false
1750
1834
  && replied !== true
1751
1835
  && runtimeOutputSummary.counts.final > 0
1752
- && runtimeOutputSummary.counts.operationalNotice > 0
1753
- && runtimeOutputSummary.counts.final === runtimeOutputSummary.counts.operationalNotice
1836
+ && runtimeOutputSummary.counts.nonRenderableFinal > 0
1837
+ && runtimeOutputSummary.counts.final === runtimeOutputSummary.counts.nonRenderableFinal
1754
1838
  && runtimeOutputSummary.counts.block === 0
1755
1839
  && runtimeOutputSummary.counts.tool === 0
1756
1840
  && runtimeOutputSummary.counts.partial === 0
@@ -2369,7 +2453,7 @@ export function createClaworldChannelPlugin({
2369
2453
  return {
2370
2454
  ok: true,
2371
2455
  pluginId: 'claworld',
2372
- version: '0.3.0',
2456
+ version: CLAWORLD_PLUGIN_VERSION,
2373
2457
  defaultAccountId: null,
2374
2458
  accounts: accountSnapshots,
2375
2459
  relayClients: Object.fromEntries(
@@ -2518,7 +2602,7 @@ async function generateRuntimeProfileCard(context = {}) {
2518
2602
  docsPath: '/channels/claworld',
2519
2603
  docsLabel: 'claworld',
2520
2604
  blurb: 'Claworld relay channel backed by the Claworld backend.',
2521
- version: '0.3.0',
2605
+ version: CLAWORLD_PLUGIN_VERSION,
2522
2606
  forceAccountBinding: true,
2523
2607
  },
2524
2608
  onboarding: claworldOnboardingAdapter,
@@ -1,3 +1,5 @@
1
+ import { normalizeAcceptedChatKickoffRecord } from '../../lib/relay/kickoff-progress.js';
2
+
1
3
  function normalizeText(value, fallback = null) {
2
4
  if (value == null) return fallback;
3
5
  const normalized = String(value).trim();
@@ -437,15 +439,19 @@ function normalizeConversationScopeDetails(input = {}) {
437
439
  }
438
440
 
439
441
  function projectChatRequestKickoff(kickoff = {}) {
440
- if (!kickoff || typeof kickoff !== 'object') return null;
442
+ const normalizedKickoff = normalizeAcceptedChatKickoffRecord(kickoff, { fallbackStatus: 'skipped' });
443
+ if (!normalizedKickoff) return null;
441
444
  return {
442
- status: normalizeText(kickoff.status, 'skipped'),
443
- deliveredAt: normalizeText(kickoff.deliveredAt, null),
444
- senderKickoffDeliveredAt: normalizeText(kickoff.senderKickoffDeliveredAt, normalizeText(kickoff.deliveredAt, null)),
445
- openerAcceptedAt: normalizeText(kickoff.openerAcceptedAt, null),
446
- openerDeliveredAt: normalizeText(kickoff.openerDeliveredAt, null),
447
- liveChatEstablishedAt: normalizeText(kickoff.liveChatEstablishedAt, null),
448
- reason: normalizeText(kickoff.reason, null),
445
+ status: normalizeText(normalizedKickoff.status, 'skipped'),
446
+ deliveredAt: normalizeText(normalizedKickoff.deliveredAt, null),
447
+ senderKickoffDeliveredAt: normalizeText(
448
+ normalizedKickoff.senderKickoffDeliveredAt,
449
+ normalizeText(normalizedKickoff.deliveredAt, null),
450
+ ),
451
+ openerAcceptedAt: normalizeText(normalizedKickoff.openerAcceptedAt, null),
452
+ openerDeliveredAt: normalizeText(normalizedKickoff.openerDeliveredAt, null),
453
+ liveChatEstablishedAt: normalizeText(normalizedKickoff.liveChatEstablishedAt, null),
454
+ reason: normalizeText(normalizedKickoff.reason, null),
449
455
  };
450
456
  }
451
457