diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 829ac4e8f55..a1245b56230 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -797,7 +797,9 @@ Move these into agent databases: - Trajectory sidecars when they are not explicit export files. Done for runtime writes: trajectory capture writes agent-database `trajectory_runtime_events` rows and mirrors run-scoped artifacts into SQLite. Legacy sidecars remain - readable only as export/migration compatibility input. + readable only as export/migration compatibility input. Runtime trajectory + capture exposes a SQLite runtime locator; JSONL path helpers are isolated to + legacy export/debug support and are not re-exported from the runtime module. Keep these file-backed for now: diff --git a/src/trajectory/paths.test.ts b/src/trajectory/paths.test.ts new file mode 100644 index 00000000000..c1e06e6aeae --- /dev/null +++ b/src/trajectory/paths.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { resolveTrajectoryFilePath, resolveTrajectoryPointerOpenFlags } from "./paths.js"; + +describe("trajectory legacy path helpers", () => { + it("resolves a session-adjacent trajectory file by default", () => { + expect( + resolveTrajectoryFilePath({ + transcriptLocator: "/tmp/session.jsonl", + sessionId: "session-1", + }), + ).toBe("/tmp/session.trajectory.jsonl"); + }); + + it("sanitizes session ids when resolving an override directory", () => { + expect( + resolveTrajectoryFilePath({ + env: { OPENCLAW_TRAJECTORY_DIR: "/tmp/traces" }, + sessionId: "../evil/session", + }), + ).toBe("/tmp/traces/___evil_session.jsonl"); + }); + + it("keeps pointer write flags usable when O_NOFOLLOW is unavailable", () => { + expect( + resolveTrajectoryPointerOpenFlags({ + O_CREAT: 0x01, + O_TRUNC: 0x02, + O_WRONLY: 0x04, + }), + ).toBe(0x07); + }); +}); diff --git a/src/trajectory/runtime.test.ts b/src/trajectory/runtime.test.ts index 844bd2253d6..eb7b22b976f 100644 --- a/src/trajectory/runtime.test.ts +++ b/src/trajectory/runtime.test.ts @@ -14,8 +14,6 @@ import { listTrajectoryRuntimeEvents } from "./runtime-store.sqlite.js"; import { TRAJECTORY_RUNTIME_EVENT_MAX_BYTES, createTrajectoryRuntimeRecorder, - resolveTrajectoryPointerOpenFlags, - resolveTrajectoryFilePath, toTrajectoryToolDefinitions, } from "./runtime.js"; @@ -46,10 +44,10 @@ afterEach(() => { function expectTrajectoryRuntimeRecorder( recorder: ReturnType, ): TrajectoryRuntimeRecorder { + expect(recorder).toEqual(expect.objectContaining({ recordEvent: expect.any(Function) })); if (recorder === null) { throw new Error("Expected trajectory runtime recorder"); } - expect(typeof recorder.recordEvent).toBe("function"); return recorder; } @@ -86,30 +84,12 @@ function useTempStateDir(): void { } describe("trajectory runtime", () => { - it("resolves a session-adjacent trajectory file by default", () => { - expect( - resolveTrajectoryFilePath({ - sessionFile: "/tmp/session.jsonl", - sessionId: "session-1", - }), - ).toBe("/tmp/session.trajectory.jsonl"); - }); - - it("sanitizes session ids when resolving an override directory", () => { - expect( - resolveTrajectoryFilePath({ - env: { OPENCLAW_TRAJECTORY_DIR: "/tmp/traces" }, - sessionId: "../evil/session", - }), - ).toBe("/tmp/traces/___evil_session.jsonl"); - }); - it("records sanitized runtime events into the agent database by default", () => { useTempStateDir(); const recorder = createTrajectoryRuntimeRecorder({ sessionId: "session-1", sessionKey: "agent:main:session-1", - sessionFile: "/tmp/session.jsonl", + transcriptLocator: "/tmp/session.jsonl", provider: "openai", modelId: "gpt-5.4", modelApi: "responses", @@ -149,7 +129,7 @@ describe("trajectory runtime", () => { sessionId: "session-1", sessionKey: "agent:main:session-1", runId: "run-1", - sessionFile: "/tmp/session.jsonl", + transcriptLocator: "/tmp/session.jsonl", provider: "openai", modelId: "gpt-5.4", modelApi: "responses", @@ -176,7 +156,7 @@ describe("trajectory runtime", () => { modelId: "gpt-5.4", modelApi: "responses", workspaceDir: "/tmp/workspace", - runtimeFile: "sqlite:main:trajectory:session-1", + runtimeLocator: "sqlite:main:trajectory:session-1", eventCount: 2, }, }); @@ -190,7 +170,7 @@ describe("trajectory runtime", () => { useTempStateDir(); const recorder = createTrajectoryRuntimeRecorder({ sessionId: "session-1", - sessionFile: "/tmp/session.jsonl", + transcriptLocator: "/tmp/session.jsonl", }); const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder); @@ -214,7 +194,7 @@ describe("trajectory runtime", () => { useTempStateDir(); const recorder = createTrajectoryRuntimeRecorder({ sessionId: "session-1", - sessionFile: "/tmp/session.jsonl", + transcriptLocator: "/tmp/session.jsonl", maxRuntimeFileBytes: 900, }); @@ -242,16 +222,6 @@ describe("trajectory runtime", () => { expect(truncated?.data?.droppedEvents).toBeGreaterThan(0); }); - it("keeps pointer write flags usable when O_NOFOLLOW is unavailable", () => { - expect( - resolveTrajectoryPointerOpenFlags({ - O_CREAT: 0x01, - O_TRUNC: 0x02, - O_WRONLY: 0x04, - }), - ).toBe(0x07); - }); - it("does not record runtime events when explicitly disabled", () => { useTempStateDir(); const recorder = createTrajectoryRuntimeRecorder({ @@ -260,7 +230,7 @@ describe("trajectory runtime", () => { }, sessionId: "session-1", sessionKey: "agent:main:session-1", - sessionFile: "/tmp/session.jsonl", + transcriptLocator: "/tmp/session.jsonl", }); expect(recorder).toBeNull(); diff --git a/src/trajectory/runtime.ts b/src/trajectory/runtime.ts index 09b6b7e5337..7d7ed579a65 100644 --- a/src/trajectory/runtime.ts +++ b/src/trajectory/runtime.ts @@ -14,11 +14,6 @@ import type { TrajectoryEvent, TrajectoryToolDefinition } from "./types.js"; export { TRAJECTORY_RUNTIME_CAPTURE_MAX_BYTES, TRAJECTORY_RUNTIME_EVENT_MAX_BYTES, - TRAJECTORY_RUNTIME_FILE_MAX_BYTES, - resolveTrajectoryFilePath, - resolveTrajectoryPointerFilePath, - resolveTrajectoryPointerOpenFlags, - safeTrajectorySessionFileName, } from "./paths.js"; type TrajectoryRuntimeInit = { @@ -28,7 +23,7 @@ type TrajectoryRuntimeInit = { runId?: string; sessionId: string; sessionKey?: string; - sessionFile?: string; + transcriptLocator?: string; provider?: string; modelId?: string; modelApi?: string | null; @@ -38,7 +33,7 @@ type TrajectoryRuntimeInit = { type TrajectoryRuntimeRecorder = { enabled: true; - filePath: string; + runtimeLocator: string; recordEvent: (type: string, data?: Record) => void; flush: () => Promise; }; @@ -173,7 +168,7 @@ export function createTrajectoryRuntimeRecorder( } const agentId = resolveAgentIdFromSessionKey(params.sessionKey); - const filePath = `sqlite:${agentId}:trajectory:${params.sessionId}`; + const runtimeLocator = `sqlite:${agentId}:trajectory:${params.sessionId}`; const maxRuntimeFileBytes = Math.max( 1, Math.floor(params.maxRuntimeFileBytes ?? TRAJECTORY_RUNTIME_CAPTURE_MAX_BYTES), @@ -240,7 +235,7 @@ export function createTrajectoryRuntimeRecorder( ...(params.modelId ? { modelId: params.modelId } : {}), ...(params.modelApi ? { modelApi: params.modelApi } : {}), ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), - runtimeFile: filePath, + runtimeLocator, eventCount: artifactEventCount, bytes: Buffer.byteLength(artifactLines.join(""), "utf8"), }, @@ -285,7 +280,7 @@ export function createTrajectoryRuntimeRecorder( return { enabled: true, - filePath, + runtimeLocator, recordEvent: (type, data) => { if (captureStopped) { droppedEvents += 1;