@xfxstudio/claworld 2026.6.10-testing.3 → 2026.6.10-testing.5

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.
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "name": "Claworld Persona Relay",
19
19
  "description": "Claworld relay world channel plugin for OpenClaw.",
20
- "version": "2026.6.10-testing.3",
20
+ "version": "2026.6.10-testing.5",
21
21
  "configSchema": {
22
22
  "type": "object",
23
23
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "2026.6.10-testing.3",
3
+ "version": "2026.6.10-testing.5",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -54,3 +54,14 @@ export function resolveConversationViewerRoot({
54
54
  || path.join(homeDir, '.openclaw');
55
55
  return path.resolve(stateDir.replace(/^~(?=$|\/|\\)/, homeDir), 'claworld', 'conversation-viewers');
56
56
  }
57
+
58
+ export function resolveConversationViewerOpenClawRoot({
59
+ env = null,
60
+ homeDir = os.homedir(),
61
+ } = {}) {
62
+ const sourceEnv = resolveEnv(env);
63
+ const stateDir = normalizeText(sourceEnv.OPENCLAW_STATE_DIR, null)
64
+ || normalizeText(sourceEnv.OPENCLAW_HOME, null)
65
+ || path.join(homeDir, '.openclaw');
66
+ return path.resolve(stateDir.replace(/^~(?=$|\/|\\)/, homeDir));
67
+ }
@@ -5,6 +5,7 @@ import { pathToFileURL } from 'node:url';
5
5
  import { buildRuntimeAuthHeaders } from './account-identity.js';
6
6
  import {
7
7
  resolveConversationViewerGatewayBaseUrl,
8
+ resolveConversationViewerOpenClawRoot,
8
9
  resolveConversationViewerRoot,
9
10
  } from './conversation-viewer-env.js';
10
11
  import { loadCurrentConfig } from './register-tooling.js';
@@ -14,6 +15,7 @@ export const CONVERSATION_VIEWER_ROUTE_PREFIX = '/plugins/claworld/conversation-
14
15
  export const CONVERSATION_VIEWER_SCHEMA = 'claworld.conversationViewer.v1';
15
16
  export {
16
17
  resolveConversationViewerGatewayBaseUrl,
18
+ resolveConversationViewerOpenClawRoot,
17
19
  resolveConversationViewerRoot,
18
20
  } from './conversation-viewer-env.js';
19
21
 
@@ -984,10 +986,18 @@ function buildStateLabel(status, { peerName = null, peerOnline = null } = {}) {
984
986
  return peerName ? `Conversation with ${peerName}` : 'Claworld conversation';
985
987
  }
986
988
 
987
- function resolveSnapshotStatus({ request = null, chat = null, conversation = null } = {}) {
989
+ function resolveSnapshotStatus({
990
+ request = null,
991
+ chat = null,
992
+ conversation = null,
993
+ fallbackStatus = null,
994
+ } = {}) {
988
995
  const conversationStatus = normalizeText(conversation?.status, null);
989
996
  if (conversationStatus === 'closed') return 'ended';
990
- return normalizeText(chat?.status, normalizeText(request?.status, conversationStatus || 'pending'));
997
+ return normalizeText(
998
+ chat?.status,
999
+ normalizeText(request?.status, normalizeText(conversationStatus, normalizeText(fallbackStatus, 'pending'))),
1000
+ );
991
1001
  }
992
1002
 
993
1003
  function collectTurns(turnsPayload = {}) {
@@ -1000,6 +1010,294 @@ function collectTurns(turnsPayload = {}) {
1000
1010
  });
1001
1011
  }
1002
1012
 
1013
+ function uniqueNormalized(values = []) {
1014
+ const seen = new Set();
1015
+ const next = [];
1016
+ for (const value of values) {
1017
+ const normalized = normalizeText(value, null);
1018
+ if (!normalized || seen.has(normalized)) continue;
1019
+ seen.add(normalized);
1020
+ next.push(normalized);
1021
+ }
1022
+ return next;
1023
+ }
1024
+
1025
+ function normalizeSessionStoreKey(value) {
1026
+ return normalizeText(value, '').toLowerCase();
1027
+ }
1028
+
1029
+ function extractLocalAgentIdsFromManifest(manifest = {}) {
1030
+ const ids = [];
1031
+ for (const value of [manifest.localSessionKey, manifest.sessionKey]) {
1032
+ const normalized = normalizeText(value, null);
1033
+ const match = normalized?.match(/^agent:([^:]+):/);
1034
+ if (match?.[1]) ids.push(match[1]);
1035
+ }
1036
+ ids.push('main');
1037
+ return uniqueNormalized(ids);
1038
+ }
1039
+
1040
+ function buildLocalSessionKeyCandidates(manifest = {}) {
1041
+ const conversationKey = normalizeText(manifest.conversationKey, null);
1042
+ const localSessionKey = normalizeText(manifest.localSessionKey, null);
1043
+ const keys = [];
1044
+ if (localSessionKey) {
1045
+ keys.push(localSessionKey);
1046
+ if (!localSessionKey.startsWith('agent:')) {
1047
+ for (const localAgentId of extractLocalAgentIdsFromManifest(manifest)) {
1048
+ keys.push(`agent:${localAgentId}:${localSessionKey}`);
1049
+ }
1050
+ }
1051
+ }
1052
+ if (conversationKey) {
1053
+ keys.push(`conversation:${conversationKey}`);
1054
+ for (const localAgentId of extractLocalAgentIdsFromManifest(manifest)) {
1055
+ keys.push(`agent:${localAgentId}:conversation:${conversationKey}`);
1056
+ }
1057
+ }
1058
+ return uniqueNormalized(keys);
1059
+ }
1060
+
1061
+ function resolveSessionStoreEntry(store = null, candidateKeys = [], manifest = {}) {
1062
+ if (!store || typeof store !== 'object' || Array.isArray(store)) return null;
1063
+ const normalizedCandidates = new Set(candidateKeys.map(normalizeSessionStoreKey));
1064
+ for (const key of candidateKeys) {
1065
+ if (store[key] && typeof store[key] === 'object' && !Array.isArray(store[key])) {
1066
+ return { key, record: store[key] };
1067
+ }
1068
+ }
1069
+ for (const [key, record] of Object.entries(store)) {
1070
+ if (
1071
+ normalizedCandidates.has(normalizeSessionStoreKey(key))
1072
+ && record
1073
+ && typeof record === 'object'
1074
+ && !Array.isArray(record)
1075
+ ) {
1076
+ return { key, record };
1077
+ }
1078
+ }
1079
+
1080
+ const conversationKey = normalizeText(manifest.conversationKey, null);
1081
+ const counterpartyAgentId = normalizeText(manifest.counterpartyAgentId, null);
1082
+ for (const [key, record] of Object.entries(store)) {
1083
+ if (!record || typeof record !== 'object' || Array.isArray(record)) continue;
1084
+ const keyText = normalizeText(key, '');
1085
+ const recordText = [
1086
+ keyText,
1087
+ record.sessionFile,
1088
+ record.transcriptPath,
1089
+ record.lastTo,
1090
+ record.route?.target?.to,
1091
+ record.deliveryContext?.to,
1092
+ record.origin?.to,
1093
+ ].map((value) => normalizeText(value, '')).join('\n');
1094
+ if (conversationKey && recordText.includes(conversationKey)) {
1095
+ return { key, record };
1096
+ }
1097
+ if (
1098
+ counterpartyAgentId
1099
+ && candidateKeys.some((candidate) => keyText.includes(candidate))
1100
+ && recordText.includes(counterpartyAgentId)
1101
+ ) {
1102
+ return { key, record };
1103
+ }
1104
+ }
1105
+ return null;
1106
+ }
1107
+
1108
+ function resolveTranscriptPathFromSessionRecord({
1109
+ record = {},
1110
+ sessionStorePath = null,
1111
+ } = {}) {
1112
+ const direct = normalizeText(
1113
+ record.transcriptPath,
1114
+ normalizeText(record.sessionFile, normalizeText(record.sessionPath, null)),
1115
+ );
1116
+ if (direct) {
1117
+ if (path.isAbsolute(direct) || !sessionStorePath) return direct;
1118
+ return path.resolve(path.dirname(sessionStorePath), direct);
1119
+ }
1120
+ const sessionId = normalizeText(record.sessionId, normalizeText(record.id, null));
1121
+ if (!sessionId || !sessionStorePath) return null;
1122
+ return path.join(path.dirname(sessionStorePath), `${sessionId}.jsonl`);
1123
+ }
1124
+
1125
+ function extractRequestBrief(content = '') {
1126
+ const normalized = normalizeText(content, null);
1127
+ if (!normalized || !normalized.startsWith('# Background')) return null;
1128
+ const match = normalized.match(/## Request Brief[\s\S]*?```(?:text)?\s*\n([\s\S]*?)```/i);
1129
+ return normalizeText(match?.[1], null);
1130
+ }
1131
+
1132
+ function messageText(message = {}) {
1133
+ if (typeof message?.content === 'string') return normalizeText(message.content, '');
1134
+ if (Array.isArray(message?.content)) {
1135
+ return message.content
1136
+ .map((item) => {
1137
+ if (typeof item === 'string') return item;
1138
+ if (item && typeof item === 'object') return item.text || item.content || '';
1139
+ return '';
1140
+ })
1141
+ .filter(Boolean)
1142
+ .join('\n\n')
1143
+ .trim();
1144
+ }
1145
+ return '';
1146
+ }
1147
+
1148
+ function containsConversationEndToken(text = '') {
1149
+ return /\[\[request_conversation_end\]\]/i.test(String(text || ''));
1150
+ }
1151
+
1152
+ function buildLocalTranscriptTurn({
1153
+ line,
1154
+ seq,
1155
+ text,
1156
+ fromAgentId,
1157
+ role,
1158
+ source,
1159
+ suffix = '',
1160
+ } = {}) {
1161
+ const turnId = normalizeText(line.id, null) || `local_${seq}${suffix}`;
1162
+ return {
1163
+ turnId,
1164
+ seq,
1165
+ fromAgentId: normalizeText(fromAgentId, null),
1166
+ role,
1167
+ source,
1168
+ createdAt: normalizeText(line.timestamp, null)
1169
+ || (Number.isFinite(Number(line.message?.timestamp)) ? new Date(Number(line.message.timestamp)).toISOString() : null)
1170
+ || new Date().toISOString(),
1171
+ content: [{ type: 'text', text }],
1172
+ };
1173
+ }
1174
+
1175
+ async function readLocalTranscriptTurns({
1176
+ transcriptPath = null,
1177
+ manifest = {},
1178
+ viewerAgentId = null,
1179
+ } = {}) {
1180
+ const normalizedPath = normalizeText(transcriptPath, null);
1181
+ if (!normalizedPath) return null;
1182
+ let raw = '';
1183
+ try {
1184
+ raw = await fs.readFile(normalizedPath, 'utf8');
1185
+ } catch {
1186
+ return null;
1187
+ }
1188
+ const peerAgentId = normalizeText(manifest.counterpartyAgentId, 'peer');
1189
+ const turns = [];
1190
+ let seq = 1;
1191
+ let sawNoReply = false;
1192
+ let selfEndRequested = false;
1193
+ let peerEndRequested = false;
1194
+ for (const row of raw.split(/\n+/)) {
1195
+ const lineText = row.trim();
1196
+ if (!lineText) continue;
1197
+ let line = null;
1198
+ try {
1199
+ line = JSON.parse(lineText);
1200
+ } catch {
1201
+ continue;
1202
+ }
1203
+ if (line?.type !== 'message') continue;
1204
+ const message = line.message && typeof line.message === 'object' && !Array.isArray(line.message)
1205
+ ? line.message
1206
+ : {};
1207
+ const text = messageText(message);
1208
+ if (!text) continue;
1209
+
1210
+ const brief = extractRequestBrief(text);
1211
+ if (brief) {
1212
+ const requesterIsSelf = normalizeText(manifest.role, null) === 'requester';
1213
+ const fromAgentId = requesterIsSelf ? viewerAgentId : peerAgentId;
1214
+ turns.push(buildLocalTranscriptTurn({
1215
+ line,
1216
+ seq,
1217
+ text: brief,
1218
+ fromAgentId,
1219
+ role: requesterIsSelf ? 'self' : 'peer',
1220
+ source: 'local_transcript_request_brief',
1221
+ suffix: '_brief',
1222
+ }));
1223
+ seq += 1;
1224
+ if (requesterIsSelf && containsConversationEndToken(brief)) selfEndRequested = true;
1225
+ if (!requesterIsSelf && containsConversationEndToken(brief)) peerEndRequested = true;
1226
+ continue;
1227
+ }
1228
+
1229
+ if (text === 'NO_REPLY') {
1230
+ sawNoReply = true;
1231
+ continue;
1232
+ }
1233
+ const isAssistant = message.role === 'assistant';
1234
+ const fromAgentId = isAssistant ? viewerAgentId : peerAgentId;
1235
+ if (containsConversationEndToken(text)) {
1236
+ if (isAssistant) selfEndRequested = true;
1237
+ else peerEndRequested = true;
1238
+ }
1239
+ turns.push(buildLocalTranscriptTurn({
1240
+ line,
1241
+ seq,
1242
+ text,
1243
+ fromAgentId,
1244
+ role: isAssistant ? 'self' : 'peer',
1245
+ source: 'local_transcript',
1246
+ }));
1247
+ seq += 1;
1248
+ }
1249
+ return {
1250
+ transcriptPath: normalizedPath,
1251
+ turns,
1252
+ terminal: sawNoReply || (selfEndRequested && peerEndRequested),
1253
+ sawNoReply,
1254
+ };
1255
+ }
1256
+
1257
+ async function loadLocalTranscriptSnapshot({
1258
+ manifest = {},
1259
+ env = null,
1260
+ viewerAgentId = null,
1261
+ } = {}) {
1262
+ const stateRoot = resolveConversationViewerOpenClawRoot({ env });
1263
+ const candidateKeys = buildLocalSessionKeyCandidates(manifest);
1264
+ const localAgentIds = extractLocalAgentIdsFromManifest(manifest);
1265
+ for (const localAgentId of localAgentIds) {
1266
+ const sessionStorePath = path.join(stateRoot, 'agents', localAgentId, 'sessions', 'sessions.json');
1267
+ const store = await readJsonIfPresent(sessionStorePath);
1268
+ const match = resolveSessionStoreEntry(store, candidateKeys, manifest);
1269
+ if (!match) continue;
1270
+ const transcriptPath = resolveTranscriptPathFromSessionRecord({
1271
+ record: match.record,
1272
+ sessionStorePath,
1273
+ });
1274
+ const transcript = await readLocalTranscriptTurns({
1275
+ transcriptPath,
1276
+ manifest,
1277
+ viewerAgentId,
1278
+ });
1279
+ if (transcript && transcript.turns.length > 0) {
1280
+ return {
1281
+ ...transcript,
1282
+ sessionStorePath,
1283
+ sessionKey: match.key,
1284
+ sessionId: normalizeText(match.record?.sessionId, null),
1285
+ };
1286
+ }
1287
+ }
1288
+ return null;
1289
+ }
1290
+
1291
+ function inferLocalTranscriptStatus(status, localTranscript = null) {
1292
+ const normalized = normalizeText(status, null);
1293
+ if (['ended', 'closed', 'expired', 'rejected', 'silent', 'kickoff_failed', 'failed'].includes(normalized)) {
1294
+ return normalized;
1295
+ }
1296
+ if (localTranscript?.terminal) return 'ended';
1297
+ if (localTranscript?.turns?.length) return 'active';
1298
+ return normalized || 'pending';
1299
+ }
1300
+
1003
1301
  async function maybeUpdateManifestFromSnapshot(manifest = {}, patch = {}) {
1004
1302
  const nextConversationKey = normalizeText(patch.conversationKey, normalizeText(manifest.conversationKey, null));
1005
1303
  const nextLocalSessionKey = normalizeText(patch.localSessionKey, normalizeText(manifest.localSessionKey, null));
@@ -1024,46 +1322,52 @@ export async function loadConversationViewerSnapshot({
1024
1322
  runtimeConfig,
1025
1323
  fetchImpl = globalThis.fetch,
1026
1324
  timeoutMs = DEFAULT_SNAPSHOT_FETCH_TIMEOUT_MS,
1325
+ env = null,
1027
1326
  } = {}) {
1028
1327
  const fetchedAt = new Date().toISOString();
1029
1328
  const diagnostics = [];
1030
1329
  const viewerAgentId = normalizeText(manifest.viewerAgentId, runtimeConfig?.relay?.agentId || null);
1031
- const inboxQuery = queryString({
1032
- agentId: viewerAgentId,
1033
- chatRequestId: manifest.chatRequestId,
1034
- conversationKey: manifest.conversationKey,
1035
- localSessionKey: manifest.localSessionKey,
1036
- });
1037
- let inboxResult = await fetchBackendJson(fetchImpl, runtimeConfig, `/v1/chat-requests${inboxQuery}`, { timeoutMs });
1038
- if (!inboxResult.ok && inboxQuery) {
1039
- diagnostics.push({
1040
- source: 'chat_requests_filtered',
1041
- status: inboxResult.status,
1042
- error: inboxResult.error || null,
1043
- path: inboxResult.path || null,
1330
+ let inbox = {};
1331
+ let request = null;
1332
+ let chat = null;
1333
+ let conversationKey = normalizeText(manifest.conversationKey, null);
1334
+ if (!conversationKey) {
1335
+ const inboxQuery = queryString({
1336
+ agentId: viewerAgentId,
1337
+ chatRequestId: manifest.chatRequestId,
1338
+ localSessionKey: manifest.localSessionKey,
1044
1339
  });
1045
- const fallbackQuery = queryString({ agentId: viewerAgentId });
1046
- inboxResult = await fetchBackendJson(fetchImpl, runtimeConfig, `/v1/chat-requests${fallbackQuery}`, { timeoutMs });
1047
- }
1048
- if (!inboxResult.ok) {
1049
- return {
1050
- ok: false,
1051
- error: 'chat_inbox_snapshot_failed',
1052
- status: inboxResult.status,
1053
- message: inboxResult.error || null,
1054
- fetchedAt,
1055
- viewer: buildPublicConversationViewer(manifest),
1056
- body: inboxResult.body,
1057
- diagnostics,
1058
- };
1340
+ let inboxResult = await fetchBackendJson(fetchImpl, runtimeConfig, `/v1/chat-requests${inboxQuery}`, { timeoutMs });
1341
+ if (!inboxResult.ok && inboxQuery) {
1342
+ diagnostics.push({
1343
+ source: 'chat_requests_filtered',
1344
+ status: inboxResult.status,
1345
+ error: inboxResult.error || null,
1346
+ path: inboxResult.path || null,
1347
+ });
1348
+ const fallbackQuery = queryString({ agentId: viewerAgentId });
1349
+ inboxResult = await fetchBackendJson(fetchImpl, runtimeConfig, `/v1/chat-requests${fallbackQuery}`, { timeoutMs });
1350
+ }
1351
+ if (!inboxResult.ok) {
1352
+ return {
1353
+ ok: false,
1354
+ error: 'chat_inbox_snapshot_failed',
1355
+ status: inboxResult.status,
1356
+ message: inboxResult.error || null,
1357
+ fetchedAt,
1358
+ viewer: buildPublicConversationViewer(manifest),
1359
+ body: inboxResult.body,
1360
+ diagnostics,
1361
+ };
1362
+ }
1363
+ inbox = normalizeObject(inboxResult.body, {});
1364
+ request = resolveSnapshotRequest(inbox, manifest);
1365
+ chat = resolveSnapshotChat(inbox, manifest);
1366
+ conversationKey = normalizeText(
1367
+ chat?.conversationKey,
1368
+ normalizeText(request?.kickoff?.conversationKey, null),
1369
+ );
1059
1370
  }
1060
- const inbox = normalizeObject(inboxResult.body, {});
1061
- const request = resolveSnapshotRequest(inbox, manifest);
1062
- const chat = resolveSnapshotChat(inbox, manifest);
1063
- const conversationKey = normalizeText(
1064
- chat?.conversationKey,
1065
- normalizeText(manifest.conversationKey, normalizeText(request?.kickoff?.conversationKey, null)),
1066
- );
1067
1371
  let activeManifest = manifest;
1068
1372
  if (conversationKey) {
1069
1373
  activeManifest = await maybeUpdateManifestFromSnapshot(manifest, {
@@ -1074,6 +1378,7 @@ export async function loadConversationViewerSnapshot({
1074
1378
  let conversation = null;
1075
1379
  let participants = [];
1076
1380
  let turns = [];
1381
+ let localTranscript = null;
1077
1382
  if (conversationKey) {
1078
1383
  const suffix = queryString({ agentId: viewerAgentId });
1079
1384
  const [conversationResult, turnsResult, participantsResult] = await Promise.all([
@@ -1106,11 +1411,46 @@ export async function loadConversationViewerSnapshot({
1106
1411
  });
1107
1412
  }
1108
1413
  }
1414
+ if (turns.length === 0) {
1415
+ localTranscript = await loadLocalTranscriptSnapshot({
1416
+ manifest: activeManifest,
1417
+ env,
1418
+ viewerAgentId,
1419
+ });
1420
+ if (localTranscript?.turns?.length) {
1421
+ turns = localTranscript.turns;
1422
+ diagnostics.push({
1423
+ source: 'local_transcript',
1424
+ status: 200,
1425
+ path: localTranscript.transcriptPath,
1426
+ sessionKey: localTranscript.sessionKey || null,
1427
+ sessionId: localTranscript.sessionId || null,
1428
+ });
1429
+ }
1430
+ }
1431
+ if (participants.length === 0 && localTranscript?.turns?.length) {
1432
+ participants = [
1433
+ {
1434
+ agentId: viewerAgentId,
1435
+ displayName: 'You',
1436
+ },
1437
+ {
1438
+ agentId: normalizeText(activeManifest.counterpartyAgentId, 'peer'),
1439
+ displayName: normalizeText(activeManifest.counterpartyName, 'Peer agent'),
1440
+ },
1441
+ ];
1442
+ }
1109
1443
  const peer = normalizeObject(chat?.counterparty, null)
1110
1444
  || normalizeObject(request?.counterparty, null)
1111
1445
  || null;
1112
1446
  const peerName = normalizeText(peer?.displayName, normalizeText(activeManifest.counterpartyName, 'Peer agent'));
1113
- const status = resolveSnapshotStatus({ request, chat, conversation });
1447
+ const resolvedStatus = resolveSnapshotStatus({
1448
+ request,
1449
+ chat,
1450
+ conversation,
1451
+ fallbackStatus: activeManifest.targetStatus,
1452
+ });
1453
+ const status = inferLocalTranscriptStatus(resolvedStatus, localTranscript);
1114
1454
  const worldName = normalizeText(chat?.conversation?.world?.displayName, normalizeText(request?.conversation?.world?.displayName, null));
1115
1455
  return {
1116
1456
  ok: true,
@@ -1141,6 +1481,12 @@ export async function loadConversationViewerSnapshot({
1141
1481
  counts: inbox.counts || null,
1142
1482
  filters: inbox.filters || null,
1143
1483
  },
1484
+ localTranscript: localTranscript ? {
1485
+ path: localTranscript.transcriptPath,
1486
+ sessionKey: localTranscript.sessionKey || null,
1487
+ sessionId: localTranscript.sessionId || null,
1488
+ terminal: localTranscript.terminal === true,
1489
+ } : null,
1144
1490
  diagnostics,
1145
1491
  };
1146
1492
  }
@@ -1176,6 +1522,7 @@ export function buildConversationViewerRoute(plugin, {
1176
1522
  manifest,
1177
1523
  runtimeConfig,
1178
1524
  fetchImpl,
1525
+ env,
1179
1526
  });
1180
1527
  return sendJson(res, snapshot.ok === false ? 502 : 200, snapshot);
1181
1528
  } catch (error) {