From 49c62f35055dfec024ac02e7c818e7ec4f0a3633 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Jul 2026 10:29:19 -0400 Subject: [PATCH] fix(codex): restore trajectory capture for canonical session targets (#115220) Attempt dispatch stopped emitting legacy `sqlite:` session-file markers in #113233, which replaced SessionTranscriptRuntimeTarget.sessionFile with storePath and started passing the canonical session key instead. The Codex plugin still re-derived its own SQLite gate from that string, so parseSqliteSessionFileMarker never matched and every Codex run bailed out with reason "non-sqlite-session-target". The host already owns SQLite target resolution and identity validation: it returns a recorder only when a committed session row resolves. Drop the stale marker gate and the now-dead trajectorySessionFile plumbing, leaving the host-provided recorder as the single gate. --- .../src/app-server/run-attempt-resources.ts | 1 - .../codex/src/app-server/run-attempt.test.ts | 1 - .../codex/src/app-server/trajectory.test.ts | 61 +++---------------- extensions/codex/src/app-server/trajectory.ts | 15 ++--- .../run/attempt-dispatch-preparation.ts | 1 - .../run/run-attempt-dispatch.ts | 2 - src/agents/embedded-agent-runner/run/types.ts | 2 - src/trajectory/runtime.test.ts | 28 +++++++++ 8 files changed, 40 insertions(+), 71 deletions(-) diff --git a/extensions/codex/src/app-server/run-attempt-resources.ts b/extensions/codex/src/app-server/run-attempt-resources.ts index caf8a7b4cd8b..69d741aaad33 100644 --- a/extensions/codex/src/app-server/run-attempt-resources.ts +++ b/extensions/codex/src/app-server/run-attempt-resources.ts @@ -53,7 +53,6 @@ export function prepareCodexAttemptResources(prompt: CodexAttemptPrompt) { developerInstructions: buildRenderedCodexDeveloperInstructions(), prompt: turnState.codexTurnPromptText, trajectoryRecorder: hostTrajectoryRecorder, - trajectorySessionFile: params.trajectorySessionFile, tools: toolBridge.availableSpecs, warn: (message, fields) => embeddedAgentLog.warn(message, fields), }); diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index db3b515ddc46..9842314cf377 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -1603,7 +1603,6 @@ describe("runCodexAppServerAttempt", () => { type: string; }> = []; Object.assign(params, { - trajectorySessionFile: `sqlite:main:session-1:${path.join(tempDir, "openclaw-agent.sqlite")}`, trajectoryRecorder: { recordEvent: (type: string, data?: { prompt?: string; systemPrompt?: string }) => { trajectoryEvents.push({ type, data }); diff --git a/extensions/codex/src/app-server/trajectory.test.ts b/extensions/codex/src/app-server/trajectory.test.ts index ca3c74c674d1..4f1a77042b4a 100644 --- a/extensions/codex/src/app-server/trajectory.test.ts +++ b/extensions/codex/src/app-server/trajectory.test.ts @@ -78,7 +78,6 @@ function createMemoryBackedRecorder(params: { ...params.attempt, } as never, trajectoryRecorder: host.recorder, - trajectorySessionFile: `sqlite:main:${sessionId}:${path.join(params.tmpDir, "sessions.json")}`, tools: params.tools, env: {}, }); @@ -115,57 +114,12 @@ function createSqliteHostTrajectoryRecorder(params: { } describe("Codex trajectory recorder", () => { - it("rejects file-backed trajectory targets without creating sidecars", () => { - const tmpDir = makeTempDir(); - const warn = vi.fn(); - const recorder = createCodexTrajectoryRecorder({ - cwd: tmpDir, - attempt: { - sessionFile: path.join(tmpDir, "session.jsonl"), - sessionId: "session-1", - model: { api: "responses" }, - } as never, - env: {}, - warn, - }); - - expect(recorder).toBeNull(); - expect(warn).toHaveBeenCalledWith( - "codex trajectory capture requires a matching SQLite session target", - { sessionId: "session-1", reason: "non-sqlite-session-target" }, - ); - expect(fs.existsSync(path.join(tmpDir, "session.trajectory.jsonl"))).toBe(false); - expect(fs.existsSync(path.join(tmpDir, "session.trajectory-path.json"))).toBe(false); - }); - - it("rejects a SQLite marker for a different session identity", () => { - const tmpDir = makeTempDir(); - const warn = vi.fn(); - const recorder = createCodexTrajectoryRecorder({ - cwd: tmpDir, - attempt: { - sessionFile: "sqlite:main:other:/tmp/openclaw-agent.sqlite", - sessionId: "session-1", - model: { api: "responses" }, - } as never, - trajectoryRecorder: createMemoryHostTrajectoryRecorder().recorder, - env: {}, - warn, - }); - - expect(recorder).toBeNull(); - expect(warn).toHaveBeenCalledWith( - "codex trajectory capture requires a matching SQLite session target", - { sessionId: "session-1", reason: "session-id-mismatch" }, - ); - }); - it("warns when the SQLite host recorder is unavailable", () => { const warn = vi.fn(); const recorder = createCodexTrajectoryRecorder({ cwd: makeTempDir(), attempt: { - sessionFile: "sqlite:main:session-1:/tmp/openclaw-agent.sqlite", + sessionFile: "agent:main:session-1", sessionId: "session-1", model: { api: "responses" }, } as never, @@ -180,20 +134,22 @@ describe("Codex trajectory recorder", () => { ); }); - it("stores SQLite-backed trajectory captures in the session database", async () => { + it("stores SQLite-backed captures for the canonical session-key target", async () => { + // Regression: the host stopped emitting legacy `sqlite:` session-file + // markers, so any marker re-derivation here drops every Codex capture. const tmpDir = makeTempDir(); const storePath = path.join(tmpDir, "sessions", "sessions.json"); - const trajectorySessionFile = `sqlite:main:session-1:${storePath}`; await upsertSessionEntry({ agentId: "main", sessionKey: "agent:main:session-1", storePath, - entry: { sessionId: "session-1", sessionFile: trajectorySessionFile, updatedAt: 10 }, + entry: { sessionId: "session-1", updatedAt: 10 }, }); const recorder = createCodexTrajectoryRecorder({ cwd: tmpDir, attempt: { - sessionFile: path.join(tmpDir, "sessions", "session.jsonl"), + sessionFile: "agent:main:session-1", + sessionKey: "agent:main:session-1", sessionId: "session-1", model: { api: "responses" }, } as never, @@ -202,7 +158,6 @@ describe("Codex trajectory recorder", () => { sessionId: "session-1", storePath, }), - trajectorySessionFile, env: {}, }); @@ -269,7 +224,7 @@ describe("Codex trajectory recorder", () => { const recorder = createCodexTrajectoryRecorder({ cwd: makeTempDir(), attempt: { - sessionFile: "sqlite:main:session-1:/tmp/openclaw-agent.sqlite", + sessionFile: "agent:main:session-1", sessionId: "session-1", model: { api: "responses" }, } as never, diff --git a/extensions/codex/src/app-server/trajectory.ts b/extensions/codex/src/app-server/trajectory.ts index a1fd92400c2b..b6eb24f6f56e 100644 --- a/extensions/codex/src/app-server/trajectory.ts +++ b/extensions/codex/src/app-server/trajectory.ts @@ -3,7 +3,6 @@ * context and completion payloads. */ import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime"; -import { parseSqliteSessionFileMarker } from "openclaw/plugin-sdk/session-store-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { attemptTerminal, type EmbeddedRunAttemptResult } from "./attempt-terminal.js"; import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js"; @@ -21,7 +20,6 @@ type CodexTrajectoryInit = { developerInstructions?: string; prompt?: string; trajectoryRecorder?: CodexHostTrajectoryRecorder | null; - trajectorySessionFile?: string; tools?: CodexDynamicToolSpec[]; env?: NodeJS.ProcessEnv; warn?: (message: string, fields: Record) => void; @@ -133,15 +131,10 @@ export function createCodexTrajectoryRecorder( return null; } - const sessionFile = params.trajectorySessionFile ?? params.attempt.sessionFile; - const sqliteMarker = parseSqliteSessionFileMarker(sessionFile); - if (!sqliteMarker || sqliteMarker.sessionId !== params.attempt.sessionId) { - params.warn?.("codex trajectory capture requires a matching SQLite session target", { - sessionId: params.attempt.sessionId, - reason: sqliteMarker ? "session-id-mismatch" : "non-sqlite-session-target", - }); - return null; - } + // The host owns SQLite target resolution and identity validation; it hands + // back a recorder only for a committed session row. Re-deriving that here + // from a session-file string silently drops every capture once the host + // stops emitting the legacy `sqlite:` marker. if (!params.trajectoryRecorder) { params.warn?.("codex trajectory capture requires the SQLite host recorder", { sessionId: params.attempt.sessionId, diff --git a/src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.ts b/src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.ts index 617374d88ea3..fec8f179a4b7 100644 --- a/src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.ts +++ b/src/agents/embedded-agent-runner/run/attempt-dispatch-preparation.ts @@ -195,7 +195,6 @@ export async function prepareAndDispatchEmbeddedRunAttempt(input: { sessionFile: sessionPromptState.sessionFile, sessionTarget: resolvedSessionTarget, sessionKey: resolvedSessionKey, - trajectorySessionFile, trajectoryRecorder: trajectoryRecorder ?? undefined, workspaceDir, isCanonicalWorkspace, diff --git a/src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts b/src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts index 1e546d2020c9..0df98021ec42 100644 --- a/src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts +++ b/src/agents/embedded-agent-runner/run/run-attempt-dispatch.ts @@ -29,7 +29,6 @@ type AttemptRuntime = { sessionFile: string; sessionTarget?: ContextEngineSessionTarget; sessionKey?: string; - trajectorySessionFile: string; trajectoryRecorder?: EmbeddedRunAttemptTrajectoryRecorder; workspaceDir: string; isCanonicalWorkspace: boolean; @@ -208,7 +207,6 @@ export async function dispatchEmbeddedRunAttempt(input: { hasRepliedRef: params.hasRepliedRef, sessionFile: runtime.sessionFile, sessionTarget: runtime.sessionTarget, - trajectorySessionFile: runtime.trajectorySessionFile, trajectoryRecorder: runtime.trajectoryRecorder, workspaceDir: runtime.workspaceDir, cwd: params.cwd, diff --git a/src/agents/embedded-agent-runner/run/types.ts b/src/agents/embedded-agent-runner/run/types.ts index e0c919bacb03..c620c2bbde48 100644 --- a/src/agents/embedded-agent-runner/run/types.ts +++ b/src/agents/embedded-agent-runner/run/types.ts @@ -137,8 +137,6 @@ export type EmbeddedRunAttemptParams = EmbeddedRunAttemptBase & { observeToolTerminal?: EmbeddedRunAttemptToolTerminalObserver; /** Host-issued scope for harnesses that mirror native child runs into task state. */ agentHarnessTaskRuntimeScope?: AgentHarnessTaskRuntimeScope; - /** Storage-neutral trajectory target for harness-owned runtime trace artifacts. */ - trajectorySessionFile?: string; /** Storage-aware trajectory recorder owned by the OpenClaw host. */ trajectoryRecorder?: EmbeddedRunAttemptTrajectoryRecorder | null; /** Live observer called after wrapped tool outcomes are recorded. */ diff --git a/src/trajectory/runtime.test.ts b/src/trajectory/runtime.test.ts index 20474993c122..b1fc98142224 100644 --- a/src/trajectory/runtime.test.ts +++ b/src/trajectory/runtime.test.ts @@ -130,6 +130,34 @@ describe("trajectory runtime", () => { ); }); + it("records runtime events for the canonical session-key target the dispatcher passes", async () => { + // Attempt dispatch stopped passing legacy `sqlite:` markers and now hands + // the canonical session key plus a complete target. Recording must not + // depend on the marker, or every harness capture silently disappears. + const tempDir = makeTempDir(); + const storePath = path.join(tempDir, "agents", "main", "sessions", "sessions.json"); + const sessionKey = "agent:main:main"; + await replaceSessionEntry({ sessionKey, storePath }, { sessionId: "session-1", updatedAt: 10 }); + const recorder = createTrajectoryRuntimeRecorder({ + sessionId: "session-1", + sessionKey, + sessionFile: sessionKey, + sessionTarget: { agentId: "main", sessionId: "session-1", sessionKey, storePath }, + provider: "openai", + modelId: "gpt-5.4", + modelApi: "responses", + workspaceDir: "/tmp/workspace", + }); + + const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder); + runtimeRecorder.recordEvent("session.started"); + await runtimeRecorder.flush(); + + await expect( + loadSqliteTrajectoryRuntimeEvents({ sessionId: "session-1", storePath }), + ).resolves.toEqual([expect.objectContaining({ source: "runtime", type: "session.started" })]); + }); + it("rejects a legacy SQLite marker for another session", () => { const storePath = path.join(makeTempDir(), "sessions.json");