@xfxstudio/claworld 2026.6.10-testing.4 → 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.4",
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.4",
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
 
@@ -1008,6 +1010,294 @@ function collectTurns(turnsPayload = {}) {
1008
1010
  });
1009
1011
  }
1010
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
+
1011
1301
  async function maybeUpdateManifestFromSnapshot(manifest = {}, patch = {}) {
1012
1302
  const nextConversationKey = normalizeText(patch.conversationKey, normalizeText(manifest.conversationKey, null));
1013
1303
  const nextLocalSessionKey = normalizeText(patch.localSessionKey, normalizeText(manifest.localSessionKey, null));
@@ -1032,6 +1322,7 @@ export async function loadConversationViewerSnapshot({
1032
1322
  runtimeConfig,
1033
1323
  fetchImpl = globalThis.fetch,
1034
1324
  timeoutMs = DEFAULT_SNAPSHOT_FETCH_TIMEOUT_MS,
1325
+ env = null,
1035
1326
  } = {}) {
1036
1327
  const fetchedAt = new Date().toISOString();
1037
1328
  const diagnostics = [];
@@ -1087,6 +1378,7 @@ export async function loadConversationViewerSnapshot({
1087
1378
  let conversation = null;
1088
1379
  let participants = [];
1089
1380
  let turns = [];
1381
+ let localTranscript = null;
1090
1382
  if (conversationKey) {
1091
1383
  const suffix = queryString({ agentId: viewerAgentId });
1092
1384
  const [conversationResult, turnsResult, participantsResult] = await Promise.all([
@@ -1119,16 +1411,46 @@ export async function loadConversationViewerSnapshot({
1119
1411
  });
1120
1412
  }
1121
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
+ }
1122
1443
  const peer = normalizeObject(chat?.counterparty, null)
1123
1444
  || normalizeObject(request?.counterparty, null)
1124
1445
  || null;
1125
1446
  const peerName = normalizeText(peer?.displayName, normalizeText(activeManifest.counterpartyName, 'Peer agent'));
1126
- const status = resolveSnapshotStatus({
1447
+ const resolvedStatus = resolveSnapshotStatus({
1127
1448
  request,
1128
1449
  chat,
1129
1450
  conversation,
1130
1451
  fallbackStatus: activeManifest.targetStatus,
1131
1452
  });
1453
+ const status = inferLocalTranscriptStatus(resolvedStatus, localTranscript);
1132
1454
  const worldName = normalizeText(chat?.conversation?.world?.displayName, normalizeText(request?.conversation?.world?.displayName, null));
1133
1455
  return {
1134
1456
  ok: true,
@@ -1159,6 +1481,12 @@ export async function loadConversationViewerSnapshot({
1159
1481
  counts: inbox.counts || null,
1160
1482
  filters: inbox.filters || null,
1161
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,
1162
1490
  diagnostics,
1163
1491
  };
1164
1492
  }
@@ -1194,6 +1522,7 @@ export function buildConversationViewerRoute(plugin, {
1194
1522
  manifest,
1195
1523
  runtimeConfig,
1196
1524
  fetchImpl,
1525
+ env,
1197
1526
  });
1198
1527
  return sendJson(res, snapshot.ok === false ? 502 : 200, snapshot);
1199
1528
  } catch (error) {