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.
This commit is contained in:
Peter Steinberger
2026-07-28 10:29:19 -04:00
committed by GitHub
parent 9feb6ad161
commit 49c62f3505
8 changed files with 40 additions and 71 deletions
@@ -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),
});
@@ -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 });
@@ -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,
+4 -11
View File
@@ -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<string, unknown>) => 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,
@@ -195,7 +195,6 @@ export async function prepareAndDispatchEmbeddedRunAttempt(input: {
sessionFile: sessionPromptState.sessionFile,
sessionTarget: resolvedSessionTarget,
sessionKey: resolvedSessionKey,
trajectorySessionFile,
trajectoryRecorder: trajectoryRecorder ?? undefined,
workspaceDir,
isCanonicalWorkspace,
@@ -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,
@@ -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. */
+28
View File
@@ -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");