fix(codex): keep sessionless mirror warnings out of onboarding (#122831)

* fix(codex): skip mirrors for sessionless runs

* fix(codex): restore openai model provider on media understanding turns

PR #122163 made thread/start modelProvider caller-supplied and updated the
web-search caller but missed media understanding, breaking its tests on main
full runs (cross-lane gap). Pass the provider explicitly and export the
retire binding prod now touches from the shared-client test mock.

* test(codex): align prompt assertions with reworded guidance

PR #121522 reworded the Skill Workshop guidance and cd7b7f639d reworded
the message-tool final-reply text; both updated core tests but missed
these codex mirror assertions (cross-lane gap breaking main full runs).

* test(codex): scope agent-projection fixture session to its agent

PR #114388 made multi-agent session ownership explicit; the atlas-scoped
projection test still used the shared main-scoped session key and now
trips AgentSelectionRequiredError (fourth cross-lane escape on main).
This commit is contained in:
Peter Steinberger
2026-08-12 18:13:16 -07:00
committed by GitHub
parent add6b20bd2
commit 469be48967
6 changed files with 129 additions and 29 deletions
@@ -12,6 +12,7 @@ const sharedClientMocks = vi.hoisted(() => ({
vi.mock("./src/app-server/shared-client.js", () => ({
createIsolatedCodexAppServerClient: sharedClientMocks.createIsolatedCodexAppServerClient,
retireSharedCodexAppServerClientIfCurrent: () => undefined,
}));
function codexModel(inputModalities: string[] = ["text", "image"]) {
@@ -76,6 +76,7 @@ async function describeCodexImages(
const { text } = await runBoundedCodexAppServerTurn({
config: req.cfg,
model: { mode: "required", id: model },
modelProvider: "openai",
profile: req.profile,
timeoutMs: req.timeoutMs,
signal: req.signal,
@@ -121,6 +122,7 @@ async function extractCodexStructured(
const { text } = await runBoundedCodexAppServerTurn({
config: req.cfg,
model: { mode: "required", id: model },
modelProvider: "openai",
profile: req.profile,
timeoutMs: req.timeoutMs,
signal: req.signal,
@@ -1048,7 +1048,7 @@ describe("Codex app-server native code mode config", () => {
expect(instructions).toContain("## Skill Workshop");
expect(instructions).toContain("Durable reusable skill/playbook/workflow work");
expect(instructions).toContain("`skill_workshop`");
expect(instructions).toContain("Generated = pending proposal");
expect(instructions).toContain("Other generated work = pending proposal");
expect(instructions).toContain("only explicit user ask");
});
@@ -1083,7 +1083,7 @@ describe("Codex app-server native code mode config", () => {
});
expect(instructions).toContain("For progress, set `final=false`.");
expect(instructions).toContain("set `final=true`");
expect(instructions).toContain("Set `final=true`, or omit it,");
});
it("keeps durable dynamic tool fingerprints scoped to loading mode", () => {
@@ -197,6 +197,7 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081
it("projects only Codex user MCP servers scoped to the current agent", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
registerCodexTestSessionIdentity(sessionFile, "session-1", "agent:atlas:session-1");
const workspaceDir = path.join(tempDir, "workspace");
const request = vi.fn(async (method: string, _params: unknown) => {
if (method === "thread/start") {
@@ -207,28 +208,33 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081
await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir, {
mcp: {
servers: {
atlas: {
transport: "streamable-http",
url: "https://atlas.example.com/mcp",
codex: {
agents: ["atlas"],
defaultToolsApprovalMode: "approve",
params: {
...createParams(sessionFile, workspaceDir, {
mcp: {
servers: {
atlas: {
transport: "streamable-http",
url: "https://atlas.example.com/mcp",
codex: {
agents: ["atlas"],
defaultToolsApprovalMode: "approve",
},
},
},
apolo: {
transport: "streamable-http",
url: "https://apolo.example.com/mcp",
codex: {
agents: ["apolo"],
defaultToolsApprovalMode: "approve",
apolo: {
transport: "streamable-http",
url: "https://apolo.example.com/mcp",
codex: {
agents: ["apolo"],
defaultToolsApprovalMode: "approve",
},
},
},
},
},
} as unknown as EmbeddedRunAttemptParams["config"]),
} as unknown as EmbeddedRunAttemptParams["config"]),
// Explicit multi-agent ownership (#114388): the session key owner must
// match the explicit agentId below.
sessionKey: "agent:atlas:session-1",
},
agentId: "atlas",
cwd: workspaceDir,
dynamicTools: [],
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { embeddedAgentLog, type AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
initializeGlobalHookRunner,
resetGlobalHookRunner,
@@ -24,6 +24,7 @@ import {
buildCodexUserPromptMessage,
codexTranscriptMirrorRuntime,
importCodexThreadHistoryToTranscript,
mirrorPromptAtTurnStartBestEffort,
projectBoundedCodexThreadHistory,
} from "./transcript-mirror.js";
import { attachCodexMirrorIdentity } from "./upstream-prompt-provenance.js";
@@ -1275,8 +1276,10 @@ describe("mirrorCodexAppServerTranscript", () => {
expect(await readMirrorMessages(target)).toEqual([]);
});
it("leaves the assistant unowned when transcript persistence fails", async () => {
it("skips transcript mirrors for sessionless embedded runs", async () => {
const root = await makeRoot("openclaw-codex-transcript-failure-");
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
const markRuntimePersistencePending = vi.fn();
const assistantMessage = attachCodexMirrorIdentity(
makeAgentAssistantMessage({
content: [{ type: "text", text: "needs fallback persistence" }],
@@ -1285,14 +1288,31 @@ describe("mirrorCodexAppServerTranscript", () => {
"turn-1:assistant",
);
const params = {
prompt: "sessionless prompt",
runId: "probe-setup-inference-sessionless",
sessionId: "session-1",
userTurnTranscriptRecorder: {
markRuntimePersistencePending,
resolveMessage: async () => undefined,
},
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"];
await mirrorPromptAtTurnStartBestEffort({
params,
sessionKey: "agent:main:setup-inference:incognito-session-1",
notifyUserMessagePersisted: () => undefined,
cwd: root,
threadId: "thread-1",
turnId: "turn-1",
upstreamUserText: "sessionless prompt",
});
const mirrorOutcome = await mirrorTranscriptBestEffort({
params: {
sessionId: "session-1",
suppressNextUserMessagePersistence: true,
} as unknown as Parameters<typeof mirrorTranscriptBestEffort>[0]["params"],
params,
result: {
messagesSnapshot: [assistantMessage],
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
sessionKey: "agent:main:setup-inference:incognito-session-1",
notifyUserMessagePersisted: () => undefined,
cwd: root,
threadId: "thread-1",
@@ -1300,6 +1320,66 @@ describe("mirrorCodexAppServerTranscript", () => {
});
expect(mirrorOutcome).toEqual({ assistantTranscriptOwned: false, mirroredMessages: [] });
expect(markRuntimePersistencePending).not.toHaveBeenCalled();
expect(warn).not.toHaveBeenCalled();
});
it("renders normal-session mirror failures in structured warnings", async () => {
const root = await makeRoot("openclaw-codex-transcript-failure-");
const blockedParent = path.join(root, "not-a-directory");
await fs.writeFile(blockedParent, "blocked");
const storePath = path.join(blockedParent, "openclaw-agent.sqlite");
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
warn.mockClear();
const runId = "run-1";
const sessionId = "session-1";
const params = {
prompt: "persist me",
runId,
sessionId,
sessionTarget: { storePath },
} as unknown as Parameters<typeof mirrorPromptAtTurnStartBestEffort>[0]["params"];
await mirrorPromptAtTurnStartBestEffort({
params,
sessionKey: "agent:main:session-1",
notifyUserMessagePersisted: () => undefined,
cwd: storePath,
threadId: "thread-1",
turnId: "turn-1",
upstreamUserText: "persist me",
});
expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server prompt at turn start", {
error: expect.any(String),
runId,
sessionId,
});
const warning = warn.mock.calls.at(-1)?.[1] as { error?: string } | undefined;
expect(warning?.error).not.toBe("");
warn.mockClear();
await mirrorTranscriptBestEffort({
params,
result: {
messagesSnapshot: [
makeAgentAssistantMessage({
content: [{ type: "text", text: "persist me too" }],
timestamp: Date.now(),
}),
],
} as Parameters<typeof mirrorTranscriptBestEffort>[0]["result"],
sessionKey: "agent:main:session-1",
notifyUserMessagePersisted: () => undefined,
cwd: root,
threadId: "thread-1",
turnId: "turn-1",
});
expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server transcript", {
error: expect.any(String),
runId,
sessionId,
});
});
it("does not attest a stale idempotency hit with the same mirror identity", async () => {
@@ -112,6 +112,9 @@ async function mirrorBestEffort(params: {
terminalAnchor?: TranscriptEntryAnchor;
mirroredMessages: MirroredAgentMessage[];
}> {
if (!params.params.sessionTarget) {
return { assistantTranscriptOwned: false, mirroredMessages: [] };
}
try {
const messages = await resolveFinalCodexMirrorMessages({
params: params.params,
@@ -182,7 +185,11 @@ async function mirrorBestEffort(params: {
mirroredMessages,
};
} catch (error) {
embeddedAgentLog.warn("failed to mirror codex app-server transcript", { error });
embeddedAgentLog.warn("failed to mirror codex app-server transcript", {
error: formatErrorMessage(error),
runId: params.params.runId,
sessionId: params.params.sessionId,
});
return { assistantTranscriptOwned: false, mirroredMessages: [] };
}
}
@@ -249,7 +256,7 @@ export async function mirrorPromptAtTurnStartBestEffort(params: {
turnId: string;
upstreamUserText: string;
}): Promise<void> {
if (params.params.suppressNextUserMessagePersistence) {
if (params.params.suppressNextUserMessagePersistence || !params.params.sessionTarget) {
return;
}
try {
@@ -281,7 +288,11 @@ export async function mirrorPromptAtTurnStartBestEffort(params: {
params.params.userTurnTranscriptRecorder?.markRuntimePersistencePending(mirrorPromise);
await mirrorPromise;
} catch (error) {
embeddedAgentLog.warn("failed to mirror codex app-server prompt at turn start", { error });
embeddedAgentLog.warn("failed to mirror codex app-server prompt at turn start", {
error: formatErrorMessage(error),
runId: params.params.runId,
sessionId: params.params.sessionId,
});
}
}