openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
795 lines (794 loc) โข 32.2 kB
JavaScript
import { i as redactSecrets, u as redactToolPayloadText } from "./redact-DduLliKq.js";
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { i as isPathInside } from "./path-BlG8lhgR.js";
import { w as pathExists } from "./fs-safe-aqmM_n6V.js";
import "./path-guards-CBe_wA_B.js";
import { r as TRAJECTORY_RUNTIME_FILE_MAX_BYTES, s as safeTrajectorySessionFileName } from "./paths-Biq9XkB5.js";
import { n as redactSupportString } from "./diagnostic-support-redaction-CiIQwYoe.js";
import { n as sanitizeDiagnosticPayload, t as safeJsonStringify } from "./safe-json-BsjZ1aqc.js";
import { a as writeSupportBundleDirectory, i as textSupportBundleFile, n as jsonlSupportBundleFile, r as supportBundleContents, t as jsonSupportBundleFile } from "./diagnostic-support-bundle-DFu7dtAD.js";
import { n as resolveTrajectoryRuntimeFile, t as isRegularNonSymlinkFile } from "./runtime-file-BxBUlhIQ.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/trajectory/export.ts
const MAX_TRAJECTORY_RUNTIME_EVENTS = 2e5;
const MAX_TRAJECTORY_TOTAL_EVENTS = 25e4;
const MAX_TRAJECTORY_SESSION_FILE_BYTES = 50 * 1024 * 1024;
const MAX_TRAJECTORY_WARNING_ROWS = 20;
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function isSessionFileEntry(value) {
if (!isRecord(value) || typeof value.type !== "string") return false;
if (value.type !== "message") return true;
const message = value.message;
return isRecord(message) && typeof message.role === "string";
}
function parseSessionEntries(content) {
const entries = [];
const warnings = [];
const rowByEntry = /* @__PURE__ */ new Map();
const rows = content.split(/\r?\n/u);
for (const [index, rawLine] of rows.entries()) {
const line = rawLine.trim();
if (!line) continue;
try {
const parsed = JSON.parse(line);
if (!isSessionFileEntry(parsed)) {
warnings.push({
source: "session",
code: "invalid-session-row",
row: index + 1,
message: "Skipped a session JSONL row that is not a session entry object."
});
continue;
}
entries.push(parsed);
rowByEntry.set(parsed, index + 1);
} catch {
warnings.push({
source: "session",
code: "invalid-session-json",
row: index + 1,
message: "Skipped a session JSONL row that is not valid JSON."
});
}
}
return {
entries,
warnings,
rowByEntry
};
}
function migrateLegacySessionEntries(entries) {
const version = entries.find((entry) => entry.type === "session")?.version ?? 1;
if (version < 2) {
let previousId = null;
let index = 0;
for (const entry of entries) {
if (entry.type === "session") {
entry.version = 2;
continue;
}
const mutable = entry;
if (typeof mutable.id !== "string") mutable.id = `legacy-${index++}`;
mutable.parentId = previousId;
const entryId = mutable.id;
previousId = typeof entryId === "string" ? entryId : null;
if (entry.type === "compaction" && typeof mutable.firstKeptEntryIndex === "number") {
const target = entries[mutable.firstKeptEntryIndex];
if (target && target.type !== "session") mutable.firstKeptEntryId = target.id;
delete mutable.firstKeptEntryIndex;
}
}
}
if (version < 3) for (const entry of entries) {
if (entry.type === "session") {
entry.version = 3;
continue;
}
if (entry.type === "message") {
const message = entry.message;
if (message?.role === "hookMessage") message.role = "custom";
}
}
}
async function readSessionBranch(filePath) {
const { entries: fileEntries, warnings, rowByEntry } = parseSessionEntries(await fs.readFile(filePath, "utf8"));
migrateLegacySessionEntries(fileEntries);
const header = fileEntries.find((entry) => entry.type === "session") ?? null;
const entries = fileEntries.filter((entry) => entry.type !== "session" && typeof entry.id === "string" && (typeof entry.timestamp === "string" || typeof entry.timestamp === "number"));
const byId = new Map(entries.map((entry) => [entry.id, entry]));
const leafId = entries.at(-1)?.id ?? null;
const branchEntries = [];
const seen = /* @__PURE__ */ new Set();
let currentId = leafId;
while (currentId) {
if (seen.has(currentId)) {
const cycleEntry = byId.get(currentId);
warnings.push({
source: "session",
code: "cyclic-session-branch",
row: cycleEntry ? rowByEntry.get(cycleEntry) ?? 0 : 0,
message: "Stopped trajectory session branch export at a cyclic parent link."
});
break;
}
seen.add(currentId);
const current = byId.get(currentId);
if (!current) {
warnings.push({
source: "session",
code: "incomplete-session-branch",
row: 0,
message: "Exported the reachable session branch suffix after a missing parent link."
});
break;
}
branchEntries.unshift(current);
const parentId = typeof current.parentId === "string" ? current.parentId : null;
if (parentId && !byId.has(parentId)) {
warnings.push({
source: "session",
code: "incomplete-session-branch",
row: rowByEntry.get(current) ?? 0,
message: "Exported the reachable session branch suffix after a missing parent link."
});
break;
}
currentId = parentId;
}
return {
header,
leafId,
branchEntries,
warnings
};
}
async function parseJsonlFile(filePath, params) {
let stat;
try {
stat = await fs.stat(filePath);
} catch (error) {
if (error.code === "ENOENT") return {
events: [],
warnings: []
};
throw error;
}
if (!stat.isFile()) return {
events: [],
warnings: []
};
if (stat.size > params.maxBytes) throw new Error(`Trajectory runtime file is too large to export (${stat.size} bytes; limit ${params.maxBytes})`);
const rows = (await fs.readFile(filePath, "utf8")).split(/\r?\n/u);
const parsed = [];
const warnings = [];
for (const [index, rawLine] of rows.entries()) {
const row = rawLine.trim();
if (!row) continue;
if (parsed.length >= params.maxEvents) throw new Error(`Trajectory runtime file has too many events to export (limit ${params.maxEvents})`);
try {
const value = JSON.parse(row);
if (!params.validate || params.validate(value)) {
const typedValue = value;
if (!params.include || params.include(typedValue)) parsed.push(typedValue);
} else warnings.push({
source: "runtime",
code: "invalid-runtime-event",
row: index + 1,
message: "Skipped a runtime trajectory JSONL row that does not match the session schema."
});
} catch {
warnings.push({
source: "runtime",
code: "invalid-runtime-json",
row: index + 1,
message: "Skipped a runtime trajectory JSONL row that is not valid JSON."
});
}
}
return {
events: parsed,
warnings
};
}
function isRuntimeTrajectoryEvent(value) {
if (!isRecord(value)) return false;
return value.traceSchema === "openclaw-trajectory" && value.schemaVersion === 1 && value.source === "runtime" && typeof value.type === "string" && typeof value.ts === "string" && !Number.isNaN(Date.parse(value.ts)) && isFiniteNumber(value.seq) && typeof value.sessionId === "string" && (!("data" in value) || value.data === void 0 || isRecord(value.data));
}
function summarizeJsonlWarnings(warnings) {
const byKey = /* @__PURE__ */ new Map();
for (const warning of warnings) {
const key = `${warning.source}:${warning.code}`;
const existing = byKey.get(key);
if (existing) {
existing.count += 1;
if (existing.rows.length < MAX_TRAJECTORY_WARNING_ROWS) existing.rows.push(warning.row);
continue;
}
byKey.set(key, {
source: warning.source,
code: warning.code,
count: 1,
rows: [warning.row],
message: warning.message
});
}
return [...byKey.values()];
}
function normalizeTimestamp(value) {
if (typeof value === "number" && Number.isFinite(value)) {
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
}
if (typeof value === "string") {
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
}
return (/* @__PURE__ */ new Date(0)).toISOString();
}
function resolveMessageEventType(message) {
if (message.role === "user") return "user.message";
if (message.role === "assistant") return "assistant.message";
if (message.role === "toolResult") return "tool.result";
return `message.${message.role}`;
}
function extractAssistantToolCalls(message) {
if (message.role !== "assistant" || !Array.isArray(message.content)) return [];
return message.content.flatMap((block, index) => {
if (!block || typeof block !== "object") return [];
const typedBlock = block;
const blockType = typeof typedBlock.type === "string" ? typedBlock.type.trim().toLowerCase() : "";
if (blockType !== "toolcall" && blockType !== "tooluse" && blockType !== "functioncall") return [];
return [{
id: typeof typedBlock.id === "string" ? typedBlock.id : void 0,
name: typeof typedBlock.name === "string" ? typedBlock.name : void 0,
arguments: typedBlock.arguments ?? typedBlock.input ?? typedBlock.parameters,
index
}];
});
}
function sanitizeTrajectoryExportValue(value) {
return redactSecrets(sanitizeDiagnosticPayload(value));
}
function buildTranscriptEvents(params) {
const events = [];
let seq = 0;
for (const entry of params.entries) {
const push = (type, data) => {
events.push({
traceSchema: "openclaw-trajectory",
schemaVersion: 1,
traceId: params.traceId,
source: "transcript",
type,
ts: normalizeTimestamp(entry.timestamp),
seq: 0,
sourceSeq: seq += 1,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
entryId: entry.id,
parentEntryId: entry.parentId,
data
});
};
switch (entry.type) {
case "message":
push(resolveMessageEventType(entry.message), { message: sanitizeDiagnosticPayload(entry.message) });
for (const toolCall of extractAssistantToolCalls(entry.message)) push("tool.call", {
toolCallId: toolCall.id,
name: toolCall.name,
arguments: sanitizeDiagnosticPayload(toolCall.arguments),
assistantEntryId: entry.id,
blockIndex: toolCall.index
});
break;
case "compaction":
push("session.compaction", {
summary: entry.summary,
firstKeptEntryId: entry.firstKeptEntryId,
tokensBefore: entry.tokensBefore,
details: sanitizeDiagnosticPayload(entry.details),
fromHook: entry.fromHook ?? false
});
break;
case "branch_summary":
push("session.branch_summary", {
fromId: entry.fromId,
summary: entry.summary,
details: sanitizeDiagnosticPayload(entry.details),
fromHook: entry.fromHook ?? false
});
break;
case "custom":
push("session.custom", {
customType: entry.customType,
data: sanitizeDiagnosticPayload(entry.data)
});
break;
case "custom_message":
push("session.custom_message", {
customType: entry.customType,
content: sanitizeDiagnosticPayload(entry.content),
details: sanitizeDiagnosticPayload(entry.details),
display: entry.display
});
break;
case "thinking_level_change":
push("session.thinking_level_change", { thinkingLevel: entry.thinkingLevel });
break;
case "model_change":
push("session.model_change", {
provider: entry.provider,
modelId: entry.modelId
});
break;
case "label":
push("session.label", {
targetId: entry.targetId,
label: entry.label
});
break;
case "session_info":
push("session.info", { name: entry.name });
break;
}
}
return events;
}
function sortTrajectoryEvents(events) {
const sourceOrder = {
runtime: 0,
transcript: 1,
export: 2
};
const sorted = events.toSorted((left, right) => {
const byTs = left.ts.localeCompare(right.ts);
if (byTs !== 0) return byTs;
const bySource = sourceOrder[left.source] - sourceOrder[right.source];
if (bySource !== 0) return bySource;
return (left.sourceSeq ?? left.seq) - (right.sourceSeq ?? right.seq);
});
for (const [index, event] of sorted.entries()) event.seq = index + 1;
return sorted;
}
function trajectoryJsonlFile(pathName, events) {
return jsonlSupportBundleFile(pathName, events.map((event) => safeJsonStringify(event)).filter((line) => Boolean(line)));
}
function redactTrajectoryBundleFileContent(file) {
return {
...file,
content: redactToolPayloadText(file.content)
};
}
function buildTrajectoryExportRedaction(params) {
const env = process.env;
return {
env,
stateDir: resolveStateDir(env),
workspaceDir: path.resolve(params.workspaceDir)
};
}
function redactWorkspacePathString(value, redaction) {
const workspaceDir = redaction.workspaceDir;
if (!workspaceDir) return value;
const normalizedWorkspaceDir = workspaceDir.replaceAll("\\", "/");
let next = value;
for (const candidate of new Set([workspaceDir, normalizedWorkspaceDir])) {
if (!candidate) continue;
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
next = next.replace(new RegExp(`${escaped}(?=$|[\\\\/])`, "gu"), "$WORKSPACE_DIR");
}
return next;
}
function maybeRedactPathString(value, redaction) {
const workspaceRedacted = redactWorkspacePathString(value, redaction);
if (workspaceRedacted !== value || path.isAbsolute(workspaceRedacted) || workspaceRedacted.includes(redaction.stateDir) || (redaction.env.HOME ? workspaceRedacted.includes(redaction.env.HOME) : false) || (redaction.env.USERPROFILE ? workspaceRedacted.includes(redaction.env.USERPROFILE) : false)) return redactSupportString(workspaceRedacted, redaction);
return workspaceRedacted;
}
function redactLocalPathValues(value, redaction) {
if (typeof value === "string") return maybeRedactPathString(value, redaction);
if (Array.isArray(value)) return value.map((entry) => redactLocalPathValues(entry, redaction));
if (!value || typeof value !== "object") return value;
const record = value;
const next = {};
for (const [key, entry] of Object.entries(record)) next[key] = redactLocalPathValues(entry, redaction);
return next;
}
function uniqueRedactedObjectKey(key, usedKeys) {
if (!usedKeys.has(key)) {
usedKeys.add(key);
return key;
}
let index = 2;
while (usedKeys.has(`${key}#${index}`)) index += 1;
const unique = `${key}#${index}`;
usedKeys.add(unique);
return unique;
}
function redactTrajectoryExportObjectKeys(value, redaction) {
if (Array.isArray(value)) return value.map((entry) => redactTrajectoryExportObjectKeys(entry, redaction));
if (!value || typeof value !== "object") return value;
const usedKeys = /* @__PURE__ */ new Set();
const next = {};
for (const [key, entry] of Object.entries(value)) {
const redactedKey = redactToolPayloadText(maybeRedactPathString(key, redaction));
next[uniqueRedactedObjectKey(redactedKey, usedKeys)] = redactTrajectoryExportObjectKeys(entry, redaction);
}
return next;
}
function redactTrajectoryExportValue(value, redaction) {
return redactTrajectoryExportObjectKeys(sanitizeTrajectoryExportValue(redactLocalPathValues(value, redaction)), redaction);
}
function redactEventForExport(event, redaction) {
return redactTrajectoryExportValue(event, redaction);
}
function resolveRuntimeContext(runtimeEvents) {
const runtimeData = runtimeEvents.slice().toReversed().find((event) => event.type === "context.compiled")?.data;
const toolsValue = Array.isArray(runtimeData?.tools) ? runtimeData.tools : void 0;
return {
systemPrompt: typeof runtimeData?.systemPrompt === "string" ? runtimeData.systemPrompt : void 0,
tools: toolsValue
};
}
function resolveLatestRuntimeEventData(runtimeEvents, type) {
return runtimeEvents.slice().toReversed().find((candidate) => candidate.type === type)?.data;
}
function normalizePathForMatch(value) {
return value.replaceAll("\\", "/").trim().toLowerCase();
}
function collectPotentialPathStrings(value) {
const found = /* @__PURE__ */ new Set();
const visit = (input) => {
if (!input || typeof input !== "object") return;
if (Array.isArray(input)) {
for (const entry of input) visit(entry);
return;
}
for (const [key, entry] of Object.entries(input)) if (typeof entry === "string" && (key.toLowerCase().includes("path") || entry.endsWith("SKILL.md") || entry.endsWith("skill.md"))) found.add(entry);
else visit(entry);
};
visit(value);
return [...found];
}
function markInvokedSkills(params) {
if (!params.skills || typeof params.skills !== "object") return params.skills;
const skillsRecord = params.skills;
if (!Array.isArray(skillsRecord.entries) || skillsRecord.entries.length === 0) return params.skills;
const invokedPaths = new Set(params.events.flatMap((event) => {
if (event.type !== "tool.call") return [];
return collectPotentialPathStrings(event.data?.arguments);
}));
const normalizedInvokedPaths = new Set([...invokedPaths].map((value) => normalizePathForMatch(value)));
const entries = skillsRecord.entries.map((entry) => {
const rawPath = typeof entry.filePath === "string" ? entry.filePath : void 0;
const normalizedPath = rawPath ? normalizePathForMatch(rawPath) : void 0;
const skillDirName = rawPath?.replaceAll("\\", "/").split("/").slice(-2, -1)[0]?.toLowerCase() ?? void 0;
const invoked = normalizedPath ? [...normalizedInvokedPaths].some((candidate) => candidate === normalizedPath || candidate.endsWith(normalizedPath) || (skillDirName ? candidate.endsWith(`/${skillDirName}/skill.md`) : false)) : false;
return invoked ? {
...entry,
invoked,
invocationDetectedBy: "tool-call-file-path"
} : {
...entry,
invoked: false
};
});
return {
...skillsRecord,
entries
};
}
function buildMetadataCapture(params) {
const runtimeMetadata = resolveLatestRuntimeEventData(params.runtimeEvents, "trace.metadata");
if (!runtimeMetadata) return;
const modelFallback = (() => {
const latest = params.runtimeEvents.slice().toReversed().find((event) => event.provider || event.modelId || event.modelApi);
if (!latest?.provider && !latest?.modelId && !latest?.modelApi) return;
return {
provider: latest.provider,
name: latest.modelId,
api: latest.modelApi
};
})();
return {
traceSchema: "openclaw-trajectory",
schemaVersion: 1,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
traceId: params.manifest.traceId,
sessionId: params.manifest.sessionId,
sessionKey: params.manifest.sessionKey,
harness: runtimeMetadata.harness,
model: runtimeMetadata.model ?? modelFallback,
config: runtimeMetadata.config,
plugins: runtimeMetadata.plugins,
skills: markInvokedSkills({
skills: runtimeMetadata.skills,
events: params.events
}),
prompting: runtimeMetadata.prompting,
redaction: runtimeMetadata.redaction,
metadata: runtimeMetadata.metadata
};
}
function buildArtifactsCapture(params) {
const runtimeArtifacts = resolveLatestRuntimeEventData(params.runtimeEvents, "trace.artifacts");
const runtimeCompletion = resolveLatestRuntimeEventData(params.runtimeEvents, "model.completed");
const runtimeEnd = resolveLatestRuntimeEventData(params.runtimeEvents, "session.ended");
if (!runtimeArtifacts && !runtimeCompletion && !runtimeEnd) return;
return {
traceSchema: "openclaw-trajectory",
schemaVersion: 1,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
traceId: params.manifest.traceId,
sessionId: params.manifest.sessionId,
sessionKey: params.manifest.sessionKey,
finalStatus: runtimeArtifacts?.finalStatus ?? runtimeEnd?.status,
aborted: runtimeArtifacts?.aborted ?? runtimeEnd?.aborted,
externalAbort: runtimeArtifacts?.externalAbort ?? runtimeEnd?.externalAbort,
timedOut: runtimeArtifacts?.timedOut ?? runtimeEnd?.timedOut,
idleTimedOut: runtimeArtifacts?.idleTimedOut ?? runtimeEnd?.idleTimedOut,
timedOutDuringCompaction: runtimeArtifacts?.timedOutDuringCompaction ?? runtimeEnd?.timedOutDuringCompaction,
timedOutDuringToolExecution: runtimeArtifacts?.timedOutDuringToolExecution ?? runtimeEnd?.timedOutDuringToolExecution,
promptError: runtimeArtifacts?.promptError ?? runtimeEnd?.promptError ?? runtimeCompletion?.promptError,
promptErrorSource: runtimeArtifacts?.promptErrorSource ?? runtimeCompletion?.promptErrorSource,
terminalError: runtimeArtifacts?.terminalError ?? runtimeEnd?.terminalError ?? runtimeCompletion?.terminalError,
usage: runtimeArtifacts?.usage ?? runtimeCompletion?.usage,
promptCache: runtimeArtifacts?.promptCache ?? runtimeCompletion?.promptCache,
compactionCount: runtimeArtifacts?.compactionCount ?? runtimeCompletion?.compactionCount,
assistantTexts: runtimeArtifacts?.assistantTexts ?? runtimeCompletion?.assistantTexts,
finalPromptText: runtimeArtifacts?.finalPromptText ?? runtimeCompletion?.finalPromptText,
itemLifecycle: runtimeArtifacts?.itemLifecycle,
toolMetas: runtimeArtifacts?.toolMetas,
didSendViaMessagingTool: runtimeArtifacts?.didSendViaMessagingTool,
successfulCronAdds: runtimeArtifacts?.successfulCronAdds,
messagingToolSentTexts: runtimeArtifacts?.messagingToolSentTexts,
messagingToolSentMediaUrls: runtimeArtifacts?.messagingToolSentMediaUrls,
messagingToolSentTargets: runtimeArtifacts?.messagingToolSentTargets,
lastToolError: runtimeArtifacts?.lastToolError
};
}
function buildPromptsCapture(params) {
const runtimeMetadata = resolveLatestRuntimeEventData(params.runtimeEvents, "trace.metadata");
const latestCompiled = resolveLatestRuntimeEventData(params.runtimeEvents, "context.compiled");
const submittedPrompts = params.runtimeEvents.filter((event) => event.type === "prompt.submitted").map((event) => event.data?.prompt).filter((prompt) => typeof prompt === "string");
const systemPrompt = (typeof latestCompiled?.systemPrompt === "string" ? latestCompiled.systemPrompt : void 0) ?? params.runtimeContext.systemPrompt;
const skillsPrompt = runtimeMetadata?.prompting && typeof runtimeMetadata.prompting === "object" && typeof runtimeMetadata.prompting.skillsPrompt === "string" ? runtimeMetadata.prompting.skillsPrompt : void 0;
const userPromptPrefixText = runtimeMetadata?.prompting && typeof runtimeMetadata.prompting === "object" && typeof runtimeMetadata.prompting.userPromptPrefixText === "string" ? runtimeMetadata.prompting.userPromptPrefixText : void 0;
const promptReport = runtimeMetadata?.prompting && typeof runtimeMetadata.prompting === "object" && typeof runtimeMetadata.prompting.systemPromptReport === "object" ? runtimeMetadata.prompting.systemPromptReport : void 0;
if (!systemPrompt && submittedPrompts.length === 0 && !skillsPrompt && !userPromptPrefixText) return;
return {
traceSchema: "openclaw-trajectory",
schemaVersion: 1,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
traceId: params.manifest.traceId,
sessionId: params.manifest.sessionId,
sessionKey: params.manifest.sessionKey,
system: systemPrompt,
submittedPrompts,
latestSubmittedPrompt: submittedPrompts.at(-1),
skillsPrompt,
userPromptPrefixText,
systemPromptReport: promptReport
};
}
function resolveDefaultTrajectoryExportDir(params) {
const timestamp = (params.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
const sessionFileName = safeTrajectorySessionFileName(params.sessionId);
return path.join(params.workspaceDir, ".openclaw", "trajectory-exports", `openclaw-trajectory-${sessionFileName.slice(0, 8)}-${timestamp}`);
}
async function exportTrajectoryBundle(params) {
const redaction = buildTrajectoryExportRedaction({ workspaceDir: params.workspaceDir });
const sessionStat = await fs.stat(params.sessionFile);
if (sessionStat.size > MAX_TRAJECTORY_SESSION_FILE_BYTES) throw new Error(`Trajectory session file is too large to export (${sessionStat.size} bytes; limit ${MAX_TRAJECTORY_SESSION_FILE_BYTES})`);
const { header, leafId, branchEntries, warnings: sessionWarnings } = await readSessionBranch(params.sessionFile);
const runtimeFile = await resolveTrajectoryRuntimeFile({
runtimeFile: params.runtimeFile,
sessionFile: params.sessionFile,
sessionId: params.sessionId
});
const runtimeParse = runtimeFile ? await parseJsonlFile(runtimeFile, {
maxBytes: TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
maxEvents: MAX_TRAJECTORY_RUNTIME_EVENTS,
include: (value) => value.sessionId === params.sessionId,
validate: isRuntimeTrajectoryEvent
}) : {
events: [],
warnings: []
};
const runtimeEvents = runtimeParse.events;
const transcriptEvents = buildTranscriptEvents({
entries: branchEntries,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
traceId: params.sessionId
});
const maxTotalEvents = params.maxTotalEvents ?? MAX_TRAJECTORY_TOTAL_EVENTS;
const totalEventCount = runtimeEvents.length + transcriptEvents.length;
if (totalEventCount > maxTotalEvents) throw new Error(`Trajectory export has too many events (${totalEventCount}; limit ${maxTotalEvents})`);
const rawEvents = sortTrajectoryEvents([...runtimeEvents, ...transcriptEvents]);
const events = rawEvents.map((event) => redactEventForExport(event, redaction));
const manifest = {
traceSchema: "openclaw-trajectory",
schemaVersion: 1,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
traceId: params.sessionId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: maybeRedactPathString(params.workspaceDir, redaction),
leafId,
eventCount: events.length,
runtimeEventCount: runtimeEvents.length,
transcriptEventCount: transcriptEvents.length,
sourceFiles: {
session: maybeRedactPathString(params.sessionFile, redaction),
runtime: runtimeFile && await isRegularNonSymlinkFile(runtimeFile) ? maybeRedactPathString(runtimeFile, redaction) : void 0
}
};
const warnings = summarizeJsonlWarnings([...sessionWarnings, ...runtimeParse.warnings]);
if (warnings.length > 0) manifest.warnings = warnings;
const bundleRuntimeContext = resolveRuntimeContext(runtimeEvents);
const files = [];
const supplementalFiles = [];
const metadataCapture = buildMetadataCapture({
manifest,
runtimeEvents,
events: rawEvents
});
const artifactsCapture = buildArtifactsCapture({
manifest,
runtimeEvents
});
const promptsCapture = buildPromptsCapture({
manifest,
runtimeEvents,
runtimeContext: bundleRuntimeContext
});
if (metadataCapture) {
files.push(jsonSupportBundleFile("metadata.json", redactTrajectoryExportValue(metadataCapture, redaction)));
supplementalFiles.push("metadata.json");
}
if (artifactsCapture) {
files.push(jsonSupportBundleFile("artifacts.json", redactTrajectoryExportValue(artifactsCapture, redaction)));
supplementalFiles.push("artifacts.json");
}
if (promptsCapture) {
files.push(jsonSupportBundleFile("prompts.json", redactTrajectoryExportValue(promptsCapture, redaction)));
supplementalFiles.push("prompts.json");
}
if (supplementalFiles.length > 0) manifest.supplementalFiles = supplementalFiles;
files.push(trajectoryJsonlFile("events.jsonl", events));
files.push(jsonSupportBundleFile("session-branch.json", redactTrajectoryExportValue({
header,
leafId,
entries: branchEntries
}, redaction)));
if (bundleRuntimeContext.systemPrompt) files.push(textSupportBundleFile("system-prompt.txt", redactTrajectoryExportValue(bundleRuntimeContext.systemPrompt, redaction)));
if (bundleRuntimeContext.tools) files.push(jsonSupportBundleFile("tools.json", redactTrajectoryExportValue(bundleRuntimeContext.tools, redaction)));
const redactedFiles = files.map(redactTrajectoryBundleFileContent);
manifest.contents = [...supportBundleContents(redactedFiles)];
const redactedManifest = redactTrajectoryExportValue(manifest, redaction);
const manifestFile = redactTrajectoryBundleFileContent(jsonSupportBundleFile("manifest.json", redactedManifest));
await writeSupportBundleDirectory({
outputDir: params.outputDir,
files: [manifestFile, ...redactedFiles]
});
return {
manifest: redactedManifest,
outputDir: params.outputDir,
events,
header,
runtimeFile: runtimeFile && await isRegularNonSymlinkFile(runtimeFile) ? runtimeFile : void 0,
supplementalFiles
};
}
//#endregion
//#region src/trajectory/command-export.ts
async function validateExistingExportDirectory(params) {
const linkStat = await fs.lstat(params.dir);
if (linkStat.isSymbolicLink() || !linkStat.isDirectory()) throw new Error(`${params.label} must be a real directory inside the workspace`);
const realDir = await fs.realpath(params.dir);
if (!isPathInside(params.realWorkspace, realDir)) throw new Error("Trajectory exports directory must stay inside the workspace");
return realDir;
}
async function mkdirIfMissingThenValidate(params) {
try {
await fs.mkdir(params.dir, { mode: 448 });
} catch (error) {
if (error.code !== "EEXIST") throw error;
}
return await validateExistingExportDirectory(params);
}
async function resolveTrajectoryExportBaseDir(workspaceDir) {
const workspacePath = path.resolve(workspaceDir);
const realWorkspace = await fs.realpath(workspacePath);
const stateDir = path.join(workspacePath, ".openclaw");
await mkdirIfMissingThenValidate({
dir: stateDir,
label: "OpenClaw state directory",
realWorkspace
});
const baseDir = path.join(stateDir, "trajectory-exports");
const realBase = await mkdirIfMissingThenValidate({
dir: baseDir,
label: "Trajectory exports directory",
realWorkspace
});
return {
baseDir: path.resolve(baseDir),
realBase
};
}
async function resolveTrajectoryCommandOutputDir(params) {
const { baseDir, realBase } = await resolveTrajectoryExportBaseDir(params.workspaceDir);
const raw = params.outputPath?.trim();
if (!raw) {
const defaultDir = resolveDefaultTrajectoryExportDir({
workspaceDir: params.workspaceDir,
sessionId: params.sessionId
});
return path.join(baseDir, path.basename(defaultDir));
}
if (path.isAbsolute(raw) || raw.startsWith("~")) throw new Error("Output path must be relative to the workspace trajectory exports directory");
const resolvedBase = path.resolve(baseDir);
const outputDir = path.resolve(resolvedBase, raw);
const relative = path.relative(resolvedBase, outputDir);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Output path must stay inside the workspace trajectory exports directory");
let existingParent = outputDir;
while (!await pathExists(existingParent)) {
const next = path.dirname(existingParent);
if (next === existingParent) break;
existingParent = next;
}
if (!isPathInside(realBase, await fs.realpath(existingParent))) throw new Error("Output path must stay inside the real trajectory exports directory");
return outputDir;
}
async function exportTrajectoryForCommand(params) {
const bundle = await exportTrajectoryBundle({
outputDir: params.outputDir ?? await resolveTrajectoryCommandOutputDir({
outputPath: params.outputPath,
workspaceDir: params.workspaceDir,
sessionId: params.sessionId
}),
sessionFile: params.sessionFile,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir
});
const relativePath = path.relative(params.workspaceDir, bundle.outputDir);
const displayPath = relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath) ? relativePath : path.basename(bundle.outputDir);
const files = [
"manifest.json",
"events.jsonl",
"session-branch.json"
];
if (bundle.events.some((event) => event.type === "context.compiled")) files.push("system-prompt.txt", "tools.json");
files.push(...bundle.supplementalFiles);
return {
outputDir: bundle.outputDir,
displayPath,
sessionId: params.sessionId,
eventCount: bundle.manifest.eventCount,
runtimeEventCount: bundle.manifest.runtimeEventCount,
transcriptEventCount: bundle.manifest.transcriptEventCount,
files
};
}
function formatTrajectoryCommandExportSummary(summary) {
return [
"โ
Trajectory exported!",
"",
`๐ฆ Bundle: ${summary.displayPath}`,
`๐งต Session: ${summary.sessionId}`,
`๐ Events: ${summary.eventCount}`,
`๐งช Runtime events: ${summary.runtimeEventCount}`,
`๐ Transcript events: ${summary.transcriptEventCount}`,
`๐ Files: ${summary.files.join(", ")}`
].join("\n");
}
//#endregion
export { formatTrajectoryCommandExportSummary as n, exportTrajectoryForCommand as t };