@xfxstudio/claworld 2026.7.13-testing.4 → 2026.7.14-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.
- package/README.md +27 -18
- package/openclaw.plugin.json +2 -3
- package/package.json +1 -2
- package/skills/claworld-help/SKILL.md +30 -10
- package/skills/claworld-main-session/SKILL.md +23 -8
- package/skills/claworld-management-session/SKILL.md +45 -47
- package/src/openclaw/plugin/claworld-channel-plugin.js +8 -69
- package/src/openclaw/plugin/register.js +3 -144
- package/src/openclaw/plugin/relay-client-shared.js +0 -14
- package/src/openclaw/runtime/tool-inventory.js +0 -1
- package/src/openclaw/runtime/working-memory.js +24 -15
- package/src/openclaw/runtime/transcript-report-comic-grid.js +0 -475
- package/src/openclaw/runtime/transcript-report-stylekit.js +0 -189
- package/src/openclaw/runtime/transcript-report.js +0 -923
|
@@ -1,923 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
CLAWORLD_TRANSCRIPT_STYLE_NAME,
|
|
7
|
-
measureTranscriptItem,
|
|
8
|
-
paginateTranscriptItems,
|
|
9
|
-
renderTranscriptPageSvg,
|
|
10
|
-
} from './transcript-report-comic-grid.js';
|
|
11
|
-
import { displayCols } from './transcript-report-stylekit.js';
|
|
12
|
-
import { appendClaworldJournalEvent } from './working-memory.js';
|
|
13
|
-
|
|
14
|
-
const DEFAULT_WIDTH = 720;
|
|
15
|
-
const DEFAULT_MAX_PAGE_HEIGHT = 2600;
|
|
16
|
-
const TIME_SPLIT_SECONDS = 5 * 60;
|
|
17
|
-
const SESSION_INDEX_RELATIVE_PATH = path.join('.claworld', 'sessions', 'index.json');
|
|
18
|
-
|
|
19
|
-
const TOP_LEVEL_RENDER_FIELDS = new Set(['mode', 'stored', 'manual', 'style', 'maxPageHeight']);
|
|
20
|
-
const MANUAL_RENDER_FIELDS = new Set(['messages', 'title', 'peerProfile', 'localLabel', 'peerLabel']);
|
|
21
|
-
const STORED_RENDER_FIELDS = new Set(['chatRequestId', 'title', 'peerProfile', 'localLabel', 'peerLabel']);
|
|
22
|
-
const MANUAL_MESSAGE_FIELDS = new Set(['from', 'text', 'createdAt']);
|
|
23
|
-
|
|
24
|
-
const OPERATIONAL_NOTICE_PATTERNS = [
|
|
25
|
-
/^🧭\s*New session:\s+\S+/iu,
|
|
26
|
-
/^🧹\s*Auto-compaction complete(?:\s*\(count \d+\))?\.$/iu,
|
|
27
|
-
/^↪️?\s*Model Fallback:/iu,
|
|
28
|
-
/^↪️?\s*Model Fallback cleared:/iu,
|
|
29
|
-
/^⚠️?\s*Agent failed before reply:/iu,
|
|
30
|
-
/^Sent the (?:reply|opener|Claworld reply)\.?$/iu,
|
|
31
|
-
/^◐\s*Session automatically reset\b/iu,
|
|
32
|
-
];
|
|
33
|
-
const RUNTIME_ERROR_PATTERNS = [
|
|
34
|
-
/^⚠️?\s*Agent failed before reply:/iu,
|
|
35
|
-
/^LLM request failed:/iu,
|
|
36
|
-
/^LLM request timed out\.$/iu,
|
|
37
|
-
/^LLM request unauthorized\.$/iu,
|
|
38
|
-
/^The AI service is temporarily overloaded\.$/iu,
|
|
39
|
-
/^The AI service returned an error\.$/iu,
|
|
40
|
-
/^⚠️?\s*API rate limit reached\.$/iu,
|
|
41
|
-
/^⚠️?\s*.+\s+returned a billing error\b/iu,
|
|
42
|
-
];
|
|
43
|
-
const OPERATIONAL_SUFFIX_PATTERNS = [
|
|
44
|
-
/^Usage:\s+.+\s+in\s+\/\s+.+\s+out(?:\s+·\s+est\s+.+)?$/iu,
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
function text(value, fallback = null) {
|
|
48
|
-
if (value == null) return fallback;
|
|
49
|
-
const normalized = String(value).trim();
|
|
50
|
-
return normalized || fallback;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isObject(value) {
|
|
54
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function compactObject(value = {}) {
|
|
58
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => (
|
|
59
|
-
entry != null && entry !== '' && (!Array.isArray(entry) || entry.length > 0)
|
|
60
|
-
)));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isoNow() {
|
|
64
|
-
return new Date().toISOString();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function readJsonObject(filePath, fallback = {}) {
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
70
|
-
return isObject(parsed) ? parsed : fallback;
|
|
71
|
-
} catch {
|
|
72
|
-
return fallback;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function atomicWriteText(filePath, content) {
|
|
77
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
78
|
-
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`);
|
|
79
|
-
try {
|
|
80
|
-
await fs.writeFile(temporaryPath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
|
|
81
|
-
await fs.rename(temporaryPath, filePath);
|
|
82
|
-
} finally {
|
|
83
|
-
await fs.rm(temporaryPath, { force: true }).catch(() => {});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function atomicWriteJson(filePath, payload) {
|
|
88
|
-
await atomicWriteText(filePath, JSON.stringify(payload, null, 2));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function sessionIndexPath(workspaceRoot) {
|
|
92
|
-
return path.join(workspaceRoot, SESSION_INDEX_RELATIVE_PATH);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function emptySessionIndex() {
|
|
96
|
-
const now = isoNow();
|
|
97
|
-
return {
|
|
98
|
-
schema: 'claworld.sessions.v1',
|
|
99
|
-
version: 1,
|
|
100
|
-
createdAt: now,
|
|
101
|
-
updatedAt: now,
|
|
102
|
-
main: {},
|
|
103
|
-
management: {},
|
|
104
|
-
conversationSessions: {},
|
|
105
|
-
conversationEpisodes: {},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function readSessionIndex(workspaceRoot) {
|
|
110
|
-
const index = await readJsonObject(sessionIndexPath(workspaceRoot), emptySessionIndex());
|
|
111
|
-
if (!isObject(index.main)) index.main = {};
|
|
112
|
-
if (!isObject(index.management)) index.management = {};
|
|
113
|
-
if (!isObject(index.conversationSessions)) index.conversationSessions = {};
|
|
114
|
-
if (!isObject(index.conversationEpisodes)) index.conversationEpisodes = {};
|
|
115
|
-
return index;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function appendUniqueDelivery(deliveries, delivery) {
|
|
119
|
-
const deliveryId = text(delivery?.deliveryId, null);
|
|
120
|
-
if (!deliveryId || (!text(delivery?.commandText, null) && !text(delivery?.contextText, null))) return deliveries;
|
|
121
|
-
const next = Array.isArray(deliveries) ? [...deliveries] : [];
|
|
122
|
-
const existingIndex = next.findIndex((item) => text(item?.deliveryId, null) === deliveryId);
|
|
123
|
-
if (existingIndex >= 0) {
|
|
124
|
-
next[existingIndex] = compactObject({ ...next[existingIndex], ...delivery });
|
|
125
|
-
} else {
|
|
126
|
-
next.push(compactObject(delivery));
|
|
127
|
-
}
|
|
128
|
-
return next;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function recordClaworldTranscriptEpisode(workspaceRoot, input = {}) {
|
|
132
|
-
const chatRequestId = text(input.chatRequestId, null);
|
|
133
|
-
const deliveryId = text(input.deliveryId, null);
|
|
134
|
-
const commandText = text(input.commandText, null);
|
|
135
|
-
const contextText = text(input.contextText, null);
|
|
136
|
-
if (!workspaceRoot || !chatRequestId || !deliveryId || (!commandText && !contextText)) {
|
|
137
|
-
return { ok: false, updated: false, reason: 'missing_transcript_identity' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const index = await readSessionIndex(workspaceRoot);
|
|
141
|
-
const previous = isObject(index.conversationEpisodes[chatRequestId])
|
|
142
|
-
? index.conversationEpisodes[chatRequestId]
|
|
143
|
-
: {};
|
|
144
|
-
let deliveries = appendUniqueDelivery(previous.deliveries, {
|
|
145
|
-
deliveryId,
|
|
146
|
-
direction: 'inbound',
|
|
147
|
-
fromAgentId: text(input.fromAgentId, null),
|
|
148
|
-
fromAgentCode: text(input.fromAgentCode, null),
|
|
149
|
-
fromDisplayIdentity: text(input.fromDisplayIdentity, null),
|
|
150
|
-
deliveryType: text(input.deliveryType, null),
|
|
151
|
-
commandText,
|
|
152
|
-
contextText,
|
|
153
|
-
untrustedContext: text(input.untrustedContext, null),
|
|
154
|
-
createdAt: text(input.createdAt, null),
|
|
155
|
-
turnCreatedAt: text(input.turnCreatedAt, null),
|
|
156
|
-
});
|
|
157
|
-
const replyText = text(input.replyText, null);
|
|
158
|
-
if (replyText) {
|
|
159
|
-
deliveries = appendUniqueDelivery(deliveries, {
|
|
160
|
-
deliveryId: `${deliveryId}:reply`,
|
|
161
|
-
direction: 'outbound',
|
|
162
|
-
deliveryType: 'reply',
|
|
163
|
-
fromAgentId: text(input.localAgentId, null),
|
|
164
|
-
commandText: replyText,
|
|
165
|
-
createdAt: text(input.replyCreatedAt, isoNow()),
|
|
166
|
-
turnCreatedAt: text(input.replyCreatedAt, isoNow()),
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const now = isoNow();
|
|
171
|
-
const deliveryIds = deliveries.map((entry) => text(entry?.deliveryId, null)).filter(Boolean);
|
|
172
|
-
index.conversationEpisodes[chatRequestId] = compactObject({
|
|
173
|
-
...previous,
|
|
174
|
-
chatRequestId,
|
|
175
|
-
chatId: text(input.chatId, previous.chatId),
|
|
176
|
-
lastActiveSessionKey: text(input.localSessionKey, previous.lastActiveSessionKey),
|
|
177
|
-
relaySessionKey: text(input.relaySessionKey, previous.relaySessionKey),
|
|
178
|
-
conversationKey: text(input.conversationKey, previous.conversationKey),
|
|
179
|
-
worldId: text(input.worldId, previous.worldId),
|
|
180
|
-
targetAgentId: text(input.targetAgentId, previous.targetAgentId),
|
|
181
|
-
fromAgentCode: text(input.fromAgentCode, previous.fromAgentCode),
|
|
182
|
-
fromDisplayIdentity: text(input.fromDisplayIdentity, previous.fromDisplayIdentity),
|
|
183
|
-
firstSeenAt: text(previous.firstSeenAt, text(input.createdAt, now)),
|
|
184
|
-
lastSeenAt: replyText
|
|
185
|
-
? text(input.replyCreatedAt, now)
|
|
186
|
-
: text(input.turnCreatedAt, text(input.createdAt, now)),
|
|
187
|
-
deliveryIds,
|
|
188
|
-
deliveryCount: deliveryIds.length,
|
|
189
|
-
deliveries,
|
|
190
|
-
updatedAt: now,
|
|
191
|
-
});
|
|
192
|
-
index.updatedAt = now;
|
|
193
|
-
await atomicWriteJson(sessionIndexPath(workspaceRoot), index);
|
|
194
|
-
return { ok: true, updated: true, chatRequestId, deliveryCount: deliveryIds.length };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function stripOperationalSuffix(content) {
|
|
198
|
-
const lines = String(content ?? '').split(/\r?\n/);
|
|
199
|
-
while (lines.length) {
|
|
200
|
-
const lastLine = String(lines.at(-1) ?? '').trim();
|
|
201
|
-
if (!lastLine) {
|
|
202
|
-
lines.pop();
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (!OPERATIONAL_SUFFIX_PATTERNS.some((pattern) => pattern.test(lastLine))) break;
|
|
206
|
-
lines.pop();
|
|
207
|
-
}
|
|
208
|
-
return lines.join('\n').trim();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function classifyReplyContent(content) {
|
|
212
|
-
const rawText = String(content ?? '');
|
|
213
|
-
const normalized = stripOperationalSuffix(rawText);
|
|
214
|
-
if (!normalized) return { text: '', silenceReason: rawText.trim() ? 'operational_notice_only' : 'empty_reply' };
|
|
215
|
-
if (normalized === 'NO_REPLY') return { text: '', silenceReason: 'no_reply' };
|
|
216
|
-
if (RUNTIME_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
217
|
-
return { text: '', silenceReason: 'runtime_failed_before_reply' };
|
|
218
|
-
}
|
|
219
|
-
if (OPERATIONAL_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
220
|
-
return { text: '', silenceReason: 'operational_notice_only' };
|
|
221
|
-
}
|
|
222
|
-
return { text: normalized, silenceReason: null };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function renderableTranscriptDelivery(delivery) {
|
|
226
|
-
if (!isObject(delivery) || text(delivery.deliveryType, null) === 'kickoff') return false;
|
|
227
|
-
const commandText = text(delivery.commandText, null);
|
|
228
|
-
return Boolean(commandText && !classifyReplyContent(commandText).silenceReason);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function episodeSummary(chatRequestId, entry) {
|
|
232
|
-
const deliveries = Array.isArray(entry.deliveries) ? entry.deliveries : [];
|
|
233
|
-
const renderable = deliveries.filter(renderableTranscriptDelivery);
|
|
234
|
-
const peerMessages = renderable.filter((delivery) => text(delivery.direction, null) !== 'outbound').length;
|
|
235
|
-
return compactObject({
|
|
236
|
-
chatRequestId: text(entry.chatRequestId, chatRequestId),
|
|
237
|
-
chatId: entry.chatId,
|
|
238
|
-
conversationKey: entry.conversationKey,
|
|
239
|
-
relaySessionKey: entry.relaySessionKey,
|
|
240
|
-
lastActiveSessionKey: entry.lastActiveSessionKey,
|
|
241
|
-
worldId: entry.worldId,
|
|
242
|
-
targetAgentId: entry.targetAgentId,
|
|
243
|
-
fromAgentCode: entry.fromAgentCode,
|
|
244
|
-
fromDisplayIdentity: entry.fromDisplayIdentity,
|
|
245
|
-
firstSeenAt: entry.firstSeenAt,
|
|
246
|
-
lastSeenAt: entry.lastSeenAt,
|
|
247
|
-
deliveryCount: entry.deliveryCount,
|
|
248
|
-
renderableMessages: renderable.length,
|
|
249
|
-
peerMessages,
|
|
250
|
-
localMessages: renderable.length - peerMessages,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function localEpisodeSummaries(index) {
|
|
255
|
-
return Object.entries(isObject(index.conversationEpisodes) ? index.conversationEpisodes : {})
|
|
256
|
-
.filter(([, entry]) => isObject(entry))
|
|
257
|
-
.map(([chatRequestId, entry]) => episodeSummary(chatRequestId, entry))
|
|
258
|
-
.sort((left, right) => String(right.lastSeenAt || right.firstSeenAt || '')
|
|
259
|
-
.localeCompare(String(left.lastSeenAt || left.firstSeenAt || '')));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function matchesEpisodeFilters(episode, filters = {}) {
|
|
263
|
-
const checks = {
|
|
264
|
-
chatRequestId: 'chatRequestId',
|
|
265
|
-
conversationKey: 'conversationKey',
|
|
266
|
-
localSessionKey: 'relaySessionKey',
|
|
267
|
-
counterpartyAgentId: 'targetAgentId',
|
|
268
|
-
worldId: 'worldId',
|
|
269
|
-
};
|
|
270
|
-
return Object.entries(checks).every(([filterKey, episodeKey]) => {
|
|
271
|
-
const expected = text(filters[filterKey], null);
|
|
272
|
-
return !expected || text(episode[episodeKey], null) === expected;
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function filterLocalEpisodes(episodes, filters = {}) {
|
|
277
|
-
return episodes.filter((episode) => matchesEpisodeFilters(episode, filters)).slice(0, 25);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function conversationItemFilters(item = {}) {
|
|
281
|
-
const related = isObject(item.relatedObjects) ? item.relatedObjects : {};
|
|
282
|
-
return compactObject({
|
|
283
|
-
chatRequestId: text(item.chatRequestId, text(related.chatRequestId, null)),
|
|
284
|
-
conversationKey: text(item.conversationKey, text(related.conversationKey, null)),
|
|
285
|
-
localSessionKey: text(item.localSessionKey, text(related.localSessionKey, null)),
|
|
286
|
-
counterpartyAgentId: text(item.counterpartyAgentId, text(related.counterpartyAgentId, null)),
|
|
287
|
-
worldId: text(item.worldId, text(related.worldId, null)),
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export async function augmentConversationPayloadWithLocalTranscriptIndex({
|
|
292
|
-
workspaceRoot,
|
|
293
|
-
payload,
|
|
294
|
-
filters = {},
|
|
295
|
-
} = {}) {
|
|
296
|
-
if (!workspaceRoot || !isObject(payload)) return payload;
|
|
297
|
-
const index = await readSessionIndex(workspaceRoot);
|
|
298
|
-
const episodes = localEpisodeSummaries(index);
|
|
299
|
-
const matching = filterLocalEpisodes(episodes, filters);
|
|
300
|
-
const result = { ...payload };
|
|
301
|
-
if (matching.length) {
|
|
302
|
-
result.localTranscriptEpisodes = matching;
|
|
303
|
-
result.localTranscriptSummary = {
|
|
304
|
-
episodeCount: matching.length,
|
|
305
|
-
chatRequestIds: matching.map((item) => item.chatRequestId).filter(Boolean),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
if (Array.isArray(result.items)) {
|
|
309
|
-
result.items = result.items.map((item) => {
|
|
310
|
-
if (!isObject(item)) return item;
|
|
311
|
-
const itemFilters = conversationItemFilters(item);
|
|
312
|
-
if (!Object.keys(itemFilters).length) return item;
|
|
313
|
-
const itemMatches = filterLocalEpisodes(episodes, itemFilters);
|
|
314
|
-
if (!itemMatches.length) return item;
|
|
315
|
-
return {
|
|
316
|
-
...item,
|
|
317
|
-
localTranscriptEpisodes: itemMatches,
|
|
318
|
-
localTranscriptSummary: {
|
|
319
|
-
episodeCount: itemMatches.length,
|
|
320
|
-
chatRequestIds: itemMatches.map((match) => match.chatRequestId).filter(Boolean),
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
return result;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function rejectUnknown(name, value, allowed) {
|
|
329
|
-
const extra = Object.keys(value).filter((key) => !allowed.has(key)).sort();
|
|
330
|
-
if (extra.length) throw new Error(`unsupported ${name} parameter(s): ${extra.join(', ')}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function normalizeRenderRequest(args = {}) {
|
|
334
|
-
if (!isObject(args)) throw new Error('render arguments must be an object');
|
|
335
|
-
rejectUnknown('transcript render', args, TOP_LEVEL_RENDER_FIELDS);
|
|
336
|
-
const mode = text(args.mode, null);
|
|
337
|
-
if (!['stored', 'manual'].includes(mode)) {
|
|
338
|
-
throw new Error('mode is required and must be one of stored or manual');
|
|
339
|
-
}
|
|
340
|
-
const style = text(args.style, CLAWORLD_TRANSCRIPT_STYLE_NAME);
|
|
341
|
-
if (style !== CLAWORLD_TRANSCRIPT_STYLE_NAME) {
|
|
342
|
-
throw new Error(`unsupported transcript report style: ${style}; expected ${CLAWORLD_TRANSCRIPT_STYLE_NAME}`);
|
|
343
|
-
}
|
|
344
|
-
const renderArgs = { mode, style, maxPageHeight: args.maxPageHeight };
|
|
345
|
-
if (mode === 'stored') {
|
|
346
|
-
if (args.manual != null) throw new Error('manual must not be provided when mode=stored');
|
|
347
|
-
if (!isObject(args.stored)) throw new Error('stored must be an object when mode=stored');
|
|
348
|
-
rejectUnknown('stored', args.stored, STORED_RENDER_FIELDS);
|
|
349
|
-
const chatRequestId = text(args.stored.chatRequestId, null);
|
|
350
|
-
if (!chatRequestId) throw new Error('stored.chatRequestId is required when mode=stored');
|
|
351
|
-
for (const key of ['title', 'peerProfile', 'localLabel', 'peerLabel']) {
|
|
352
|
-
renderArgs[key] = args.stored[key];
|
|
353
|
-
}
|
|
354
|
-
return { mode, chatRequestId, renderArgs };
|
|
355
|
-
}
|
|
356
|
-
if (args.stored != null) throw new Error('stored must not be provided when mode=manual');
|
|
357
|
-
if (!isObject(args.manual)) throw new Error('manual must be an object when mode=manual');
|
|
358
|
-
rejectUnknown('manual', args.manual, MANUAL_RENDER_FIELDS);
|
|
359
|
-
for (const key of ['title', 'peerProfile', 'localLabel', 'peerLabel']) {
|
|
360
|
-
if (!text(args.manual[key], null)) throw new Error(`manual.${key} is required when mode=manual`);
|
|
361
|
-
renderArgs[key] = args.manual[key];
|
|
362
|
-
}
|
|
363
|
-
if (!Array.isArray(args.manual.messages) || !args.manual.messages.length) {
|
|
364
|
-
throw new Error('manual.messages must be a non-empty array when mode=manual');
|
|
365
|
-
}
|
|
366
|
-
args.manual.messages.forEach((message, index) => {
|
|
367
|
-
const position = index + 1;
|
|
368
|
-
if (!isObject(message)) throw new Error(`manual.messages[${position}] must be an object`);
|
|
369
|
-
rejectUnknown(`manual.messages[${position}]`, message, MANUAL_MESSAGE_FIELDS);
|
|
370
|
-
if (!['peer', 'local'].includes(text(message.from, null))) {
|
|
371
|
-
throw new Error(`manual.messages[${position}].from must be peer or local`);
|
|
372
|
-
}
|
|
373
|
-
if (!text(message.text, null)) throw new Error(`manual.messages[${position}].text is required`);
|
|
374
|
-
if (!text(message.createdAt, null)) throw new Error(`manual.messages[${position}].createdAt is required`);
|
|
375
|
-
});
|
|
376
|
-
return { mode, messages: args.manual.messages, renderArgs };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function loadSourceMessages(request, workspaceRoot) {
|
|
380
|
-
if (request.mode === 'manual') {
|
|
381
|
-
return { messages: request.messages, summary: { kind: 'manual', messageCount: request.messages.length } };
|
|
382
|
-
}
|
|
383
|
-
const index = await readSessionIndex(workspaceRoot);
|
|
384
|
-
const episode = isObject(index.conversationEpisodes?.[request.chatRequestId])
|
|
385
|
-
? index.conversationEpisodes[request.chatRequestId]
|
|
386
|
-
: null;
|
|
387
|
-
if (!episode) {
|
|
388
|
-
throw new Error(`chatRequestId was not found in local Claworld transcript index: ${request.chatRequestId}`);
|
|
389
|
-
}
|
|
390
|
-
const deliveries = Array.isArray(episode.deliveries) ? episode.deliveries : [];
|
|
391
|
-
if (!deliveries.length) {
|
|
392
|
-
throw new Error(`chatRequestId was found but no deliveries were indexed: ${request.chatRequestId}`);
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
messages: deliveries,
|
|
396
|
-
summary: compactObject({
|
|
397
|
-
kind: 'chatRequestId',
|
|
398
|
-
chatRequestId: request.chatRequestId,
|
|
399
|
-
chatId: episode.chatId,
|
|
400
|
-
conversationKey: episode.conversationKey,
|
|
401
|
-
relaySessionKey: episode.relaySessionKey,
|
|
402
|
-
lastActiveSessionKey: episode.lastActiveSessionKey,
|
|
403
|
-
firstSeenAt: episode.firstSeenAt,
|
|
404
|
-
lastSeenAt: episode.lastSeenAt,
|
|
405
|
-
indexSource: 'conversationEpisodes',
|
|
406
|
-
}),
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function markdownSection(value, title, level) {
|
|
411
|
-
const hashes = '#'.repeat(level);
|
|
412
|
-
const pattern = new RegExp(`^${hashes}\\s+${title}\\s*$`, 'imu');
|
|
413
|
-
const match = pattern.exec(value);
|
|
414
|
-
if (!match) return '';
|
|
415
|
-
const start = match.index + match[0].length;
|
|
416
|
-
const remainder = value.slice(start);
|
|
417
|
-
const nextHeading = new RegExp(`^#{1,${level}}\\s+`, 'mu').exec(remainder);
|
|
418
|
-
return remainder.slice(0, nextHeading?.index ?? remainder.length).trim();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function markdownNamedCodeBlock(value, title, level) {
|
|
422
|
-
const section = markdownSection(value, title, level);
|
|
423
|
-
if (!section) return '';
|
|
424
|
-
const fenced = /```[^\n]*\n([\s\S]*?)\n```/u.exec(section);
|
|
425
|
-
if (fenced) return fenced[1].trim();
|
|
426
|
-
return section.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#')).join('\n');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function squashWhitespace(value) {
|
|
430
|
-
const lines = String(value ?? '').split(/\r?\n/).map((line) => line.replace(/[ \t]+/gu, ' ').trim());
|
|
431
|
-
const compact = [];
|
|
432
|
-
let blank = false;
|
|
433
|
-
for (const line of lines) {
|
|
434
|
-
if (!line) {
|
|
435
|
-
if (!blank && compact.length) compact.push('');
|
|
436
|
-
blank = true;
|
|
437
|
-
} else {
|
|
438
|
-
compact.push(line);
|
|
439
|
-
blank = false;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return compact.join('\n').trim();
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function parseHeaderContextCandidate(value, source) {
|
|
446
|
-
const content = String(value ?? '').trim();
|
|
447
|
-
if (!content) return {};
|
|
448
|
-
const parsed = {};
|
|
449
|
-
const modeMatch = /^\s*-\s*Mode:\s*`?([A-Za-z_-]+)`?\s*$/imu.exec(content)
|
|
450
|
-
|| /\bconversation[_\s-]*mode\b\s*[:=]\s*`?([A-Za-z_-]+)`?/iu.exec(content);
|
|
451
|
-
const mode = text(modeMatch?.[1]?.toLowerCase(), null);
|
|
452
|
-
if (['world', 'direct'].includes(mode)) parsed.conversationMode = mode;
|
|
453
|
-
const worldMatch = /^\s*-\s*World:\s*([^\n(`]+?)\s*(?:\(`([^`]+)`\))?\s*$/imu.exec(content);
|
|
454
|
-
if (worldMatch) {
|
|
455
|
-
parsed.worldName = text(worldMatch[1], null);
|
|
456
|
-
parsed.worldId = text(worldMatch[2], null);
|
|
457
|
-
}
|
|
458
|
-
const localSection = markdownSection(content, 'You', 2);
|
|
459
|
-
const peerSection = markdownSection(content, 'Peer', 2);
|
|
460
|
-
const identity = (section) => text(/^\s*-\s*Identity:\s*`?([^`\n]+)`?\s*$/imu.exec(section)?.[1], null);
|
|
461
|
-
if (localSection) parsed.localIdentity = identity(localSection);
|
|
462
|
-
if (peerSection) {
|
|
463
|
-
parsed.peerIdentity = identity(peerSection);
|
|
464
|
-
const globalProfile = markdownNamedCodeBlock(peerSection, 'Global Profile', 3);
|
|
465
|
-
const worldProfile = markdownNamedCodeBlock(peerSection, 'World Membership Profile', 3);
|
|
466
|
-
if (globalProfile) {
|
|
467
|
-
parsed.globalProfile = squashWhitespace(globalProfile);
|
|
468
|
-
parsed.globalProfileSource = source;
|
|
469
|
-
}
|
|
470
|
-
if (worldProfile) {
|
|
471
|
-
parsed.worldProfile = squashWhitespace(worldProfile);
|
|
472
|
-
parsed.worldProfileSource = source;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (!localSection && !peerSection && source === 'untrustedContext') {
|
|
476
|
-
const plainProfile = plainProfileCandidate(content);
|
|
477
|
-
if (plainProfile) {
|
|
478
|
-
parsed.globalProfile = plainProfile;
|
|
479
|
-
parsed.globalProfileSource = source;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return compactObject(parsed);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function plainProfileCandidate(value) {
|
|
486
|
-
const candidate = squashWhitespace(value);
|
|
487
|
-
if (!candidate || candidate.includes('# ') || candidate.includes('```')) return '';
|
|
488
|
-
return displayCols(candidate) <= 420 ? candidate : '';
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function headerProfileSourcePriority(source) {
|
|
492
|
-
return { contextText: 4, untrustedContext: 3, rawKickoffText: 2, transcript: 1 }[source] || 0;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function mergeHeaderContextCandidate(merged, candidate, source) {
|
|
496
|
-
if (!candidate) return;
|
|
497
|
-
const parsed = parseHeaderContextCandidate(candidate, source);
|
|
498
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
499
|
-
if (!text(value, null)) continue;
|
|
500
|
-
const sourceKey = {
|
|
501
|
-
globalProfile: 'globalProfileSource',
|
|
502
|
-
worldProfile: 'worldProfileSource',
|
|
503
|
-
globalProfileSource: 'globalProfileSource',
|
|
504
|
-
worldProfileSource: 'worldProfileSource',
|
|
505
|
-
}[key];
|
|
506
|
-
if (!sourceKey) {
|
|
507
|
-
merged[key] = value;
|
|
508
|
-
} else {
|
|
509
|
-
const incomingSource = key.endsWith('Source') ? value : parsed[sourceKey];
|
|
510
|
-
if (headerProfileSourcePriority(incomingSource) >= headerProfileSourcePriority(merged[sourceKey])) {
|
|
511
|
-
merged[key] = value;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function extractTranscriptHeaderContext(rawMessages) {
|
|
518
|
-
const merged = {};
|
|
519
|
-
for (const raw of rawMessages) {
|
|
520
|
-
if (!isObject(raw)) continue;
|
|
521
|
-
mergeHeaderContextCandidate(merged, text(raw.contextText, null), 'contextText');
|
|
522
|
-
mergeHeaderContextCandidate(merged, text(raw.untrustedContext, null), 'untrustedContext');
|
|
523
|
-
if (text(raw.deliveryType, null) === 'kickoff') {
|
|
524
|
-
mergeHeaderContextCandidate(merged, text(raw.commandText, null), 'rawKickoffText');
|
|
525
|
-
}
|
|
526
|
-
if (!merged.peerIdentity && text(raw.fromDisplayIdentity, null)) {
|
|
527
|
-
merged.peerIdentity = text(raw.fromDisplayIdentity, null);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
let peerProfile = '';
|
|
531
|
-
let profileSource = '';
|
|
532
|
-
if (merged.conversationMode === 'world' && merged.worldProfile) {
|
|
533
|
-
peerProfile = merged.worldProfile;
|
|
534
|
-
profileSource = merged.worldProfileSource || 'transcript';
|
|
535
|
-
} else if (merged.conversationMode === 'direct' && merged.globalProfile) {
|
|
536
|
-
peerProfile = merged.globalProfile;
|
|
537
|
-
profileSource = merged.globalProfileSource || 'transcript';
|
|
538
|
-
} else if (merged.worldProfile) {
|
|
539
|
-
peerProfile = merged.worldProfile;
|
|
540
|
-
profileSource = merged.worldProfileSource || 'transcript';
|
|
541
|
-
} else if (merged.globalProfile) {
|
|
542
|
-
peerProfile = merged.globalProfile;
|
|
543
|
-
profileSource = merged.globalProfileSource || 'transcript';
|
|
544
|
-
}
|
|
545
|
-
return compactObject({ ...merged, peerProfile, profileSource });
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function formatTimestamp(value) {
|
|
549
|
-
if (value == null || value === '') return '';
|
|
550
|
-
let date = null;
|
|
551
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
552
|
-
date = new Date(value * 1000);
|
|
553
|
-
} else {
|
|
554
|
-
const parsed = new Date(String(value).trim());
|
|
555
|
-
if (!Number.isNaN(parsed.getTime())) date = parsed;
|
|
556
|
-
}
|
|
557
|
-
if (!date) return String(value).trim();
|
|
558
|
-
const pad = (part) => String(part).padStart(2, '0');
|
|
559
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function parseTimeish(value) {
|
|
563
|
-
if (value == null || value === '') return null;
|
|
564
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
565
|
-
if (/^\d+(?:\.\d+)?$/u.test(String(value).trim())) return Number(value);
|
|
566
|
-
const timestamp = Date.parse(String(value).trim());
|
|
567
|
-
return Number.isNaN(timestamp) ? null : timestamp / 1000;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function extractControlTags(value) {
|
|
571
|
-
const matches = [];
|
|
572
|
-
const claimed = [];
|
|
573
|
-
const patterns = [
|
|
574
|
-
[/\[\[?\s*request[_\s-]*(?:conversation[_\s-]*)?end\s*\]?\]?/giu, 'request end'],
|
|
575
|
-
[/\[\s*requeset\s+end\s*\]/giu, 'request end'],
|
|
576
|
-
[/\[\[?\s*end\s*\]?\]?/giu, 'request end'],
|
|
577
|
-
[/\[\[?\s*like\s*\]?\]?/giu, 'like'],
|
|
578
|
-
[/\[\[?\s*dislike\s*\]?\]?/giu, 'dislike'],
|
|
579
|
-
];
|
|
580
|
-
const record = (start, end, label) => {
|
|
581
|
-
if (claimed.some(([claimedStart, claimedEnd]) => start < claimedEnd && end > claimedStart)) return;
|
|
582
|
-
claimed.push([start, end]);
|
|
583
|
-
matches.push([start, end, label]);
|
|
584
|
-
};
|
|
585
|
-
const source = String(value ?? '');
|
|
586
|
-
for (const [pattern, label] of patterns) {
|
|
587
|
-
for (const match of source.matchAll(pattern)) record(match.index, match.index + match[0].length, label);
|
|
588
|
-
}
|
|
589
|
-
for (const match of source.matchAll(/\[\[\s*([A-Za-z0-9][A-Za-z0-9 _-]{0,24})\s*\]\]/gu)) {
|
|
590
|
-
const label = squashWhitespace(match[1].replaceAll('_', ' ').replaceAll('-', ' ')).toLowerCase().slice(0, 24).trim();
|
|
591
|
-
if (label) record(match.index, match.index + match[0].length, label);
|
|
592
|
-
}
|
|
593
|
-
const sorted = matches.sort((left, right) => left[0] - right[0]);
|
|
594
|
-
const tags = [...new Set(sorted.map(([, , label]) => label))];
|
|
595
|
-
let cursor = 0;
|
|
596
|
-
const pieces = [];
|
|
597
|
-
for (const [start, end] of sorted) {
|
|
598
|
-
pieces.push(source.slice(cursor, start));
|
|
599
|
-
cursor = Math.max(cursor, end);
|
|
600
|
-
}
|
|
601
|
-
pieces.push(source.slice(cursor));
|
|
602
|
-
return { text: squashWhitespace(pieces.join('')), tags };
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function redactText(value) {
|
|
606
|
-
return String(value ?? '')
|
|
607
|
-
.replace(/\b(api[_-]?key|app[_-]?token|access[_-]?token|refresh[_-]?token|secret|password|authorization|bearer)\b\s*[:=]\s*[^\s,;]+/giu, '$1=[redacted]')
|
|
608
|
-
.replace(/\b(?:sk|rk|pk|ghp|glpat)-[A-Za-z0-9_-]{12,}\b/gu, '[redacted-token]')
|
|
609
|
-
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gu, '[redacted-email]')
|
|
610
|
-
.replace(/(?<!\d)(?:\+?\d[\d\s().-]{8,}\d)(?!\d)/gu, '[redacted-phone]');
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function stripInternalMarkup(value) {
|
|
614
|
-
let cleaned = String(value ?? '').replace(/```(?:json|text|markdown)?/giu, '').replaceAll('```', '');
|
|
615
|
-
for (const marker of [
|
|
616
|
-
'Routing metadata:',
|
|
617
|
-
'Claworld live conversation rules:',
|
|
618
|
-
'Backend-authored Claworld context:',
|
|
619
|
-
'Backend-authored Claworld command:',
|
|
620
|
-
'Relay untrusted context:',
|
|
621
|
-
]) {
|
|
622
|
-
if (cleaned.includes(marker)) cleaned = cleaned.split(marker, 1)[0];
|
|
623
|
-
}
|
|
624
|
-
return squashWhitespace(cleaned);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function normalizeMessages(rawMessages, localAgentId, args, headerContext = {}) {
|
|
628
|
-
const localIdentity = text(headerContext.localIdentity, null);
|
|
629
|
-
const peerIdentity = text(headerContext.peerIdentity, text(headerContext.peerId, null));
|
|
630
|
-
const localId = text(localIdentity, text(localAgentId, 'local-agent'));
|
|
631
|
-
const peerId = text(peerIdentity, 'peer-agent');
|
|
632
|
-
const localLabel = publicHeaderValue(args.localLabel) || publicHeaderValue(localIdentity) || 'Me';
|
|
633
|
-
const peerLabel = publicHeaderValue(args.peerLabel) || publicHeaderValue(peerIdentity) || 'Peer';
|
|
634
|
-
const normalized = [];
|
|
635
|
-
rawMessages.forEach((raw, index) => {
|
|
636
|
-
if (!isObject(raw)) return;
|
|
637
|
-
let side;
|
|
638
|
-
let messageText;
|
|
639
|
-
let createdAt;
|
|
640
|
-
let messageId;
|
|
641
|
-
let participantId;
|
|
642
|
-
let participantLabel;
|
|
643
|
-
if (['peer', 'local'].includes(raw.from)) {
|
|
644
|
-
side = raw.from === 'peer' ? 'left' : 'right';
|
|
645
|
-
messageText = text(raw.text, null);
|
|
646
|
-
createdAt = formatTimestamp(raw.createdAt);
|
|
647
|
-
messageId = text(raw.id, `msg-${index + 1}`);
|
|
648
|
-
participantId = side === 'left' ? peerId : localId;
|
|
649
|
-
participantLabel = side === 'left' ? peerLabel : localLabel;
|
|
650
|
-
} else {
|
|
651
|
-
if (text(raw.deliveryType, null) === 'kickoff') return;
|
|
652
|
-
const classification = classifyReplyContent(text(raw.commandText, ''));
|
|
653
|
-
if (classification.silenceReason) return;
|
|
654
|
-
messageText = classification.text;
|
|
655
|
-
createdAt = formatTimestamp(raw.turnCreatedAt || raw.createdAt);
|
|
656
|
-
messageId = text(raw.deliveryId, `msg-${index + 1}`);
|
|
657
|
-
side = text(raw.direction, null) === 'outbound'
|
|
658
|
-
|| (!text(raw.direction, null) && text(raw.fromAgentId, null) === text(localAgentId, null))
|
|
659
|
-
? 'right'
|
|
660
|
-
: 'left';
|
|
661
|
-
participantId = side === 'left' ? peerId : localId;
|
|
662
|
-
participantLabel = side === 'left' ? peerLabel : localLabel;
|
|
663
|
-
}
|
|
664
|
-
if (!messageText) return;
|
|
665
|
-
const extracted = extractControlTags(messageText);
|
|
666
|
-
const cleanedText = stripInternalMarkup(redactText(extracted.text));
|
|
667
|
-
if (!cleanedText && !extracted.tags.length) return;
|
|
668
|
-
normalized.push({
|
|
669
|
-
id: messageId,
|
|
670
|
-
side,
|
|
671
|
-
participantId,
|
|
672
|
-
participantLabel,
|
|
673
|
-
text: cleanedText,
|
|
674
|
-
createdAt,
|
|
675
|
-
tags: extracted.tags,
|
|
676
|
-
sourceIndex: index,
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
return normalized;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function selectionSummary(request, messageCount) {
|
|
683
|
-
return compactObject({
|
|
684
|
-
mode: request.mode,
|
|
685
|
-
chatRequestId: request.mode === 'stored' ? request.chatRequestId : null,
|
|
686
|
-
messageCount,
|
|
687
|
-
omittedBefore: 0,
|
|
688
|
-
omittedAfter: 0,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function formatTimeMarker(value) {
|
|
693
|
-
const timestamp = parseTimeish(value);
|
|
694
|
-
if (timestamp != null) {
|
|
695
|
-
const date = new Date(timestamp * 1000);
|
|
696
|
-
if (!Number.isNaN(date.getTime())) {
|
|
697
|
-
const pad = (part) => String(part).padStart(2, '0');
|
|
698
|
-
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
const match = /(?:(\d{4})[-/])?(\d{1,2})[-/](\d{1,2})[ T](\d{1,2}):(\d{2})/u.exec(String(value ?? '').trim());
|
|
702
|
-
return match ? `${String(Number(match[2])).padStart(2, '0')}-${String(Number(match[3])).padStart(2, '0')} ${String(Number(match[4])).padStart(2, '0')}:${match[5]}` : '';
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function decorateSelection(messages, selection) {
|
|
706
|
-
const items = [];
|
|
707
|
-
let previousTimestamp = null;
|
|
708
|
-
messages.forEach((message, index) => {
|
|
709
|
-
const timestamp = parseTimeish(message.createdAt);
|
|
710
|
-
if (message.createdAt && (index === 0 || (
|
|
711
|
-
previousTimestamp != null && timestamp != null && timestamp - previousTimestamp > TIME_SPLIT_SECONDS
|
|
712
|
-
))) {
|
|
713
|
-
const label = formatTimeMarker(message.createdAt);
|
|
714
|
-
if (label) items.push({ kind: 'time', label, timestamp: message.createdAt });
|
|
715
|
-
}
|
|
716
|
-
items.push({ kind: 'message', message });
|
|
717
|
-
previousTimestamp = timestamp ?? previousTimestamp;
|
|
718
|
-
});
|
|
719
|
-
if (selection.omittedAfter > 0) {
|
|
720
|
-
items.push({ kind: 'ellipsis', omitted: selection.omittedAfter, label: `${selection.omittedAfter} later messages omitted` });
|
|
721
|
-
}
|
|
722
|
-
return items;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function participants(messages) {
|
|
726
|
-
const seen = new Map();
|
|
727
|
-
for (const message of messages) {
|
|
728
|
-
if (!seen.has(message.participantId)) seen.set(message.participantId, message);
|
|
729
|
-
}
|
|
730
|
-
return [...seen.entries()].map(([participantId, message]) => ({
|
|
731
|
-
id: participantId,
|
|
732
|
-
name: message.participantLabel,
|
|
733
|
-
side: message.side,
|
|
734
|
-
avatar: String(message.participantLabel || 'A').replace(/[^A-Za-z0-9]/gu, '').slice(0, 2).toUpperCase() || 'A',
|
|
735
|
-
}));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function bubbleMessagePayload(item) {
|
|
739
|
-
if (item.kind === 'time') return { kind: 'time', label: item.label, timestamp: item.timestamp || '' };
|
|
740
|
-
if (item.kind === 'ellipsis') return { kind: 'ellipsis', omitted: item.omitted, label: item.label };
|
|
741
|
-
const { message } = item;
|
|
742
|
-
return {
|
|
743
|
-
id: message.id,
|
|
744
|
-
kind: 'text',
|
|
745
|
-
from: message.participantId,
|
|
746
|
-
side: message.side,
|
|
747
|
-
speaker: message.participantLabel,
|
|
748
|
-
text: message.text,
|
|
749
|
-
createdAt: message.createdAt,
|
|
750
|
-
tags: [...message.tags],
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function publicHeaderValue(value) {
|
|
755
|
-
const normalized = text(value, '') || '';
|
|
756
|
-
if (/(?:^|[\s(])(?:agt|req|wld|dlv|conversation|management)[_:-][a-z0-9]/iu.test(normalized)) {
|
|
757
|
-
return '';
|
|
758
|
-
}
|
|
759
|
-
return normalized;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function displayName(identity) {
|
|
763
|
-
return publicHeaderValue(String(identity || '').split('#', 1)[0]);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function semanticHeaderTitle(peerName, worldName) {
|
|
767
|
-
if (peerName && worldName) return `${peerName} — ${worldName}`;
|
|
768
|
-
return peerName || worldName || 'Claworld conversation';
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function headerText(args, messages, headerContext) {
|
|
772
|
-
const explicitTitle = publicHeaderValue(args.title);
|
|
773
|
-
let peerIdentity = publicHeaderValue(headerContext.peerIdentity)
|
|
774
|
-
|| publicHeaderValue(headerContext.peerId);
|
|
775
|
-
if (!peerIdentity) {
|
|
776
|
-
peerIdentity = publicHeaderValue(
|
|
777
|
-
messages.find((message) => message.side === 'left')?.participantLabel,
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
const peerName = displayName(peerIdentity);
|
|
781
|
-
const worldName = publicHeaderValue(headerContext.worldName);
|
|
782
|
-
const title = explicitTitle || semanticHeaderTitle(peerName, worldName);
|
|
783
|
-
const explicitProfile = publicHeaderValue(args.peerProfile);
|
|
784
|
-
if (explicitProfile) return { title, subtitle: explicitProfile };
|
|
785
|
-
const profile = publicHeaderValue(headerContext.peerProfile);
|
|
786
|
-
const subtitleParts = [peerIdentity, profile].filter(Boolean);
|
|
787
|
-
if (!subtitleParts.length && worldName) subtitleParts.push(worldName);
|
|
788
|
-
return { title, subtitle: subtitleParts.join(' · ') || 'Conversation transcript' };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function outputDirectories(workspaceRoot) {
|
|
792
|
-
const base = path.join(workspaceRoot, '.claworld', 'reports', 'transcripts');
|
|
793
|
-
return { images: path.join(base, 'images'), documents: path.join(base, 'documents') };
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function artifactId(source, selection, styleName) {
|
|
797
|
-
const now = new Date();
|
|
798
|
-
const pad = (part) => String(part).padStart(2, '0');
|
|
799
|
-
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
800
|
-
const styleSlug = styleName.toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '') || 'style';
|
|
801
|
-
const digest = createHash('sha256').update(JSON.stringify({ source, selection, style: styleName, now: timestamp })).digest('hex').slice(0, 10);
|
|
802
|
-
return `claworld-transcript-${styleSlug}-${timestamp}-${digest}`;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async function sha256(filePath) {
|
|
806
|
-
return createHash('sha256').update(await fs.readFile(filePath)).digest('hex');
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function intValue(value, fallback, minimum, maximum) {
|
|
810
|
-
const parsed = Number.parseInt(value, 10);
|
|
811
|
-
return Math.min(maximum, Math.max(minimum, Number.isFinite(parsed) ? parsed : fallback));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async function writePng(svg, pngPath, page) {
|
|
815
|
-
const sharpModule = await import('sharp');
|
|
816
|
-
const sharp = sharpModule.default || sharpModule;
|
|
817
|
-
await sharp(Buffer.from(svg), { density: 192 })
|
|
818
|
-
.resize(page.width, page.height, { fit: 'fill' })
|
|
819
|
-
.png()
|
|
820
|
-
.toFile(pngPath);
|
|
821
|
-
return { renderer: 'sharp-svg' };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function artifactPage(item) {
|
|
825
|
-
return compactObject({
|
|
826
|
-
page: item.page,
|
|
827
|
-
format: item.format,
|
|
828
|
-
path: item.path,
|
|
829
|
-
width: item.width,
|
|
830
|
-
height: item.height,
|
|
831
|
-
sha256: item.sha256,
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
export async function renderTranscriptReport({ workspaceRoot, localAgentId = null, args = {} } = {}) {
|
|
836
|
-
if (!workspaceRoot) throw new Error('OpenClaw workspace root is required for transcript rendering');
|
|
837
|
-
workspaceRoot = path.resolve(workspaceRoot);
|
|
838
|
-
const request = normalizeRenderRequest(args);
|
|
839
|
-
const source = await loadSourceMessages(request, workspaceRoot);
|
|
840
|
-
const headerContext = extractTranscriptHeaderContext(source.messages);
|
|
841
|
-
const normalized = normalizeMessages(source.messages, localAgentId, request.renderArgs, headerContext);
|
|
842
|
-
if (!normalized.length) throw new Error('no visible transcript messages were found for rendering');
|
|
843
|
-
|
|
844
|
-
const selection = selectionSummary(request, normalized.length);
|
|
845
|
-
const width = DEFAULT_WIDTH;
|
|
846
|
-
const maxPageHeight = intValue(request.renderArgs.maxPageHeight, DEFAULT_MAX_PAGE_HEIGHT, 900, 8000);
|
|
847
|
-
const header = headerText(request.renderArgs, normalized, headerContext);
|
|
848
|
-
const decorated = decorateSelection(normalized, selection);
|
|
849
|
-
const measured = decorated.map((item) => measureTranscriptItem(item, width));
|
|
850
|
-
const pages = paginateTranscriptItems(measured, width, maxPageHeight, header.title, header.subtitle);
|
|
851
|
-
const currentArtifactId = artifactId(source.summary, selection, CLAWORLD_TRANSCRIPT_STYLE_NAME);
|
|
852
|
-
const directories = outputDirectories(workspaceRoot);
|
|
853
|
-
await Promise.all([fs.mkdir(directories.images, { recursive: true }), fs.mkdir(directories.documents, { recursive: true })]);
|
|
854
|
-
|
|
855
|
-
const files = [];
|
|
856
|
-
for (const page of pages) {
|
|
857
|
-
const suffix = `p${String(page.page).padStart(2, '0')}`;
|
|
858
|
-
const svgPath = path.join(directories.documents, `${currentArtifactId}-${suffix}.svg`);
|
|
859
|
-
const pngPath = path.join(directories.images, `${currentArtifactId}-${suffix}.png`);
|
|
860
|
-
const svg = renderTranscriptPageSvg(page);
|
|
861
|
-
await atomicWriteText(svgPath, svg);
|
|
862
|
-
const pngResult = await writePng(svg, pngPath, page);
|
|
863
|
-
files.push({ page: page.page, format: 'svg', path: svgPath, width: page.width, height: page.height, sha256: await sha256(svgPath), role: 'source' });
|
|
864
|
-
files.push({ page: page.page, format: 'png', path: pngPath, width: page.width, height: page.height, sha256: await sha256(pngPath), role: 'primary', renderer: pngResult.renderer });
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const bubbleSpec = {
|
|
868
|
-
version: '1',
|
|
869
|
-
kind: 'claworld.transcript_report',
|
|
870
|
-
scene: {
|
|
871
|
-
title: header.title,
|
|
872
|
-
subtitle: header.subtitle,
|
|
873
|
-
peerId: header.title,
|
|
874
|
-
peerProfile: header.subtitle,
|
|
875
|
-
peerProfileSource: publicHeaderValue(request.renderArgs.peerProfile)
|
|
876
|
-
? 'explicit'
|
|
877
|
-
: headerContext.profileSource || 'fallback',
|
|
878
|
-
generatedAt: isoNow(),
|
|
879
|
-
source: source.summary,
|
|
880
|
-
selection,
|
|
881
|
-
},
|
|
882
|
-
canvas: { width, style: CLAWORLD_TRANSCRIPT_STYLE_NAME, maxPageHeight },
|
|
883
|
-
participants: participants(normalized),
|
|
884
|
-
messages: decorated.map(bubbleMessagePayload),
|
|
885
|
-
};
|
|
886
|
-
const specPath = path.join(directories.documents, `${currentArtifactId}.bubblespec.json`);
|
|
887
|
-
await atomicWriteJson(specPath, bubbleSpec);
|
|
888
|
-
|
|
889
|
-
const pngPages = files.filter((item) => item.format === 'png').map(artifactPage);
|
|
890
|
-
const svgPages = files.filter((item) => item.format === 'svg').map(artifactPage);
|
|
891
|
-
const stats = {
|
|
892
|
-
sourceMessages: source.messages.length,
|
|
893
|
-
normalizedMessages: normalized.length,
|
|
894
|
-
renderedMessages: normalized.length,
|
|
895
|
-
pages: pages.length,
|
|
896
|
-
omittedBefore: selection.omittedBefore || 0,
|
|
897
|
-
omittedAfter: selection.omittedAfter || 0,
|
|
898
|
-
};
|
|
899
|
-
const result = compactObject({
|
|
900
|
-
status: 'ok',
|
|
901
|
-
mode: request.mode,
|
|
902
|
-
chatRequestId: request.mode === 'stored' ? request.chatRequestId : null,
|
|
903
|
-
artifactId: currentArtifactId,
|
|
904
|
-
messageCount: normalized.length,
|
|
905
|
-
pageCount: pages.length,
|
|
906
|
-
style: CLAWORLD_TRANSCRIPT_STYLE_NAME,
|
|
907
|
-
artifacts: {
|
|
908
|
-
bubbleSpec: { format: 'bubblespec', path: specPath, sha256: await sha256(specPath) },
|
|
909
|
-
pngPages,
|
|
910
|
-
svgPages,
|
|
911
|
-
},
|
|
912
|
-
diagnostics: { source: source.summary, stats },
|
|
913
|
-
});
|
|
914
|
-
await appendClaworldJournalEvent(workspaceRoot, {
|
|
915
|
-
kind: 'transcript_report',
|
|
916
|
-
scope: request.mode === 'stored' ? 'conversation' : 'main',
|
|
917
|
-
summary: `Rendered ${normalized.length} visible Claworld transcript messages across ${pages.length} page(s).`,
|
|
918
|
-
refs: { chatRequestId: request.mode === 'stored' ? request.chatRequestId : null },
|
|
919
|
-
artifacts: { artifactId: currentArtifactId, bubbleSpec: specPath, pngPages: pngPages.map((item) => item.path), svgPages: svgPages.map((item) => item.path) },
|
|
920
|
-
maintenance: { selection, stats },
|
|
921
|
-
});
|
|
922
|
-
return result;
|
|
923
|
-
}
|