mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
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:
committed by
GitHub
parent
9feb6ad161
commit
49c62f3505
@@ -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,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user