From 1888242bd30a636a0827e32ffd6a5d8b53ce6636 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 12:47:57 +0100 Subject: [PATCH] perf: split trajectory export paths --- src/trajectory/export.test.ts | 21 +++++---- src/trajectory/export.ts | 2 +- src/trajectory/paths.ts | 69 +++++++++++++++++++++++++++++ src/trajectory/runtime.ts | 82 +++++++---------------------------- 4 files changed, 99 insertions(+), 75 deletions(-) create mode 100644 src/trajectory/paths.ts diff --git a/src/trajectory/export.test.ts b/src/trajectory/export.test.ts index b1044a7a36a..f709e0e0a46 100644 --- a/src/trajectory/export.test.ts +++ b/src/trajectory/export.test.ts @@ -2,16 +2,17 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { Message, Usage } from "@mariozechner/pi-ai"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { exportTrajectoryBundle, resolveDefaultTrajectoryExportDir } from "./export.js"; -import { resolveTrajectoryPointerFilePath } from "./runtime.js"; +import { resolveTrajectoryPointerFilePath } from "./paths.js"; import type { TrajectoryEvent } from "./types.js"; -const tempDirs: string[] = []; +let tempRoot = ""; +let tempDirId = 0; function makeTempDir(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trajectory-")); - tempDirs.push(dir); + const dir = path.join(tempRoot, `case-${tempDirId++}`); + fs.mkdirSync(dir, { recursive: true }); return dir; } @@ -179,9 +180,13 @@ function writeToolCallSessionFile(sessionFile: string): void { ); } -afterEach(() => { - for (const dir of tempDirs.splice(0)) { - fs.rmSync(dir, { recursive: true, force: true }); +beforeAll(() => { + tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trajectory-")); +}); + +afterAll(() => { + if (tempRoot) { + fs.rmSync(tempRoot, { recursive: true, force: true }); } }); diff --git a/src/trajectory/export.ts b/src/trajectory/export.ts index 14908259ce6..63237b4729d 100644 --- a/src/trajectory/export.ts +++ b/src/trajectory/export.ts @@ -23,7 +23,7 @@ import { resolveTrajectoryFilePath, resolveTrajectoryPointerFilePath, safeTrajectorySessionFileName, -} from "./runtime.js"; +} from "./paths.js"; import type { TrajectoryBundleManifest, TrajectoryEvent, diff --git a/src/trajectory/paths.ts b/src/trajectory/paths.ts new file mode 100644 index 00000000000..76915989671 --- /dev/null +++ b/src/trajectory/paths.ts @@ -0,0 +1,69 @@ +import fs from "node:fs"; +import path from "node:path"; +import { resolveHomeRelativePath } from "../infra/home-dir.js"; + +export const TRAJECTORY_RUNTIME_FILE_MAX_BYTES = 50 * 1024 * 1024; +export const TRAJECTORY_RUNTIME_EVENT_MAX_BYTES = 256 * 1024; + +type TrajectoryPointerOpenFlagConstants = Pick< + typeof fs.constants, + "O_CREAT" | "O_TRUNC" | "O_WRONLY" +> & + Partial>; + +export function safeTrajectorySessionFileName(sessionId: string): string { + const safe = sessionId.replaceAll(/[^A-Za-z0-9_-]/g, "_").slice(0, 120); + return /[A-Za-z0-9]/u.test(safe) ? safe : "session"; +} + +export function resolveTrajectoryPointerOpenFlags( + constants: TrajectoryPointerOpenFlagConstants = fs.constants, +): number { + const noFollow = constants.O_NOFOLLOW; + return ( + constants.O_CREAT | + constants.O_TRUNC | + constants.O_WRONLY | + (typeof noFollow === "number" ? noFollow : 0) + ); +} + +function resolveContainedPath(baseDir: string, fileName: string): string { + const resolvedBase = path.resolve(baseDir); + const resolvedFile = path.resolve(resolvedBase, fileName); + const relative = path.relative(resolvedBase, resolvedFile); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + throw new Error("Trajectory file path escaped its configured directory"); + } + return resolvedFile; +} + +export function resolveTrajectoryFilePath(params: { + env?: NodeJS.ProcessEnv; + sessionFile?: string; + sessionId: string; +}): string { + const env = params.env ?? process.env; + const dirOverride = env.OPENCLAW_TRAJECTORY_DIR?.trim(); + if (dirOverride) { + return resolveContainedPath( + resolveHomeRelativePath(dirOverride), + `${safeTrajectorySessionFileName(params.sessionId)}.jsonl`, + ); + } + if (!params.sessionFile) { + return path.join( + process.cwd(), + `${safeTrajectorySessionFileName(params.sessionId)}.trajectory.jsonl`, + ); + } + return params.sessionFile.endsWith(".jsonl") + ? `${params.sessionFile.slice(0, -".jsonl".length)}.trajectory.jsonl` + : `${params.sessionFile}.trajectory.jsonl`; +} + +export function resolveTrajectoryPointerFilePath(sessionFile: string): string { + return sessionFile.endsWith(".jsonl") + ? `${sessionFile.slice(0, -".jsonl".length)}.trajectory-path.json` + : `${sessionFile}.trajectory-path.json`; +} diff --git a/src/trajectory/runtime.ts b/src/trajectory/runtime.ts index c2f6dfd93ea..af2b5e1f93f 100644 --- a/src/trajectory/runtime.ts +++ b/src/trajectory/runtime.ts @@ -3,11 +3,26 @@ import path from "node:path"; import { sanitizeDiagnosticPayload } from "../agents/payload-redaction.js"; import { getQueuedFileWriter, type QueuedFileWriter } from "../agents/queued-file-writer.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveUserPath } from "../utils.js"; import { parseBooleanValue } from "../utils/boolean.js"; import { safeJsonStringify } from "../utils/safe-json.js"; +import { + TRAJECTORY_RUNTIME_EVENT_MAX_BYTES, + TRAJECTORY_RUNTIME_FILE_MAX_BYTES, + resolveTrajectoryFilePath, + resolveTrajectoryPointerFilePath, + resolveTrajectoryPointerOpenFlags, +} from "./paths.js"; import type { TrajectoryEvent, TrajectoryToolDefinition } from "./types.js"; +export { + TRAJECTORY_RUNTIME_EVENT_MAX_BYTES, + TRAJECTORY_RUNTIME_FILE_MAX_BYTES, + resolveTrajectoryFilePath, + resolveTrajectoryPointerFilePath, + resolveTrajectoryPointerOpenFlags, + safeTrajectorySessionFileName, +} from "./paths.js"; + type TrajectoryRuntimeInit = { cfg?: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -30,73 +45,8 @@ type TrajectoryRuntimeRecorder = { }; const writers = new Map(); -export const TRAJECTORY_RUNTIME_FILE_MAX_BYTES = 50 * 1024 * 1024; -export const TRAJECTORY_RUNTIME_EVENT_MAX_BYTES = 256 * 1024; const MAX_TRAJECTORY_WRITERS = 100; -type TrajectoryPointerOpenFlagConstants = Pick< - typeof fs.constants, - "O_CREAT" | "O_TRUNC" | "O_WRONLY" -> & - Partial>; - -export function safeTrajectorySessionFileName(sessionId: string): string { - const safe = sessionId.replaceAll(/[^A-Za-z0-9_-]/g, "_").slice(0, 120); - return /[A-Za-z0-9]/u.test(safe) ? safe : "session"; -} - -export function resolveTrajectoryPointerOpenFlags( - constants: TrajectoryPointerOpenFlagConstants = fs.constants, -): number { - const noFollow = constants.O_NOFOLLOW; - return ( - constants.O_CREAT | - constants.O_TRUNC | - constants.O_WRONLY | - (typeof noFollow === "number" ? noFollow : 0) - ); -} - -function resolveContainedPath(baseDir: string, fileName: string): string { - const resolvedBase = path.resolve(baseDir); - const resolvedFile = path.resolve(resolvedBase, fileName); - const relative = path.relative(resolvedBase, resolvedFile); - if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { - throw new Error("Trajectory file path escaped its configured directory"); - } - return resolvedFile; -} - -export function resolveTrajectoryFilePath(params: { - env?: NodeJS.ProcessEnv; - sessionFile?: string; - sessionId: string; -}): string { - const env = params.env ?? process.env; - const dirOverride = env.OPENCLAW_TRAJECTORY_DIR?.trim(); - if (dirOverride) { - return resolveContainedPath( - resolveUserPath(dirOverride), - `${safeTrajectorySessionFileName(params.sessionId)}.jsonl`, - ); - } - if (!params.sessionFile) { - return path.join( - process.cwd(), - `${safeTrajectorySessionFileName(params.sessionId)}.trajectory.jsonl`, - ); - } - return params.sessionFile.endsWith(".jsonl") - ? `${params.sessionFile.slice(0, -".jsonl".length)}.trajectory.jsonl` - : `${params.sessionFile}.trajectory.jsonl`; -} - -export function resolveTrajectoryPointerFilePath(sessionFile: string): string { - return sessionFile.endsWith(".jsonl") - ? `${sessionFile.slice(0, -".jsonl".length)}.trajectory-path.json` - : `${sessionFile}.trajectory-path.json`; -} - function writeTrajectoryPointerBestEffort(params: { filePath: string; sessionFile?: string;